File size: 11,229 Bytes
6e62ad1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28ad71f
6e62ad1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de43da2
6e62ad1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28ad71f
6e62ad1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
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>
  )
}