2026-02-25 11:41:44 +00:00
|
|
|
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",
|
2026-02-25 18:19:33 +00:00
|
|
|
whiteSpace: msg.role === "tool" ? "pre-wrap" : "normal",
|
2026-02-25 11:41:44 +00:00
|
|
|
lineHeight: "1.6",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{msg.role === "user" ? (
|
2026-02-25 18:08:08 +00:00
|
|
|
<div className="user-markdown-body">
|
|
|
|
|
<Markdown components={{ code: CodeBlock }}>{msg.content}</Markdown>
|
|
|
|
|
</div>
|
2026-02-25 11:41:44 +00:00
|
|
|
) : 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);
|