Gemma-4-WebGPU / src /components /MessageList.jsx
shreyask's picture
Upload folder using huggingface_hub
45f314a verified
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>
);
}