quick question feature
This commit is contained in:
@@ -12,9 +12,16 @@ type Props = {
|
||||
|
||||
type ToolLogMetadata = {
|
||||
kind: "tool_call";
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
status?: "completed" | "failed";
|
||||
summary?: string;
|
||||
args?: Record<string, unknown>;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
durationMs?: number;
|
||||
error?: string | null;
|
||||
resultPreview?: string | null;
|
||||
};
|
||||
|
||||
function asToolLogMetadata(value: unknown): ToolLogMetadata | null {
|
||||
@@ -26,10 +33,26 @@ function asToolLogMetadata(value: unknown): ToolLogMetadata | null {
|
||||
|
||||
function getToolSummary(message: Message, metadata: ToolLogMetadata) {
|
||||
if (typeof metadata.summary === "string" && metadata.summary.trim()) return metadata.summary.trim();
|
||||
if (metadata.status === "failed" && typeof metadata.error === "string" && metadata.error.trim()) {
|
||||
return `Tool failed: ${metadata.error.trim()}`;
|
||||
}
|
||||
if (typeof metadata.resultPreview === "string" && metadata.resultPreview.trim()) return metadata.resultPreview.trim();
|
||||
if (message.content.trim()) return message.content.trim();
|
||||
const toolName = metadata.toolName?.trim() || message.name?.trim() || "unknown_tool";
|
||||
return `Ran tool '${toolName}'.`;
|
||||
}
|
||||
|
||||
function getToolLabel(message: Message, metadata: ToolLogMetadata) {
|
||||
const raw = metadata.toolName?.trim() || message.name?.trim();
|
||||
if (!raw) return "Tool call";
|
||||
return raw
|
||||
.replace(/_/g, " ")
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function getToolIconName(toolName: string | null | undefined) {
|
||||
const lowered = toolName?.toLowerCase() ?? "";
|
||||
if (lowered.includes("search")) return "search";
|
||||
@@ -37,6 +60,27 @@ function getToolIconName(toolName: string | null | undefined) {
|
||||
return "generic";
|
||||
}
|
||||
|
||||
function formatDuration(durationMs: unknown) {
|
||||
if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs <= 0) return null;
|
||||
return `${Math.round(durationMs)} ms`;
|
||||
}
|
||||
|
||||
function formatToolTimestamp(...values: Array<string | null | undefined>) {
|
||||
const value = values.find((candidate) => candidate && !Number.isNaN(new Date(candidate).getTime()));
|
||||
if (!value) return null;
|
||||
return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(new Date(value));
|
||||
}
|
||||
|
||||
function getToolDetailLabel(message: Message, metadata: ToolLogMetadata, isFailed: boolean) {
|
||||
return [
|
||||
isFailed ? "Failed" : "Completed",
|
||||
formatDuration(metadata.durationMs),
|
||||
formatToolTimestamp(message.createdAt, metadata.completedAt, metadata.startedAt),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
}
|
||||
|
||||
export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
const hasPendingAssistant = messages.some((message) => message.id.startsWith("temp-assistant-") && message.content.trim().length === 0);
|
||||
|
||||
@@ -51,19 +95,38 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
const Icon = iconKind === "search" ? Globe2 : iconKind === "fetch" ? Link2 : Wrench;
|
||||
const isFailed = toolLogMetadata.status === "failed";
|
||||
const toolSummary = getToolSummary(message, toolLogMetadata);
|
||||
const toolLabel = getToolLabel(message, toolLogMetadata);
|
||||
const toolDetailLabel = getToolDetailLabel(message, toolLogMetadata, isFailed);
|
||||
return (
|
||||
<div key={message.id} className="flex justify-start">
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex max-w-[85%] min-w-0 items-center gap-3 overflow-hidden rounded-lg border px-3.5 py-2 text-sm leading-5 shadow-[inset_0_1px_0_hsl(180_100%_88%_/_0.06)]",
|
||||
"inline-flex max-w-[85%] min-w-0 items-start gap-3 overflow-hidden rounded-xl border px-3 py-2.5 shadow-[inset_0_1px_0_hsl(180_100%_88%_/_0.06)]",
|
||||
isFailed
|
||||
? "border-rose-500/40 bg-rose-950/18 text-rose-200"
|
||||
: "border-cyan-400/34 bg-cyan-950/18 text-cyan-100"
|
||||
? "border-rose-400/34 bg-[linear-gradient(90deg,hsl(350_72%_44%_/_0.18),hsl(342_66%_9%_/_0.72))]"
|
||||
: "border-cyan-400/34 bg-[linear-gradient(90deg,hsl(184_89%_21%_/_0.70),hsl(208_66%_12%_/_0.78))]"
|
||||
)}
|
||||
title={toolSummary}
|
||||
title={`${toolSummary}\n${toolLabel} • ${toolDetailLabel}`}
|
||||
>
|
||||
<Icon className="h-4 w-4 shrink-0 text-cyan-300" />
|
||||
<span className="min-w-0 flex-1 truncate">{toolSummary}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-lg border",
|
||||
isFailed ? "border-rose-400/34 bg-rose-400/13 text-rose-300" : "border-cyan-300/34 bg-cyan-300/13 text-cyan-300"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 space-y-1">
|
||||
<span className={cn("block truncate text-sm leading-5", isFailed ? "text-rose-200" : "text-violet-50/95")}>
|
||||
{toolSummary}
|
||||
</span>
|
||||
<span className="flex min-w-0 items-center gap-1.5 text-[11px] leading-4">
|
||||
<span className={cn("min-w-0 truncate font-semibold", isFailed ? "text-rose-300/85" : "text-cyan-200/90")}>
|
||||
{toolLabel}
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-violet-200/64">{toolDetailLabel}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user