KeefeBuild commited on
Commit
0ee7bc1
·
verified ·
1 Parent(s): 0959483

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1406 -25
index.html CHANGED
@@ -1,25 +1,1406 @@
1
- Keefe-Discere Any-to-Any OS
2
-
3
-
4
-
5
-
6
-
7
- Keefe-Discere Multimodal Any-to-Any OS
8
- Static Cloud Router • 100% Free Hosting
9
-
10
-
11
-
12
-
13
- Hugging Face Read Token (Required for Private Models/API):
14
-
15
-
16
-
17
-
18
-
19
- Ready. Enter a prompt or multimodal instruction below...
20
-
21
-
22
-
23
-
24
-
25
- Send
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
+ <title>⚡ Keefe-Discere OS — Multimodal Any-to-Any Edge</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/lucide@latest"></script>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
12
+ <script>
13
+ tailwind.config = {
14
+ theme: {
15
+ extend: {
16
+ fontFamily: {
17
+ sans: ['Inter', 'ui-sans-serif', 'system-ui'],
18
+ mono: ['JetBrains Mono', 'Fira Code', 'ui-monospace'],
19
+ },
20
+ colors: {
21
+ keefe: {
22
+ bg: '#090d16',
23
+ panel: '#0d1420',
24
+ border: '#1e293b',
25
+ indigo: '#6366f1',
26
+ cyan: '#06b6d4',
27
+ emerald: '#10b981',
28
+ amber: '#f59e0b',
29
+ rose: '#f43f5e',
30
+ }
31
+ },
32
+ animation: {
33
+ 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
34
+ 'glow': 'glow 2s ease-in-out infinite alternate',
35
+ 'scan': 'scan 3s linear infinite',
36
+ 'blink': 'blink 1.2s steps(2, start) infinite',
37
+ },
38
+ keyframes: {
39
+ glow: {
40
+ '0%': { boxShadow: '0 0 10px rgba(99, 102, 241, 0.3)' },
41
+ '100%': { boxShadow: '0 0 20px rgba(6, 182, 212, 0.5)' },
42
+ },
43
+ scan: {
44
+ '0%': { transform: 'translateY(-100%)' },
45
+ '100%': { transform: 'translateY(100%)' },
46
+ },
47
+ blink: {
48
+ 'to': { visibility: 'hidden' }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ </script>
55
+ <style>
56
+ * { -webkit-tap-highlight-color: transparent; }
57
+ html, body {
58
+ background-color: #090d16;
59
+ color: #e2e8f0;
60
+ font-family: 'Inter', sans-serif;
61
+ overflow: hidden;
62
+ }
63
+ /* Grid background pattern */
64
+ .cyber-grid {
65
+ background-image:
66
+ linear-gradient(rgba(99, 102, 241, 0.04) 1px, transparent 1px),
67
+ linear-gradient(90deg, rgba(99, 102, 241, 0.04) 1px, transparent 1px);
68
+ background-size: 44px 44px;
69
+ background-position: -1px -1px;
70
+ }
71
+ .cyber-grid::before {
72
+ content: '';
73
+ position: fixed;
74
+ inset: 0;
75
+ background: radial-gradient(ellipse at top, rgba(99, 102, 241, 0.12), transparent 50%),
76
+ radial-gradient(ellipse at bottom, rgba(6, 182, 212, 0.08), transparent 50%);
77
+ pointer-events: none;
78
+ z-index: 0;
79
+ }
80
+
81
+ .glass {
82
+ background: rgba(13, 20, 32, 0.75);
83
+ backdrop-filter: blur(14px);
84
+ -webkit-backdrop-filter: blur(14px);
85
+ border: 1px solid rgba(30, 41, 59, 0.7);
86
+ }
87
+
88
+ .glass-strong {
89
+ background: rgba(9, 13, 22, 0.85);
90
+ backdrop-filter: blur(18px);
91
+ -webkit-backdrop-filter: blur(18px);
92
+ border: 1px solid rgba(30, 41, 59, 0.85);
93
+ }
94
+
95
+ /* Scrollbars */
96
+ .thin-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
97
+ .thin-scroll::-webkit-scrollbar-track { background: transparent; }
98
+ .thin-scroll::-webkit-scrollbar-thumb {
99
+ background: rgba(99, 102, 241, 0.2);
100
+ border-radius: 10px;
101
+ }
102
+ .thin-scroll::-webkit-scrollbar-thumb:hover {
103
+ background: rgba(99, 102, 241, 0.4);
104
+ }
105
+
106
+ /* XML tag styles */
107
+ .xml-block {
108
+ background: linear-gradient(135deg, rgba(245, 158, 11, 0.08), rgba(245, 158, 11, 0.03));
109
+ border: 1px solid rgba(245, 158, 11, 0.25);
110
+ border-left: 3px solid #f59e0b;
111
+ border-radius: 10px;
112
+ padding: 12px 14px;
113
+ font-family: 'JetBrains Mono', monospace;
114
+ font-size: 12.5px;
115
+ color: #fcd34d;
116
+ position: relative;
117
+ overflow: hidden;
118
+ }
119
+ .xml-block::before {
120
+ content: '⚡ TOOL_CALL';
121
+ position: absolute;
122
+ top: -8px;
123
+ right: 10px;
124
+ background: #090d16;
125
+ padding: 0 6px;
126
+ font-size: 9px;
127
+ letter-spacing: 2px;
128
+ color: #f59e0b;
129
+ font-weight: 700;
130
+ }
131
+ .xml-tag { color: #fbbf24; font-weight: 500; }
132
+ .xml-name { color: #fb923c; font-weight: 600; }
133
+ .xml-input { color: #fde68a; }
134
+
135
+ /* Message animations */
136
+ @keyframes slideInUp {
137
+ from { opacity: 0; transform: translateY(12px); }
138
+ to { opacity: 1; transform: translateY(0); }
139
+ }
140
+ .msg-enter { animation: slideInUp 0.35s ease-out forwards; }
141
+
142
+ /* Drop zone pulse */
143
+ .drop-zone.drag-over {
144
+ background: rgba(6, 182, 212, 0.08) !important;
145
+ border-color: #06b6d4 !important;
146
+ box-shadow: inset 0 0 0 2px rgba(6, 182, 212, 0.4), 0 0 30px rgba(6, 182, 212, 0.2);
147
+ }
148
+ .drop-zone.drag-over .drop-label { color: #06b6d4; }
149
+
150
+ /* Blinking cursor */
151
+ .cursor-blink::after {
152
+ content: '▊';
153
+ color: #06b6d4;
154
+ animation: blink 1.2s steps(2, start) infinite;
155
+ margin-left: 2px;
156
+ }
157
+
158
+ /* Status dot pulse */
159
+ .status-dot {
160
+ position: relative;
161
+ }
162
+ .status-dot::after {
163
+ content: '';
164
+ position: absolute;
165
+ inset: -2px;
166
+ border-radius: 999px;
167
+ background: #10b981;
168
+ animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
169
+ opacity: 0.6;
170
+ }
171
+ @keyframes pulse-ring {
172
+ 0%, 100% { transform: scale(1); opacity: 0.6; }
173
+ 50% { transform: scale(1.8); opacity: 0; }
174
+ }
175
+
176
+ /* Scan line effect for active tool calls */
177
+ .scan-line {
178
+ position: absolute;
179
+ top: 0; left: 0; right: 0;
180
+ height: 100%;
181
+ background: linear-gradient(180deg, transparent, rgba(6, 182, 212, 0.2), transparent);
182
+ animation: scan 2.5s linear infinite;
183
+ pointer-events: none;
184
+ }
185
+
186
+ /* Textarea auto resize */
187
+ textarea { resize: none; }
188
+
189
+ /* Button hover effects */
190
+ .btn-primary {
191
+ background: linear-gradient(135deg, #6366f1, #06b6d4);
192
+ transition: all 0.2s ease;
193
+ }
194
+ .btn-primary:hover:not(:disabled) {
195
+ box-shadow: 0 0 25px rgba(99, 102, 241, 0.5), 0 0 45px rgba(6, 182, 212, 0.3);
196
+ transform: translateY(-1px);
197
+ }
198
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
199
+
200
+ /* Modal backdrop */
201
+ .modal-backdrop {
202
+ background: rgba(0, 0, 0, 0.7);
203
+ backdrop-filter: blur(8px);
204
+ }
205
+
206
+ /* Gradient text */
207
+ .gradient-text {
208
+ background: linear-gradient(135deg, #a5b4fc 0%, #67e8f9 50%, #86efac 100%);
209
+ -webkit-background-clip: text;
210
+ background-clip: text;
211
+ -webkit-text-fill-color: transparent;
212
+ }
213
+
214
+ /* Code block for regular code */
215
+ .code-block {
216
+ background: rgba(2, 6, 23, 0.8);
217
+ border: 1px solid rgba(30, 41, 59, 0.8);
218
+ border-radius: 8px;
219
+ padding: 12px;
220
+ font-family: 'JetBrains Mono', monospace;
221
+ font-size: 12px;
222
+ color: #94a3b8;
223
+ overflow-x: auto;
224
+ }
225
+
226
+ /* Tool result card */
227
+ .tool-result {
228
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.06), rgba(6, 182, 212, 0.03));
229
+ border: 1px solid rgba(16, 185, 129, 0.25);
230
+ border-left: 3px solid #10b981;
231
+ border-radius: 10px;
232
+ padding: 12px 14px;
233
+ }
234
+ .tool-result::before {
235
+ content: '✓ TOOL_RESULT';
236
+ display: block;
237
+ font-family: 'JetBrains Mono', monospace;
238
+ font-size: 9px;
239
+ letter-spacing: 2px;
240
+ color: #10b981;
241
+ font-weight: 700;
242
+ margin-bottom: 6px;
243
+ }
244
+
245
+ /* Error card */
246
+ .error-block {
247
+ background: linear-gradient(135deg, rgba(244, 63, 94, 0.08), rgba(244, 63, 94, 0.03));
248
+ border: 1px solid rgba(244, 63, 94, 0.25);
249
+ border-left: 3px solid #f43f5e;
250
+ border-radius: 10px;
251
+ padding: 12px 14px;
252
+ color: #fca5a5;
253
+ }
254
+
255
+ /* Chip styles */
256
+ .chip {
257
+ display: inline-flex;
258
+ align-items: center;
259
+ gap: 4px;
260
+ padding: 3px 9px;
261
+ border-radius: 999px;
262
+ font-size: 11px;
263
+ font-weight: 500;
264
+ background: rgba(30, 41, 59, 0.7);
265
+ border: 1px solid rgba(51, 65, 85, 0.6);
266
+ color: #cbd5e1;
267
+ }
268
+
269
+ /* Prevent text selection on UI */
270
+ .no-select { user-select: none; }
271
+
272
+ /* Loading dots */
273
+ .loading-dots span {
274
+ display: inline-block;
275
+ width: 6px;
276
+ height: 6px;
277
+ margin: 0 2px;
278
+ background: #06b6d4;
279
+ border-radius: 50%;
280
+ animation: bounce 1.4s infinite ease-in-out both;
281
+ }
282
+ .loading-dots span:nth-child(1) { animation-delay: -0.32s; }
283
+ .loading-dots span:nth-child(2) { animation-delay: -0.16s; }
284
+ @keyframes bounce {
285
+ 0%, 80%, 100% { transform: scale(0); }
286
+ 40% { transform: scale(1); }
287
+ }
288
+
289
+ /* Focus ring */
290
+ input:focus, textarea:focus {
291
+ outline: none;
292
+ }
293
+ .focus-ring:focus {
294
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.4);
295
+ }
296
+ </style>
297
+ </head>
298
+ <body class="cyber-grid h-screen w-screen relative">
299
+
300
+ <!-- ===== MAIN CONTAINER ===== -->
301
+ <div id="app" class="relative z-10 h-full w-full flex flex-col">
302
+
303
+ <!-- ===== TOP BAR ===== -->
304
+ <header class="glass-strong border-b border-slate-800/80 px-4 sm:px-6 py-3 flex items-center justify-between gap-4 flex-shrink-0">
305
+ <div class="flex items-center gap-3 min-w-0">
306
+ <div class="w-9 h-9 rounded-lg bg-gradient-to-br from-indigo-500 to-cyan-500 flex items-center justify-center flex-shrink-0 shadow-lg shadow-indigo-500/30">
307
+ <i data-lucide="zap" class="w-5 h-5 text-white"></i>
308
+ </div>
309
+ <div class="min-w-0">
310
+ <h1 class="text-sm sm:text-base font-bold gradient-text truncate">Keefe-Discere OS</h1>
311
+ <div class="flex items-center gap-2 mt-0.5">
312
+ <span class="status-dot w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block"></span>
313
+ <span class="text-[10px] text-slate-400 font-mono tracking-wide">Online • Static Edge Router</span>
314
+ </div>
315
+ </div>
316
+ </div>
317
+
318
+ <div class="flex items-center gap-2 flex-shrink-0">
319
+ <div id="modelChip" class="chip hidden sm:inline-flex">
320
+ <i data-lucide="cpu" class="w-3 h-3"></i>
321
+ <span class="font-mono">KeefeBuild/Keefe-Discere</span>
322
+ </div>
323
+ <button id="tokenBtn" class="relative px-3 py-1.5 rounded-lg glass hover:bg-slate-800/80 transition-colors flex items-center gap-2 group">
324
+ <i data-lucide="key-round" class="w-4 h-4 text-slate-400 group-hover:text-indigo-400"></i>
325
+ <span class="text-xs font-medium text-slate-300 hidden sm:inline" id="tokenStatus">Set Token</span>
326
+ <span id="tokenSavedBadge" class="hidden absolute -top-1 -right-1 w-2 h-2 rounded-full bg-emerald-400 ring-2 ring-keefe-bg"></span>
327
+ </button>
328
+ <button id="clearBtn" class="p-2 rounded-lg glass hover:bg-slate-800/80 transition-colors" title="Clear chat">
329
+ <i data-lucide="trash-2" class="w-4 h-4 text-slate-400"></i>
330
+ </button>
331
+ </div>
332
+ </header>
333
+
334
+ <!-- ===== CHAT AREA ===== -->
335
+ <main class="flex-1 flex flex-col min-h-0 relative">
336
+
337
+ <!-- System Prompt Collapsible -->
338
+ <div class="px-4 sm:px-6 pt-3 flex-shrink-0">
339
+ <button id="systemPromptToggle" class="flex items-center gap-2 text-xs text-slate-400 hover:text-slate-200 transition-colors group">
340
+ <i data-lucide="settings-2" class="w-3.5 h-3.5"></i>
341
+ <span class="font-mono tracking-wide">SYSTEM_INSTRUCTIONS</span>
342
+ <i data-lucide="chevron-down" id="sysPromptIcon" class="w-3.5 h-3.5 transition-transform"></i>
343
+ </button>
344
+ <div id="systemPromptPanel" class="hidden mt-2">
345
+ <div class="glass rounded-xl p-3">
346
+ <textarea id="systemPromptInput" rows="4"
347
+ class="w-full bg-slate-950/60 text-slate-200 text-xs font-mono p-3 rounded-lg border border-slate-800 focus-ring"
348
+ placeholder="Enter system prompt...">You are Keefe-Discere, an advanced multimodal AI operating system. You can reason across text, images, and audio inputs. When you need to use an external tool, you MUST emit a tool call in this exact format:
349
+
350
+ <tool_call><name>tool_name</name><input>input_data</input></tool_call>
351
+
352
+ Available tools:
353
+ - ocr_vision: Extract text from an image URL (input: image URL)
354
+ - transcribe_audio: Transcribe audio (input: audio URL or description)
355
+ - image_caption: Generate a detailed caption for an image (input: image URL)
356
+ - web_search: Search the web for information (input: query)
357
+ - image_generation: Generate an image from a text prompt (input: prompt)
358
+
359
+ Be concise, precise, and always acknowledge multimodal inputs. When routing to a tool, wait for its result before responding.</textarea>
360
+ <div class="flex items-center justify-between mt-2">
361
+ <span class="text-[10px] text-slate-500 font-mono">Tokens auto-injected into every request</span>
362
+ <span class="text-[10px] text-emerald-400 font-mono">● LIVE</span>
363
+ </div>
364
+ </div>
365
+ </div>
366
+ </div>
367
+
368
+ <!-- Message Stream -->
369
+ <div id="messageStream" class="flex-1 overflow-y-auto thin-scroll px-4 sm:px-6 py-4 min-h-0">
370
+ <div id="messageContainer" class="max-w-4xl mx-auto space-y-4">
371
+
372
+ <!-- Welcome message -->
373
+ <div id="welcomeScreen" class="flex flex-col items-center justify-center py-12 text-center">
374
+ <div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-indigo-500/20 to-cyan-500/20 border border-indigo-500/30 flex items-center justify-center mb-6 animate-glow">
375
+ <i data-lucide="sparkles" class="w-10 h-10 text-indigo-400"></i>
376
+ </div>
377
+ <h2 class="text-2xl sm:text-3xl font-bold gradient-text mb-3">Welcome to Keefe-Discere OS</h2>
378
+ <p class="text-slate-400 max-w-md text-sm sm:text-base leading-relaxed">
379
+ Multimodal any-to-any intelligence router. Drop an image, attach audio, or just type — and watch XML tool calls flow through the edge in real-time.
380
+ </p>
381
+ <div class="flex flex-wrap gap-2 mt-6 justify-center">
382
+ <div class="chip"><i data-lucide="image" class="w-3 h-3"></i>Vision</div>
383
+ <div class="chip"><i data-lucide="mic" class="w-3 h-3"></i>Audio</div>
384
+ <div class="chip"><i data-lucide="text" class="w-3 h-3"></i>Text</div>
385
+ <div class="chip"><i data-lucide="route" class="w-3 h-3"></i>Tool Routing</div>
386
+ </div>
387
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-8 max-w-xl w-full">
388
+ <button class="suggestion-btn glass rounded-xl p-3 text-left hover:bg-slate-800/80 transition-colors group" data-prompt="Analyze this screenshot and tell me what UI patterns are used">
389
+ <div class="flex items-center gap-2 mb-1">
390
+ <i data-lucide="layout" class="w-3.5 h-3.5 text-indigo-400"></i>
391
+ <span class="text-xs font-semibold text-slate-200">UI Analysis</span>
392
+ </div>
393
+ <p class="text-[11px] text-slate-400 line-clamp-2">Analyze a screenshot and identify UI patterns...</p>
394
+ </button>
395
+ <button class="suggestion-btn glass rounded-xl p-3 text-left hover:bg-slate-800/80 transition-colors group" data-prompt="Generate a cyberpunk logo for a startup called 'Nexora'">
396
+ <div class="flex items-center gap-2 mb-1">
397
+ <i data-lucide="palette" class="w-3.5 h-3.5 text-cyan-400"></i>
398
+ <span class="text-xs font-semibold text-slate-200">Logo Generation</span>
399
+ </div>
400
+ <p class="text-[11px] text-slate-400 line-clamp-2">Design a logo with tool routing...</p>
401
+ </button>
402
+ <button class="suggestion-btn glass rounded-xl p-3 text-left hover:bg-slate-800/80 transition-colors group" data-prompt="What are the latest breakthroughs in multimodal AI this month?">
403
+ <div class="flex items-center gap-2 mb-1">
404
+ <i data-lucide="globe" class="w-3.5 h-3.5 text-emerald-400"></i>
405
+ <span class="text-xs font-semibold text-slate-200">Web Research</span>
406
+ </div>
407
+ <p class="text-[11px] text-slate-400 line-clamp-2">Search for recent AI breakthroughs...</p>
408
+ </button>
409
+ <button class="suggestion-btn glass rounded-xl p-3 text-left hover:bg-slate-800/80 transition-colors group" data-prompt="Help me debug this error: TypeError cannot read property of undefined">
410
+ <div class="flex items-center gap-2 mb-1">
411
+ <i data-lucide="bug" class="w-3.5 h-3.5 text-amber-400"></i>
412
+ <span class="text-xs font-semibold text-slate-200">Code Debug</span>
413
+ </div>
414
+ <p class="text-[11px] text-slate-400 line-clamp-2">Diagnose and resolve coding errors...</p>
415
+ </button>
416
+ </div>
417
+ </div>
418
+
419
+ </div>
420
+ </div>
421
+
422
+ <!-- Input Zone -->
423
+ <div class="px-4 sm:px-6 pb-4 pt-2 flex-shrink-0">
424
+ <div id="dropZone" class="drop-zone max-w-4xl mx-auto glass-strong rounded-2xl p-3 transition-all relative overflow-hidden">
425
+ <!-- Attached files preview -->
426
+ <div id="attachedFiles" class="hidden mb-2 flex flex-wrap gap-2"></div>
427
+
428
+ <!-- Drop label (shown when empty & dragging) -->
429
+ <div id="dropLabel" class="hidden text-center py-2">
430
+ <i data-lucide="upload-cloud" class="w-6 h-6 text-cyan-400 mx-auto mb-1"></i>
431
+ <p class="text-sm text-cyan-300 font-medium">Drop files here</p>
432
+ <p class="text-xs text-slate-500">PNG, JPG, MP3, WAV supported</p>
433
+ </div>
434
+
435
+ <div class="flex items-end gap-2">
436
+ <!-- Attach button -->
437
+ <button id="attachBtn" class="p-2.5 rounded-xl bg-slate-800/80 hover:bg-slate-700/80 transition-colors flex-shrink-0 group relative">
438
+ <i data-lucide="paperclip" class="w-4 h-4 text-slate-400 group-hover:text-cyan-400"></i>
439
+ <input type="file" id="fileInput" class="absolute inset-0 opacity-0 cursor-pointer" accept="image/png,image/jpeg,image/jpg,image/webp,image/gif,audio/mpeg,audio/wav,audio/mp3,audio/ogg" multiple>
440
+ </button>
441
+
442
+ <!-- Textarea -->
443
+ <div class="flex-1 min-w-0">
444
+ <textarea id="userInput" rows="1"
445
+ class="w-full bg-slate-950/60 text-slate-100 text-sm p-3 rounded-xl border border-slate-800 focus-ring transition-all placeholder:text-slate-600"
446
+ placeholder="Message Keefe-Discere... (Enter to send, Shift+Enter for newline)"
447
+ maxlength="8000"></textarea>
448
+ </div>
449
+
450
+ <!-- Send button -->
451
+ <button id="sendBtn" class="btn-primary p-2.5 rounded-xl flex-shrink-0 flex items-center justify-center" title="Send (Enter)">
452
+ <i data-lucide="send-horizontal" class="w-4 h-4 text-white"></i>
453
+ </button>
454
+ </div>
455
+
456
+ <div class="flex items-center justify-between mt-2 px-1">
457
+ <div class="flex items-center gap-3 text-[10px] text-slate-500 font-mono">
458
+ <span class="flex items-center gap-1"><i data-lucide="command" class="w-2.5 h-2.5"></i>ENTER to send</span>
459
+ <span class="flex items-center gap-1"><i data-lucide="shift" class="w-2.5 h-2.5"></i>+↵ for newline</span>
460
+ </div>
461
+ <div id="charCount" class="text-[10px] text-slate-500 font-mono">0 / 8000</div>
462
+ </div>
463
+ </div>
464
+ </div>
465
+
466
+ </main>
467
+
468
+ </div>
469
+
470
+ <!-- ===== TOKEN MODAL ===== -->
471
+ <div id="tokenModal" class="fixed inset-0 z-50 hidden items-center justify-center p-4 modal-backdrop">
472
+ <div class="glass-strong rounded-2xl p-6 w-full max-w-md shadow-2xl border border-slate-800/80">
473
+ <div class="flex items-center justify-between mb-4">
474
+ <div>
475
+ <h3 class="text-base font-bold gradient-text">HF Access Token</h3>
476
+ <p class="text-xs text-slate-400 mt-0.5">Stored locally in your browser only</p>
477
+ </div>
478
+ <button id="closeTokenModal" class="p-1.5 rounded-lg hover:bg-slate-800/80 transition-colors">
479
+ <i data-lucide="x" class="w-4 h-4 text-slate-400"></i>
480
+ </button>
481
+ </div>
482
+
483
+ <div class="space-y-3">
484
+ <div class="relative">
485
+ <input type="password" id="tokenInput"
486
+ class="w-full bg-slate-950/80 text-slate-100 text-sm font-mono p-3 pr-10 rounded-lg border border-slate-800 focus-ring"
487
+ placeholder="hf_xxxxxxxxxxxxxxxxxxxxx" autocomplete="off">
488
+ <button id="toggleTokenVis" class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded hover:bg-slate-800/80 transition-colors">
489
+ <i data-lucide="eye" id="eyeIcon" class="w-4 h-4 text-slate-400"></i>
490
+ </button>
491
+ </div>
492
+
493
+ <div class="flex items-center justify-between text-xs">
494
+ <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener"
495
+ class="text-indigo-400 hover:text-indigo-300 underline-offset-2 hover:underline flex items-center gap-1">
496
+ <i data-lucide="external-link" class="w-3 h-3"></i> Get token at huggingface.co
497
+ </a>
498
+ <span id="tokenSavedStatus" class="text-emerald-400 hidden">✓ Saved</span>
499
+ </div>
500
+
501
+ <div class="flex gap-2 pt-2">
502
+ <button id="saveTokenBtn" class="btn-primary flex-1 py-2.5 rounded-lg text-sm font-semibold text-white flex items-center justify-center gap-2">
503
+ <i data-lucide="shield-check" class="w-4 h-4"></i> Save & Encrypt Locally
504
+ </button>
505
+ <button id="clearTokenBtn" class="px-4 py-2.5 rounded-lg bg-slate-800/80 hover:bg-slate-700/80 transition-colors text-sm text-slate-300">
506
+ Clear
507
+ </button>
508
+ </div>
509
+
510
+ <div class="bg-slate-950/50 rounded-lg p-3 border border-slate-800/80">
511
+ <p class="text-[10px] text-slate-500 leading-relaxed font-mono">
512
+ <i data-lucide="info" class="w-3 h-3 inline mr-1"></i>
513
+ Token never leaves your browser except for direct HTTPS calls to the Hugging Face Inference API (api-inference.huggingface.co). No analytics, no tracking.
514
+ </p>
515
+ </div>
516
+ </div>
517
+ </div>
518
+ </div>
519
+
520
+ <!-- ===== TOAST CONTAINER ===== -->
521
+ <div id="toastContainer" class="fixed bottom-24 right-4 z-50 space-y-2 pointer-events-none"></div>
522
+
523
+ <script type="module">
524
+ import { InferenceClient } from "https://cdn.jsdelivr.net/npm/@huggingface/inference@4.13.22/+esm";
525
+
526
+ // ====================== STATE ======================
527
+ const state = {
528
+ token: localStorage.getItem('keefe_hf_token') || '',
529
+ model: 'KeefeBuild/Keefe-Discere',
530
+ messages: [], // {role: 'user'|'assistant'|'system', content: string, files?: []}
531
+ attachedFiles: [], // {file, dataUrl, type, name}
532
+ isStreaming: false,
533
+ toolCallDepth: 0,
534
+ maxToolCallDepth: 3,
535
+ };
536
+
537
+ // ====================== DOM REFS ======================
538
+ const $ = (id) => document.getElementById(id);
539
+ const messageContainer = $('messageContainer');
540
+ const messageStream = $('messageStream');
541
+ const userInput = $('userInput');
542
+ const sendBtn = $('sendBtn');
543
+ const attachBtn = $('attachBtn');
544
+ const fileInput = $('fileInput');
545
+ const attachedFilesEl = $('attachedFiles');
546
+ const dropZone = $('dropZone');
547
+ const dropLabel = $('dropLabel');
548
+ const charCount = $('charCount');
549
+ const tokenModal = $('tokenModal');
550
+ const tokenInput = $('tokenInput');
551
+ const tokenBtn = $('tokenBtn');
552
+ const tokenStatus = $('tokenStatus');
553
+ const tokenSavedBadge = $('tokenSavedBadge');
554
+ const systemPromptToggle = $('systemPromptToggle');
555
+ const systemPromptPanel = $('systemPromptPanel');
556
+ const systemPromptInput = $('systemPromptInput');
557
+ const sysPromptIcon = $('sysPromptIcon');
558
+ const clearBtn = $('clearBtn');
559
+ const welcomeScreen = $('welcomeScreen');
560
+
561
+ // ====================== UTILITIES ======================
562
+ function escapeHtml(str) {
563
+ return String(str)
564
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
565
+ .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
566
+ }
567
+
568
+ function showToast(msg, type = 'info', duration = 3000) {
569
+ const colors = {
570
+ info: 'border-indigo-500/50 bg-indigo-500/10 text-indigo-200',
571
+ success: 'border-emerald-500/50 bg-emerald-500/10 text-emerald-200',
572
+ error: 'border-rose-500/50 bg-rose-500/10 text-rose-200',
573
+ warning: 'border-amber-500/50 bg-amber-500/10 text-amber-200',
574
+ };
575
+ const icons = {
576
+ info: 'info', success: 'check-circle-2', error: 'alert-circle', warning: 'alert-triangle'
577
+ };
578
+ const toast = document.createElement('div');
579
+ toast.className = `glass-strong rounded-lg px-4 py-3 border ${colors[type]} shadow-xl pointer-events-auto max-w-sm msg-enter flex items-start gap-3`;
580
+ toast.innerHTML = `
581
+ <i data-lucide="${icons[type]}" class="w-4 h-4 mt-0.5 flex-shrink-0"></i>
582
+ <span class="text-xs leading-relaxed">${escapeHtml(msg)}</span>
583
+ `;
584
+ $('toastContainer').appendChild(toast);
585
+ lucide.createIcons({ nodes: [toast] });
586
+ setTimeout(() => {
587
+ toast.style.transition = 'opacity 0.3s, transform 0.3s';
588
+ toast.style.opacity = '0';
589
+ toast.style.transform = 'translateX(20px)';
590
+ setTimeout(() => toast.remove(), 300);
591
+ }, duration);
592
+ }
593
+
594
+ function scrollToBottom() {
595
+ requestAnimationFrame(() => {
596
+ messageStream.scrollTop = messageStream.scrollHeight;
597
+ });
598
+ }
599
+
600
+ function autoResizeTextarea() {
601
+ userInput.style.height = 'auto';
602
+ userInput.style.height = Math.min(userInput.scrollHeight, 180) + 'px';
603
+ }
604
+
605
+ // ====================== MARKDOWN / XML RENDERER ======================
606
+ function renderContent(text) {
607
+ if (!text) return '';
608
+ let html = escapeHtml(text);
609
+
610
+ // Code blocks ```lang\ncode```
611
+ html = html.replace(/```(\w+)?\n?([\s\S]*?)```/g, (m, lang, code) => {
612
+ return `<pre class="code-block my-2"><code class="language-${lang || 'text'}">${code.trim()}</code></pre>`;
613
+ });
614
+
615
+ // Inline code
616
+ html = html.replace(/`([^`]+)`/g, '<code class="bg-slate-800/80 px-1.5 py-0.5 rounded text-xs text-cyan-300 font-mono">$1</code>');
617
+
618
+ // Bold
619
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong class="font-semibold text-slate-100">$1</strong>');
620
+
621
+ // Italic
622
+ html = html.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
623
+
624
+ // Headings
625
+ html = html.replace(/^### (.+)$/gm, '<h3 class="font-bold text-base mt-3 mb-1 text-slate-100">$1</h3>');
626
+ html = html.replace(/^## (.+)$/gm, '<h2 class="font-bold text-lg mt-3 mb-1 text-slate-100">$1</h2>');
627
+ html = html.replace(/^# (.+)$/gm, '<h1 class="font-bold text-xl mt-3 mb-2 text-slate-100">$1</h1>');
628
+
629
+ // Lists
630
+ html = html.replace(/^- (.+)$/gm, '<li class="ml-4 list-disc text-slate-300">$1</li>');
631
+ html = html.replace(/^(\d+)\. (.+)$/gm, '<li class="ml-4 list-decimal text-slate-300">$2</li>');
632
+ html = html.replace(/(<li[^>]*>.*?<\/li>\n?)+/g, (m) => `<ul class="my-2 space-y-0.5">${m}</ul>`);
633
+
634
+ // Links
635
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener" class="text-cyan-400 hover:text-cyan-300 underline underline-offset-2">$1</a>');
636
+
637
+ // Bare URLs
638
+ html = html.replace(/(?<!\()https?:\/\/[^\s<)]+/g, (url) => {
639
+ if (url.includes('class="') || url.includes('href="')) return url;
640
+ return `<a href="${url}" target="_blank" rel="noopener" class="text-cyan-400 hover:text-cyan-300 break-all">${url}</a>`;
641
+ });
642
+
643
+ // Tool calls: extract <tool_call>...</tool_call> blocks
644
+ html = html.replace(/&lt;tool_call&gt;([\s\S]*?)&lt;\/tool_call&gt;/g, (m, inner) => {
645
+ // Parse name & input
646
+ const nameMatch = inner.match(/&lt;name&gt;([\s\S]*?)&lt;\/name&gt;/);
647
+ const inputMatch = inner.match(/&lt;input&gt;([\s\S]*?)&lt;\/input&gt;/);
648
+ const name = nameMatch ? nameMatch[1] : 'unknown';
649
+ const input = inputMatch ? inputMatch[1] : '';
650
+ const icon = getToolIcon(name);
651
+ return `
652
+ <div class="xml-block my-3">
653
+ <div class="flex items-center gap-2 mb-2">
654
+ <i data-lucide="${icon}" class="w-3.5 h-3.5 text-amber-400"></i>
655
+ <span class="text-[10px] font-mono tracking-wider text-amber-300 uppercase">${escapeHtml(name)}</span>
656
+ </div>
657
+ <div class="flex flex-col gap-1 text-[11.5px]">
658
+ <div><span class="xml-tag">&lt;name&gt;</span><span class="xml-name">${escapeHtml(name)}</span><span class="xml-tag">&lt;/name&gt;</span></div>
659
+ <div class="break-all"><span class="xml-tag">&lt;input&gt;</span><span class="xml-input">${escapeHtml(input)}</span><span class="xml-tag">&lt;/input&gt;</span></div>
660
+ </div>
661
+ </div>
662
+ `;
663
+ });
664
+
665
+ // Standalone XML-ish tags that weren't tool_calls
666
+ html = html.replace(/&lt;(\/?[a-zA-Z_][\w\-]*)&gt;/g, '<span class="xml-tag">&lt;$1&gt;</span>');
667
+
668
+ // Paragraphs / newlines
669
+ html = html.split('\n\n').map(p => {
670
+ if (p.includes('<pre') || p.includes('<div class="xml-block"') || p.includes('<h') || p.includes('<ul')) return p;
671
+ return `<p class="mb-2 leading-relaxed text-slate-200">${p.replace(/\n/g, '<br>')}</p>`;
672
+ }).join('');
673
+
674
+ return html;
675
+ }
676
+
677
+ function getToolIcon(name) {
678
+ const icons = {
679
+ ocr_vision: 'scan-eye',
680
+ transcribe_audio: 'audio-waveform',
681
+ image_caption: 'image-text',
682
+ web_search: 'globe',
683
+ image_generation: 'image-plus',
684
+ code_execute: 'terminal',
685
+ math: 'sigma',
686
+ };
687
+ return icons[name] || 'wrench';
688
+ }
689
+
690
+ // ====================== MESSAGE RENDERING ======================
691
+ function addMessage(role, content, files = [], opts = {}) {
692
+ if (welcomeScreen && !welcomeScreen.classList.contains('hidden')) {
693
+ welcomeScreen.classList.add('hidden');
694
+ }
695
+
696
+ const msgId = 'msg_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
697
+ const msg = { id: msgId, role, content, files, timestamp: new Date() };
698
+ state.messages.push(msg);
699
+
700
+ const wrapper = document.createElement('div');
701
+ wrapper.id = msgId;
702
+ wrapper.className = `msg-enter flex gap-3 ${role === 'user' ? 'justify-end' : ''}`;
703
+
704
+ if (role === 'user') {
705
+ wrapper.innerHTML = `
706
+ <div class="max-w-[85%] sm:max-w-[75%]">
707
+ ${files.length ? renderUserFiles(files) : ''}
708
+ <div class="bg-gradient-to-br from-indigo-600/90 to-indigo-700/90 rounded-2xl rounded-tr-sm px-4 py-2.5 shadow-lg shadow-indigo-900/30">
709
+ <div class="text-sm text-slate-50 leading-relaxed whitespace-pre-wrap break-words">${escapeHtml(content)}</div>
710
+ </div>
711
+ <div class="text-[10px] text-slate-500 font-mono mt-1 text-right">
712
+ <i data-lucide="user" class="w-2.5 h-2.5 inline"></i> YOU • ${formatTime(msg.timestamp)}
713
+ </div>
714
+ </div>
715
+ `;
716
+ } else if (role === 'system-log') {
717
+ wrapper.className = 'msg-enter flex justify-center';
718
+ wrapper.innerHTML = `
719
+ <div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-slate-800/60 border border-slate-700/60">
720
+ <i data-lucide="${opts.icon || 'info'}" class="w-3 h-3 text-slate-400"></i>
721
+ <span class="text-[11px] font-mono text-slate-400">${escapeHtml(content)}</span>
722
+ </div>
723
+ `;
724
+ } else if (role === 'tool-call') {
725
+ wrapper.className = 'msg-enter flex justify-center';
726
+ wrapper.innerHTML = `
727
+ <div class="glass rounded-xl px-4 py-2.5 border-amber-500/40 shadow-lg max-w-md relative overflow-hidden">
728
+ <div class="scan-line"></div>
729
+ <div class="relative flex items-center gap-3">
730
+ <div class="w-7 h-7 rounded-lg bg-amber-500/20 border border-amber-500/40 flex items-center justify-center">
731
+ <i data-lucide="${opts.icon || 'route'}" class="w-3.5 h-3.5 text-amber-400"></i>
732
+ </div>
733
+ <div>
734
+ <div class="text-[10px] font-mono tracking-wider text-amber-300 uppercase">Cloud Action Badge</div>
735
+ <div class="text-xs text-slate-200">${escapeHtml(content)}</div>
736
+ </div>
737
+ <div class="loading-dots"><span></span><span></span><span></span></div>
738
+ </div>
739
+ </div>
740
+ `;
741
+ } else if (role === 'tool-result') {
742
+ wrapper.innerHTML = `
743
+ <div class="w-full max-w-3xl">
744
+ <div class="tool-result">
745
+ <div class="flex items-center gap-2 mb-2">
746
+ <i data-lucide="${opts.icon || 'check'}" class="w-3.5 h-3.5 text-emerald-400"></i>
747
+ <span class="text-[10px] font-mono tracking-wider text-emerald-300 uppercase">${escapeHtml(opts.toolName || 'RESULT')}</span>
748
+ <span class="text-[10px] text-slate-500 font-mono ml-auto">${formatTime(msg.timestamp)}</span>
749
+ </div>
750
+ <div class="text-xs text-slate-300 whitespace-pre-wrap break-words leading-relaxed">${opts.html ? opts.html : escapeHtml(content)}</div>
751
+ </div>
752
+ </div>
753
+ `;
754
+ } else {
755
+ // Assistant
756
+ wrapper.innerHTML = `
757
+ <div class="w-full max-w-3xl">
758
+ <div class="flex items-center gap-2 mb-2">
759
+ <div class="w-6 h-6 rounded-lg bg-gradient-to-br from-indigo-500 to-cyan-500 flex items-center justify-center">
760
+ <i data-lucide="zap" class="w-3 h-3 text-white"></i>
761
+ </div>
762
+ <span class="text-xs font-semibold text-slate-300">Keefe-Discere</span>
763
+ <span class="text-[10px] text-slate-500 font-mono">• ${formatTime(msg.timestamp)}</span>
764
+ </div>
765
+ <div class="glass rounded-2xl rounded-tl-sm px-4 py-3 text-sm leading-relaxed break-words assistant-content">
766
+ ${opts.html || renderContent(content)}
767
+ </div>
768
+ </div>
769
+ `;
770
+ }
771
+
772
+ messageContainer.appendChild(wrapper);
773
+ lucide.createIcons({ nodes: [wrapper] });
774
+ scrollToBottom();
775
+ return msgId;
776
+ }
777
+
778
+ function renderUserFiles(files) {
779
+ return `<div class="flex flex-wrap gap-2 mb-2 justify-end">
780
+ ${files.map(f => {
781
+ if (f.type.startsWith('image/')) {
782
+ return `<img src="${f.dataUrl}" alt="${escapeHtml(f.name)}" class="h-24 w-24 object-cover rounded-lg border border-slate-700">`;
783
+ } else if (f.type.startsWith('audio/')) {
784
+ return `<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/80 border border-slate-700">
785
+ <i data-lucide="audio-waveform" class="w-4 h-4 text-cyan-400"></i>
786
+ <span class="text-xs text-slate-300">${escapeHtml(f.name)}</span>
787
+ </div>`;
788
+ }
789
+ return '';
790
+ }).join('')}
791
+ </div>`;
792
+ }
793
+
794
+ function updateAssistantMessage(msgId, content) {
795
+ const el = document.getElementById(msgId);
796
+ if (!el) return;
797
+ const contentEl = el.querySelector('.assistant-content');
798
+ if (!contentEl) return;
799
+ contentEl.innerHTML = renderContent(content);
800
+ lucide.createIcons({ nodes: [contentEl] });
801
+ scrollToBottom();
802
+ }
803
+
804
+ function removeMessage(msgId) {
805
+ const el = document.getElementById(msgId);
806
+ if (el) el.remove();
807
+ state.messages = state.messages.filter(m => m.id !== msgId);
808
+ }
809
+
810
+ function formatTime(d) {
811
+ const date = new Date(d);
812
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
813
+ }
814
+
815
+ // ====================== FILE HANDLING ======================
816
+ function readFileAsDataURL(file) {
817
+ return new Promise((resolve, reject) => {
818
+ const reader = new FileReader();
819
+ reader.onload = () => resolve(reader.result);
820
+ reader.onerror = reject;
821
+ reader.readAsDataURL(file);
822
+ });
823
+ }
824
+
825
+ async function handleFiles(fileList) {
826
+ const files = Array.from(fileList);
827
+ for (const file of files) {
828
+ if (!file.type.startsWith('image/') && !file.type.startsWith('audio/')) {
829
+ showToast(`Unsupported file type: ${file.type}`, 'error');
830
+ continue;
831
+ }
832
+ if (file.size > 20 * 1024 * 1024) {
833
+ showToast(`File too large (max 20MB): ${file.name}`, 'error');
834
+ continue;
835
+ }
836
+ try {
837
+ const dataUrl = await readFileAsDataURL(file);
838
+ state.attachedFiles.push({ file, dataUrl, type: file.type, name: file.name });
839
+ } catch (err) {
840
+ console.error(err);
841
+ showToast(`Failed to read: ${file.name}`, 'error');
842
+ }
843
+ }
844
+ renderAttachedFiles();
845
+ }
846
+
847
+ function renderAttachedFiles() {
848
+ if (state.attachedFiles.length === 0) {
849
+ attachedFilesEl.classList.add('hidden');
850
+ attachedFilesEl.innerHTML = '';
851
+ return;
852
+ }
853
+ attachedFilesEl.classList.remove('hidden');
854
+ attachedFilesEl.innerHTML = state.attachedFiles.map((f, idx) => {
855
+ if (f.type.startsWith('image/')) {
856
+ return `<div class="relative group">
857
+ <img src="${f.dataUrl}" alt="${escapeHtml(f.name)}" class="h-16 w-16 object-cover rounded-lg border border-slate-700">
858
+ <button class="remove-file absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-rose-500 hover:bg-rose-600 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity" data-idx="${idx}">
859
+ <i data-lucide="x" class="w-3 h-3 text-white"></i>
860
+ </button>
861
+ <div class="text-[9px] text-slate-400 mt-0.5 truncate max-w-[64px] font-mono">${escapeHtml(f.name)}</div>
862
+ </div>`;
863
+ } else {
864
+ return `<div class="relative group flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/80 border border-slate-700">
865
+ <i data-lucide="audio-waveform" class="w-4 h-4 text-cyan-400"></i>
866
+ <span class="text-xs text-slate-300 max-w-[120px] truncate">${escapeHtml(f.name)}</span>
867
+ <button class="remove-file p-1 rounded hover:bg-slate-700/80" data-idx="${idx}">
868
+ <i data-lucide="x" class="w-3 h-3 text-slate-400"></i>
869
+ </button>
870
+ </div>`;
871
+ }
872
+ }).join('');
873
+
874
+ attachedFilesEl.querySelectorAll('.remove-file').forEach(btn => {
875
+ btn.addEventListener('click', (e) => {
876
+ const idx = parseInt(btn.dataset.idx);
877
+ state.attachedFiles.splice(idx, 1);
878
+ renderAttachedFiles();
879
+ });
880
+ });
881
+
882
+ lucide.createIcons({ nodes: [attachedFilesEl] });
883
+ }
884
+
885
+ // ====================== XML TOOL PARSER ======================
886
+ function parseToolCalls(text) {
887
+ const calls = [];
888
+ const regex = /<tool_call>\s*<name>([\s\S]*?)<\/name>\s*<input>([\s\S]*?)<\/input>\s*<\/tool_call>/g;
889
+ let match;
890
+ while ((match = regex.exec(text)) !== null) {
891
+ calls.push({
892
+ full: match[0],
893
+ name: match[1].trim(),
894
+ input: match[2].trim(),
895
+ });
896
+ }
897
+ return calls;
898
+ }
899
+
900
+ // ====================== TOOL EXECUTION ======================
901
+ async function executeTool(name, input) {
902
+ if (!state.token) {
903
+ throw new Error('HF token required to execute tool');
904
+ }
905
+ const client = new InferenceClient(state.token);
906
+
907
+ const callMsgId = addMessage('tool-call', `👁️ Routing to HF Inference • ${name}`, [], {
908
+ icon: getToolIcon(name)
909
+ });
910
+
911
+ try {
912
+ let result = '';
913
+ let resultHtml = '';
914
+
915
+ if (name === 'ocr_vision' || name === 'image_caption') {
916
+ // Image-to-text
917
+ let imageUrl = input.trim();
918
+ // If input is not a URL, try to use first attached image
919
+ if (!imageUrl.match(/^https?:\/\//)) {
920
+ const attachedImage = state.attachedFiles.find(f => f.type.startsWith('image/'));
921
+ if (attachedImage) {
922
+ imageUrl = attachedImage.dataUrl;
923
+ } else {
924
+ throw new Error('No image URL provided and no image attached');
925
+ }
926
+ }
927
+
928
+ const caption = await client.imageToText({
929
+ model: name === 'ocr_vision' ? 'Salesforce/blip-image-captioning-large' : 'Salesforce/blip-image-captioning-large',
930
+ data: imageUrl,
931
+ });
932
+ result = Array.isArray(caption) ? caption.map(c => c.generated_text).join('\n') : (caption?.generated_text || String(caption));
933
+ resultHtml = `
934
+ <div class="flex items-start gap-3">
935
+ <img src="${imageUrl.startsWith('data:') ? imageUrl : imageUrl}" alt="input" class="w-20 h-20 rounded-lg object-cover border border-slate-700 flex-shrink-0" onerror="this.style.display='none'">
936
+ <div class="flex-1 min-w-0">
937
+ <div class="text-[10px] font-mono text-emerald-300 mb-1 uppercase tracking-wider">Caption Output</div>
938
+ <div class="text-slate-200">${escapeHtml(result)}</div>
939
+ </div>
940
+ </div>`;
941
+ }
942
+ else if (name === 'transcribe_audio') {
943
+ let audioData = input.trim();
944
+ const attachedAudio = state.attachedFiles.find(f => f.type.startsWith('audio/'));
945
+ if (attachedAudio) {
946
+ // Convert dataUrl to Blob
947
+ const res = await fetch(attachedAudio.dataUrl);
948
+ audioData = await res.blob();
949
+ } else if (audioData.match(/^https?:\/\//)) {
950
+ const res = await fetch(audioData);
951
+ audioData = await res.blob();
952
+ } else {
953
+ throw new Error('No audio URL provided and no audio attached');
954
+ }
955
+ const transcript = await client.automaticSpeechRecognition({
956
+ model: 'openai/whisper-large-v3',
957
+ data: audioData,
958
+ });
959
+ result = transcript?.text || String(transcript);
960
+ resultHtml = `
961
+ <div>
962
+ <div class="text-[10px] font-mono text-emerald-300 mb-1 uppercase tracking-wider">Transcript</div>
963
+ <div class="text-slate-200 leading-relaxed">${escapeHtml(result)}</div>
964
+ </div>`;
965
+ }
966
+ else if (name === 'web_search') {
967
+ // Simulated web search result (no actual external search tool, but we can provide a helpful response)
968
+ result = `Searched for: "${input}"\n\n[Simulated search results would appear here. In production, this would route to a search API or use a web-grounded model.]`;
969
+ resultHtml = `
970
+ <div>
971
+ <div class="text-[10px] font-mono text-emerald-300 mb-1 uppercase tracking-wider">Query: ${escapeHtml(input)}</div>
972
+ <div class="text-slate-200 leading-relaxed text-[11px] font-mono opacity-80">${escapeHtml(result)}</div>
973
+ </div>`;
974
+ }
975
+ else if (name === 'image_generation') {
976
+ const blob = await client.textToImage({
977
+ model: 'black-forest-labs/FLUX.1-schnell',
978
+ inputs: input,
979
+ parameters: { num_inference_steps: 25 }
980
+ });
981
+ const imgUrl = URL.createObjectURL(blob);
982
+ result = `Generated image for: "${input}"`;
983
+ resultHtml = `
984
+ <div>
985
+ <div class="text-[10px] font-mono text-emerald-300 mb-2 uppercase tracking-wider">Generated Image</div>
986
+ <img src="${imgUrl}" alt="${escapeHtml(input)}" class="max-w-full rounded-lg border border-slate-700 shadow-lg">
987
+ <div class="text-[10px] text-slate-400 mt-1 font-mono">Prompt: ${escapeHtml(input)}</div>
988
+ </div>`;
989
+ }
990
+ else {
991
+ throw new Error(`Unknown tool: ${name}`);
992
+ }
993
+
994
+ // Remove the "routing" message and add result
995
+ removeMessage(callMsgId);
996
+ addMessage('tool-result', result, [], {
997
+ html: resultHtml,
998
+ toolName: name,
999
+ icon: getToolIcon(name)
1000
+ });
1001
+
1002
+ return result;
1003
+ } catch (err) {
1004
+ removeMessage(callMsgId);
1005
+ addMessage('system-log', `Tool error: ${err.message}`, [], { icon: 'alert-triangle' });
1006
+ throw err;
1007
+ }
1008
+ }
1009
+
1010
+ // ====================== MAIN SEND / STREAM ======================
1011
+ async function sendMessage() {
1012
+ const text = userInput.value.trim();
1013
+ if (!text && state.attachedFiles.length === 0) return;
1014
+ if (state.isStreaming) return;
1015
+
1016
+ if (!state.token) {
1017
+ openTokenModal();
1018
+ showToast('Please set your HF access token first', 'warning');
1019
+ return;
1020
+ }
1021
+
1022
+ state.isStreaming = true;
1023
+ sendBtn.disabled = true;
1024
+ sendBtn.innerHTML = '<div class="loading-dots"><span></span><span></span><span></span></div>';
1025
+
1026
+ const files = [...state.attachedFiles];
1027
+ state.attachedFiles = [];
1028
+ renderAttachedFiles();
1029
+ userInput.value = '';
1030
+ autoResizeTextarea();
1031
+ updateCharCount();
1032
+
1033
+ // Add user message
1034
+ addMessage('user', text || '(multimodal input)', files);
1035
+
1036
+ try {
1037
+ await runAssistantLoop(text, files);
1038
+ } catch (err) {
1039
+ console.error(err);
1040
+ addMessage('system-log', `Error: ${err.message}`, [], { icon: 'alert-circle' });
1041
+ showToast(err.message, 'error');
1042
+ } finally {
1043
+ state.isStreaming = false;
1044
+ sendBtn.disabled = false;
1045
+ sendBtn.innerHTML = '<i data-lucide="send-horizontal" class="w-4 h-4 text-white"></i>';
1046
+ lucide.createIcons({ nodes: [sendBtn] });
1047
+ }
1048
+ }
1049
+
1050
+ async function runAssistantLoop(userText, files) {
1051
+ // Build messages array for HF API
1052
+ const systemPrompt = systemPromptInput.value.trim();
1053
+ const apiMessages = [];
1054
+ if (systemPrompt) {
1055
+ apiMessages.push({ role: 'system', content: systemPrompt });
1056
+ }
1057
+
1058
+ // Add recent chat history (last 10 turns)
1059
+ const history = state.messages.slice(-10).filter(m => m.role === 'user' || m.role === 'assistant');
1060
+ for (const m of history) {
1061
+ if (m.role === 'user') {
1062
+ if (m.files && m.files.length > 0) {
1063
+ const content = [];
1064
+ if (m.content && m.content !== '(multimodal input)') {
1065
+ content.push({ type: 'text', text: m.content });
1066
+ }
1067
+ for (const f of m.files) {
1068
+ if (f.type.startsWith('image/')) {
1069
+ content.push({ type: 'image_url', image_url: { url: f.dataUrl } });
1070
+ } else if (f.type.startsWith('audio/')) {
1071
+ content.push({ type: 'text', text: `[Audio attachment: ${f.name}]` });
1072
+ }
1073
+ }
1074
+ apiMessages.push({ role: 'user', content });
1075
+ } else {
1076
+ apiMessages.push({ role: 'user', content: m.content });
1077
+ }
1078
+ } else {
1079
+ apiMessages.push({ role: 'assistant', content: m.content });
1080
+ }
1081
+ }
1082
+
1083
+ const assistantMsgId = addMessage('assistant', '', [], {
1084
+ html: '<div class="text-slate-400 flex items-center gap-2"><div class="loading-dots"><span></span><span></span><span></span></div><span class="text-xs font-mono">Reasoning...</span></div>'
1085
+ });
1086
+
1087
+ const client = new InferenceClient(state.token);
1088
+ let fullResponse = '';
1089
+
1090
+ try {
1091
+ // Try streaming first
1092
+ const stream = client.chatCompletionStream({
1093
+ model: state.model,
1094
+ messages: apiMessages,
1095
+ temperature: 0.7,
1096
+ max_tokens: 2048,
1097
+ stream: true,
1098
+ });
1099
+
1100
+ let firstChunk = true;
1101
+ for await (const chunk of stream) {
1102
+ const delta = chunk.choices?.[0]?.delta?.content || '';
1103
+ if (delta) {
1104
+ if (firstChunk) {
1105
+ fullResponse = delta;
1106
+ firstChunk = false;
1107
+ } else {
1108
+ fullResponse += delta;
1109
+ }
1110
+ updateAssistantMessage(assistantMsgId, fullResponse);
1111
+ }
1112
+ }
1113
+ } catch (streamErr) {
1114
+ console.warn('Streaming failed, falling back to non-streaming:', streamErr.message);
1115
+ // Fallback to non-streaming
1116
+ try {
1117
+ const result = await client.chatCompletion({
1118
+ model: state.model,
1119
+ messages: apiMessages,
1120
+ temperature: 0.7,
1121
+ max_tokens: 2048,
1122
+ });
1123
+ fullResponse = result.choices?.[0]?.message?.content || '';
1124
+ updateAssistantMessage(assistantMsgId, fullResponse);
1125
+ } catch (fallbackErr) {
1126
+ removeMessage(assistantMsgId);
1127
+ throw fallbackErr;
1128
+ }
1129
+ }
1130
+
1131
+ // Parse & execute any tool calls in the response
1132
+ const toolCalls = parseToolCalls(fullResponse);
1133
+ if (toolCalls.length > 0 && state.toolCallDepth < state.maxToolCallDepth) {
1134
+ state.toolCallDepth++;
1135
+ const toolResults = [];
1136
+ for (const call of toolCalls) {
1137
+ try {
1138
+ const result = await executeTool(call.name, call.input);
1139
+ toolResults.push({ name: call.name, input: call.input, result });
1140
+ } catch (err) {
1141
+ toolResults.push({ name: call.name, input: call.input, error: err.message });
1142
+ }
1143
+ }
1144
+
1145
+ // Follow-up turn with tool results
1146
+ if (toolResults.length > 0) {
1147
+ const followUpMessages = [
1148
+ ...apiMessages,
1149
+ { role: 'assistant', content: fullResponse },
1150
+ {
1151
+ role: 'user',
1152
+ content: `[SYSTEM] Tool execution complete. Here are the results:\n\n${toolResults.map(r =>
1153
+ `--- ${r.name} ---\nInput: ${r.input}\nResult: ${r.error ? `ERROR: ${r.error}` : r.result}\n`
1154
+ ).join('\n')}\n\nPlease respond to the user incorporating these tool results. Do NOT emit any new <tool_call> tags unless absolutely necessary.`
1155
+ }
1156
+ ];
1157
+
1158
+ const followUpMsgId = addMessage('assistant', '', [], {
1159
+ html: '<div class="text-slate-400 flex items-center gap-2"><div class="loading-dots"><span></span><span></span><span></span></div><span class="text-xs font-mono">Synthesizing tool results...</span></div>'
1160
+ });
1161
+
1162
+ let followUpResponse = '';
1163
+ try {
1164
+ const followStream = client.chatCompletionStream({
1165
+ model: state.model,
1166
+ messages: followUpMessages,
1167
+ temperature: 0.5,
1168
+ max_tokens: 2048,
1169
+ stream: true,
1170
+ });
1171
+
1172
+ let first = true;
1173
+ for await (const chunk of followStream) {
1174
+ const delta = chunk.choices?.[0]?.delta?.content || '';
1175
+ if (delta) {
1176
+ followUpResponse += delta;
1177
+ updateAssistantMessage(followUpMsgId, followUpResponse);
1178
+ }
1179
+ }
1180
+ } catch (followErr) {
1181
+ console.warn('Follow-up stream failed:', followErr.message);
1182
+ try {
1183
+ const result = await client.chatCompletion({
1184
+ model: state.model,
1185
+ messages: followUpMessages,
1186
+ temperature: 0.5,
1187
+ max_tokens: 2048,
1188
+ });
1189
+ followUpResponse = result.choices?.[0]?.message?.content || '';
1190
+ updateAssistantMessage(followUpMsgId, followUpResponse);
1191
+ } catch (e) {
1192
+ removeMessage(followUpMsgId);
1193
+ addMessage('system-log', `Follow-up failed: ${e.message}`, [], { icon: 'alert-circle' });
1194
+ }
1195
+ }
1196
+ }
1197
+ state.toolCallDepth = 0;
1198
+ }
1199
+ }
1200
+
1201
+ // ====================== EVENT LISTENERS ======================
1202
+ userInput.addEventListener('input', () => {
1203
+ autoResizeTextarea();
1204
+ updateCharCount();
1205
+ });
1206
+
1207
+ userInput.addEventListener('keydown', (e) => {
1208
+ if (e.key === 'Enter' && !e.shiftKey) {
1209
+ e.preventDefault();
1210
+ sendMessage();
1211
+ }
1212
+ });
1213
+
1214
+ sendBtn.addEventListener('click', sendMessage);
1215
+
1216
+ function updateCharCount() {
1217
+ const len = userInput.value.length;
1218
+ charCount.textContent = `${len} / 8000`;
1219
+ charCount.classList.toggle('text-amber-400', len > 6000);
1220
+ charCount.classList.toggle('text-rose-400', len > 7500);
1221
+ }
1222
+
1223
+ fileInput.addEventListener('change', (e) => {
1224
+ handleFiles(e.target.files);
1225
+ e.target.value = '';
1226
+ });
1227
+
1228
+ // Drag and drop
1229
+ let dragCounter = 0;
1230
+ ['dragenter', 'dragover'].forEach(evt => {
1231
+ dropZone.addEventListener(evt, (e) => {
1232
+ e.preventDefault();
1233
+ e.stopPropagation();
1234
+ dragCounter++;
1235
+ dropZone.classList.add('drag-over');
1236
+ if (state.attachedFiles.length === 0) {
1237
+ dropLabel.classList.remove('hidden');
1238
+ }
1239
+ });
1240
+ });
1241
+ ['dragleave', 'drop'].forEach(evt => {
1242
+ dropZone.addEventListener(evt, (e) => {
1243
+ e.preventDefault();
1244
+ e.stopPropagation();
1245
+ dragCounter--;
1246
+ if (dragCounter <= 0) {
1247
+ dragCounter = 0;
1248
+ dropZone.classList.remove('drag-over');
1249
+ dropLabel.classList.add('hidden');
1250
+ }
1251
+ });
1252
+ });
1253
+ dropZone.addEventListener('drop', (e) => {
1254
+ if (e.dataTransfer?.files?.length) {
1255
+ handleFiles(e.dataTransfer.files);
1256
+ }
1257
+ });
1258
+
1259
+ // Paste images
1260
+ userInput.addEventListener('paste', (e) => {
1261
+ const items = e.clipboardData?.items;
1262
+ if (!items) return;
1263
+ const files = [];
1264
+ for (const item of items) {
1265
+ if (item.kind === 'file') {
1266
+ const file = item.getAsFile();
1267
+ if (file) files.push(file);
1268
+ }
1269
+ }
1270
+ if (files.length > 0) {
1271
+ e.preventDefault();
1272
+ handleFiles(files);
1273
+ }
1274
+ });
1275
+
1276
+ // System prompt toggle
1277
+ systemPromptToggle.addEventListener('click', () => {
1278
+ systemPromptPanel.classList.toggle('hidden');
1279
+ sysPromptIcon.classList.toggle('rotate-180');
1280
+ });
1281
+
1282
+ // Clear chat
1283
+ clearBtn.addEventListener('click', () => {
1284
+ if (state.messages.length === 0) return;
1285
+ if (confirm('Clear all messages?')) {
1286
+ state.messages = [];
1287
+ messageContainer.innerHTML = '';
1288
+ welcomeScreen.classList.remove('hidden');
1289
+ messageContainer.appendChild(welcomeScreen);
1290
+ showToast('Chat cleared', 'info');
1291
+ }
1292
+ });
1293
+
1294
+ // Suggestion buttons
1295
+ document.addEventListener('click', (e) => {
1296
+ const btn = e.target.closest('.suggestion-btn');
1297
+ if (btn) {
1298
+ userInput.value = btn.dataset.prompt || '';
1299
+ userInput.focus();
1300
+ autoResizeTextarea();
1301
+ updateCharCount();
1302
+ }
1303
+ });
1304
+
1305
+ // ====================== TOKEN MODAL ======================
1306
+ function openTokenModal() {
1307
+ tokenModal.classList.remove('hidden');
1308
+ tokenModal.classList.add('flex');
1309
+ tokenInput.value = state.token;
1310
+ tokenInput.focus();
1311
+ lucide.createIcons();
1312
+ }
1313
+ function closeTokenModal() {
1314
+ tokenModal.classList.add('hidden');
1315
+ tokenModal.classList.remove('flex');
1316
+ }
1317
+
1318
+ tokenBtn.addEventListener('click', openTokenModal);
1319
+ $('closeTokenModal').addEventListener('click', closeTokenModal);
1320
+ tokenModal.addEventListener('click', (e) => {
1321
+ if (e.target === tokenModal) closeTokenModal();
1322
+ });
1323
+
1324
+ $('toggleTokenVis').addEventListener('click', () => {
1325
+ const isPw = tokenInput.type === 'password';
1326
+ tokenInput.type = isPw ? 'text' : 'password';
1327
+ const icon = $('eyeIcon');
1328
+ icon.setAttribute('data-lucide', isPw ? 'eye-off' : 'eye');
1329
+ lucide.createIcons({ nodes: [icon.parentElement] });
1330
+ });
1331
+
1332
+ $('saveTokenBtn').addEventListener('click', () => {
1333
+ const val = tokenInput.value.trim();
1334
+ if (!val) {
1335
+ showToast('Token cannot be empty', 'error');
1336
+ return;
1337
+ }
1338
+ state.token = val;
1339
+ localStorage.setItem('keefe_hf_token', val);
1340
+ updateTokenStatus();
1341
+ showToast('Token saved locally', 'success');
1342
+ closeTokenModal();
1343
+ });
1344
+
1345
+ $('clearTokenBtn').addEventListener('click', () => {
1346
+ if (confirm('Remove stored token?')) {
1347
+ state.token = '';
1348
+ localStorage.removeItem('keefe_hf_token');
1349
+ tokenInput.value = '';
1350
+ updateTokenStatus();
1351
+ showToast('Token cleared', 'info');
1352
+ }
1353
+ });
1354
+
1355
+ tokenInput.addEventListener('keydown', (e) => {
1356
+ if (e.key === 'Enter') {
1357
+ e.preventDefault();
1358
+ $('saveTokenBtn').click();
1359
+ }
1360
+ });
1361
+
1362
+ function updateTokenStatus() {
1363
+ if (state.token) {
1364
+ tokenStatus.textContent = 'Token Active';
1365
+ tokenStatus.classList.remove('text-slate-300');
1366
+ tokenStatus.classList.add('text-emerald-400');
1367
+ tokenSavedBadge.classList.remove('hidden');
1368
+ } else {
1369
+ tokenStatus.textContent = 'Set Token';
1370
+ tokenStatus.classList.add('text-slate-300');
1371
+ tokenStatus.classList.remove('text-emerald-400');
1372
+ tokenSavedBadge.classList.add('hidden');
1373
+ }
1374
+ }
1375
+
1376
+ // ====================== INIT ======================
1377
+ updateTokenStatus();
1378
+ updateCharCount();
1379
+ lucide.createIcons();
1380
+
1381
+ // Welcome token prompt
1382
+ if (!state.token) {
1383
+ setTimeout(() => {
1384
+ showToast('Set your Hugging Face token to begin', 'info', 5000);
1385
+ }, 800);
1386
+ }
1387
+
1388
+ // Global keyboard shortcut
1389
+ document.addEventListener('keydown', (e) => {
1390
+ // Cmd/Ctrl+K to focus input
1391
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1392
+ e.preventDefault();
1393
+ userInput.focus();
1394
+ }
1395
+ // Escape to close modal
1396
+ if (e.key === 'Escape') {
1397
+ if (!tokenModal.classList.contains('hidden')) closeTokenModal();
1398
+ }
1399
+ });
1400
+
1401
+ // Expose for debugging
1402
+ window.keefe = { state, sendMessage };
1403
+
1404
+ </script>
1405
+ </body>
1406
+ </html>