|
@@ -60,6 +60,10 @@ export function ChatMessage({
|
|
|
|
|
|
|
|
const hasThinking = Boolean(thinkingContent) || (isStreaming && streamingThinking !== undefined)
|
|
const hasThinking = Boolean(thinkingContent) || (isStreaming && streamingThinking !== undefined)
|
|
|
|
|
|
|
|
|
|
+ // Check if there's actual content to display (not just whitespace)
|
|
|
|
|
+ const hasContent = Boolean(content && content.trim())
|
|
|
|
|
+ const hasToolCalls = message.tool_calls && message.tool_calls.length > 0
|
|
|
|
|
+
|
|
|
const formattedTime = useMemo(() => {
|
|
const formattedTime = useMemo(() => {
|
|
|
const date = new Date(message.created_at * 1000)
|
|
const date = new Date(message.created_at * 1000)
|
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
@@ -93,69 +97,10 @@ export function ChatMessage({
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- <div
|
|
|
|
|
- className={`rounded-2xl px-4 py-2 ${
|
|
|
|
|
- isUser
|
|
|
|
|
- ? 'bg-primary-600 text-white'
|
|
|
|
|
- : 'bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-gray-100'
|
|
|
|
|
- }`}
|
|
|
|
|
- >
|
|
|
|
|
- {isUser ? (
|
|
|
|
|
- <p className="whitespace-pre-wrap text-sm">{content}</p>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <div className="prose prose-sm max-w-none dark:prose-invert prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 prose-pre:my-2">
|
|
|
|
|
- <ReactMarkdown
|
|
|
|
|
- remarkPlugins={[remarkGfm]}
|
|
|
|
|
- components={{
|
|
|
|
|
- code({ className, children, ...props }) {
|
|
|
|
|
- const match = /language-(\w+)/.exec(className || '')
|
|
|
|
|
- const isInline = !match && !className
|
|
|
|
|
- return isInline ? (
|
|
|
|
|
- <code
|
|
|
|
|
- className="rounded bg-gray-200 px-1 py-0.5 text-sm dark:bg-gray-600"
|
|
|
|
|
- {...props}
|
|
|
|
|
- >
|
|
|
|
|
- {children}
|
|
|
|
|
- </code>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <pre className="overflow-x-auto rounded-md bg-gray-800 p-3 text-gray-100">
|
|
|
|
|
- <code className={className} {...props}>
|
|
|
|
|
- {children}
|
|
|
|
|
- </code>
|
|
|
|
|
- </pre>
|
|
|
|
|
- )
|
|
|
|
|
- },
|
|
|
|
|
- a({ href, children }) {
|
|
|
|
|
- return (
|
|
|
|
|
- <a
|
|
|
|
|
- href={href}
|
|
|
|
|
- target="_blank"
|
|
|
|
|
- rel="noopener noreferrer"
|
|
|
|
|
- className="text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
- >
|
|
|
|
|
- {children}
|
|
|
|
|
- </a>
|
|
|
|
|
- )
|
|
|
|
|
- },
|
|
|
|
|
- pre({ children }) {
|
|
|
|
|
- // pre is handled by code component
|
|
|
|
|
- return <>{children}</>
|
|
|
|
|
- },
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- {content}
|
|
|
|
|
- </ReactMarkdown>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- {isStreaming && (
|
|
|
|
|
- <span className="inline-block h-2 w-2 animate-pulse rounded-full bg-current opacity-75" />
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* Tool calls */}
|
|
|
|
|
- {message.tool_calls && message.tool_calls.length > 0 && (
|
|
|
|
|
- <div className="mt-2 w-full max-w-md">
|
|
|
|
|
- {message.tool_calls.map((toolCall) => {
|
|
|
|
|
|
|
+ {/* Tool calls - shown first for assistant messages */}
|
|
|
|
|
+ {!isUser && hasToolCalls && (
|
|
|
|
|
+ <div className="w-full max-w-md mb-2">
|
|
|
|
|
+ {message.tool_calls!.map((toolCall) => {
|
|
|
const toolState = toolStates.find((t) => t.tool_call.id === toolCall.id) || {
|
|
const toolState = toolStates.find((t) => t.tool_call.id === toolCall.id) || {
|
|
|
tool_call: toolCall,
|
|
tool_call: toolCall,
|
|
|
status: 'completed' as const,
|
|
status: 'completed' as const,
|
|
@@ -171,6 +116,68 @@ export function ChatMessage({
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
|
|
+ {/* Content bubble - only show if there's actual content */}
|
|
|
|
|
+ {(isUser || hasContent || isStreaming) && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`rounded-2xl px-4 py-2 ${
|
|
|
|
|
+ isUser
|
|
|
|
|
+ ? 'bg-primary-600 text-white'
|
|
|
|
|
+ : 'bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-gray-100'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ {isUser ? (
|
|
|
|
|
+ <p className="whitespace-pre-wrap text-sm">{content}</p>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="prose prose-sm max-w-none dark:prose-invert prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 prose-pre:my-2">
|
|
|
|
|
+ <ReactMarkdown
|
|
|
|
|
+ remarkPlugins={[remarkGfm]}
|
|
|
|
|
+ components={{
|
|
|
|
|
+ code({ className, children, ...props }) {
|
|
|
|
|
+ const match = /language-(\w+)/.exec(className || '')
|
|
|
|
|
+ const isInline = !match && !className
|
|
|
|
|
+ return isInline ? (
|
|
|
|
|
+ <code
|
|
|
|
|
+ className="rounded bg-gray-200 px-1 py-0.5 text-sm dark:bg-gray-600"
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ >
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </code>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <pre className="overflow-x-auto rounded-md bg-gray-800 p-3 text-gray-100">
|
|
|
|
|
+ <code className={className} {...props}>
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </code>
|
|
|
|
|
+ </pre>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ a({ href, children }) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <a
|
|
|
|
|
+ href={href}
|
|
|
|
|
+ target="_blank"
|
|
|
|
|
+ rel="noopener noreferrer"
|
|
|
|
|
+ className="text-blue-600 hover:underline dark:text-blue-400"
|
|
|
|
|
+ >
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </a>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ pre({ children }) {
|
|
|
|
|
+ // pre is handled by code component
|
|
|
|
|
+ return <>{children}</>
|
|
|
|
|
+ },
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {content}
|
|
|
|
|
+ </ReactMarkdown>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {isStreaming && (
|
|
|
|
|
+ <span className="inline-block h-2 w-2 animate-pulse rounded-full bg-current opacity-75" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
{/* Timestamp and context info */}
|
|
{/* Timestamp and context info */}
|
|
|
<div className="mt-1 flex items-center gap-2 text-xs text-gray-500">
|
|
<div className="mt-1 flex items-center gap-2 text-xs text-gray-500">
|
|
|
<span>{formattedTime}</span>
|
|
<span>{formattedTime}</span>
|