palimpseste-max / web /src /components /ChatPanel.tsx
thefinalboss's picture
Upload web/src/components/ChatPanel.tsx with huggingface_hub
28ad71f verified
Raw
History Blame Contribute Delete
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>
)
}