minor(gui): A lot of UI Improvements

This commit is contained in:
2026-07-12 12:02:52 +05:30
parent 607a68fdb5
commit 01b2e650f3
8 changed files with 520 additions and 270 deletions

View File

@@ -39,7 +39,9 @@ export function ConfigView() {
const [config, setConfig] = useState<ConfigStatus | null>(null);
const [instances, setInstances] = useState<ModelProviderInstance[]>([]);
const [mappings, setMappings] = useState<Record<string, string>>({});
const [availableProviders, setAvailableProviders] = useState<ModelProviderMeta[]>([]);
const [availableProviders, setAvailableProviders] = useState<
ModelProviderMeta[]
>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
@@ -74,10 +76,13 @@ export function ConfigView() {
loadAll();
}, [loadAll]);
const handleUpdateMapping = async (task: string, providerInstanceId: string) => {
const handleUpdateMapping = async (
task: string,
providerInstanceId: string,
) => {
if (task === "embeddings" && mappings[task] !== providerInstanceId) {
const confirmChange = window.confirm(
"Changing the embeddings provider will delete all existing embeddings and regenerate them from scratch. Are you sure you want to do this?"
"Changing the embeddings provider will delete all existing embeddings and regenerate them from scratch. Are you sure you want to do this?",
);
if (!confirmChange) return;
}
@@ -98,9 +103,17 @@ export function ConfigView() {
return (
<div className="mx-auto max-w-[800px] px-10 py-12">
<h1 className="mb-6 text-headline-lg text-primary animate-fade-in">Configuration</h1>
{config === null && loading && <p className="text-body-md text-muted-foreground">Loading configuration...</p>}
<h1 className="mb-6 text-headline-lg text-primary animate-fade-in">
Configuration
</h1>
<h2 className="mb-3 text-headline-md text-foreground">
Manage Model Instances
</h2>
{config === null && loading && (
<p className="text-body-md text-muted-foreground">
Loading configuration...
</p>
)}
{error && (
<div className="mb-4 border border-destructive bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
@@ -108,7 +121,13 @@ export function ConfigView() {
)}
{config && (
<div className={loading ? "opacity-60 pointer-events-none transition-opacity duration-200" : "transition-opacity duration-200"}>
<div
className={
loading
? "opacity-60 pointer-events-none transition-opacity duration-200"
: "transition-opacity duration-200"
}
>
<ProviderInstancesConfig
instances={instances}
availableProviders={availableProviders}
@@ -120,7 +139,9 @@ export function ConfigView() {
/>
<section className="border-b border-dotted border-border/20 mb-8 pb-8">
<h2 className="mb-3 text-headline-md text-foreground">Task Provider Routing</h2>
<h2 className="mb-3 text-headline-md text-foreground">
Task Provider Routing
</h2>
<p className="my-4 border border-border/20 bg-secondary px-3 py-2 text-label-sm text-foreground/80">
Configure which LLM Provider Key Instance should handle each
specific simulation task. Mappings default to the currently{" "}
@@ -128,12 +149,42 @@ export function ConfigView() {
</p>
<div className="mt-4 grid grid-cols-1 gap-6 md:grid-cols-2">
{[
{ key: "actor-prose", label: "Actor Prose Generation", desc: "Generates roleplay/narrative prose for Non-Player Characters.", type: "generative" },
{ key: "llm-validator", label: "LLM Validator", desc: "Arbitrates and validates proposed actions against the world state rules.", type: "generative" },
{ key: "intent-decoder", label: "Intent Decoder", desc: "Splits raw prose actions into structured intents (Player and NPC).", type: "generative" },
{ key: "timedelta", label: "TimeDelta Generator", desc: "Calculates the duration of character actions to advance the game clock.", type: "generative" },
{ key: "handoff", label: "Memory Handoff Engine", desc: "Promotes entities' working memories to the long-term Ledger via LLM summarization and pruning.", type: "generative" },
{ key: "embeddings", label: "Text Embeddings Generator", desc: "Generates vector embeddings for long-term memory retrieval.", type: "embedding" },
{
key: "actor-prose",
label: "Actor Prose Generation",
desc: "Generates roleplay/narrative prose for Non-Player Characters.",
type: "generative",
},
{
key: "llm-validator",
label: "LLM Validator",
desc: "Arbitrates and validates proposed actions against the world state rules.",
type: "generative",
},
{
key: "intent-decoder",
label: "Intent Decoder",
desc: "Splits raw prose actions into structured intents (Player and NPC).",
type: "generative",
},
{
key: "timedelta",
label: "TimeDelta Generator",
desc: "Calculates the duration of character actions to advance the game clock.",
type: "generative",
},
{
key: "handoff",
label: "Memory Handoff Engine",
desc: "Promotes entities' working memories to the long-term Ledger via LLM summarization and pruning.",
type: "generative",
},
{
key: "embeddings",
label: "Text Embeddings Generator",
desc: "Generates vector embeddings for long-term memory retrieval.",
type: "embedding",
},
].map((task) => (
<div
key={task.key}
@@ -143,7 +194,9 @@ export function ConfigView() {
<strong className="text-body-md text-foreground">
{task.label}
</strong>
<span className="mt-0.5 text-xs text-muted-foreground">{task.desc}</span>
<span className="mt-0.5 text-xs text-muted-foreground">
{task.desc}
</span>
</div>
<Select
value={mappings[task.key] || ""}
@@ -152,16 +205,30 @@ export function ConfigView() {
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="-- Use Active Key (Default) --" />
<SelectValue placeholder="-- Use Active Key (Default) --">
{(() => {
const inst = instances.find(
(i) => i.id === mappings[task.key],
);
return inst
? `${inst.name} (${inst.providerName})${inst.isActive ? " [Active]" : ""}`
: null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="">-- Use Active Key (Default) --</SelectItem>
<SelectItem value="">
-- Use Active Key (Default) --
</SelectItem>
{instances
.filter((inst) => (inst.type || "generative") === task.type)
.filter(
(inst) => (inst.type || "generative") === task.type,
)
.map((inst) => (
<SelectItem key={inst.id} value={inst.id}>
{inst.name} ({inst.providerName}){inst.isActive ? " [Active]" : ""}
{inst.name} ({inst.providerName})
{inst.isActive ? " [Active]" : ""}
</SelectItem>
))}
</SelectGroup>
@@ -173,7 +240,9 @@ export function ConfigView() {
</section>
<section className="mb-8">
<h2 className="mb-3 text-headline-md text-foreground">Available Scenarios</h2>
<h2 className="mb-3 text-headline-md text-foreground">
Available Scenarios
</h2>
{config.availableScenarios.length === 0 ? (
<p className="mt-3 border border-accent bg-accent/25 px-3 py-2 text-label-sm text-foreground/80">
No scenarios found in{" "}

View File

@@ -26,7 +26,6 @@ import {
Card,
CardHeader,
CardContent,
CardFooter,
CardTitle,
CardAction,
} from "@/components/ui/card";
@@ -237,7 +236,6 @@ export function ProviderInstancesConfig({
return (
<section className="mb-8">
<h2 className="mb-3 text-lg">LLM Provider Instances</h2>
{error && (
<div className="mb-4 rounded border-2 border-red-500 bg-red-50 px-3 py-2 text-sm text-red-700">
{error}
@@ -281,11 +279,12 @@ export function ProviderInstancesConfig({
>
<ItemContent>
<ItemTitle>{inst.name}</ItemTitle>
<ItemDescription>
{inst.providerName} ({inst.type || "generative"})
</ItemDescription>
<ItemDescription>{inst.providerName}</ItemDescription>
<div className="flex flex-row gap-1.5">
{inst.isActive && <Badge>Active</Badge>}
<Badge variant="outline">{inst.type === "generative" ? "gen" : "embed"}</Badge>
</div>
</ItemContent>
{inst.isActive && <Badge>Active</Badge>}
</Item>
))}
</ItemGroup>
@@ -335,7 +334,7 @@ export function ProviderInstancesConfig({
}
items={[
{
label: "Generative (Chat / Text Completion)",
label: "Generative (Text Completion)",
value: "generative",
},
{
@@ -373,7 +372,10 @@ export function ProviderInstancesConfig({
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent align="end" className="min-w-[var(--anchor-width)]">
<SelectContent
align="end"
className="min-w-[var(--anchor-width)]"
>
<SelectGroup>
{availableProviders.map((p) => (
<SelectItem key={p.id} value={p.id}>
@@ -438,7 +440,7 @@ export function ProviderInstancesConfig({
</div>
)}
<div className="mb-4 flex flex-row items-center gap-2">
<div className="flex flex-row items-center gap-2">
<Checkbox
id="formActive"
checked={editIsActive}
@@ -448,25 +450,25 @@ export function ProviderInstancesConfig({
Set as Active Instance
</Label>
</div>
</CardContent>
<CardFooter className="justify-between">
<div>
{selectedInstanceId !== "new" && (
<Button
type="button"
variant="destructive"
onClick={handleDelete}
disabled={loading}
>
Delete
</Button>
)}
<div className="flex flex-row items-center justify-between gap-2">
<div>
{selectedInstanceId !== "new" && (
<Button
type="button"
variant="destructive"
onClick={handleDelete}
disabled={loading}
>
Delete
</Button>
)}
</div>
<Button type="submit" disabled={loading}>
{loading ? "Saving..." : "Save"}
</Button>
</div>
<Button type="submit" disabled={loading}>
{loading ? "Saving..." : "Save"}
</Button>
</CardFooter>
</CardContent>
</form>
)}
</Card>

View File

@@ -130,6 +130,11 @@ export function PlayView() {
const logEndRef = useRef<HTMLDivElement>(null);
const steppingRef = useRef(false);
const pauseRequestedRef = useRef(false);
const snapshotRef = useRef<SimSnapshot | null>(null);
useEffect(() => {
snapshotRef.current = snapshot;
}, [snapshot]);
const scrollToBottom = useCallback(() => {
setTimeout(
@@ -151,7 +156,7 @@ export function PlayView() {
pauseRequestedRef.current = false;
try {
let current = snapshot;
let current = snapshotRef.current;
while (true) {
if (pauseRequestedRef.current) {
break;
@@ -190,7 +195,7 @@ export function PlayView() {
setStatusText("");
}
},
[snapshot],
[],
);
const handleResume = useCallback(async (id: string) => {

View File

@@ -8,13 +8,14 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/components/ui/accordion";
import { PromptSwitcher } from "@/components/play/PromptSwitcher";
interface PromptModalProps {
entry: SimSnapshot["log"][number];
@@ -24,41 +25,57 @@ interface PromptModalProps {
export function PromptModal({ entry, onClose }: PromptModalProps) {
const [activeTab, setActiveTab] = useState<"actor" | "decoder">("actor");
const parseActorPrompt = (systemPrompt: string, userContext: string, inputTokens: number) => {
const memoryHeader = "=== YOUR RECENT MEMORY ===";
const idx = userContext.indexOf(memoryHeader);
const parseActorPrompt = (
systemPrompt: string,
userContext: string,
inputTokens: number,
) => {
const recentHeader = "=== RECENT EVENTS ===";
const ledgerHeader = "=== YOUR MEMORIES ===";
const recentIdx = userContext.indexOf(recentHeader);
let worldStr = userContext;
let memStr = "";
let recentStr = "";
let ledgerStr = "";
if (idx !== -1) {
worldStr = userContext.substring(0, idx).trim();
memStr = userContext.substring(idx).trim();
if (recentIdx !== -1) {
worldStr = userContext.substring(0, recentIdx).trim();
const rest = userContext.substring(recentIdx).trim();
const ledgerIdx = rest.indexOf(ledgerHeader);
if (ledgerIdx !== -1) {
recentStr = rest.substring(0, ledgerIdx).trim();
ledgerStr = rest.substring(ledgerIdx).trim();
} else {
recentStr = rest;
}
}
const sysLen = systemPrompt.length;
const worldLen = worldStr.length;
const memLen = memStr.length;
const totalLen = sysLen + worldLen + memLen;
const sections: { label: string; type: string; content: string }[] = [
{ label: "System Prompt", type: "system", content: systemPrompt },
{ label: "World Info", type: "world", content: worldStr },
{ label: "Recent Events", type: "events", content: recentStr || "(No recent events.)" },
{ label: "Long-Term Memories", type: "memories", content: ledgerStr || "(No long-term memories.)" },
];
const totalLen = sections.reduce((sum, s) => sum + s.content.length, 0);
if (totalLen === 0) return null;
const sysPct = (sysLen / totalLen) * 100;
const worldPct = (worldLen / totalLen) * 100;
const memPct = (memLen / totalLen) * 100;
const sysTokens = Math.round((sysLen / totalLen) * inputTokens);
const worldTokens = Math.round((worldLen / totalLen) * inputTokens);
const memTokens = Math.max(0, inputTokens - sysTokens - worldTokens);
return [
{ label: "System Prompt", pct: sysPct, relativePct: sysPct, tokens: sysTokens, type: "system", content: systemPrompt },
{ label: "World Info", pct: worldPct, relativePct: worldPct, tokens: worldTokens, type: "world", content: worldStr },
{ label: "Recent Memories", pct: memPct, relativePct: memPct, tokens: memTokens, type: "memories", content: memStr || "(No memories yet.)" },
];
return sections.map((s) => {
const pct = (s.content.length / totalLen) * 100;
return {
...s,
pct,
relativePct: pct,
tokens: Math.round((s.content.length / totalLen) * inputTokens),
};
});
};
const parseDecoderPrompt = (systemPrompt: string, userContext: string, inputTokens: number) => {
const parseDecoderPrompt = (
systemPrompt: string,
userContext: string,
inputTokens: number,
) => {
const proseHeader = "=== NARRATIVE PROSE ===";
const idx = userContext.indexOf(proseHeader);
@@ -86,34 +103,84 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
const proseTokens = Math.max(0, inputTokens - sysTokens - worldTokens);
return [
{ label: "System Prompt", pct: sysPct, relativePct: sysPct, tokens: sysTokens, type: "system", content: systemPrompt },
{ label: "Decoder Context", pct: worldPct, relativePct: worldPct, tokens: worldTokens, type: "world", content: worldStr },
{ label: "Narrative Prose", pct: prosePct, relativePct: prosePct, tokens: proseTokens, type: "memories", content: proseStr },
{
label: "System Prompt",
pct: sysPct,
relativePct: sysPct,
tokens: sysTokens,
type: "system",
content: systemPrompt,
},
{
label: "Decoder Context",
pct: worldPct,
relativePct: worldPct,
tokens: worldTokens,
type: "world",
content: worldStr,
},
{
label: "Narrative Prose",
pct: prosePct,
relativePct: prosePct,
tokens: proseTokens,
type: "memories",
content: proseStr,
},
];
};
const actorBreakdown = (entry.rawPrompt && entry.usage) ? parseActorPrompt(entry.rawPrompt.systemPrompt, entry.rawPrompt.userContext, entry.usage.inputTokens) : null;
const decoderBreakdown = (entry.decoderPrompt && entry.decoderUsage) ? parseDecoderPrompt(entry.decoderPrompt.systemPrompt, entry.decoderPrompt.userContext, entry.decoderUsage.inputTokens) : null;
const actorBreakdown =
entry.rawPrompt && entry.usage
? parseActorPrompt(
entry.rawPrompt.systemPrompt,
entry.rawPrompt.userContext,
entry.usage.inputTokens,
)
: null;
const decoderBreakdown =
entry.decoderPrompt && entry.decoderUsage
? parseDecoderPrompt(
entry.decoderPrompt.systemPrompt,
entry.decoderPrompt.userContext,
entry.decoderUsage.inputTokens,
)
: null;
const actorMaxContext = entry.usage?.maxContext !== undefined ? entry.usage.maxContext : 32768;
const actorMaxContext =
entry.usage?.maxContext !== undefined ? entry.usage.maxContext : 32768;
const actorUsedTokens = entry.usage?.inputTokens || 0;
const actorUsagePctOfContext = actorMaxContext > 0 ? (actorUsedTokens / actorMaxContext) * 100 : 0;
const actorUsagePctOfContext =
actorMaxContext > 0 ? (actorUsedTokens / actorMaxContext) * 100 : 0;
const isActorAbsolute = actorMaxContext > 0 && actorUsagePctOfContext >= 20;
const scaledActorBreakdown = actorBreakdown ? actorBreakdown.map((item) => ({
...item,
pct: isActorAbsolute ? item.relativePct * (actorUsedTokens / actorMaxContext) : item.relativePct
})) : null;
const scaledActorBreakdown = actorBreakdown
? actorBreakdown.map((item) => ({
...item,
pct: isActorAbsolute
? item.relativePct * (actorUsedTokens / actorMaxContext)
: item.relativePct,
}))
: null;
const decoderMaxContext = entry.decoderUsage?.maxContext !== undefined ? entry.decoderUsage.maxContext : 32768;
const decoderMaxContext =
entry.decoderUsage?.maxContext !== undefined
? entry.decoderUsage.maxContext
: 32768;
const decoderUsedTokens = entry.decoderUsage?.inputTokens || 0;
const decoderUsagePctOfContext = decoderMaxContext > 0 ? (decoderUsedTokens / decoderMaxContext) * 100 : 0;
const isDecoderAbsolute = decoderMaxContext > 0 && decoderUsagePctOfContext >= 20;
const decoderUsagePctOfContext =
decoderMaxContext > 0 ? (decoderUsedTokens / decoderMaxContext) * 100 : 0;
const isDecoderAbsolute =
decoderMaxContext > 0 && decoderUsagePctOfContext >= 20;
const scaledDecoderBreakdown = decoderBreakdown ? decoderBreakdown.map((item) => ({
...item,
pct: isDecoderAbsolute ? item.relativePct * (decoderUsedTokens / decoderMaxContext) : item.relativePct
})) : null;
const scaledDecoderBreakdown = decoderBreakdown
? decoderBreakdown.map((item) => ({
...item,
pct: isDecoderAbsolute
? item.relativePct * (decoderUsedTokens / decoderMaxContext)
: item.relativePct,
}))
: null;
useEffect(() => {
if (!entry.rawPrompt && entry.decoderPrompt) {
@@ -123,201 +190,261 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-[750px] sm:max-w-[750px] max-h-[85vh] overflow-hidden flex flex-col p-0 gap-0">
<DialogHeader className="px-5 pt-4 pb-3 border-b">
<DialogTitle>Raw Prompts & Token Usage ({entry.entityName})</DialogTitle>
<DialogContent className="max-w-[750px] sm:max-w-[750px] h-[90vh] overflow-hidden flex flex-col p-0 gap-0">
<DialogHeader className="px-6 pt-5 pb-4 border-b">
<DialogTitle className="text-lg">
Raw Prompts & Token Usage ({entry.entityName})
</DialogTitle>
</DialogHeader>
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as "actor" | "decoder")}>
<TabsList className="w-full rounded-none border-b bg-muted/50 px-5">
<TabsTrigger value="actor" disabled={!entry.rawPrompt} className="flex-1">
Actor Prompt {entry.usage ? "📊" : ""}
</TabsTrigger>
<TabsTrigger value="decoder" disabled={!entry.decoderPrompt} className="flex-1">
Intent Decoder {entry.decoderUsage ? "📊" : ""}
</TabsTrigger>
</TabsList>
<PromptSwitcher
activeTab={activeTab}
onTabChange={setActiveTab}
hasActor={!!entry.rawPrompt}
hasDecoder={!!entry.decoderPrompt}
showActorStats={!!entry.usage}
showDecoderStats={!!entry.decoderUsage}
/>
<div className="overflow-y-auto flex-1 p-5">
<TabsContent value="actor">
{entry.rawPrompt && (
<div className="flex flex-col gap-4">
{entry.usage ? (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
<strong>LLM Instance:</strong> <span>{entry.usage.providerInstanceName || "Default"}</span>
{entry.usage.modelName && (
<span> ({entry.usage.modelName})</span>
)}
</div>
) : (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm italic text-muted-foreground">
No LLM token usage (Player turn used fixed prose).
</div>
<div className="overflow-y-auto flex-1 p-5">
{activeTab === "actor" && entry.rawPrompt && (
<div className="flex flex-col gap-4">
{entry.usage ? (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
<strong>LLM Instance:</strong>{" "}
<span>{entry.usage.providerInstanceName || "Default"}</span>
{entry.usage.modelName && (
<span> ({entry.usage.modelName})</span>
)}
</div>
) : (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm italic text-muted-foreground">
No LLM token usage (Player turn used fixed prose).
</div>
)}
{scaledActorBreakdown && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
<span className="font-semibold">Input Prompt Breakdown</span>
{scaledActorBreakdown && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
<span className="font-semibold">
Input Prompt Breakdown
</span>
<span>
Total Input Tokens: <strong>{actorUsedTokens}</strong>
{actorMaxContext > 0 ? (
<span>
Total Input Tokens: <strong>{actorUsedTokens}</strong>
{actorMaxContext > 0 ? (
<span> / {actorMaxContext} ({actorUsagePctOfContext.toFixed(1)}% used)</span>
) : (
<span> (infinite context)</span>
)}
{" "}
/ {actorMaxContext} (
{actorUsagePctOfContext.toFixed(1)}% used)
</span>
</div>
<div className="flex h-6 w-full rounded overflow-hidden bg-muted shadow-inner mb-2">
{scaledActorBreakdown.map((item, idx) => {
const displayPct = actorMaxContext > 0 ? (item.tokens / actorMaxContext) * 100 : item.relativePct;
return (
<div
key={idx}
className={`h-full transition-all duration-300 ${
item.type === "system" ? "bg-blue-500" : item.type === "world" ? "bg-emerald-500" : "bg-amber-500"
) : (
<span> (infinite context)</span>
)}
</span>
</div>
<div className="flex h-6 w-full rounded border overflow-hidden bg-muted shadow-inner mb-2">
{scaledActorBreakdown.map((item, idx) => {
const displayPct =
actorMaxContext > 0
? (item.tokens / actorMaxContext) * 100
: item.relativePct;
return (
<div
key={idx}
className={`h-full transition-all duration-300 ${
item.type === "system"
? "bg-blue-500"
: item.type === "world"
? "bg-emerald-500"
: item.type === "memories"
? "bg-purple-500"
: "bg-amber-500"
}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
/>
);
})}
{isActorAbsolute && (
<div
className="bg-white h-full"
style={{ width: `${100 - actorUsagePctOfContext}%` }}
title={`Available: ${actorMaxContext - actorUsedTokens} tokens (${(100 - actorUsagePctOfContext).toFixed(1)}% remaining)`}
/>
)}
</div>
<Accordion type="multiple">
{scaledActorBreakdown.map((item, idx) => {
const displayPct =
actorMaxContext > 0
? (item.tokens / actorMaxContext) * 100
: item.relativePct;
return (
<AccordionItem key={idx} value={String(idx)}>
<AccordionTrigger className="text-sm">
<span
className={`inline-block w-2.5 h-2.5 rounded-sm mr-2 ${
item.type === "system"
? "bg-blue-500"
: item.type === "world"
? "bg-emerald-500"
: item.type === "memories"
? "bg-purple-500"
: "bg-amber-500"
}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
/>
);
})}
{isActorAbsolute && (
<div
className="bg-white h-full"
style={{ width: `${100 - actorUsagePctOfContext}%` }}
title={`Available: ${actorMaxContext - actorUsedTokens} tokens (${(100 - actorUsagePctOfContext).toFixed(1)}% remaining)`}
/>
)}
</div>
<Accordion type="multiple" defaultValue={["0"]}>
{scaledActorBreakdown.map((item, idx) => {
const displayPct = actorMaxContext > 0 ? (item.tokens / actorMaxContext) * 100 : item.relativePct;
return (
<AccordionItem key={idx} value={String(idx)}>
<AccordionTrigger className="text-sm">
<span className={`inline-block w-2.5 h-2.5 rounded-sm mr-2 ${
item.type === "system" ? "bg-blue-500" : item.type === "world" ? "bg-emerald-500" : "bg-amber-500"
}`} />
{item.label}: <strong>{item.tokens}</strong> tokens ({displayPct.toFixed(0)}%)
</AccordionTrigger>
<AccordionContent>
<pre className="m-0 p-2 bg-muted rounded text-xs font-mono whitespace-pre-wrap max-h-[250px] overflow-y-auto text-foreground">
{item.content}
</pre>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
)}
{item.label}: <strong>{item.tokens}</strong> tokens
({displayPct.toFixed(0)}%)
</AccordionTrigger>
<AccordionContent>
<pre className="m-0 p-2 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground">
{item.content}
</pre>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
)}
{entry.usage && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2">
<span className="font-semibold">LLM Output</span>
<span>Total Output Tokens: <strong>{entry.usage.outputTokens}</strong></span>
</div>
<div className="rounded border-2">
<pre className="m-0 p-2 bg-muted text-xs font-mono whitespace-pre-wrap max-h-[250px] overflow-y-auto text-foreground">
{entry.narrativeProse}
</pre>
</div>
</div>
{entry.usage && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2">
<span className="font-semibold">LLM Output</span>
<span>
Total Output Tokens:{" "}
<strong>{entry.usage.outputTokens}</strong>
</span>
</div>
<div className="rounded border-2">
<pre className="m-0 p-2 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground">
{entry.narrativeProse}
</pre>
</div>
</div>
)}
</div>
)}
{activeTab === "decoder" && entry.decoderPrompt && (
<div className="flex flex-col gap-4">
{entry.decoderUsage && (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
<strong>LLM Instance:</strong>{" "}
<span>
{entry.decoderUsage.providerInstanceName || "Default"}
</span>
{entry.decoderUsage.modelName && (
<span> ({entry.decoderUsage.modelName})</span>
)}
</div>
)}
</TabsContent>
<TabsContent value="decoder">
{entry.decoderPrompt && (
<div className="flex flex-col gap-4">
{entry.decoderUsage && (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
<strong>LLM Instance:</strong> <span>{entry.decoderUsage.providerInstanceName || "Default"}</span>
{entry.decoderUsage.modelName && (
<span> ({entry.decoderUsage.modelName})</span>
)}
</div>
)}
{scaledDecoderBreakdown && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
<span className="font-semibold">Input Prompt Breakdown</span>
{scaledDecoderBreakdown && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
<span className="font-semibold">
Input Prompt Breakdown
</span>
<span>
Total Input Tokens: <strong>{decoderUsedTokens}</strong>
{decoderMaxContext > 0 ? (
<span>
Total Input Tokens: <strong>{decoderUsedTokens}</strong>
{decoderMaxContext > 0 ? (
<span> / {decoderMaxContext} ({decoderUsagePctOfContext.toFixed(1)}% used)</span>
) : (
<span> (infinite context)</span>
)}
{" "}
/ {decoderMaxContext} (
{decoderUsagePctOfContext.toFixed(1)}% used)
</span>
</div>
<div className="flex h-6 w-full rounded overflow-hidden bg-muted shadow-inner mb-2">
{scaledDecoderBreakdown.map((item, idx) => {
const displayPct = decoderMaxContext > 0 ? (item.tokens / decoderMaxContext) * 100 : item.relativePct;
return (
<div
key={idx}
className={`h-full transition-all duration-300 ${
item.type === "system" ? "bg-blue-500" : item.type === "world" ? "bg-emerald-500" : "bg-amber-500"
) : (
<span> (infinite context)</span>
)}
</span>
</div>
<div className="flex h-6 w-full rounded border overflow-hidden bg-muted shadow-inner mb-2">
{scaledDecoderBreakdown.map((item, idx) => {
const displayPct =
decoderMaxContext > 0
? (item.tokens / decoderMaxContext) * 100
: item.relativePct;
return (
<div
key={idx}
className={`h-full transition-all duration-300 ${
item.type === "system"
? "bg-blue-500"
: item.type === "world"
? "bg-emerald-500"
: item.type === "memories"
? "bg-purple-500"
: "bg-amber-500"
}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
/>
);
})}
{isDecoderAbsolute && (
<div
className="bg-white h-full"
style={{ width: `${100 - decoderUsagePctOfContext}%` }}
title={`Available: ${decoderMaxContext - decoderUsedTokens} tokens (${(100 - decoderUsagePctOfContext).toFixed(1)}% remaining)`}
/>
)}
</div>
<Accordion type="multiple">
{scaledDecoderBreakdown.map((item, idx) => {
const displayPct =
decoderMaxContext > 0
? (item.tokens / decoderMaxContext) * 100
: item.relativePct;
return (
<AccordionItem key={idx} value={String(idx)}>
<AccordionTrigger className="text-sm">
<span
className={`inline-block w-2.5 h-2.5 rounded-sm mr-2 ${
item.type === "system"
? "bg-blue-500"
: item.type === "world"
? "bg-emerald-500"
: item.type === "memories"
? "bg-purple-500"
: "bg-amber-500"
}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
/>
);
})}
{isDecoderAbsolute && (
<div
className="bg-white h-full"
style={{ width: `${100 - decoderUsagePctOfContext}%` }}
title={`Available: ${decoderMaxContext - decoderUsedTokens} tokens (${(100 - decoderUsagePctOfContext).toFixed(1)}% remaining)`}
/>
)}
</div>
<Accordion type="multiple" defaultValue={["0"]}>
{scaledDecoderBreakdown.map((item, idx) => {
const displayPct = decoderMaxContext > 0 ? (item.tokens / decoderMaxContext) * 100 : item.relativePct;
return (
<AccordionItem key={idx} value={String(idx)}>
<AccordionTrigger className="text-sm">
<span className={`inline-block w-2.5 h-2.5 rounded-sm mr-2 ${
item.type === "system" ? "bg-blue-500" : item.type === "world" ? "bg-emerald-500" : "bg-amber-500"
}`} />
{item.label}: <strong>{item.tokens}</strong> tokens ({displayPct.toFixed(0)}%)
</AccordionTrigger>
<AccordionContent>
<pre className="m-0 p-2 bg-muted rounded text-xs font-mono whitespace-pre-wrap max-h-[250px] overflow-y-auto text-foreground">
{item.content}
</pre>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
)}
{entry.decoderUsage && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2">
<span className="font-semibold">LLM Output</span>
<span>Total Output Tokens: <strong>{entry.decoderUsage.outputTokens}</strong></span>
</div>
<div className="rounded border-2">
<pre className="m-0 p-2 bg-muted text-xs font-mono whitespace-pre-wrap max-h-[250px] overflow-y-auto text-foreground">
{JSON.stringify(entry.intents, null, 2)}
</pre>
</div>
</div>
)}
{item.label}: <strong>{item.tokens}</strong> tokens
({displayPct.toFixed(0)}%)
</AccordionTrigger>
<AccordionContent>
<pre className="m-0 p-2 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground">
{item.content}
</pre>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
)}
</TabsContent>
</div>
</Tabs>
{entry.decoderUsage && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2">
<span className="font-semibold">LLM Output</span>
<span>
Total Output Tokens:{" "}
<strong>{entry.decoderUsage.outputTokens}</strong>
</span>
</div>
<div className="rounded border-2">
<pre className="m-0 p-2 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground">
{JSON.stringify(entry.intents, null, 2)}
</pre>
</div>
</div>
)}
</div>
)}
</div>
</DialogContent>
</Dialog>
);

View File

@@ -0,0 +1,47 @@
"use client";
interface PromptSwitcherProps {
activeTab: "actor" | "decoder";
onTabChange: (tab: "actor" | "decoder") => void;
hasActor: boolean;
hasDecoder: boolean;
showActorStats: boolean;
showDecoderStats: boolean;
}
export function PromptSwitcher({
activeTab,
onTabChange,
hasActor,
hasDecoder,
showActorStats,
showDecoderStats,
}: PromptSwitcherProps) {
return (
<div className="flex items-center justify-center gap-4 border-b bg-muted/50 px-5 py-4">
<button
onClick={() => onTabChange("actor")}
disabled={!hasActor}
className={`flex h-14 w-40 items-center justify-center border-2 text-sm font-medium transition-all ${
activeTab === "actor"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border/30 bg-card text-foreground hover:border-primary/50"
} disabled:cursor-not-allowed disabled:opacity-40`}
>
Actor Prompt
</button>
<span className="text-xl text-muted-foreground"></span>
<button
onClick={() => onTabChange("decoder")}
disabled={!hasDecoder}
className={`flex h-14 w-44 items-center justify-center border-2 text-sm font-medium transition-all ${
activeTab === "decoder"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border/30 bg-card text-foreground hover:border-primary/50"
} disabled:cursor-not-allowed disabled:opacity-40`}
>
Intent Decoder
</button>
</div>
);
}

View File

@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded border-2 px-2 py-0.5 text-xs font-head font-medium whitespace-nowrap shadow-sm transition-all focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive [&>svg]:pointer-events-none [&>svg]:size-3!",
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded border px-2 py-0.5 text-xs font-head font-medium whitespace-nowrap transition-all focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {

View File

@@ -84,7 +84,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-footer"
className={cn(
"flex items-center border-t border-dotted border-border/20 bg-muted/30 p-(--card-spacing)",
"flex items-center border-t border-dotted border-border/20 bg-muted/30 px-(--card-spacing) pt-(--card-spacing)",
className
)}
{...props}

View File

@@ -70,7 +70,7 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
variant="outline"
className="absolute top-2 right-2"
size="icon"
>