ZARAI / frontend /src /components /chat /StreamingMessageFeed.jsx
Srajal2579
Deploy JARVIS
3e646d9
Raw History Blame Contribute Delete
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>
)
}