Download web/src/components/ChatPanel.tsx from thefinalboss/palimpseste-max: direct link, hf CLI and curl.
- Browser
- Download file 11.2 kB
-
https://huggingface.co/thefinalboss/palimpseste-max/resolve/main/web/src/components/ChatPanel.tsx
- Command line
-
hf download hf://thefinalboss/palimpseste-max/web/src/components/ChatPanel.tsx
-
curl -L -o ChatPanel.tsx https://huggingface.co/thefinalboss/palimpseste-max/resolve/main/web/src/components/ChatPanel.tsx
11.2 kB
| import { useState, useRef, useEffect } from 'react' | |
| import type { ChatMessage } from '../types' | |
| import { SourceBadge } from './SourceBadge' | |
| interface ChatPanelProps { | |
| onSend: (message: string, temperature?: number, maxTokens?: number) => Promise<any> | |
| loading: boolean | |
| } | |
| function formatNum(n: number): string { | |
| if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` | |
| if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` | |
| return n.toString() | |
| } | |
| export function ChatPanel({ onSend, loading }: ChatPanelProps) { | |
| const [messages, setMessages] = useState<ChatMessage[]>([]) | |
| const [input, setInput] = useState('') | |
| const [temperature, setTemperature] = useState(0.0) | |
| const [showSettings, setShowSettings] = useState(false) | |
| const scrollRef = useRef<HTMLDivElement>(null) | |
| useEffect(() => { | |
| if (scrollRef.current) { | |
| scrollRef.current.scrollTop = scrollRef.current.scrollHeight | |
| } | |
| }, [messages, loading]) | |
| const handleSend = async () => { | |
| const msg = input.trim() | |
| if (!msg || loading) return | |
| const userMsg: ChatMessage = { | |
| id: `u-${Date.now()}`, | |
| role: 'user', | |
| text: msg, | |
| timestamp: Date.now(), | |
| } | |
| setMessages(prev => [...prev, userMsg]) | |
| setInput('') | |
| try { | |
| const resp = await onSend(msg, temperature) | |
| const botMsg: ChatMessage = { | |
| id: `b-${Date.now()}`, | |
| role: 'palimpseste', | |
| text: resp.response, | |
| confidence: resp.confidence, | |
| source: resp.source, | |
| explanation: resp.explanation, | |
| chain: resp.chain, | |
| elapsed_ms: resp.elapsed_ms, | |
| timestamp: Date.now(), | |
| } | |
| setMessages(prev => [...prev, botMsg]) | |
| } catch (err) { | |
| setMessages(prev => [...prev, { | |
| id: `e-${Date.now()}`, | |
| role: 'palimpseste', | |
| text: '⚠️ Connection error. Is the API server running on :3332?', | |
| source: 'fallback', | |
| confidence: 0, | |
| timestamp: Date.now(), | |
| }]) | |
| } | |
| } | |
| const handleKeyDown = (e: React.KeyboardEvent) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault() | |
| handleSend() | |
| } | |
| } | |
| const clearChat = () => setMessages([]) | |
| return ( | |
| <div className="flex flex-col h-full glass-card overflow-hidden"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between px-5 py-3 border-b border-pal-border"> | |
| <div className="flex items-center gap-3"> | |
| <div className="relative"> | |
| <div className="w-2.5 h-2.5 rounded-full bg-pal-green" /> | |
| <div className="absolute inset-0 w-2.5 h-2.5 rounded-full bg-pal-green animate-ping" /> | |
| </div> | |
| <h2 className="text-sm font-semibold text-pal-text">Cortex Chat</h2> | |
| <span className="text-xs text-pal-muted"> | |
| {messages.length} message{messages.length !== 1 ? 's' : ''} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={() => setShowSettings(!showSettings)} | |
| className="text-pal-muted hover:text-pal-text transition-colors p-1.5 rounded-lg hover:bg-pal-card" | |
| title="Settings" | |
| > | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <circle cx="12" cy="12" r="3" /> | |
| <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /> | |
| </svg> | |
| </button> | |
| {messages.length > 0 && ( | |
| <button | |
| onClick={clearChat} | |
| className="text-pal-muted hover:text-pal-red transition-colors p-1.5 rounded-lg hover:bg-pal-card text-xs" | |
| > | |
| Clear | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| {/* Settings drawer */} | |
| {showSettings && ( | |
| <div className="px-5 py-3 border-b border-pal-border bg-pal-surface/50 animate-slide-up"> | |
| <div className="flex items-center gap-4"> | |
| <label className="text-xs text-pal-muted font-medium whitespace-nowrap"> | |
| Temperature: <span className="text-pal-accent2 font-mono">{temperature.toFixed(2)}</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="1" | |
| step="0.05" | |
| value={temperature} | |
| onChange={e => setTemperature(parseFloat(e.target.value))} | |
| className="flex-1 accent-pal-accent" | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| {/* Messages */} | |
| <div ref={scrollRef} className="flex-1 overflow-y-auto px-5 py-4 space-y-4"> | |
| {messages.length === 0 && ( | |
| <div className="flex flex-col items-center justify-center h-full text-center gap-4 animate-fade-in"> | |
| <div className="text-5xl animate-float">🧠</div> | |
| <div> | |
| <h3 className="text-lg font-bold gradient-text mb-1">PALIMPSESTE</h3> | |
| <p className="text-sm text-pal-muted max-w-xs"> | |
| A self-referential hypervectorial cortex.<br/> | |
| No weights. No gradient. No GPU. | |
| </p> | |
| </div> | |
| <div className="flex flex-wrap gap-2 justify-center max-w-md"> | |
| {['hello', 'who are you?', 'what is python?', 'what is the capital of france'].map(q => ( | |
| <button | |
| key={q} | |
| onClick={() => setInput(q)} | |
| className="px-3 py-1.5 text-xs rounded-full glass hover:border-pal-accent/40 transition-all text-pal-muted hover:text-pal-text" | |
| > | |
| {q} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {messages.map(msg => ( | |
| <div | |
| key={msg.id} | |
| className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} animate-slide-up`} | |
| > | |
| <div | |
| className={`max-w-[80%] rounded-2xl px-4 py-2.5 ${ | |
| msg.role === 'user' | |
| ? 'bg-gradient-to-br from-pal-accent/30 to-pal-accent/10 border border-pal-accent/30' | |
| : 'glass border border-pal-border' | |
| }`} | |
| > | |
| <p className="text-sm text-pal-text whitespace-pre-wrap break-words">{msg.text}</p> | |
| {msg.role === 'palimpseste' && msg.source && ( | |
| <div className="mt-2 pt-2 border-t border-pal-border/50 space-y-1.5"> | |
| <div className="flex items-center justify-between gap-2 flex-wrap"> | |
| <SourceBadge source={msg.source} confidence={msg.confidence ?? 0} /> | |
| {msg.elapsed_ms !== undefined && ( | |
| <span className="text-[10px] text-pal-muted font-mono"> | |
| {msg.elapsed_ms < 1 ? '<1' : msg.elapsed_ms.toFixed(0)}ms | |
| </span> | |
| )} | |
| </div> | |
| {msg.explanation && msg.source !== 'fallback' && ( | |
| <p className="text-[11px] text-pal-muted leading-relaxed"> | |
| <span className="text-pal-accent2/70">💡 </span> | |
| {msg.explanation.split('\n').map((line, i) => ( | |
| <span key={i}>{i > 0 && <br/>}{line}</span> | |
| ))} | |
| </p> | |
| )} | |
| {msg.chain && msg.chain.success && ( | |
| <ChainVisualization chain={msg.chain} /> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| {loading && ( | |
| <div className="flex justify-start animate-fade-in"> | |
| <div className="glass border border-pal-border rounded-2xl px-4 py-3"> | |
| <div className="flex gap-1.5"> | |
| <span className="w-2 h-2 rounded-full bg-pal-accent animate-bounce" style={{ animationDelay: '0ms' }} /> | |
| <span className="w-2 h-2 rounded-full bg-pal-accent animate-bounce" style={{ animationDelay: '150ms' }} /> | |
| <span className="w-2 h-2 rounded-full bg-pal-accent animate-bounce" style={{ animationDelay: '300ms' }} /> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Input */} | |
| <div className="px-4 py-3 border-t border-pal-border"> | |
| <div className="flex gap-2 items-end"> | |
| <textarea | |
| value={input} | |
| onChange={e => setInput(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder="Ask PALIMPSESTE anything..." | |
| rows={1} | |
| className="flex-1 bg-pal-surface border border-pal-border rounded-xl px-4 py-2.5 text-sm text-pal-text placeholder:text-pal-muted focus:outline-none focus:border-pal-accent/50 focus:glow-accent transition-all resize-none max-h-32" | |
| style={{ minHeight: '42px' }} | |
| /> | |
| <button | |
| onClick={handleSend} | |
| disabled={!input.trim() || loading} | |
| className="px-4 py-2.5 rounded-xl bg-gradient-to-br from-pal-accent to-pal-accent2 text-white font-semibold text-sm transition-all hover:scale-105 hover:glow-accent disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:scale-100" | |
| > | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"> | |
| <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| function ChainVisualization({ chain }: { chain: NonNullable<ChatMessage['chain']> }) { | |
| return ( | |
| <div className="mt-2 p-2.5 rounded-lg bg-pal-surface/70 border border-pal-accent/20"> | |
| <div className="text-[10px] text-pal-accent font-semibold mb-1.5 uppercase tracking-wide"> | |
| 🔗 Chain ({chain.n_hops} hops) | |
| </div> | |
| <div className="space-y-1"> | |
| {chain.steps.map((step, i) => ( | |
| <div key={i} className="flex items-start gap-2 text-[11px]"> | |
| <span className="text-pal-muted font-mono mt-0.5">{i + 1}.</span> | |
| <div className="flex-1"> | |
| <span className="text-pal-text">{step.sub_question}</span> | |
| <span className="text-pal-accent2 mx-1">→</span> | |
| <span className="text-pal-green font-medium">{step.sub_answer}</span> | |
| </div> | |
| </div> | |
| ))} | |
| <div className="flex items-center gap-2 text-[11px] pt-1 border-t border-pal-border/50"> | |
| <span className="text-pal-muted">Result:</span> | |
| <span className="text-pal-gold font-semibold">{chain.answer}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |