fleet-memory/hindsight-control-plane/src/components/think-view.tsx
Nicolò Boschi 90e370ef35
fix: misc fixes for observations and mental models (#209)
* fix: misc fixes for observations and mental models

* feat: improve graph retrieval for observations

- Update LinkExpansionRetriever to traverse through source_memory_ids
  for observation entity connections (avoiding data duplication)
- Remove entity link copy from world facts to observations in consolidator
- Add tests for link expansion graph retrieval
- Add directives_applied field to ReflectResult
- Include user's other changes (CLI, docs, client updates)

* fix: CI test failures

- Add mental_model_id parameter to create_mental_model function
- Fix ToolCallTrace not including reason field from ToolCall
- Improve test_link_expansion_observation_graph_retrieval to wait for consolidation with retry

* chore: reduce link expansion log verbosity

* Revert "chore: reduce link expansion log verbosity"

This reverts commit 3ce759391cead1012157785fa78fef16ef9bfe3b.

* feat: add semantic/temporal/entity links as fallback in graph retrieval

- Add fallback query for semantic, temporal, and entity links from memory_links
- Check both directions (outgoing and incoming links)
- Weight fallback results at 0.5x to prioritize entity links via unit_entities
- Fixes graph retrieval returning 0 when data has cross-cluster temporal connections

* fix: enable observations fixture for link expansion test

- Add enable_observations fixture to ensure observations are created
- Increase wait time from 10 to 30 seconds for CI reliability
2026-01-27 15:37:57 +01:00

1050 lines
48 KiB
TypeScript

"use client";
import { useState } from "react";
import { client } from "@/lib/api";
import { useBank } from "@/lib/bank-context";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Sparkles,
Info,
Tag,
Clock,
Database,
Brain,
MessageSquare,
Shield,
X,
Check,
Play,
} from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import JsonView from "react18-json-view";
import "react18-json-view/src/style.css";
import { MemoryDetailModal } from "./memory-detail-modal";
import { MentalModelDetailModal } from "./mental-model-detail-modal";
type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
type ViewMode = "answer" | "trace" | "json";
type BasedOnTab = "directives" | "mental_models" | "observations" | "world" | "experience";
export function ThinkView() {
const { currentBank } = useBank();
const [query, setQuery] = useState("");
const [budget, setBudget] = useState<"low" | "mid" | "high">("mid");
const [maxTokens, setMaxTokens] = useState<number>(4096);
const [includeFacts, setIncludeFacts] = useState(true);
const [includeToolCalls, setIncludeToolCalls] = useState(true);
const [viewMode, setViewMode] = useState<ViewMode>("answer");
const [result, setResult] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [tags, setTags] = useState("");
const [tagsMatch, setTagsMatch] = useState<TagsMatch>("any");
const [feedback, setFeedback] = useState("");
const [feedbackSubmitting, setFeedbackSubmitting] = useState(false);
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
const [selectedMemoryId, setSelectedMemoryId] = useState<string | null>(null);
const [selectedDirective, setSelectedDirective] = useState<any | null>(null);
const [fullDirective, setFullDirective] = useState<any | null>(null);
const [loadingDirective, setLoadingDirective] = useState(false);
const [selectedObservation, setSelectedObservation] = useState<any | null>(null);
const [fullObservation, setFullObservation] = useState<any | null>(null);
const [loadingObservation, setLoadingObservation] = useState(false);
const [selectedMentalModelId, setSelectedMentalModelId] = useState<string | null>(null);
const [activeBasedOnTab, setActiveBasedOnTab] = useState<BasedOnTab>("world");
const FEEDBACK_DIRECTIVE_NAME = "General Feedback";
// Load full directive data when one is selected
const handleSelectDirective = async (directive: any) => {
setSelectedDirective(directive);
setFullDirective(null);
if (!currentBank || !directive?.id) return;
setLoadingDirective(true);
try {
const directives = await client.listDirectives(currentBank);
const fullDir = directives.items?.find((d: any) => d.id === directive.id);
setFullDirective(fullDir || directive);
} catch (error) {
console.error("Failed to load directive:", error);
setFullDirective(directive); // Fall back to partial data
} finally {
setLoadingDirective(false);
}
};
// Load full observation data when one is selected
const handleSelectObservation = async (observation: any) => {
setSelectedObservation(observation);
setFullObservation(null);
if (!currentBank || !observation?.id) return;
setLoadingObservation(true);
try {
const observations = await client.listObservations(currentBank);
const fullObs = observations.items?.find((o: any) => o.id === observation.id);
setFullObservation(fullObs || observation);
} catch (error) {
console.error("Failed to load observation:", error);
setFullObservation(observation); // Fall back to partial data
} finally {
setLoadingObservation(false);
}
};
const submitFeedback = async () => {
if (!currentBank || !feedback.trim()) return;
setFeedbackSubmitting(true);
try {
// Find existing "General Feedback" directive
const directives = await client.listDirectives(currentBank);
const existingDirective = directives.items?.find((d) => d.name === FEEDBACK_DIRECTIVE_NAME);
if (existingDirective) {
// Append to existing directive content
const newContent = existingDirective.content
? `${existingDirective.content}\n${feedback.trim()}`
: feedback.trim();
await client.updateDirective(currentBank, existingDirective.id, {
content: newContent,
});
} else {
// Create new directive
await client.createDirective(currentBank, {
name: FEEDBACK_DIRECTIVE_NAME,
content: feedback.trim(),
});
}
setFeedback("");
setFeedbackSubmitted(true);
setTimeout(() => setFeedbackSubmitted(false), 3000);
} catch (error) {
console.error("Error submitting feedback:", error);
alert("Error submitting feedback: " + (error as Error).message);
} finally {
setFeedbackSubmitting(false);
}
};
const runReflect = async () => {
if (!currentBank || !query) return;
setLoading(true);
setViewMode("answer");
try {
// Parse tags from comma-separated string
const parsedTags = tags
.split(",")
.map((t) => t.trim())
.filter((t) => t.length > 0);
const data: any = await client.reflect({
bank_id: currentBank,
query,
budget,
max_tokens: maxTokens,
include_facts: includeFacts,
include_tool_calls: includeToolCalls,
...(parsedTags.length > 0 && { tags: parsedTags, tags_match: tagsMatch }),
});
setResult(data);
} catch (error) {
console.error("Error running reflect:", error);
alert("Error running reflect: " + (error as Error).message);
} finally {
setLoading(false);
}
};
if (!currentBank) {
return (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Database className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">No Bank Selected</h3>
<p className="text-muted-foreground">Select a memory bank to start reflecting.</p>
</CardContent>
</Card>
);
}
return (
<div className="space-y-6">
{/* Query Input */}
<Card>
<CardContent className="pt-6">
<div className="flex gap-3">
<div className="flex-1 relative">
<Sparkles className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="What would you like to reflect on?"
className="pl-10 h-12 text-lg"
onKeyDown={(e) => e.key === "Enter" && runReflect()}
/>
</div>
<Button onClick={runReflect} disabled={loading || !query} className="h-12 px-8">
{loading ? "Reflecting..." : "Reflect"}
</Button>
</div>
{/* Filters */}
<div className="flex flex-wrap items-center gap-6 mt-4 pt-4 border-t">
{/* Budget */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Budget:</span>
<Select value={budget} onValueChange={(value: any) => setBudget(value)}>
<SelectTrigger className="w-24 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="mid">Mid</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
{/* Max Tokens */}
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Tokens:</span>
<Input
type="number"
value={maxTokens}
onChange={(e) => setMaxTokens(parseInt(e.target.value) || 4096)}
className="w-24 h-8"
/>
</div>
<div className="h-6 w-px bg-border" />
{/* Include options */}
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={includeFacts}
onCheckedChange={(c) => setIncludeFacts(c as boolean)}
/>
<span className="text-sm">Include Source</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={includeToolCalls}
onCheckedChange={(c) => setIncludeToolCalls(c as boolean)}
/>
<span className="text-sm">Include Tools</span>
</label>
</div>
</div>
{/* Tags Filter */}
<div className="flex items-center gap-4 mt-4 pt-4 border-t">
<Tag className="h-4 w-4 text-muted-foreground" />
<div className="flex-1 max-w-md">
<Input
type="text"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="Filter by tags (comma-separated)"
className="h-8"
/>
</div>
<Select value={tagsMatch} onValueChange={(v) => setTagsMatch(v as TagsMatch)}>
<SelectTrigger className="w-40 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="any">Any (incl. untagged)</SelectItem>
<SelectItem value="all">All (incl. untagged)</SelectItem>
<SelectItem value="any_strict">Any (strict)</SelectItem>
<SelectItem value="all_strict">All (strict)</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Loading State */}
{loading && (
<Card>
<CardContent className="flex flex-col items-center justify-center py-16">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mb-4" />
<p className="text-muted-foreground">Reflecting on memories...</p>
</CardContent>
</Card>
)}
{/* Results */}
{!loading && result && (
<div className="space-y-4">
{/* Summary Stats & Tabs */}
<div className="flex items-center gap-6 text-sm">
{result.usage && (
<>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Input tokens:</span>
<span className="font-semibold">
{result.usage.input_tokens?.toLocaleString()}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Output tokens:</span>
<span className="font-semibold">
{result.usage.output_tokens?.toLocaleString()}
</span>
</div>
</>
)}
{result.trace?.tool_calls && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Tool calls:</span>
<span className="font-semibold">{result.trace.tool_calls.length}</span>
<span className="text-muted-foreground">
(
{result.trace.tool_calls.reduce(
(sum: number, tc: any) => sum + tc.duration_ms,
0
)}
ms)
</span>
</div>
)}
{result.trace?.llm_calls && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground">LLM calls:</span>
<span className="font-semibold">{result.trace.llm_calls.length}</span>
<span className="text-muted-foreground">
(
{result.trace.llm_calls.reduce((sum: number, lc: any) => sum + lc.duration_ms, 0)}
ms)
</span>
</div>
)}
<div className="flex-1" />
{/* View Mode Tabs */}
<div className="flex gap-1 bg-muted p-1 rounded-lg">
{(["answer", "trace", "json"] as ViewMode[]).map((mode) => (
<button
key={mode}
onClick={() => setViewMode(mode)}
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
viewMode === mode
? "bg-background shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{mode === "answer" ? "Answer" : mode === "trace" ? "Trace" : "JSON"}
</button>
))}
</div>
</div>
{/* Answer View */}
{viewMode === "answer" && (
<div className="space-y-6">
{/* Main Answer */}
<Card>
<CardHeader>
<CardTitle>Answer</CardTitle>
</CardHeader>
<CardContent>
<div className="text-base leading-relaxed whitespace-pre-wrap">{result.text}</div>
</CardContent>
</Card>
{/* New Opinions Formed */}
{result.new_opinions && result.new_opinions.length > 0 && (
<Card className="border-green-200 dark:border-green-800">
<CardHeader className="bg-green-50 dark:bg-green-950">
<CardTitle className="flex items-center gap-2">
<Sparkles className="w-5 h-5" />
New Opinions Formed
</CardTitle>
<CardDescription>New beliefs generated from this interaction</CardDescription>
</CardHeader>
<CardContent className="pt-6">
<div className="space-y-3">
{result.new_opinions.map((opinion: any, i: number) => (
<div key={i} className="p-3 bg-muted rounded-lg border border-border">
<div className="font-semibold text-foreground">{opinion.text}</div>
<div className="text-sm text-muted-foreground mt-1">
Confidence: {opinion.confidence?.toFixed(2)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Directive */}
<Card className="border-blue-200 dark:border-blue-800">
<CardHeader className="py-4">
<CardTitle className="flex items-center gap-2 text-base">
<MessageSquare className="w-4 h-4" />
Add Directive
</CardTitle>
<CardDescription className="text-xs">
Hard rules injected into prompts that the agent must follow
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
{feedbackSubmitted ? (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<span className="text-lg">&#10003;</span>
<span className="text-sm font-medium">
Directive saved to {FEEDBACK_DIRECTIVE_NAME}
</span>
</div>
) : (
<div className="flex gap-3">
<Textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="e.g., Always respond in formal English..."
className="flex-1 min-h-[60px] resize-none"
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
submitFeedback();
}
}}
/>
<Button
onClick={submitFeedback}
disabled={feedbackSubmitting || !feedback.trim()}
className="self-end"
>
{feedbackSubmitting ? "Saving..." : "Save"}
</Button>
</div>
)}
</CardContent>
</Card>
</div>
)}
{/* Trace View - Split Layout */}
{viewMode === "trace" && (
<div className="space-y-4">
{/* Observations Created */}
{result.observations_created && result.observations_created.length > 0 && (
<Card className="border-emerald-200 dark:border-emerald-800">
<CardHeader className="bg-emerald-50 dark:bg-emerald-950 py-3">
<CardTitle className="flex items-center gap-2 text-base">
<Brain className="w-4 h-4 text-emerald-600" />
Observations Created ({result.observations_created.length})
</CardTitle>
<CardDescription className="text-xs">
New observations learned during this reflection
</CardDescription>
</CardHeader>
<CardContent className="pt-4">
<div className="space-y-2">
{result.observations_created.map((obs: any, i: number) => (
<div
key={i}
className="p-3 bg-emerald-50 dark:bg-emerald-950/50 rounded-lg border border-emerald-200 dark:border-emerald-800"
>
<div className="font-medium text-sm text-emerald-900 dark:text-emerald-100">
{obs.name}
</div>
<div className="text-xs text-emerald-700 dark:text-emerald-300 mt-1">
{obs.description}
</div>
<div className="text-[10px] text-muted-foreground mt-2 font-mono">
ID: {obs.id}
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: Execution Trace (LLM + Tool Calls) */}
<Card className="h-fit">
<CardHeader className="pb-3">
<CardTitle className="text-base">Execution Trace</CardTitle>
<CardDescription className="text-xs">
{result.iterations || 0} iteration
{(result.iterations || 0) !== 1 ? "s" : ""} {" "}
{(result.trace?.llm_calls?.reduce(
(sum: number, lc: any) => sum + lc.duration_ms,
0
) || 0) +
(result.trace?.tool_calls?.reduce(
(sum: number, tc: any) => sum + tc.duration_ms,
0
) || 0)}
ms total
</CardDescription>
</CardHeader>
<CardContent>
{!includeToolCalls ? (
<div className="flex items-start gap-3 p-3 bg-muted border border-border rounded-lg">
<Info className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-sm text-foreground">Not included</p>
<p className="text-xs text-muted-foreground mt-0.5">
Enable "Include Tool Calls" to see trace.
</p>
</div>
</div>
) : (result.trace?.llm_calls && result.trace.llm_calls.length > 0) ||
(result.trace?.tool_calls && result.trace.tool_calls.length > 0) ? (
<div className="max-h-[500px] overflow-y-auto pr-2">
{/* Build timeline: LLM -> Tools -> LLM -> Tools */}
{(() => {
const llmCalls = result.trace?.llm_calls || [];
const toolCalls = result.trace?.tool_calls || [];
// Build interleaved timeline
const timeline: Array<{
type: "llm" | "tools";
llm?: any;
tools?: any[];
iteration: number;
isFinal?: boolean;
}> = [];
llmCalls.forEach((lc: any, idx: number) => {
// Add tools for this iteration (using iteration field from tool trace)
const iterTools = toolCalls.filter(
(tc: any) => tc.iteration === idx + 1
);
// Determine if this is the final LLM call:
// - scope includes "final", OR
// - it's the last LLM call AND no tools were called after it
const isLastLLMCall = idx === llmCalls.length - 1;
const isFinal =
lc.scope.includes("final") ||
(isLastLLMCall && iterTools.length === 0);
const iterNum = isFinal ? llmCalls.length : idx + 1;
// Add LLM call
timeline.push({
type: "llm",
llm: lc,
iteration: iterNum,
isFinal,
});
if (iterTools.length > 0) {
timeline.push({
type: "tools",
tools: iterTools,
iteration: idx + 1,
});
}
});
return timeline.map((item, idx) => (
<div key={idx} className="relative">
{/* Timeline connector */}
{idx < timeline.length - 1 && (
<div className="absolute left-3 top-6 bottom-0 w-0.5 bg-border" />
)}
{item.type === "llm" ? (
// LLM Call
<div className="flex items-start gap-3 pb-3">
<div
className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${
item.isFinal
? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400"
: "bg-primary/10 text-primary"
}`}
>
{item.isFinal ? (
<Check className="w-3.5 h-3.5" strokeWidth={2.5} />
) : (
<span className="text-[10px] font-semibold">
{item.iteration}
</span>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">
{item.isFinal ? "Response generated" : "Agent decided"}
</span>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="w-3 h-3" />
{item.llm.duration_ms}ms
</span>
</div>
<span className="text-xs text-muted-foreground">
{item.isFinal ? "Final answer" : "Called tools below"}
</span>
</div>
</div>
) : (
// Tool Calls
<div className="flex items-start gap-3 pb-3">
<div className="w-6 h-6 rounded-full flex items-center justify-center bg-blue-500/15 text-blue-600 dark:text-blue-400 flex-shrink-0">
<Play className="w-3 h-3" fill="currentColor" />
</div>
<div className="flex-1 min-w-0 space-y-2">
<div className="text-xs text-muted-foreground">
Executing {item.tools?.length} tool
{item.tools?.length !== 1 ? "s" : ""}
</div>
{item.tools?.map((tc: any, tcIdx: number) => (
<div
key={tcIdx}
className="border border-border rounded-lg overflow-hidden"
>
<div className="flex items-center justify-between px-3 py-1.5 bg-muted/50">
<span className="font-medium text-sm text-foreground">
{tc.tool}
</span>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="w-3 h-3" />
{tc.duration_ms}ms
</span>
</div>
<div className="p-2 space-y-2">
<div>
<p className="text-[10px] font-semibold text-muted-foreground mb-1">
Input:
</p>
<div className="bg-muted p-1.5 rounded text-xs overflow-auto max-h-32">
<JsonView
src={tc.input}
collapsed={1}
theme="default"
/>
</div>
</div>
{tc.output && (
<div>
<p className="text-[10px] font-semibold text-muted-foreground mb-1">
Output:
</p>
<div className="bg-muted p-1.5 rounded text-xs overflow-auto max-h-32">
<JsonView
src={tc.output}
collapsed={1}
theme="default"
/>
</div>
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
));
})()}
</div>
) : (
<div className="flex items-start gap-3 p-3 bg-muted border border-border rounded-lg">
<Info className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-sm text-foreground">No operations</p>
<p className="text-xs text-muted-foreground mt-0.5">
No LLM or tool calls were made during this reflection.
</p>
</div>
</div>
)}
</CardContent>
</Card>
{/* Right: Based On Facts */}
<Card className="h-fit">
<CardHeader className="pb-3">
<CardTitle className="text-base">Based On</CardTitle>
<CardDescription className="text-xs">
{(result.based_on?.memories?.length || 0) +
(result.based_on?.observations?.filter(
(o: any) => o.subtype !== "directive"
)?.length || 0) +
(result.based_on?.directives?.length || 0)}{" "}
items used
</CardDescription>
</CardHeader>
<CardContent>
{!includeFacts ? (
<div className="flex items-start gap-3 p-3 bg-muted border border-border rounded-lg">
<Info className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-sm text-foreground">Not included</p>
<p className="text-xs text-muted-foreground mt-0.5">
Enable "Include Source" to see memories.
</p>
</div>
</div>
) : (result.based_on?.memories && result.based_on.memories.length > 0) ||
(result.based_on?.mental_models &&
result.based_on.mental_models.length > 0) ||
(result.based_on?.directives && result.based_on.directives.length > 0) ||
(result.based_on?.observations && result.based_on.observations.length > 0) ? (
(() => {
const memories = result.based_on?.memories || [];
const worldFacts = memories.filter((f: any) => f.type === "world");
const experienceFacts = memories.filter(
(f: any) => f.type === "experience"
);
// Mental models are in based_on.mental_models
const mentalModelFacts = result.based_on?.mental_models || [];
const observations = (result.based_on?.observations || []).filter(
(o: any) => o.subtype !== "directive"
);
// Directives are in based_on.directives
const directives = result.based_on?.directives || [];
// Build tabs array with all categories
const tabs: { id: BasedOnTab; label: string; count: number }[] = [
{ id: "directives", label: "Directives", count: directives.length },
{
id: "mental_models",
label: "Mental Models",
count: mentalModelFacts.length,
},
{ id: "observations", label: "Observations", count: observations.length },
{ id: "world", label: "World", count: worldFacts.length },
{ id: "experience", label: "Experience", count: experienceFacts.length },
];
const currentTab = activeBasedOnTab;
const getCurrentFacts = () => {
switch (currentTab) {
case "directives":
return directives;
case "mental_models":
return mentalModelFacts;
case "observations":
return observations;
case "world":
return worldFacts;
case "experience":
return experienceFacts;
default:
return [];
}
};
const currentFacts = getCurrentFacts();
return (
<div>
{/* Tabs */}
<div className="flex items-center gap-1 bg-muted rounded-lg p-1 mb-4">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveBasedOnTab(tab.id)}
className={`flex-1 px-3 py-1.5 rounded-md text-sm font-medium transition-all ${
currentTab === tab.id
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab.label} ({tab.count})
</button>
))}
</div>
{/* Tab Content */}
{currentFacts.length > 0 ? (
<div className="max-h-[400px] overflow-y-auto pr-2 space-y-3">
{currentFacts.map((item: any, i: number) => (
<div
key={item.id || i}
className={`p-4 bg-muted/50 rounded-lg border border-border/50 ${
currentTab !== "directives"
? "cursor-pointer hover:bg-muted/80 transition-colors"
: ""
}`}
onClick={() => {
if (currentTab === "directives") return; // Not clickable
if (currentTab === "observations")
handleSelectObservation(item);
else if (currentTab === "mental_models")
setSelectedMentalModelId(item.id);
else setSelectedMemoryId(item.id);
}}
>
{currentTab === "directives" ? (
<>
<div className="font-medium text-sm">{item.name}</div>
{item.content && (
<p className="mt-1 text-xs text-muted-foreground line-clamp-2">
{item.content}
</p>
)}
</>
) : currentTab === "observations" ? (
<div className="font-medium text-sm">{item.name}</div>
) : currentTab === "mental_models" ? (
(() => {
const colonIdx = item.text?.indexOf(": ") ?? -1;
const name =
colonIdx > 0 ? item.text.slice(0, colonIdx) : item.id;
return (
<>
<div className="font-medium text-sm">{name}</div>
<code className="text-xs font-mono text-muted-foreground">
{item.id}
</code>
</>
);
})()
) : (
<>
<p className="text-sm text-foreground leading-relaxed">
{item.text}
</p>
{item.context && (
<div className="text-xs text-muted-foreground mt-2">
{item.context}
</div>
)}
</>
)}
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-4">
No {currentTab} items
</p>
)}
</div>
);
})()
) : (
<div className="flex items-start gap-3 p-3 bg-amber-50 dark:bg-amber-950 border border-amber-200 dark:border-amber-800 rounded-lg">
<Info className="w-4 h-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-sm text-amber-900 dark:text-amber-100">
No facts found
</p>
<p className="text-xs text-amber-700 dark:text-amber-300 mt-0.5">
No memories were used to generate this answer.
</p>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
)}
{/* JSON View */}
{viewMode === "json" && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Raw Response</CardTitle>
</CardHeader>
<CardContent>
<div className="bg-muted p-4 rounded-lg overflow-auto max-h-[600px]">
<JsonView src={result} collapsed={2} theme="default" />
</div>
</CardContent>
</Card>
)}
</div>
)}
{/* Empty State */}
{!loading && !result && (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Sparkles className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Ready to Reflect</h3>
<p className="text-muted-foreground text-center max-w-md">
Enter a question above to query the memory bank and generate a disposition-aware
response.
</p>
</CardContent>
</Card>
)}
{/* Memory Detail Modal */}
<MemoryDetailModal memoryId={selectedMemoryId} onClose={() => setSelectedMemoryId(null)} />
{/* Directive Detail Panel */}
{selectedDirective && (
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l shadow-2xl z-50 overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5" />
<h2 className="text-lg font-semibold">Directive</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => {
setSelectedDirective(null);
setFullDirective(null);
}}
>
<X className="w-4 h-4" />
</Button>
</div>
{loadingDirective ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Name</h3>
<p className="mt-1 font-medium">
{fullDirective?.name || selectedDirective.name}
</p>
</div>
{fullDirective?.description && (
<div>
<h3 className="text-sm font-medium text-muted-foreground">Description</h3>
<p className="mt-1 text-sm">{fullDirective.description}</p>
</div>
)}
{fullDirective?.tags && fullDirective.tags.length > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-1">Tags</h3>
<div className="flex flex-wrap gap-1">
{fullDirective.tags.map((tag: string) => (
<span
key={tag}
className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground flex items-center gap-1"
>
<Tag className="w-2.5 h-2.5" />
{tag}
</span>
))}
</div>
</div>
)}
{/* Show content from directive */}
{(fullDirective?.content || selectedDirective.content) && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">Content</h3>
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground whitespace-pre-wrap">
{fullDirective?.content || selectedDirective.content}
</div>
</div>
</div>
)}
<div className="pt-2 border-t">
<h3 className="text-sm font-medium text-muted-foreground">ID</h3>
<p className="mt-1 font-mono text-xs text-muted-foreground">
{selectedDirective.id}
</p>
</div>
</div>
)}
</div>
</div>
)}
{/* Observation Detail Panel */}
{selectedObservation && (
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l shadow-2xl z-50 overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Brain className="w-5 h-5" />
<h2 className="text-lg font-semibold">Observation</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => {
setSelectedObservation(null);
setFullObservation(null);
}}
>
<X className="w-4 h-4" />
</Button>
</div>
{loadingObservation ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Text</h3>
<p className="mt-1 font-medium">
{fullObservation?.text || selectedObservation.text}
</p>
</div>
{fullObservation?.tags && fullObservation.tags.length > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-1">Tags</h3>
<div className="flex flex-wrap gap-1">
{fullObservation.tags.map((tag: string) => (
<span
key={tag}
className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground flex items-center gap-1"
>
<Tag className="w-2.5 h-2.5" />
{tag}
</span>
))}
</div>
</div>
)}
{fullObservation?.source_memories && fullObservation.source_memories.length > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">
Source Memories ({fullObservation.source_memories.length})
</h3>
<div className="space-y-2">
{fullObservation.source_memories.map((mem: any, i: number) => (
<div key={i} className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground whitespace-pre-wrap">
{mem.text || (typeof mem === "string" ? mem : "")}
</div>
</div>
))}
</div>
</div>
)}
<div className="pt-2 border-t">
<h3 className="text-sm font-medium text-muted-foreground">ID</h3>
<p className="mt-1 font-mono text-xs text-muted-foreground">
{selectedObservation.id}
</p>
</div>
</div>
)}
</div>
</div>
)}
{/* Mental Model Detail Modal */}
<MentalModelDetailModal
mentalModelId={selectedMentalModelId}
onClose={() => setSelectedMentalModelId(null)}
/>
</div>
);
}