Download frontend/src/components/chat/StreamingMessageFeed.jsx from Rixhot/ZARAI: direct link, hf CLI and curl.
- Browser
- Download file 7.99 kB
-
https://huggingface.co/spaces/Rixhot/ZARAI/resolve/main/frontend/src/components/chat/StreamingMessageFeed.jsx
- Command line
-
hf download hf://spaces/Rixhot/ZARAI/frontend/src/components/chat/StreamingMessageFeed.jsx
-
curl -L -o StreamingMessageFeed.jsx https://huggingface.co/spaces/Rixhot/ZARAI/resolve/main/frontend/src/components/chat/StreamingMessageFeed.jsx
7.99 kB
| /** | |
| * JARVIS β StreamingMessageFeed | |
| * Drop-in replacement for MessageFeed that shows streaming tokens in real-time. | |
| * Uses useStreamingChat (WebSocket) with REST fallback. | |
| * | |
| * Replace <MessageFeed /> with <StreamingMessageFeed /> in ChatPage.jsx | |
| * to enable live token streaming. | |
| */ | |
| import React, { useEffect, useRef, useState } from 'react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| import { Bot, User, Mic, Image, Wrench, Brain, Zap } from 'lucide-react' | |
| import clsx from 'clsx' | |
| import useAppStore from '../../stores/appStore' | |
| import MessageBubble from './MessageBubble' | |
| import { useStreamingChat } from '../../hooks/useStreamingChat' | |
| export default function StreamingMessageFeed() { | |
| const { getActiveMessages, activeSessionId, isLoading } = useAppStore() | |
| const messages = getActiveMessages() | |
| const bottomRef = useRef(null) | |
| // WebSocket streaming state | |
| const { | |
| sendMessage: wsSend, | |
| isStreaming, | |
| currentTokens, | |
| toolEvents, | |
| error: wsError, | |
| } = useStreamingChat(activeSessionId) | |
| // Auto-scroll | |
| useEffect(() => { | |
| bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) | |
| }, [messages.length, currentTokens, isLoading]) | |
| if (messages.length === 0 && !isStreaming) { | |
| return <StreamingEmptyState /> | |
| } | |
| return ( | |
| <div className="h-full overflow-y-auto py-6 px-4 space-y-4"> | |
| <AnimatePresence initial={false}> | |
| {messages.map((msg) => ( | |
| <motion.div | |
| key={msg.id} | |
| layout | |
| initial={{ opacity: 0, y: 12 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.2 }} | |
| > | |
| <MessageRow message={msg} isUser={msg.role === 'user'} /> | |
| </motion.div> | |
| ))} | |
| </AnimatePresence> | |
| {/* Live streaming bubble */} | |
| <AnimatePresence> | |
| {(isStreaming || isLoading) && ( | |
| <motion.div | |
| key="streaming" | |
| initial={{ opacity: 0, y: 12 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0 }} | |
| > | |
| <StreamingBubble | |
| tokens={currentTokens} | |
| toolEvents={toolEvents} | |
| isLoading={isLoading && !isStreaming} | |
| /> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| <div ref={bottomRef} className="h-1" /> | |
| </div> | |
| ) | |
| } | |
| // ββ Message row (reused from MessageFeed) ββββββββββββββββββββββ | |
| function MessageRow({ message, isUser }) { | |
| if (message.content === '__typing__') return <TypingBubble /> | |
| return ( | |
| <div className={clsx('flex gap-3', isUser ? 'flex-row-reverse' : 'flex-row')}> | |
| <Avatar isUser={isUser} modality={message.modality} /> | |
| <div className={clsx('max-w-[72%] flex flex-col gap-1', isUser ? 'items-end' : 'items-start')}> | |
| <MessageBubble message={message} isUser={isUser} /> | |
| {message.used_tools?.length > 0 && ( | |
| <div className="flex gap-1 flex-wrap mt-1"> | |
| {message.used_tools.map((tool) => ( | |
| <ToolBadge key={tool} name={tool} /> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // ββ Live streaming bubble ββββββββββββββββββββββββββββββββββββββ | |
| function StreamingBubble({ tokens, toolEvents, isLoading }) { | |
| return ( | |
| <div className="flex gap-3 flex-row"> | |
| <Avatar isUser={false} /> | |
| <div className="max-w-[72%] flex flex-col gap-2"> | |
| {/* Tool use events */} | |
| <AnimatePresence> | |
| {toolEvents.map((evt, i) => ( | |
| <motion.div | |
| key={i} | |
| initial={{ opacity: 0, x: -4 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| className="flex items-center gap-2 text-[11px] font-mono | |
| text-jarvis-accent bg-jarvis-accent/8 | |
| border border-jarvis-accent/20 rounded-lg px-3 py-1.5" | |
| > | |
| <Zap size={10} className="animate-pulse" /> | |
| {evt} | |
| </motion.div> | |
| ))} | |
| </AnimatePresence> | |
| {/* Token stream or typing indicator */} | |
| <div className="msg-assistant"> | |
| {isLoading ? ( | |
| <TypingDots /> | |
| ) : tokens && tokens !== '...' ? ( | |
| <div className="prose-jarvis text-sm whitespace-pre-wrap break-words"> | |
| {tokens} | |
| <span className="inline-block w-0.5 h-4 bg-jarvis-accent ml-0.5 animate-pulse align-text-bottom" /> | |
| </div> | |
| ) : ( | |
| <div className="flex items-center gap-2 text-jarvis-dim text-xs font-mono"> | |
| <Brain size={12} className="animate-pulse text-jarvis-accent" /> | |
| Reasoning⦠| |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // ββ Sub-components βββββββββββββββββββββββββββββββββββββββββββββ | |
| function Avatar({ isUser, modality }) { | |
| const Icon = isUser ? User : Bot | |
| return ( | |
| <div className={clsx( | |
| 'w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5', | |
| isUser | |
| ? 'bg-jarvis-accent2/20 border border-jarvis-accent2/30' | |
| : 'bg-jarvis-accent/10 border border-jarvis-accent/30', | |
| )}> | |
| <Icon size={14} className={isUser ? 'text-jarvis-accent2' : 'text-jarvis-accent'} /> | |
| </div> | |
| ) | |
| } | |
| function ToolBadge({ name }) { | |
| return ( | |
| <span className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] | |
| bg-jarvis-accent/10 border border-jarvis-accent/20 | |
| text-jarvis-accent font-mono"> | |
| <Wrench size={8} /> | |
| {name} | |
| </span> | |
| ) | |
| } | |
| function TypingDots() { | |
| return ( | |
| <div className="flex items-center gap-1 py-1"> | |
| <span className="typing-dot" /> | |
| <span className="typing-dot" /> | |
| <span className="typing-dot" /> | |
| </div> | |
| ) | |
| } | |
| function TypingBubble() { | |
| return ( | |
| <div className="flex gap-3"> | |
| <div className="w-8 h-8 rounded-lg bg-jarvis-accent/10 border border-jarvis-accent/30 | |
| flex items-center justify-center flex-shrink-0"> | |
| <Bot size={14} className="text-jarvis-accent" /> | |
| </div> | |
| <div className="msg-assistant"> | |
| <TypingDots /> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| function StreamingEmptyState() { | |
| return ( | |
| <div className="h-full flex flex-col items-center justify-center gap-6 px-8 text-center"> | |
| <div className="relative"> | |
| <div className="w-20 h-20 rounded-2xl bg-jarvis-accent/10 border border-jarvis-accent/30 | |
| flex items-center justify-center animate-glow"> | |
| <Bot size={36} className="text-jarvis-accent" /> | |
| </div> | |
| <div className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-jarvis-success | |
| border-2 border-jarvis-bg animate-pulse-slow" /> | |
| </div> | |
| <div> | |
| <h2 className="font-display text-2xl font-bold text-jarvis-accent tracking-wider mb-2"> | |
| JARVIS ONLINE | |
| </h2> | |
| <p className="text-jarvis-muted text-sm max-w-xs"> | |
| Multi-modal AI assistant with real-time streaming. Speak, upload, or type. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-3 gap-3 w-full max-w-sm"> | |
| {[ | |
| { icon: 'π¬', label: 'Text Chat', hint: 'Streaming responses' }, | |
| { icon: 'πΌοΈ', label: 'Vision', hint: 'BLIP + CLIP analysis' }, | |
| { icon: 'π€', label: 'Voice', hint: 'Whisper + TTS' }, | |
| ].map(({ icon, label, hint }) => ( | |
| <div key={label} | |
| className="panel p-3 text-center hover:border-jarvis-accent/30 transition-colors"> | |
| <div className="text-2xl mb-1">{icon}</div> | |
| <p className="text-xs font-medium text-jarvis-text">{label}</p> | |
| <p className="text-[10px] text-jarvis-dim">{hint}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| } | |