mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 12:02:49 +05:30
feat(gui): Added handoff llm call log details to gui
This commit is contained in:
231
apps/gui/src/components/play/HandoffModal.tsx
Normal file
231
apps/gui/src/components/play/HandoffModal.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { SimSnapshot } from "@/lib/simulation-types";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface HandoffModalProps {
|
||||
entry: SimSnapshot["log"][number];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function HandoffModal({ entry, onClose }: HandoffModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<"chunks" | "prompt" | "output">(
|
||||
"chunks",
|
||||
);
|
||||
|
||||
const handoffResult = entry.handoffResult;
|
||||
const chunks = handoffResult?.chunks || [];
|
||||
|
||||
const getImportanceColor = (score: number) => {
|
||||
if (score >= 8)
|
||||
return "bg-destructive/10 text-destructive border-destructive/30";
|
||||
if (score >= 5) return "bg-amber-500/10 text-amber-500 border-amber-500/30";
|
||||
return "bg-emerald-500/10 text-emerald-500 border-emerald-500/30";
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-[800px] sm:max-w-[800px] h-[85vh] overflow-hidden flex flex-col p-0 gap-0 border-2">
|
||||
<DialogHeader className="px-6 pt-5 pb-4 border-b">
|
||||
<DialogTitle className="text-lg font-head tracking-wide text-primary flex items-center justify-between">
|
||||
<span>Memory Handoff Details — {entry.entityName}</span>
|
||||
{entry.usage && (
|
||||
<span className="text-xs font-mono font-normal text-muted-foreground">
|
||||
{entry.usage.modelName || "Handoff Model"}
|
||||
</span>
|
||||
)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Custom Tab Switcher */}
|
||||
<div className="flex border-b bg-muted/20 px-6 py-2 gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab("chunks")}
|
||||
className={`px-3 py-1.5 text-xs font-medium border transition-all duration-100 ${
|
||||
activeTab === "chunks"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[1px_1px_0_0_var(--primary)]"
|
||||
: "border-transparent hover:bg-secondary text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
Promoted Chunks ({chunks.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("prompt")}
|
||||
className={`px-3 py-1.5 text-xs font-medium border transition-all duration-100 ${
|
||||
activeTab === "prompt"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[1px_1px_0_0_var(--primary)]"
|
||||
: "border-transparent hover:bg-secondary text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
Raw LLM Prompt
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("output")}
|
||||
className={`px-3 py-1.5 text-xs font-medium border transition-all duration-100 ${
|
||||
activeTab === "output"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[1px_1px_0_0_var(--primary)]"
|
||||
: "border-transparent hover:bg-secondary text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
Raw JSON Output
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto flex-1 p-6 space-y-4">
|
||||
{activeTab === "chunks" && (
|
||||
<div className="space-y-4">
|
||||
{entry.usage && (
|
||||
<div className="grid grid-cols-3 gap-4 border border-dotted border-border/20 p-3 bg-secondary/10 rounded text-xs font-mono">
|
||||
<div>
|
||||
<span className="text-muted-foreground block uppercase tracking-wider text-[10px]">
|
||||
Input Tokens
|
||||
</span>
|
||||
<strong className="text-foreground">
|
||||
{entry.usage.inputTokens}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground block uppercase tracking-wider text-[10px]">
|
||||
Output Tokens
|
||||
</span>
|
||||
<strong className="text-foreground">
|
||||
{entry.usage.outputTokens}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground block uppercase tracking-wider text-[10px]">
|
||||
Total Tokens
|
||||
</span>
|
||||
<strong className="text-foreground">
|
||||
{entry.usage.totalTokens}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{chunks.length === 0 ? (
|
||||
<div className="border border-dotted border-border/30 p-8 text-center bg-card text-muted-foreground rounded">
|
||||
<p className="text-sm">
|
||||
No memories were promoted to the Ledger during this turn.
|
||||
</p>
|
||||
<p className="text-xs mt-1">
|
||||
All working memory buffer entries were summarized or
|
||||
forgotten.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
|
||||
Ledger Additions
|
||||
</h3>
|
||||
{chunks.map((chunk: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-border/30 bg-card p-4 shadow-[2px_2px_0_0_var(--border)] relative flex flex-col gap-3"
|
||||
>
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1 text-sm text-foreground/90 leading-relaxed font-sans">
|
||||
{chunk.content}
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`font-mono text-xs ${getImportanceColor(chunk.importance)}`}
|
||||
>
|
||||
Importance: {chunk.importance}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{chunk.quotes && chunk.quotes.length > 0 && (
|
||||
<div className="bg-secondary/10 border-l-2 border-primary/50 p-2.5 my-1 text-xs italic text-muted-foreground space-y-1">
|
||||
{chunk.quotes.map((quote: string, qIdx: number) => (
|
||||
<div key={qIdx}>“{quote}”</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs pt-2 border-t border-dotted border-border/10">
|
||||
{chunk.retainInBuffer ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-primary/5 text-primary border-primary/20 text-[10px] font-mono"
|
||||
>
|
||||
Pinned in Buffer
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-muted text-muted-foreground border-border/20 text-[10px] font-mono"
|
||||
>
|
||||
Pruned from Buffer
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{chunk.involvedEntityIds &&
|
||||
chunk.involvedEntityIds.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 ml-auto text-[10px] font-mono text-muted-foreground">
|
||||
<span>Entities:</span>
|
||||
{chunk.involvedEntityIds.map((entId: string) => (
|
||||
<Badge
|
||||
key={entId}
|
||||
variant="outline"
|
||||
className="text-[10px] px-1 py-0 border-border/20 font-mono"
|
||||
>
|
||||
{entId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "prompt" && entry.rawPrompt && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono mb-2">
|
||||
System Prompt
|
||||
</h4>
|
||||
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[250px] overflow-y-auto">
|
||||
{entry.rawPrompt.systemPrompt}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono mb-2">
|
||||
User Context (Candidates)
|
||||
</h4>
|
||||
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[350px] overflow-y-auto font-sans leading-relaxed font-mono">
|
||||
{entry.rawPrompt.userContext}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "output" && (
|
||||
<div className="space-y-2 h-full flex flex-col">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
|
||||
Raw JSON Output
|
||||
</h4>
|
||||
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border flex-1 overflow-y-auto max-h-[500px]">
|
||||
{handoffResult
|
||||
? JSON.stringify(handoffResult, null, 2)
|
||||
: "No JSON Output recorded."}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,13 @@ import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { PromptModal } from "./PromptModal";
|
||||
import { HandoffModal } from "./HandoffModal";
|
||||
import {
|
||||
Alert,
|
||||
AlertAction,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import {
|
||||
@@ -163,6 +170,9 @@ export function PlayView() {
|
||||
const [selectedEntryForModal, setSelectedEntryForModal] = useState<
|
||||
SimSnapshot["log"][number] | null
|
||||
>(null);
|
||||
const [selectedHandoffForModal, setSelectedHandoffForModal] = useState<
|
||||
SimSnapshot["log"][number] | null
|
||||
>(null);
|
||||
|
||||
const logEndRef = useRef<HTMLDivElement>(null);
|
||||
const steppingRef = useRef(false);
|
||||
@@ -490,14 +500,43 @@ export function PlayView() {
|
||||
const playerEntity = snapshot.entities.find(
|
||||
(e) => e.isPlayer,
|
||||
);
|
||||
return snapshot.log.map((entry, i) => (
|
||||
<LogEntryCard
|
||||
key={i}
|
||||
entry={entry}
|
||||
onShowPrompt={setSelectedEntryForModal}
|
||||
isPlayerCard={entry.entityId === playerEntity?.id}
|
||||
/>
|
||||
));
|
||||
return snapshot.log.map((entry, i) => {
|
||||
if (entry.isHandoff) {
|
||||
return (
|
||||
<Alert
|
||||
key={i}
|
||||
className="max-w-md border-dashed bg-secondary/10"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<AlertTitle>
|
||||
Handoff triggered for {entry.entityName}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Memories were transferred from Buffer to Memory
|
||||
Ledger
|
||||
</AlertDescription>
|
||||
</div>
|
||||
<AlertAction>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="default"
|
||||
onClick={() => setSelectedHandoffForModal(entry)}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
</AlertAction>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LogEntryCard
|
||||
key={i}
|
||||
entry={entry}
|
||||
onShowPrompt={setSelectedEntryForModal}
|
||||
isPlayerCard={entry.entityId === playerEntity?.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
{loading && (
|
||||
<div className="flex items-center gap-2 text-sm italic text-muted-foreground p-2 font-mono">
|
||||
@@ -662,6 +701,13 @@ export function PlayView() {
|
||||
onClose={() => setSelectedEntryForModal(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedHandoffForModal && (
|
||||
<HandoffModal
|
||||
entry={selectedHandoffForModal}
|
||||
onClose={() => setSelectedHandoffForModal(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
62
apps/gui/src/components/ui/alert.tsx
Normal file
62
apps/gui/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(
|
||||
"relative w-full rounded border border-border/30 bg-card p-4 text-sm shadow-[2px_2px_0_0_var(--border)] flex flex-col md:flex-row md:items-center gap-3 justify-between",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"font-head font-bold leading-none tracking-tight text-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-xs text-muted-foreground mt-1 flex-1 leading-relaxed md:mt-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
const AlertAction = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("shrink-0 flex items-center mt-2 md:mt-0 md:ml-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertAction.displayName = "AlertAction";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
||||
@@ -22,6 +22,7 @@ const buttonVariants = cva(
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 px-3 text-xs",
|
||||
xs: "h-7 px-2.5 text-xs",
|
||||
lg: "h-11 px-8 text-base",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface LogEntry {
|
||||
narrativeProse: string;
|
||||
intents: IntentInfo[];
|
||||
timestamp: string;
|
||||
isHandoff?: boolean;
|
||||
handoffResult?: any;
|
||||
rawPrompt?: {
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
|
||||
@@ -33,11 +33,36 @@ export async function runHandoffResolution(session: SimSession): Promise<void> {
|
||||
maxContext,
|
||||
);
|
||||
if (trigger !== "none") {
|
||||
await handoffEngine.runHandoff(
|
||||
const ran = await handoffEngine.runHandoff(
|
||||
entity,
|
||||
bufferEntries,
|
||||
worldState.clock.get(),
|
||||
);
|
||||
if (ran) {
|
||||
const lastCall =
|
||||
session.handoffProvider.lastCalls?.[
|
||||
(session.handoffProvider.lastCalls?.length || 0) - 1
|
||||
];
|
||||
const info = session.entities.find((e) => e.id === entity.id);
|
||||
const entityName = info?.name || entity.id;
|
||||
session.log.push({
|
||||
turn: session.turn,
|
||||
entityId: entity.id,
|
||||
entityName,
|
||||
narrativeProse: `Handoff triggered for ${entityName}: memories were transferred from Buffer to Memory Ledger`,
|
||||
intents: [],
|
||||
timestamp: worldState.clock.get().toISOString(),
|
||||
isHandoff: true,
|
||||
rawPrompt: lastCall
|
||||
? {
|
||||
systemPrompt: lastCall.systemPrompt,
|
||||
userContext: lastCall.userContext,
|
||||
}
|
||||
: undefined,
|
||||
usage: lastCall?.usage,
|
||||
handoffResult: lastCall?.response,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user