mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Density bar: every dialog field, column header, filter, badge, and status pill explains itself — grounded in the backend (ladder normalization, rate-limit park/probe, self-hosted fallback, reindex scope). Disabled buttons span-wrap so tips fire; tabs re-assert data-state.
291 lines
10 KiB
TypeScript
291 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { HelpTip } from "@/components/ui/help-tip";
|
|
import { MentorAskResponse } from "@/types";
|
|
import { RAGCitationCard } from "./rag-citation-card";
|
|
import { Markdown } from "@/components/ui/markdown";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import {
|
|
Brain,
|
|
BookOpen,
|
|
MessageSquareText,
|
|
Sparkles,
|
|
AlertCircle,
|
|
Lightbulb,
|
|
BarChart3,
|
|
MessageCircleQuestion,
|
|
User,
|
|
BookMarked,
|
|
} from "lucide-react";
|
|
|
|
interface MentorAnswerDisplayProps {
|
|
response: MentorAskResponse | null;
|
|
isLoading: boolean;
|
|
question: string | null;
|
|
error?: string | null;
|
|
onFollowUp?: (question: string) => void;
|
|
}
|
|
|
|
export function MentorAnswerDisplay({
|
|
response,
|
|
isLoading,
|
|
question,
|
|
error,
|
|
onFollowUp,
|
|
}: MentorAnswerDisplayProps) {
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<Skeleton className="h-5 w-5 rounded-full" />
|
|
<Skeleton className="h-5 w-32" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-4 w-3/4" />
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-4 w-5/6" />
|
|
</CardContent>
|
|
</Card>
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-4 w-24" />
|
|
<Skeleton className="h-20 w-full" />
|
|
<Skeleton className="h-20 w-full" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!response && !question && !error) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="relative mb-4">
|
|
<Brain className="h-12 w-12 text-muted-foreground/50" />
|
|
<Sparkles className="h-5 w-5 text-yellow-500 absolute -top-1 -right-1" />
|
|
</div>
|
|
<h3 className="text-lg font-medium mb-1">AI Mentor</h3>
|
|
<p className="text-sm text-muted-foreground max-w-md">
|
|
Your personalized AI mentor that knows your role and past experiences.
|
|
Ask questions about standards, workflows, or get guidance on your
|
|
tasks.
|
|
</p>
|
|
<div className="flex gap-2 mt-4 flex-wrap justify-center">
|
|
<Badge variant="outline" className="text-xs">
|
|
Role-aware
|
|
</Badge>
|
|
<Badge variant="outline" className="text-xs">
|
|
Personal context
|
|
</Badge>
|
|
<Badge variant="outline" className="text-xs">
|
|
Follow-ups
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="space-y-4">
|
|
{question && (
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
|
<MessageSquareText className="h-4 w-4" />
|
|
</div>
|
|
<div className="flex-1 pt-1">
|
|
<p className="text-sm font-medium">{question}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<Card className="border-red-500/50 bg-red-50 dark:bg-red-950/20">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2 text-red-600 dark:text-red-400">
|
|
<AlertCircle className="h-4 w-4" />
|
|
Mentor Query Failed
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!response) {
|
|
return null;
|
|
}
|
|
|
|
// Calculate total sources searched
|
|
const totalSearched = response.search_stats
|
|
? Object.values(response.search_stats).reduce(
|
|
(sum, count) => sum + (count > 0 ? count : 0),
|
|
0,
|
|
)
|
|
: 0;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Question */}
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
|
<MessageSquareText className="h-4 w-4" />
|
|
</div>
|
|
<div className="flex-1 pt-1">
|
|
<p className="text-sm font-medium">{question}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Personalization Context - What makes this different from Ask AI */}
|
|
{(response.agent_role || response.journal_entries_used) && (
|
|
<div className="flex items-center gap-3 p-3 bg-primary/5 rounded-lg border border-primary/20">
|
|
<Brain className="h-5 w-5 text-primary shrink-0" />
|
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
|
{response.agent_role && (
|
|
<HelpTip label="Detected from your agent identity — shapes which standards, journals, and playbooks the mentor draws on">
|
|
<span className="flex items-center gap-1.5 w-fit">
|
|
<User className="h-3.5 w-3.5 text-muted-foreground" />
|
|
<span className="text-muted-foreground">Role:</span>
|
|
<Badge variant="secondary" className="font-medium">
|
|
{response.agent_role}
|
|
</Badge>
|
|
{response.agent_team && (
|
|
<span className="text-muted-foreground">
|
|
({response.agent_team})
|
|
</span>
|
|
)}
|
|
</span>
|
|
</HelpTip>
|
|
)}
|
|
{response.journal_entries_used !== undefined &&
|
|
response.journal_entries_used > 0 && (
|
|
<HelpTip label="Entries from your own journal only — never another agent's">
|
|
<span className="flex items-center gap-1.5 w-fit">
|
|
<BookMarked className="h-3.5 w-3.5 text-muted-foreground" />
|
|
<span className="text-muted-foreground">
|
|
{response.journal_entries_used} personal journal
|
|
{response.journal_entries_used !== 1 ? "s" : ""} used
|
|
</span>
|
|
</span>
|
|
</HelpTip>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Answer */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
<Brain className="h-4 w-4 text-primary" />
|
|
Mentor Response
|
|
<HelpTip label="Knowledge-base chunks retrieved and handed to the model as context for this answer">
|
|
<span className="text-xs text-muted-foreground font-normal ml-auto w-fit">
|
|
{response.sources.length} sources used
|
|
</span>
|
|
</HelpTip>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="prose prose-sm dark:prose-invert max-w-none">
|
|
<Markdown>{response.answer}</Markdown>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Suggested Follow-ups */}
|
|
{response.suggested_followups &&
|
|
response.suggested_followups.length > 0 && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<Lightbulb className="h-4 w-4 text-yellow-500" />
|
|
Follow-up Questions
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{response.suggested_followups.map((followup, index) => (
|
|
<HelpTip key={index} label="Ask this next, in the same conversation">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-xs h-auto py-1.5 px-3"
|
|
onClick={() => onFollowUp?.(followup)}
|
|
>
|
|
<MessageCircleQuestion className="h-3 w-3 mr-1.5" />
|
|
{followup}
|
|
</Button>
|
|
</HelpTip>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Search Stats */}
|
|
{response.search_stats &&
|
|
Object.keys(response.search_stats).length > 0 && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
|
Search Stats
|
|
<span className="text-xs text-muted-foreground font-normal">
|
|
({totalSearched} total results)
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{Object.entries(response.search_stats).map(
|
|
([indexType, count]) => (
|
|
<HelpTip
|
|
key={indexType}
|
|
label={
|
|
count === -1
|
|
? `The ${indexType} index failed to search — its results may be missing from this answer.`
|
|
: `${count} matching result${count === 1 ? "" : "s"} from the ${indexType} knowledge-base index.`
|
|
}
|
|
>
|
|
<Badge
|
|
variant={count > 0 ? "secondary" : "outline"}
|
|
className={`text-xs ${count === -1 ? "text-red-500" : ""}`}
|
|
>
|
|
{indexType}: {count === -1 ? "error" : count}
|
|
</Badge>
|
|
</HelpTip>
|
|
),
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Citations */}
|
|
{response.sources.length > 0 && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<BookOpen className="h-4 w-4 text-muted-foreground" />
|
|
Sources ({response.sources.length})
|
|
</div>
|
|
<div className="space-y-2">
|
|
{response.sources.map((source, index) => (
|
|
<RAGCitationCard
|
|
key={index}
|
|
citation={{
|
|
content: source.content,
|
|
source: source.source,
|
|
score: source.score,
|
|
index_type: source.index_type,
|
|
metadata: source.metadata,
|
|
}}
|
|
index={index}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|