storytellurx-v1/app/components/chat/llm-request-viewer.tsx
Elias Rhouzlane cfe3526f6b fix: localize
2025-07-14 14:08:57 +02:00

158 lines
No EOL
5.6 KiB
TypeScript

"use client";
import { useState } from "react";
import cn from "@bureaudouble/outils/cn.ts";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog.tsx";
import { Button } from "../ui/button.tsx";
import type { NarrativeMessage } from "../../game-engine/narrative-provider.tsx";
import { useClientLocale } from "../../game-engine/locale-provider.tsx";
// A sub-component to render a single message for clarity.
const FormattedMessage = (
{ message }: { message: NarrativeMessage },
) => {
const { t } = useClientLocale();
const { role, parts, metadata } = message;
const content = parts.filter((v) => v.type === "text").map((v) => v.text)
.join("");
let formattedContent = content;
const isJsonContent = role === "assistant" || content.trim().startsWith("{");
if (isJsonContent) {
try {
const parsedContent = JSON.parse(content);
formattedContent = JSON.stringify(parsedContent, null, 2);
} catch (e) {
formattedContent = content;
}
}
const roleStyles = {
user: "border-blue-500/30 bg-blue-900/10",
assistant: "border-green-500/30 bg-green-900/10",
system: "border-border/30 bg-neutral-900/10",
};
return (
<div
className={cn(
"p-2 border my-2 text-xs",
roleStyles[role as keyof typeof roleStyles] ?? roleStyles.system,
)}
>
<div className="flex justify-between gap-2 text-muted-foreground uppercase font-bold mb-1">
<div>{role}</div>
<div>{t("tokens")}{metadata?.tokenCount ?? 0}</div>
</div>
<pre className="whitespace-pre-wrap break-all text-neutral-200">
{formattedContent}
</pre>
</div>
);
};
export function LLMRequestViewer(
{ messages, children }: {
messages: NarrativeMessage[];
children: React.ReactNode;
},
) {
const { t } = useClientLocale();
const [viewMode, setViewMode] = useState<"formatted" | "raw">("formatted");
const lastMessage = messages.at(-1);
const rawContent = JSON.stringify(messages, null, 2);
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-w-[90vw] md:max-w-4xl h-[90vh] flex flex-col gap-2">
<DialogHeader>
<DialogTitle>{t("llmRequestPayload")}</DialogTitle>
<DialogDescription>
{t("llmRequestDescription")}{" "}
{lastMessage?.id ?? t("notAvailable")}.
</DialogDescription>
</DialogHeader>
<div className="flex-none flex items-center gap-2 mt-2">
<Button
variant={viewMode === "formatted" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("formatted")}
className="text-xs px-3"
>
{t("formatted")}
</Button>
<Button
variant={viewMode === "raw" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("raw")}
className="text-xs px-3"
>
{t("rawJson")}
</Button>
<div className="text-xs text-muted-foreground uppercase font-bold ml-auto">
{t("tokens")}{lastMessage?.metadata?.totalTokenCount}
</div>
</div>
<div className="flex-grow overflow-auto border border-ui p-2 text-xs bg-background/50">
{viewMode === "formatted"
? (
<div>
{lastMessage?.metadata?.model && (
<div className="mb-4">
<div className="flex text-xs justify-between gap-2 text-muted-foreground uppercase font-bold mb-1">
<div>{t("model")}</div>
</div>
<div className="p-2 border border-dashed border-border bg-neutral-900/50">
<pre className="whitespace-pre-wrap break-all text-xs text-neutral-200">
{JSON.stringify(lastMessage?.metadata?.model, null, 2)}
</pre>
</div>
</div>
)}
{lastMessage?.metadata?.system?.content && (
<div className="mb-4">
<div className="flex text-xs justify-between gap-2 text-muted-foreground uppercase font-bold mb-1">
<div>{t("systemPrompt")}</div>
<div>
{t("tokens")}{lastMessage?.metadata?.system.tokenCount ??
0}
</div>
</div>
<div className="p-2 border border-dashed border-border bg-neutral-900/50">
<pre className="whitespace-pre-wrap break-all text-xs text-neutral-200">
{lastMessage?.metadata?.system.content}
</pre>
</div>
</div>
)}
<div>
<div className="flex text-xs justify-between gap-2 text-muted-foreground uppercase font-bold mb-1">
<div>{t("messages")}</div>
<div>
{t("tokens")}{messages?.reduce?.(
(p, v) => p + (v?.metadata?.tokenCount ?? 0),
0,
)}
</div>
</div>
{messages.map((msg, index) => (
<FormattedMessage key={index} message={msg} />
))}
</div>
</div>
)
: <pre className="whitespace-pre-wrap break-all">{rawContent}</pre>}
</div>
</DialogContent>
</Dialog>
);
}