Files
storkit/frontend/src/components/MessageItem.tsx

163 lines
4.1 KiB
TypeScript
Raw Normal View History

import * as React from "react";
import Markdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import type { Message, ToolCall } from "../types";
// biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props
function CodeBlock({ className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || "");
const isInline = !className;
return !isInline && match ? (
<SyntaxHighlighter
// biome-ignore lint/suspicious/noExplicitAny: oneDark style types are incompatible
style={oneDark as any}
language={match[1]}
PreTag="div"
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
interface MessageItemProps {
msg: Message;
}
function MessageItemInner({ msg }: MessageItemProps) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: msg.role === "user" ? "flex-end" : "flex-start",
}}
>
<div
style={{
maxWidth: "100%",
padding: msg.role === "user" ? "10px 16px" : "0",
borderRadius: msg.role === "user" ? "20px" : "0",
background:
msg.role === "user"
? "#2f2f2f"
: msg.role === "tool"
? "#222"
: "transparent",
color: "#ececec",
border: msg.role === "tool" ? "1px solid #333" : "none",
fontFamily: msg.role === "tool" ? "monospace" : "inherit",
fontSize: msg.role === "tool" ? "0.85em" : "1em",
fontWeight: "500",
whiteSpace: msg.role === "tool" ? "pre-wrap" : "normal",
lineHeight: "1.6",
}}
>
{msg.role === "user" ? (
<div className="user-markdown-body">
<Markdown components={{ code: CodeBlock }}>{msg.content}</Markdown>
</div>
) : msg.role === "tool" ? (
<details style={{ cursor: "pointer" }}>
<summary
style={{
color: "#aaa",
fontSize: "0.9em",
marginBottom: "8px",
listStyle: "none",
display: "flex",
alignItems: "center",
gap: "6px",
}}
>
<span style={{ fontSize: "0.8em" }}></span>
<span>
Tool Output
{msg.tool_call_id && ` (${msg.tool_call_id})`}
</span>
</summary>
<pre
style={{
maxHeight: "300px",
overflow: "auto",
margin: 0,
padding: "8px",
background: "#1a1a1a",
borderRadius: "4px",
fontSize: "0.85em",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{msg.content}
</pre>
</details>
) : (
<div className="markdown-body">
<Markdown components={{ code: CodeBlock }}>{msg.content}</Markdown>
</div>
)}
{msg.tool_calls && (
<div
style={{
marginTop: "12px",
fontSize: "0.85em",
color: "#aaa",
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
{msg.tool_calls.map((tc: ToolCall, i: number) => {
let argsSummary = "";
try {
const args = JSON.parse(tc.function.arguments);
const firstKey = Object.keys(args)[0];
if (firstKey && args[firstKey]) {
argsSummary = String(args[firstKey]);
if (argsSummary.length > 50) {
argsSummary = `${argsSummary.substring(0, 47)}...`;
}
}
} catch (_e) {
// ignore
}
return (
<div
key={`tool-${i}-${tc.function.name}`}
style={{
display: "flex",
alignItems: "center",
gap: "8px",
fontFamily: "monospace",
}}
>
<span style={{ color: "#888" }}></span>
<span
style={{
background: "#333",
padding: "2px 6px",
borderRadius: "4px",
}}
>
{tc.function.name}
{argsSummary && `(${argsSummary})`}
</span>
</div>
);
})}
</div>
)}
</div>
</div>
);
}
export const MessageItem = React.memo(MessageItemInner);