yummyfiles commited on
Commit
dc4efed
·
verified ·
1 Parent(s): 9b8124a

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app.js +269 -269
  2. fix_escape.py +15 -0
app.js CHANGED
@@ -1,270 +1,270 @@
1
- import { pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
2
-
3
- const MODEL_ID = 'Xenova/tinyllama-1.1b-chat-v1.0';
4
-
5
- const inputEl = document.getElementById('input');
6
- const chatEl = document.getElementById('chat');
7
- const executeBtn = document.getElementById('execute');
8
- const statusEl = document.getElementById('status');
9
- const chatListEl = document.getElementById('chat-list');
10
- const newChatBtn = document.getElementById('new-chat-btn');
11
- const chatTitleEl = document.getElementById('chat-title');
12
- const sidebar = document.getElementById('sidebar');
13
- const sidebarOverlay = document.getElementById('sidebar-overlay');
14
- const menuBtn = document.getElementById('menu-btn');
15
-
16
- let generator = null;
17
- let isLoading = false;
18
- let currentChatId = null;
19
- let chats = JSON.parse(localStorage.getItem('bob_chats') || '{}');
20
-
21
- const customTools = {
22
- get_crypto_price: async (symbol) => {
23
- try {
24
- const response = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${symbol.toLowerCase()}&vs_currencies=usd`);
25
- const data = await response.json();
26
- if (data[symbol.toLowerCase()]) return `$${data[symbol.toLowerCase()].usd.toLocaleString()}`;
27
- return `Price unavailable for ${symbol}`;
28
- } catch { return `Error fetching price for ${symbol}`; }
29
- },
30
- multiply_numbers: (a, b) => a * b
31
- };
32
-
33
- function saveChats() { localStorage.setItem('bob_chats', JSON.stringify(chats)); }
34
-
35
- function createChat() {
36
- const id = Date.now().toString();
37
- chats[id] = { id, title: 'New Chat', messages: [], created: Date.now(), updated: Date.now() };
38
- saveChats();
39
- return id;
40
- }
41
-
42
- function deleteChat(id) { delete chats[id]; saveChats(); renderChatList(); }
43
-
44
- function switchChat(id) {
45
- currentChatId = id;
46
- renderChatList();
47
- renderChat();
48
- sidebar.classList.remove('open');
49
- sidebarOverlay.classList.remove('open');
50
- }
51
-
52
- function updateChatTitle(id, title) {
53
- if (chats[id]) { chats[id].title = title; chats[id].updated = Date.now(); saveChats(); renderChatList(); }
54
- }
55
-
56
- function renderChatList() {
57
- const sorted = Object.values(chats).sort((a, b) => b.updated - a.updated);
58
- if (sorted.length === 0) {
59
- chatListEl.innerHTML = '<div class="empty-chats">No chats yet.<br>Click "NEW CHAT" to start.</div>';
60
- return;
61
- }
62
- chatListEl.innerHTML = sorted.map(c => `
63
- <div class="chat-item ${c.id === currentChatId ? 'active' : ''}" data-id="${c.id}">
64
- <div class="chat-item-title">${escapeHtml(c.title)}</div>
65
- <div class="chat-item-preview">${escapeHtml(c.messages[0]?.content?.slice(0, 40) || '')}</div>
66
- <div class="chat-item-time">${new Date(c.updated).toLocaleString()}</div>
67
- </div>
68
- `).join('');
69
- chatListEl.querySelectorAll('.chat-item').forEach(el => {
70
- el.addEventListener('click', () => switchChat(el.dataset.id));
71
- });
72
- }
73
-
74
- function escapeHtml(s) { return s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
75
-
76
- function renderChat() {
77
- if (!currentChatId || !chats[currentChatId]) { showWelcome(); return; }
78
- const chat = chats[currentChatId];
79
- chatTitleEl.textContent = chat.title;
80
- chatEl.innerHTML = '';
81
- if (chat.messages.length === 0) { showWelcome(); return; }
82
- chat.messages.forEach(msg => appendMessage(msg.role, msg.content, false));
83
- chatEl.scrollTop = chatEl.scrollHeight;
84
- }
85
-
86
- function showWelcome() {
87
- chatTitleEl.textContent = currentChatId ? chats[currentChatId]?.title || 'New Chat' : 'Select a chat or start new';
88
- chatEl.innerHTML = `
89
- <div class="welcome">
90
- <h2>BOB</h2>
91
- <p>Local-first AI running 100% in your browser via WebAssembly.<br>No servers. No cloud. No tracking.</p>
92
- <div class="hint">Ctrl+Enter to send · Try "What's Bitcoin price?" or "Multiply 144 by 37"</div>
93
- </div>
94
- `;
95
- }
96
-
97
- function appendMessage(role, content, save = true) {
98
- if (chatEl.querySelector('.welcome')) chatEl.innerHTML = '';
99
- const wrapper = document.createElement('div');
100
- wrapper.className = `message ${role}`;
101
- const label = document.createElement('div');
102
- label.className = 'message-label';
103
- label.textContent = role === 'user' ? 'YOU' : 'BOB';
104
- wrapper.appendChild(label);
105
- const contentDiv = document.createElement('div');
106
- contentDiv.className = 'message-content';
107
- contentDiv.textContent = content;
108
- wrapper.appendChild(contentDiv);
109
- chatEl.appendChild(wrapper);
110
- chatEl.scrollTop = chatEl.scrollHeight;
111
- if (save && currentChatId && chats[currentChatId]) {
112
- chats[currentChatId].messages.push({ role, content, time: Date.now() });
113
- chats[currentChatId].updated = Date.now();
114
- if (chats[currentChatId].messages.length === 1) {
115
- chats[currentChatId].title = content.slice(0, 40);
116
- }
117
- saveChats();
118
- renderChatList();
119
- }
120
- }
121
-
122
- function appendToolResult(toolName, result) {
123
- const div = document.createElement('div');
124
- div.className = 'message tool-result';
125
- div.textContent = `[TOOL: ${toolName} => ${result}]`;
126
- chatEl.appendChild(div);
127
- chatEl.scrollTop = chatEl.scrollHeight;
128
- }
129
-
130
- async function initModel() {
131
- if (generator) return generator;
132
- isLoading = true;
133
- executeBtn.disabled = true;
134
- executeBtn.textContent = 'LOADING...';
135
- statusEl.textContent = `Loading ${MODEL_ID}... (first run downloads ~600MB)`;
136
-
137
- try {
138
- generator = await pipeline('text-generation', MODEL_ID, {
139
- dtype: 'q4',
140
- progress_callback: (progress) => {
141
- if (progress.status === 'downloading') {
142
- const mb = (progress.loaded / 1024 / 1024).toFixed(1);
143
- const totalMb = (progress.total / 1024 / 1024).toFixed(1);
144
- statusEl.textContent = `Downloading: ${mb} / ${totalMb} MB`;
145
- } else if (progress.status === 'progress') {
146
- statusEl.textContent = `Initializing: ${Math.round(progress.progress * 100)}%`;
147
- }
148
- }
149
- });
150
- statusEl.textContent = 'Ready. Model loaded locally.';
151
- } catch (err) { statusEl.textContent = `Error: ${err.message}`; console.error(err); }
152
- finally { isLoading = false; executeBtn.disabled = false; executeBtn.textContent = 'SEND'; }
153
- return generator;
154
- }
155
-
156
- function formatPrompt(userInput, history = []) {
157
- let prompt = `<|begin_of_text|><|start_header_id|>system<|end_header_id|>
158
- You are Bob, a local AI assistant running in the browser. You have access to tools. When you need to use a tool, respond with EXACTLY this format:
159
- <call_tool>function_name({"arg": "value"})</call_tool>
160
-
161
- Available tools:
162
- - get_crypto_price: Get crypto price. Args: {"symbol": "BTC"}
163
- - multiply_numbers: Multiply two numbers. Args: {"a": 5, "b": 10}
164
-
165
- Only call tools when needed. Respond normally for regular questions.
166
- <|eot_id|>`;
167
-
168
- for (const msg of history.slice(-6)) {
169
- prompt += `<|start_header_id|>${msg.role}<|end_header_id|>\n${msg.content}<|eot_id|>`;
170
- }
171
- prompt += `<|start_header_id|>user<|end_header_id|>\n${userInput}<|eot_id|>\n<|start_header_id|>assistant<|end_header_id|>\n`;
172
- return prompt;
173
- }
174
-
175
- async function executeToolCalls(text) {
176
- const toolCallRegex = /<call_tool>(\w+)\((\{.*?\})\)<\/call_tool>/g;
177
- let result = text;
178
- let match;
179
- while ((match = toolCallRegex.exec(text)) !== null) {
180
- const funcName = match[1];
181
- let args;
182
- try { args = JSON.parse(match[2]); } catch { continue; }
183
- if (customTools[funcName]) {
184
- try {
185
- const toolResult = await customTools[funcName](...Object.values(args));
186
- result = result.replace(match[0], `[TOOL RESULT: ${funcName} => ${toolResult}]`);
187
- appendToolResult(funcName, toolResult);
188
- } catch (err) { result = result.replace(match[0], `[TOOL ERROR: ${err.message}]`); }
189
- }
190
- }
191
- return result;
192
- }
193
-
194
- async function runInference(prompt, history) {
195
- if (!generator) await initModel();
196
- if (!generator) return 'Model not loaded.';
197
-
198
- const formatted = formatPrompt(prompt, history);
199
- statusEl.textContent = 'Processing locally...';
200
-
201
- try {
202
- const output = await generator(formatted, {
203
- max_new_tokens: 256, temperature: 0.1, top_p: 0.9,
204
- do_sample: true, return_full_text: false, repetition_penalty: 1.2
205
- });
206
- let generated = output[0].generated_text.trim();
207
- generated = generated
208
- .replace(/^.*?<\|assistant\|>\s*/s, '')
209
- .replace(/^.*?<\|user\|>\s*/s, '')
210
- .replace(/^.*?<\|system\|>\s*/s, '')
211
- .replace(/^.*?You are Bob.*?\n/s, '')
212
- .replace(/Available tools:[\s\S]*?Only call tools[\s\S]*?\n/, '')
213
- .trim();
214
-
215
- const toolCallRegex = /<call_tool>(\w+)\((\{.*?\})\)<\/call_tool>/g;
216
- let match;
217
- while ((match = toolCallRegex.exec(generated)) !== null) {
218
- const funcName = match[1];
219
- let args;
220
- try { args = JSON.parse(match[2]); } catch { continue; }
221
- if (customTools[funcName]) {
222
- try {
223
- const toolResult = await customTools[funcName](...Object.values(args));
224
- generated = generated.replace(match[0], `[TOOL RESULT: ${funcName} => ${toolResult}]`);
225
- } catch (err) { generated = generated.replace(match[0], `[TOOL ERROR: ${err.message}]`); }
226
- }
227
- }
228
- return generated.trim();
229
- } catch (err) { console.error(err); return `Error: ${err.message}`; }
230
- }
231
-
232
- async function handleSend() {
233
- console.log('handleSend called, isLoading:', isLoading);
234
- if (isLoading) { console.log('Model still loading'); return; }
235
- const prompt = inputEl.value.trim();
236
- console.log('Prompt:', prompt);
237
- if (!prompt) return;
238
-
239
- if (!currentChatId) currentChatId = createChat();
240
- appendMessage('user', prompt);
241
- inputEl.value = '';
242
-
243
- executeBtn.disabled = true;
244
- executeBtn.textContent = 'THINKING...';
245
-
246
- const history = chats[currentChatId]?.messages.slice(-6) || [];
247
- console.log('History:', history);
248
- const result = await runInference(prompt, history);
249
- console.log('Result:', result);
250
- appendMessage('assistant', result);
251
-
252
- executeBtn.disabled = false;
253
- executeBtn.textContent = 'SEND';
254
- statusEl.textContent = 'Ready.';
255
- }
256
-
257
- executeBtn.addEventListener('click', handleSend);
258
- inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) handleSend(); });
259
-
260
- newChatBtn.addEventListener('click', () => { currentChatId = createChat(); switchChat(currentChatId); inputEl.focus(); });
261
-
262
- menuBtn.addEventListener('click', () => { sidebar.classList.toggle('open'); sidebarOverlay.classList.toggle('open'); });
263
- sidebarOverlay.addEventListener('click', () => { sidebar.classList.remove('open'); sidebarOverlay.classList.remove('open'); });
264
-
265
- renderChatList();
266
- if (Object.keys(chats).length > 0) {
267
- const latest = Object.values(chats).sort((a,b)=>b.updated-a.updated)[0];
268
- switchChat(latest.id);
269
- }
270
  initModel();
 
1
+ import { pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
2
+
3
+ const MODEL_ID = 'Xenova/tinyllama-1.1b-chat-v1.0';
4
+
5
+ const inputEl = document.getElementById('input');
6
+ const chatEl = document.getElementById('chat');
7
+ const executeBtn = document.getElementById('execute');
8
+ const statusEl = document.getElementById('status');
9
+ const chatListEl = document.getElementById('chat-list');
10
+ const newChatBtn = document.getElementById('new-chat-btn');
11
+ const chatTitleEl = document.getElementById('chat-title');
12
+ const sidebar = document.getElementById('sidebar');
13
+ const sidebarOverlay = document.getElementById('sidebar-overlay');
14
+ const menuBtn = document.getElementById('menu-btn');
15
+
16
+ let generator = null;
17
+ let isLoading = false;
18
+ let currentChatId = null;
19
+ let chats = JSON.parse(localStorage.getItem('bob_chats') || '{}');
20
+
21
+ const customTools = {
22
+ get_crypto_price: async (symbol) => {
23
+ try {
24
+ const response = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${symbol.toLowerCase()}&vs_currencies=usd`);
25
+ const data = await response.json();
26
+ if (data[symbol.toLowerCase()]) return `$${data[symbol.toLowerCase()].usd.toLocaleString()}`;
27
+ return `Price unavailable for ${symbol}`;
28
+ } catch { return `Error fetching price for ${symbol}`; }
29
+ },
30
+ multiply_numbers: (a, b) => a * b
31
+ };
32
+
33
+ function saveChats() { localStorage.setItem('bob_chats', JSON.stringify(chats)); }
34
+
35
+ function createChat() {
36
+ const id = Date.now().toString();
37
+ chats[id] = { id, title: 'New Chat', messages: [], created: Date.now(), updated: Date.now() };
38
+ saveChats();
39
+ return id;
40
+ }
41
+
42
+ function deleteChat(id) { delete chats[id]; saveChats(); renderChatList(); }
43
+
44
+ function switchChat(id) {
45
+ currentChatId = id;
46
+ renderChatList();
47
+ renderChat();
48
+ sidebar.classList.remove('open');
49
+ sidebarOverlay.classList.remove('open');
50
+ }
51
+
52
+ function updateChatTitle(id, title) {
53
+ if (chats[id]) { chats[id].title = title; chats[id].updated = Date.now(); saveChats(); renderChatList(); }
54
+ }
55
+
56
+ function renderChatList() {
57
+ const sorted = Object.values(chats).sort((a, b) => b.updated - a.updated);
58
+ if (sorted.length === 0) {
59
+ chatListEl.innerHTML = '<div class="empty-chats">No chats yet.<br>Click "NEW CHAT" to start.</div>';
60
+ return;
61
+ }
62
+ chatListEl.innerHTML = sorted.map(c => `
63
+ <div class="chat-item ${c.id === currentChatId ? 'active' : ''}" data-id="${c.id}">
64
+ <div class="chat-item-title">${escapeHtml(c.title)}</div>
65
+ <div class="chat-item-preview">${escapeHtml(c.messages[0]?.content?.slice(0, 40) || '')}</div>
66
+ <div class="chat-item-time">${new Date(c.updated).toLocaleString()}</div>
67
+ </div>
68
+ `).join('');
69
+ chatListEl.querySelectorAll('.chat-item').forEach(el => {
70
+ el.addEventListener('click', () => switchChat(el.dataset.id));
71
+ });
72
+ }
73
+
74
+ function escapeHtml(s) { return s.replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",""":""","'":"'"}[c])); }
75
+
76
+ function renderChat() {
77
+ if (!currentChatId || !chats[currentChatId]) { showWelcome(); return; }
78
+ const chat = chats[currentChatId];
79
+ chatTitleEl.textContent = chat.title;
80
+ chatEl.innerHTML = '';
81
+ if (chat.messages.length === 0) { showWelcome(); return; }
82
+ chat.messages.forEach(msg => appendMessage(msg.role, msg.content, false));
83
+ chatEl.scrollTop = chatEl.scrollHeight;
84
+ }
85
+
86
+ function showWelcome() {
87
+ chatTitleEl.textContent = currentChatId ? chats[currentChatId]?.title || 'New Chat' : 'Select a chat or start new';
88
+ chatEl.innerHTML = `
89
+ <div class="welcome">
90
+ <h2>BOB</h2>
91
+ <p>Local-first AI running 100% in your browser via WebAssembly.<br>No servers. No cloud. No tracking.</p>
92
+ <div class="hint">Ctrl+Enter to send · Try "What's Bitcoin price?" or "Multiply 144 by 37"</div>
93
+ </div>
94
+ `;
95
+ }
96
+
97
+ function appendMessage(role, content, save = true) {
98
+ if (chatEl.querySelector('.welcome')) chatEl.innerHTML = '';
99
+ const wrapper = document.createElement('div');
100
+ wrapper.className = `message ${role}`;
101
+ const label = document.createElement('div');
102
+ label.className = 'message-label';
103
+ label.textContent = role === 'user' ? 'YOU' : 'BOB';
104
+ wrapper.appendChild(label);
105
+ const contentDiv = document.createElement('div');
106
+ contentDiv.className = 'message-content';
107
+ contentDiv.textContent = content;
108
+ wrapper.appendChild(contentDiv);
109
+ chatEl.appendChild(wrapper);
110
+ chatEl.scrollTop = chatEl.scrollHeight;
111
+ if (save && currentChatId && chats[currentChatId]) {
112
+ chats[currentChatId].messages.push({ role, content, time: Date.now() });
113
+ chats[currentChatId].updated = Date.now();
114
+ if (chats[currentChatId].messages.length === 1) {
115
+ chats[currentChatId].title = content.slice(0, 40);
116
+ }
117
+ saveChats();
118
+ renderChatList();
119
+ }
120
+ }
121
+
122
+ function appendToolResult(toolName, result) {
123
+ const div = document.createElement('div');
124
+ div.className = 'message tool-result';
125
+ div.textContent = `[TOOL: ${toolName} => ${result}]`;
126
+ chatEl.appendChild(div);
127
+ chatEl.scrollTop = chatEl.scrollHeight;
128
+ }
129
+
130
+ async function initModel() {
131
+ if (generator) return generator;
132
+ isLoading = true;
133
+ executeBtn.disabled = true;
134
+ executeBtn.textContent = 'LOADING...';
135
+ statusEl.textContent = `Loading ${MODEL_ID}... (first run downloads ~600MB)`;
136
+
137
+ try {
138
+ generator = await pipeline('text-generation', MODEL_ID, {
139
+ dtype: 'q4',
140
+ progress_callback: (progress) => {
141
+ if (progress.status === 'downloading') {
142
+ const mb = (progress.loaded / 1024 / 1024).toFixed(1);
143
+ const totalMb = (progress.total / 1024 / 1024).toFixed(1);
144
+ statusEl.textContent = `Downloading: ${mb} / ${totalMb} MB`;
145
+ } else if (progress.status === 'progress') {
146
+ statusEl.textContent = `Initializing: ${Math.round(progress.progress * 100)}%`;
147
+ }
148
+ }
149
+ });
150
+ statusEl.textContent = 'Ready. Model loaded locally.';
151
+ } catch (err) { statusEl.textContent = `Error: ${err.message}`; console.error(err); }
152
+ finally { isLoading = false; executeBtn.disabled = false; executeBtn.textContent = 'SEND'; }
153
+ return generator;
154
+ }
155
+
156
+ function formatPrompt(userInput, history = []) {
157
+ let prompt = `<|begin_of_text|><|start_header_id|>system<|end_header_id|>
158
+ You are Bob, a local AI assistant running in the browser. You have access to tools. When you need to use a tool, respond with EXACTLY this format:
159
+ <call_tool>function_name({"arg": "value"})</call_tool>
160
+
161
+ Available tools:
162
+ - get_crypto_price: Get crypto price. Args: {"symbol": "BTC"}
163
+ - multiply_numbers: Multiply two numbers. Args: {"a": 5, "b": 10}
164
+
165
+ Only call tools when needed. Respond normally for regular questions.
166
+ <|eot_id|>`;
167
+
168
+ for (const msg of history.slice(-6)) {
169
+ prompt += `<|start_header_id|>${msg.role}<|end_header_id|>\n${msg.content}<|eot_id|>`;
170
+ }
171
+ prompt += `<|start_header_id|>user<|end_header_id|>\n${userInput}<|eot_id|>\n<|start_header_id|>assistant<|end_header_id|>\n`;
172
+ return prompt;
173
+ }
174
+
175
+ async function executeToolCalls(text) {
176
+ const toolCallRegex = /<call_tool>(\w+)\((\{.*?\})\)<\/call_tool>/g;
177
+ let result = text;
178
+ let match;
179
+ while ((match = toolCallRegex.exec(text)) !== null) {
180
+ const funcName = match[1];
181
+ let args;
182
+ try { args = JSON.parse(match[2]); } catch { continue; }
183
+ if (customTools[funcName]) {
184
+ try {
185
+ const toolResult = await customTools[funcName](...Object.values(args));
186
+ result = result.replace(match[0], `[TOOL RESULT: ${funcName} => ${toolResult}]`);
187
+ appendToolResult(funcName, toolResult);
188
+ } catch (err) { result = result.replace(match[0], `[TOOL ERROR: ${err.message}]`); }
189
+ }
190
+ }
191
+ return result;
192
+ }
193
+
194
+ async function runInference(prompt, history) {
195
+ if (!generator) await initModel();
196
+ if (!generator) return 'Model not loaded.';
197
+
198
+ const formatted = formatPrompt(prompt, history);
199
+ statusEl.textContent = 'Processing locally...';
200
+
201
+ try {
202
+ const output = await generator(formatted, {
203
+ max_new_tokens: 256, temperature: 0.1, top_p: 0.9,
204
+ do_sample: true, return_full_text: false, repetition_penalty: 1.2
205
+ });
206
+ let generated = output[0].generated_text.trim();
207
+ generated = generated
208
+ .replace(/^.*?<\|assistant\|>\s*/s, '')
209
+ .replace(/^.*?<\|user\|>\s*/s, '')
210
+ .replace(/^.*?<\|system\|>\s*/s, '')
211
+ .replace(/^.*?You are Bob.*?\n/s, '')
212
+ .replace(/Available tools:[\s\S]*?Only call tools[\s\S]*?\n/, '')
213
+ .trim();
214
+
215
+ const toolCallRegex = /<call_tool>(\w+)\((\{.*?\})\)<\/call_tool>/g;
216
+ let match;
217
+ while ((match = toolCallRegex.exec(generated)) !== null) {
218
+ const funcName = match[1];
219
+ let args;
220
+ try { args = JSON.parse(match[2]); } catch { continue; }
221
+ if (customTools[funcName]) {
222
+ try {
223
+ const toolResult = await customTools[funcName](...Object.values(args));
224
+ generated = generated.replace(match[0], `[TOOL RESULT: ${funcName} => ${toolResult}]`);
225
+ } catch (err) { generated = generated.replace(match[0], `[TOOL ERROR: ${err.message}]`); }
226
+ }
227
+ }
228
+ return generated.trim();
229
+ } catch (err) { console.error(err); return `Error: ${err.message}`; }
230
+ }
231
+
232
+ async function handleSend() {
233
+ console.log('handleSend called, isLoading:', isLoading);
234
+ if (isLoading) { console.log('Model still loading'); return; }
235
+ const prompt = inputEl.value.trim();
236
+ console.log('Prompt:', prompt);
237
+ if (!prompt) return;
238
+
239
+ if (!currentChatId) currentChatId = createChat();
240
+ appendMessage('user', prompt);
241
+ inputEl.value = '';
242
+
243
+ executeBtn.disabled = true;
244
+ executeBtn.textContent = 'THINKING...';
245
+
246
+ const history = chats[currentChatId]?.messages.slice(-6) || [];
247
+ console.log('History:', history);
248
+ const result = await runInference(prompt, history);
249
+ console.log('Result:', result);
250
+ appendMessage('assistant', result);
251
+
252
+ executeBtn.disabled = false;
253
+ executeBtn.textContent = 'SEND';
254
+ statusEl.textContent = 'Ready.';
255
+ }
256
+
257
+ executeBtn.addEventListener('click', handleSend);
258
+ inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) handleSend(); });
259
+
260
+ newChatBtn.addEventListener('click', () => { currentChatId = createChat(); switchChat(currentChatId); inputEl.focus(); });
261
+
262
+ menuBtn.addEventListener('click', () => { sidebar.classList.toggle('open'); sidebarOverlay.classList.toggle('open'); });
263
+ sidebarOverlay.addEventListener('click', () => { sidebar.classList.remove('open'); sidebarOverlay.classList.remove('open'); });
264
+
265
+ renderChatList();
266
+ if (Object.keys(chats).length > 0) {
267
+ const latest = Object.values(chats).sort((a,b)=>b.updated-a.updated)[0];
268
+ switchChat(latest.id);
269
+ }
270
  initModel();
fix_escape.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ with open('app.js', 'r', encoding='utf-8') as f:
2
+ content = f.read()
3
+
4
+ # The issue is the escapeHtml function has smart quotes
5
+ # Find the line and replace with ASCII-only version
6
+ lines = content.split('\n')
7
+ for i, line in enumerate(lines):
8
+ if 'escapeHtml' in line and 'function' in line:
9
+ # Use chr() to build the replacement character map
10
+ lines[i] = 'function escapeHtml(s) { return s.replace(/[&<>"\']/g, c => ({"&":"&","<":"<",">":">","\"":"\"","\'":"\'"}[c])); }'
11
+ print(f'Fixed line {i+1}')
12
+
13
+ with open('app.js', 'w', encoding='utf-8') as f:
14
+ f.write('\n'.join(lines))
15
+ print('Done')