158 lines
No EOL
5.6 KiB
TypeScript
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>
|
|
);
|
|
} |