fix(chat): improve message handling, fix type errors and migrate changes to enhance branch (#50)

This commit is contained in:
Felix
2026-02-11 17:10:53 +08:00
committed by GitHub
parent bc7da0085b
commit fcba8b86d5
5 changed files with 395 additions and 66 deletions

View File

@@ -16,25 +16,38 @@ interface ChatMessageProps {
message: RawMessage;
showThinking: boolean;
isStreaming?: boolean;
streamingTools?: Array<{
id?: string;
toolCallId?: string;
name: string;
status: 'running' | 'completed' | 'error';
durationMs?: number;
summary?: string;
}>;
}
export const ChatMessage = memo(function ChatMessage({
message,
showThinking,
isStreaming = false,
streamingTools = [],
}: ChatMessageProps) {
const isUser = message.role === 'user';
const isToolResult = message.role === 'toolresult';
const role = typeof message.role === 'string' ? message.role.toLowerCase() : '';
const isToolResult = role === 'toolresult' || role === 'tool_result';
const text = extractText(message);
const hasText = text.trim().length > 0;
const thinking = extractThinking(message);
const images = extractImages(message);
const tools = extractToolUse(message);
const visibleThinking = showThinking ? thinking : null;
const visibleTools = showThinking ? tools : [];
// Don't render empty tool results when thinking is hidden
if (isToolResult && !showThinking) return null;
// Never render tool result messages in chat UI
if (isToolResult) return null;
// Don't render empty messages
if (!text && !thinking && images.length === 0 && tools.length === 0) return null;
if (!hasText && !visibleThinking && images.length === 0 && visibleTools.length === 0) return null;
return (
<div
@@ -56,23 +69,32 @@ export const ChatMessage = memo(function ChatMessage({
</div>
{/* Content */}
<div className={cn('max-w-[80%] space-y-2', isUser && 'items-end')}>
<div
className={cn(
'flex flex-col w-full max-w-[80%] space-y-2',
isUser ? 'items-end' : 'items-start',
)}
>
{showThinking && isStreaming && !isUser && streamingTools.length > 0 && (
<ToolStatusBar tools={streamingTools} />
)}
{/* Thinking section */}
{showThinking && thinking && (
<ThinkingBlock content={thinking} />
{visibleThinking && (
<ThinkingBlock content={visibleThinking} />
)}
{/* Tool use cards */}
{showThinking && tools.length > 0 && (
{visibleTools.length > 0 && (
<div className="space-y-1">
{tools.map((tool, i) => (
{visibleTools.map((tool, i) => (
<ToolCard key={tool.id || i} name={tool.name} input={tool.input} />
))}
</div>
)}
{/* Main text bubble */}
{text && (
{hasText && (
<MessageBubble
text={text}
isUser={isUser}
@@ -99,6 +121,51 @@ export const ChatMessage = memo(function ChatMessage({
);
});
function formatDuration(durationMs?: number): string | null {
if (!durationMs || !Number.isFinite(durationMs)) return null;
if (durationMs < 1000) return `${Math.round(durationMs)}ms`;
return `${(durationMs / 1000).toFixed(1)}s`;
}
function ToolStatusBar({
tools,
}: {
tools: Array<{
id?: string;
toolCallId?: string;
name: string;
status: 'running' | 'completed' | 'error';
durationMs?: number;
summary?: string;
}>;
}) {
return (
<div className="w-full rounded-lg border border-border/50 bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
<div className="space-y-1">
{tools.map((tool) => {
const duration = formatDuration(tool.durationMs);
const statusLabel = tool.status === 'running' ? 'running' : (tool.status === 'error' ? 'error' : 'done');
return (
<div key={tool.toolCallId || tool.id || tool.name} className="flex flex-wrap items-center gap-2">
<span className={cn(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px]',
tool.status === 'error' ? 'bg-destructive/10 text-destructive' : 'bg-foreground/5 text-muted-foreground',
)}>
<span className="font-mono">{tool.name}</span>
<span className="opacity-70">{statusLabel}</span>
</span>
{duration && <span className="text-[11px] opacity-70">{duration}</span>}
{tool.summary && (
<span className="truncate text-[11px]">{tool.summary}</span>
)}
</div>
);
})}
</div>
</div>
);
}
// ── Message Bubble ──────────────────────────────────────────────
function MessageBubble({
@@ -124,6 +191,7 @@ function MessageBubble({
<div
className={cn(
'relative rounded-2xl px-4 py-3',
!isUser && 'w-full',
isUser
? 'bg-primary text-primary-foreground'
: 'bg-muted',
@@ -205,7 +273,7 @@ function ThinkingBlock({ content }: { content: string }) {
const [expanded, setExpanded] = useState(false);
return (
<div className="rounded-lg border border-border/50 bg-muted/30 text-sm">
<div className="w-full rounded-lg border border-border/50 bg-muted/30 text-sm">
<button
className="flex items-center gap-2 w-full px-3 py-2 text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setExpanded(!expanded)}