Spaces:
Running
Running
| import { useState } from "react"; | |
| import Markdown from "./Markdown.jsx"; | |
| const THINK_START = "‹‹THINK››"; | |
| const THINK_END = "‹‹/THINK››"; | |
| function splitThinking(text) { | |
| const startIdx = text.indexOf(THINK_START); | |
| const endIdx = text.indexOf(THINK_END); | |
| if (startIdx !== -1 && endIdx !== -1) { | |
| const thinking = text.slice(startIdx + THINK_START.length, endIdx).trim(); | |
| const response = text.slice(endIdx + THINK_END.length).trim(); | |
| return { thinking, response }; | |
| } | |
| // Still streaming inside think block | |
| if (startIdx !== -1 && endIdx === -1) { | |
| const thinking = text.slice(startIdx + THINK_START.length).trim(); | |
| return { thinking, response: null }; | |
| } | |
| return { thinking: null, response: text }; | |
| } | |
| function ThinkingBlock({ thinking }) { | |
| const [open, setOpen] = useState(false); | |
| if (!thinking) return null; | |
| return ( | |
| <div className="mb-3"> | |
| <button | |
| onClick={() => setOpen((v) => !v)} | |
| className="flex items-center gap-1.5 text-[11px] text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors cursor-pointer" | |
| > | |
| <span className={`transition-transform ${open ? "rotate-90" : ""}`}>▶</span> | |
| <span>💭 Thinking</span> | |
| </button> | |
| {open && ( | |
| <div className="mt-1.5 pl-4 border-l-2 border-[var(--color-blue)]/20 text-xs text-[var(--color-text-secondary)]/80 leading-relaxed whitespace-pre-wrap"> | |
| {thinking} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function AssistantContent({ text }) { | |
| const { thinking, response } = splitThinking(text); | |
| return ( | |
| <> | |
| <ThinkingBlock thinking={thinking} /> | |
| {response && <Markdown>{response}</Markdown>} | |
| {response === null && thinking && ( | |
| <span className="text-[11px] text-[var(--color-text-secondary)] italic">thinking...</span> | |
| )} | |
| </> | |
| ); | |
| } | |
| export default function MessageList({ messages, streamingText, isStreaming, processingStep }) { | |
| return ( | |
| <div className="px-4 py-4 space-y-6"> | |
| {messages.map((msg, i) => ( | |
| <Message key={i} msg={msg} /> | |
| ))} | |
| {isStreaming && processingStep && !streamingText && ( | |
| <div className="flex gap-3"> | |
| <div className="w-7 h-7 rounded-lg bg-gradient-to-br from-[#3186FF] to-[#4FA0FF] flex items-center justify-center text-white text-xs font-bold shrink-0"> | |
| G | |
| </div> | |
| <div className="flex items-center gap-2 text-sm text-[var(--color-text-secondary)] pt-1"> | |
| <span className="inline-block w-1.5 h-1.5 rounded-full bg-[var(--color-blue)] animate-pulse" /> | |
| <span className="capitalize">{processingStep}...</span> | |
| </div> | |
| </div> | |
| )} | |
| {isStreaming && streamingText && ( | |
| <div className="flex gap-3"> | |
| <div className="w-7 h-7 rounded-lg bg-gradient-to-br from-[#3186FF] to-[#4FA0FF] flex items-center justify-center text-white text-xs font-bold shrink-0"> | |
| G | |
| </div> | |
| <div className="text-sm text-[var(--color-text)] leading-relaxed pt-1 min-w-0"> | |
| <AssistantContent text={streamingText} /> | |
| <span className="inline-block w-1.5 h-4 bg-[var(--color-blue)] animate-pulse ml-0.5 align-text-bottom rounded-sm" /> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function Message({ msg }) { | |
| const isUser = msg.role === "user"; | |
| return ( | |
| <div className="flex gap-3"> | |
| <div | |
| className={`w-7 h-7 rounded-lg flex items-center justify-center text-xs font-bold shrink-0 ${ | |
| isUser | |
| ? "bg-[var(--color-surface-high)] text-[var(--color-text-secondary)]" | |
| : "bg-gradient-to-br from-[#3186FF] to-[#4FA0FF] text-white" | |
| }`} | |
| > | |
| {isUser ? "Y" : "G"} | |
| </div> | |
| <div className="flex-1 min-w-0 pt-0.5"> | |
| {msg.videoUrl ? ( | |
| <video controls src={msg.videoUrl} className="max-w-sm max-h-48 rounded-xl mb-2 border border-[var(--color-outline)]" /> | |
| ) : msg.imageUrl ? ( | |
| <img | |
| src={msg.imageUrl} | |
| alt="Attached" | |
| className="max-w-xs max-h-48 rounded-xl mb-2 border border-[var(--color-outline)]" | |
| /> | |
| ) : null} | |
| {msg.audioUrl && ( | |
| <audio controls src={msg.audioUrl} className="mb-2 h-8 max-w-xs" /> | |
| )} | |
| {isUser ? ( | |
| <div className="text-sm text-[var(--color-text)] leading-relaxed"> | |
| {msg.content.filter((c) => c.type === "text").map((c) => c.text).join("")} | |
| </div> | |
| ) : ( | |
| <div className="text-sm text-[var(--color-text)] leading-relaxed"> | |
| <AssistantContent text={msg.content} /> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |