luckyd-code 1.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- luckyd_code/__init__.py +54 -0
- luckyd_code/__main__.py +5 -0
- luckyd_code/_agent_loop.py +551 -0
- luckyd_code/_data_dir.py +73 -0
- luckyd_code/agent.py +38 -0
- luckyd_code/analytics/__init__.py +18 -0
- luckyd_code/analytics/reporter.py +195 -0
- luckyd_code/analytics/scanner.py +443 -0
- luckyd_code/analytics/smells.py +316 -0
- luckyd_code/analytics/trends.py +303 -0
- luckyd_code/api.py +473 -0
- luckyd_code/audit_daemon.py +845 -0
- luckyd_code/autonomous_fixer.py +473 -0
- luckyd_code/background.py +159 -0
- luckyd_code/backup.py +237 -0
- luckyd_code/brain/__init__.py +84 -0
- luckyd_code/brain/assembler.py +100 -0
- luckyd_code/brain/chunker.py +345 -0
- luckyd_code/brain/constants.py +73 -0
- luckyd_code/brain/embedder.py +163 -0
- luckyd_code/brain/graph.py +311 -0
- luckyd_code/brain/indexer.py +316 -0
- luckyd_code/brain/parser.py +140 -0
- luckyd_code/brain/retriever.py +234 -0
- luckyd_code/cli.py +894 -0
- luckyd_code/cli_commands/__init__.py +1 -0
- luckyd_code/cli_commands/audit.py +120 -0
- luckyd_code/cli_commands/background.py +83 -0
- luckyd_code/cli_commands/brain.py +87 -0
- luckyd_code/cli_commands/config.py +75 -0
- luckyd_code/cli_commands/dispatcher.py +695 -0
- luckyd_code/cli_commands/sessions.py +41 -0
- luckyd_code/cli_entry.py +147 -0
- luckyd_code/cli_utils.py +112 -0
- luckyd_code/config.py +205 -0
- luckyd_code/context.py +214 -0
- luckyd_code/cost_tracker.py +209 -0
- luckyd_code/error_reporter.py +508 -0
- luckyd_code/exceptions.py +39 -0
- luckyd_code/export.py +126 -0
- luckyd_code/feedback_analyzer.py +290 -0
- luckyd_code/file_watcher.py +258 -0
- luckyd_code/git/__init__.py +11 -0
- luckyd_code/git/auto_commit.py +157 -0
- luckyd_code/git/tools.py +85 -0
- luckyd_code/hooks.py +236 -0
- luckyd_code/indexer.py +280 -0
- luckyd_code/init.py +39 -0
- luckyd_code/keybindings.py +77 -0
- luckyd_code/log.py +55 -0
- luckyd_code/mcp/__init__.py +6 -0
- luckyd_code/mcp/client.py +184 -0
- luckyd_code/memory/__init__.py +19 -0
- luckyd_code/memory/manager.py +339 -0
- luckyd_code/metrics/__init__.py +5 -0
- luckyd_code/model_registry.py +131 -0
- luckyd_code/orchestrator.py +204 -0
- luckyd_code/permissions/__init__.py +1 -0
- luckyd_code/permissions/manager.py +103 -0
- luckyd_code/planner.py +361 -0
- luckyd_code/plugins.py +91 -0
- luckyd_code/py.typed +0 -0
- luckyd_code/retry.py +57 -0
- luckyd_code/router.py +417 -0
- luckyd_code/sandbox.py +156 -0
- luckyd_code/self_critique.py +2 -0
- luckyd_code/self_improve.py +274 -0
- luckyd_code/sessions.py +114 -0
- luckyd_code/settings.py +72 -0
- luckyd_code/skills/__init__.py +8 -0
- luckyd_code/skills/review.py +22 -0
- luckyd_code/skills/security.py +17 -0
- luckyd_code/tasks/__init__.py +1 -0
- luckyd_code/tasks/manager.py +102 -0
- luckyd_code/templates/icon-192.png +0 -0
- luckyd_code/templates/icon-512.png +0 -0
- luckyd_code/templates/index.html +1965 -0
- luckyd_code/templates/manifest.json +14 -0
- luckyd_code/templates/src/app.js +694 -0
- luckyd_code/templates/src/body.html +767 -0
- luckyd_code/templates/src/cdn.txt +2 -0
- luckyd_code/templates/src/style.css +474 -0
- luckyd_code/templates/sw.js +31 -0
- luckyd_code/templates/test.html +6 -0
- luckyd_code/themes.py +48 -0
- luckyd_code/tools/__init__.py +97 -0
- luckyd_code/tools/agent_tools.py +65 -0
- luckyd_code/tools/bash.py +360 -0
- luckyd_code/tools/brain_tools.py +137 -0
- luckyd_code/tools/browser.py +369 -0
- luckyd_code/tools/datetime_tool.py +34 -0
- luckyd_code/tools/dockerfile_gen.py +212 -0
- luckyd_code/tools/file_ops.py +381 -0
- luckyd_code/tools/game_gen.py +360 -0
- luckyd_code/tools/git_tools.py +130 -0
- luckyd_code/tools/git_worktree.py +63 -0
- luckyd_code/tools/path_validate.py +64 -0
- luckyd_code/tools/project_gen.py +187 -0
- luckyd_code/tools/readme_gen.py +227 -0
- luckyd_code/tools/registry.py +157 -0
- luckyd_code/tools/shell_detect.py +109 -0
- luckyd_code/tools/web.py +89 -0
- luckyd_code/tools/youtube.py +187 -0
- luckyd_code/tools_bridge.py +144 -0
- luckyd_code/undo.py +126 -0
- luckyd_code/update.py +60 -0
- luckyd_code/verify.py +360 -0
- luckyd_code/web_app.py +176 -0
- luckyd_code/web_routes/__init__.py +23 -0
- luckyd_code/web_routes/background.py +73 -0
- luckyd_code/web_routes/brain.py +109 -0
- luckyd_code/web_routes/cost.py +12 -0
- luckyd_code/web_routes/files.py +133 -0
- luckyd_code/web_routes/memories.py +94 -0
- luckyd_code/web_routes/misc.py +67 -0
- luckyd_code/web_routes/project.py +48 -0
- luckyd_code/web_routes/review.py +20 -0
- luckyd_code/web_routes/sessions.py +44 -0
- luckyd_code/web_routes/settings.py +43 -0
- luckyd_code/web_routes/static.py +70 -0
- luckyd_code/web_routes/update.py +19 -0
- luckyd_code/web_routes/ws.py +237 -0
- luckyd_code-1.2.2.dist-info/METADATA +297 -0
- luckyd_code-1.2.2.dist-info/RECORD +127 -0
- luckyd_code-1.2.2.dist-info/WHEEL +4 -0
- luckyd_code-1.2.2.dist-info/entry_points.txt +3 -0
- luckyd_code-1.2.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "LuckyD Code",
|
|
3
|
+
"short_name": "LuckyD",
|
|
4
|
+
"description": "AI-powered coding assistant with voice",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#0d1117",
|
|
8
|
+
"theme_color": "#0d1117",
|
|
9
|
+
"orientation": "any",
|
|
10
|
+
"icons": [
|
|
11
|
+
{"src": "/icon-192.png", "sizes": "192x192", "type": "image/png"},
|
|
12
|
+
{"src": "/icon-512.png", "sizes": "512x512", "type": "image/png"}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
// Configure marked
|
|
2
|
+
marked.setOptions({
|
|
3
|
+
highlight: function(code, lang) {
|
|
4
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
5
|
+
try { return hljs.highlight(code, { language: lang }).value; } catch(e) {}
|
|
6
|
+
}
|
|
7
|
+
try { return hljs.highlightAuto(code).value; } catch(e) {}
|
|
8
|
+
return code;
|
|
9
|
+
},
|
|
10
|
+
breaks: true,
|
|
11
|
+
gfm: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// State
|
|
15
|
+
let ws = null;
|
|
16
|
+
let isProcessing = false;
|
|
17
|
+
let currentAssistantMsg = null;
|
|
18
|
+
let currentAssistantDiv = null;
|
|
19
|
+
let currentToolMsg = null;
|
|
20
|
+
let reconnectTimer = null;
|
|
21
|
+
let reconnectAttempts = 0;
|
|
22
|
+
let lastSendTime = 0;
|
|
23
|
+
let messageQueue = [];
|
|
24
|
+
let renderTimeout = null;
|
|
25
|
+
|
|
26
|
+
// DOM refs
|
|
27
|
+
const messagesEl = document.getElementById('messages');
|
|
28
|
+
const inputEl = document.getElementById('input');
|
|
29
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
30
|
+
const voiceBtn = document.getElementById('voiceBtn');
|
|
31
|
+
const statusDot = document.getElementById('statusDot');
|
|
32
|
+
const fileList = document.getElementById('fileList');
|
|
33
|
+
const filePath = document.getElementById('filePath');
|
|
34
|
+
const sidebar = document.getElementById('sidebar');
|
|
35
|
+
const overlay = document.getElementById('sidebarOverlay');
|
|
36
|
+
|
|
37
|
+
// Auto-resize textarea
|
|
38
|
+
inputEl.addEventListener('input', function() {
|
|
39
|
+
this.style.height = 'auto';
|
|
40
|
+
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Enter to send, Shift+Enter for newline, Ctrl+Enter also sends
|
|
44
|
+
inputEl.addEventListener('keydown', function(e) {
|
|
45
|
+
if ((e.key === 'Enter' && !e.shiftKey) || (e.key === 'Enter' && e.ctrlKey)) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
sendMessage();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// WebSocket connection with exponential backoff
|
|
52
|
+
function connect() {
|
|
53
|
+
if (ws && ws.readyState === WebSocket.OPEN) return;
|
|
54
|
+
|
|
55
|
+
statusDot.className = 'status-dot connecting';
|
|
56
|
+
|
|
57
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
58
|
+
const url = `${protocol}//${location.host}/ws`;
|
|
59
|
+
|
|
60
|
+
ws = new WebSocket(url);
|
|
61
|
+
|
|
62
|
+
ws.onopen = function() {
|
|
63
|
+
statusDot.className = 'status-dot connected';
|
|
64
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
65
|
+
reconnectAttempts = 0;
|
|
66
|
+
removeSystemMessage('disconnected');
|
|
67
|
+
addSystemMessage('Connected');
|
|
68
|
+
|
|
69
|
+
// Flush queued messages
|
|
70
|
+
while (messageQueue.length > 0) {
|
|
71
|
+
var q = messageQueue.shift();
|
|
72
|
+
ws.send(q);
|
|
73
|
+
}
|
|
74
|
+
// Re-enable input if stuck
|
|
75
|
+
if (isProcessing) {
|
|
76
|
+
isProcessing = false;
|
|
77
|
+
setInputEnabled(true);
|
|
78
|
+
removeTyping();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
ws.onclose = function() {
|
|
83
|
+
statusDot.className = 'status-dot disconnected';
|
|
84
|
+
if (!reconnectTimer) {
|
|
85
|
+
reconnectAttempts++;
|
|
86
|
+
var delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
87
|
+
addSystemMessage('Reconnecting in ' + Math.round(delay / 1000) + 's...');
|
|
88
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
ws.onerror = function() {
|
|
93
|
+
statusDot.className = 'status-dot disconnected';
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
ws.onmessage = function(event) {
|
|
97
|
+
try {
|
|
98
|
+
var msg = JSON.parse(event.data);
|
|
99
|
+
handleMessage(msg);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error('Parse error:', e);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Message handler
|
|
107
|
+
function handleMessage(msg) {
|
|
108
|
+
switch (msg.type) {
|
|
109
|
+
case 'text':
|
|
110
|
+
if (!currentAssistantMsg) {
|
|
111
|
+
currentAssistantDiv = createMessageDiv('assistant');
|
|
112
|
+
currentAssistantMsg = {div: currentAssistantDiv, text: ''};
|
|
113
|
+
removeTyping();
|
|
114
|
+
}
|
|
115
|
+
currentAssistantMsg.text += msg.content;
|
|
116
|
+
currentAssistantMsg.div.innerHTML = marked.parse(currentAssistantMsg.text);
|
|
117
|
+
// Debounced syntax highlighting
|
|
118
|
+
if (renderTimeout) clearTimeout(renderTimeout);
|
|
119
|
+
renderTimeout = setTimeout(function() {
|
|
120
|
+
currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
|
|
121
|
+
hljs.highlightElement(b);
|
|
122
|
+
});
|
|
123
|
+
}, 200);
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'tool':
|
|
127
|
+
// Tool call start
|
|
128
|
+
removeTyping();
|
|
129
|
+
if (currentAssistantMsg) {
|
|
130
|
+
currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
|
|
131
|
+
hljs.highlightElement(b);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
currentToolMsg = createMessageDiv('tool-call');
|
|
135
|
+
currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span> Running...`;
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case 'tool_result':
|
|
139
|
+
if (currentToolMsg) {
|
|
140
|
+
currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span><div class="tool-result">${escapeHtml(msg.content)}</div>`;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'error':
|
|
145
|
+
removeTyping();
|
|
146
|
+
createMessageDiv('error').textContent = msg.content;
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case 'done':
|
|
150
|
+
isProcessing = false;
|
|
151
|
+
// Speak the response and auto-loop
|
|
152
|
+
const spokenText = currentAssistantMsg ? currentAssistantMsg.text : '';
|
|
153
|
+
currentAssistantMsg = null;
|
|
154
|
+
currentAssistantDiv = null;
|
|
155
|
+
currentToolMsg = null;
|
|
156
|
+
setInputEnabled(true);
|
|
157
|
+
if (renderTimeout) clearTimeout(renderTimeout);
|
|
158
|
+
renderTimeout = null;
|
|
159
|
+
if (spokenText) {
|
|
160
|
+
speakText(spokenText).then(() => startAutoLoop());
|
|
161
|
+
} else {
|
|
162
|
+
startAutoLoop();
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case 'cleared':
|
|
167
|
+
messagesEl.innerHTML = '';
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
scrollToBottom();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createMessageDiv(role) {
|
|
175
|
+
const div = document.createElement('div');
|
|
176
|
+
div.className = `message ${role}`;
|
|
177
|
+
messagesEl.appendChild(div);
|
|
178
|
+
return div;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderAndScroll(el) {
|
|
182
|
+
const text = el.textContent || el.innerText;
|
|
183
|
+
el.innerHTML = marked.parse(text);
|
|
184
|
+
el.querySelectorAll('pre code').forEach(function(b) {
|
|
185
|
+
hljs.highlightElement(b);
|
|
186
|
+
// Add copy button
|
|
187
|
+
const pre = b.parentElement;
|
|
188
|
+
if (pre && !pre.querySelector('.copy-btn')) {
|
|
189
|
+
const btn = document.createElement('button');
|
|
190
|
+
btn.className = 'copy-btn';
|
|
191
|
+
btn.innerHTML = '<i class="fas fa-copy"></i>';
|
|
192
|
+
btn.onclick = function() {
|
|
193
|
+
navigator.clipboard.writeText(b.textContent).then(function() {
|
|
194
|
+
btn.innerHTML = '<i class="fas fa-check"></i>';
|
|
195
|
+
setTimeout(function() { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 2000);
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
pre.style.position = 'relative';
|
|
199
|
+
pre.appendChild(btn);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Escape HTML for tool results
|
|
205
|
+
function escapeHtml(text) {
|
|
206
|
+
const d = document.createElement('div');
|
|
207
|
+
d.textContent = text;
|
|
208
|
+
return d.innerHTML;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function removeTyping() {
|
|
212
|
+
const typing = document.querySelector('.typing');
|
|
213
|
+
if (typing) typing.remove();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function scrollToBottom() {
|
|
217
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function addSystemMessage(text) {
|
|
221
|
+
const div = document.createElement('div');
|
|
222
|
+
div.style.cssText = 'text-align:center;font-size:12px;color:var(--text-dim);padding:4px;';
|
|
223
|
+
div.textContent = text;
|
|
224
|
+
messagesEl.appendChild(div);
|
|
225
|
+
scrollToBottom();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Send message
|
|
229
|
+
function removeSystemMessage(id) {
|
|
230
|
+
var el = document.querySelector('.system-msg-' + id);
|
|
231
|
+
if (el) el.remove();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function sendMessage() {
|
|
235
|
+
var text = inputEl.value.trim();
|
|
236
|
+
if (!text || isProcessing) return;
|
|
237
|
+
|
|
238
|
+
// Rate limiting: max 1 message per 500ms
|
|
239
|
+
var now = Date.now();
|
|
240
|
+
if (now - lastSendTime < 500) {
|
|
241
|
+
addSystemMessage('Please wait...');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
246
|
+
addSystemMessage('Not connected. Message queued.', 'disconnected');
|
|
247
|
+
messageQueue.push(JSON.stringify({ type: 'message', content: text }));
|
|
248
|
+
connect();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Input length limit
|
|
253
|
+
if (text.length > 10000) {
|
|
254
|
+
addSystemMessage('Message too long (max 10000 characters)');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Add user message
|
|
259
|
+
var userDiv = createMessageDiv('user');
|
|
260
|
+
userDiv.textContent = text;
|
|
261
|
+
scrollToBottom();
|
|
262
|
+
|
|
263
|
+
inputEl.value = '';
|
|
264
|
+
inputEl.style.height = 'auto';
|
|
265
|
+
|
|
266
|
+
lastSendTime = now;
|
|
267
|
+
isProcessing = true;
|
|
268
|
+
setInputEnabled(false);
|
|
269
|
+
|
|
270
|
+
// Add typing indicator
|
|
271
|
+
var typing = document.createElement('div');
|
|
272
|
+
typing.className = 'typing';
|
|
273
|
+
typing.innerHTML = '<span></span><span></span><span></span>';
|
|
274
|
+
messagesEl.appendChild(typing);
|
|
275
|
+
scrollToBottom();
|
|
276
|
+
|
|
277
|
+
// Reset accumulators
|
|
278
|
+
currentAssistantMsg = null;
|
|
279
|
+
currentToolMsg = null;
|
|
280
|
+
|
|
281
|
+
ws.send(JSON.stringify({ type: 'message', content: text }));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function setInputEnabled(enabled) {
|
|
285
|
+
inputEl.disabled = !enabled;
|
|
286
|
+
sendBtn.disabled = !enabled;
|
|
287
|
+
if (enabled) inputEl.focus();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Clear chat
|
|
291
|
+
function clearChat() {
|
|
292
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
293
|
+
ws.send(JSON.stringify({ type: 'clear' }));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Voice input
|
|
297
|
+
let recognition = null;
|
|
298
|
+
let isListening = false;
|
|
299
|
+
|
|
300
|
+
function toggleVoice() {
|
|
301
|
+
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
|
302
|
+
addSystemMessage('Voice input not supported in this browser');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (isListening) {
|
|
307
|
+
recognition.stop();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
312
|
+
recognition = new SpeechRecognition();
|
|
313
|
+
recognition.lang = 'en-US';
|
|
314
|
+
recognition.interimResults = true;
|
|
315
|
+
recognition.continuous = true;
|
|
316
|
+
|
|
317
|
+
recognition.onstart = function() {
|
|
318
|
+
isListening = true;
|
|
319
|
+
voiceBtn.classList.add('listening');
|
|
320
|
+
voiceBtn.title = 'Listening...';
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
recognition.onresult = function(e) {
|
|
324
|
+
let transcript = '';
|
|
325
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
326
|
+
transcript += e.results[i][0].transcript;
|
|
327
|
+
}
|
|
328
|
+
inputEl.value = transcript;
|
|
329
|
+
inputEl.style.height = 'auto';
|
|
330
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
recognition.onend = function() {
|
|
334
|
+
isListening = false;
|
|
335
|
+
voiceBtn.classList.remove('listening');
|
|
336
|
+
voiceBtn.title = 'Voice input';
|
|
337
|
+
// Auto-send if we got something
|
|
338
|
+
if (inputEl.value.trim()) {
|
|
339
|
+
sendMessage();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
recognition.onerror = function(e) {
|
|
344
|
+
isListening = false;
|
|
345
|
+
voiceBtn.classList.remove('listening');
|
|
346
|
+
voiceBtn.title = 'Voice input';
|
|
347
|
+
if (e.error !== 'no-speech') {
|
|
348
|
+
addSystemMessage('Voice error: ' + e.error);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
recognition.start();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Voice output (TTS)
|
|
356
|
+
let speakEnabled = true;
|
|
357
|
+
let autoLoopEnabled = false;
|
|
358
|
+
|
|
359
|
+
function toggleSpeak() {
|
|
360
|
+
speakEnabled = !speakEnabled;
|
|
361
|
+
const btn = document.getElementById('speakToggle');
|
|
362
|
+
if (speakEnabled) {
|
|
363
|
+
btn.innerHTML = '<i class="fas fa-volume-up"></i>';
|
|
364
|
+
btn.style.color = '';
|
|
365
|
+
} else {
|
|
366
|
+
btn.innerHTML = '<i class="fas fa-volume-mute"></i>';
|
|
367
|
+
btn.style.color = 'var(--danger)';
|
|
368
|
+
window.speechSynthesis.cancel();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function toggleAutoLoop() {
|
|
373
|
+
autoLoopEnabled = !autoLoopEnabled;
|
|
374
|
+
const btn = document.getElementById('autoLoopToggle');
|
|
375
|
+
if (autoLoopEnabled) {
|
|
376
|
+
btn.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i>';
|
|
377
|
+
btn.style.color = 'var(--accent)';
|
|
378
|
+
speakEnabled = true;
|
|
379
|
+
document.getElementById('speakToggle').innerHTML = '<i class="fas fa-volume-up"></i>';
|
|
380
|
+
document.getElementById('speakToggle').style.color = '';
|
|
381
|
+
} else {
|
|
382
|
+
btn.innerHTML = '<i class="fas fa-sync-alt"></i>';
|
|
383
|
+
btn.style.color = '';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function speakText(text) {
|
|
388
|
+
if (!speakEnabled || !window.speechSynthesis) return Promise.resolve();
|
|
389
|
+
// Clean text for speech: remove markdown, code blocks, etc.
|
|
390
|
+
let clean = text
|
|
391
|
+
.replace(/```[\s\S]*?```/g, ' code block omitted ')
|
|
392
|
+
.replace(/`([^`]+)`/g, ' $1 ')
|
|
393
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
394
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
395
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
396
|
+
.replace(/#{1,6}\s*/g, '')
|
|
397
|
+
.replace(/[-*]\s/g, '')
|
|
398
|
+
.replace(/\n{2}/g, '. ')
|
|
399
|
+
.replace(/\n/g, ' ')
|
|
400
|
+
.trim();
|
|
401
|
+
|
|
402
|
+
if (!clean) return Promise.resolve();
|
|
403
|
+
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
const utterance = new SpeechSynthesisUtterance(clean);
|
|
406
|
+
utterance.lang = 'en-US';
|
|
407
|
+
utterance.rate = 1.1;
|
|
408
|
+
utterance.pitch = 1.0;
|
|
409
|
+
utterance.volume = 1.0;
|
|
410
|
+
utterance.onend = resolve;
|
|
411
|
+
utterance.onerror = resolve;
|
|
412
|
+
window.speechSynthesis.speak(utterance);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function startAutoLoop() {
|
|
417
|
+
if (!autoLoopEnabled) return;
|
|
418
|
+
// Small delay then re-activate mic
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
if (autoLoopEnabled && !isListening && document.visibilityState === 'visible') {
|
|
421
|
+
toggleVoice();
|
|
422
|
+
}
|
|
423
|
+
}, 800);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Sidebar toggle
|
|
427
|
+
function toggleSidebar() {
|
|
428
|
+
sidebar.classList.toggle('open');
|
|
429
|
+
overlay.classList.toggle('show');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
overlay.addEventListener('click', function() {
|
|
433
|
+
sidebar.classList.remove('open');
|
|
434
|
+
overlay.classList.remove('show');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// File browser
|
|
438
|
+
async function loadFiles(dir) {
|
|
439
|
+
if (!dir) dir = '.';
|
|
440
|
+
try {
|
|
441
|
+
fileList.innerHTML = '<div style="color:var(--text-dim);padding:16px;text-align:center;font-size:13px;">Loading...</div>';
|
|
442
|
+
const resp = await fetch(`/api/files?dir=${encodeURIComponent(dir)}`);
|
|
443
|
+
const data = await resp.json();
|
|
444
|
+
|
|
445
|
+
if (data.error) {
|
|
446
|
+
fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Error: ' + data.error + '</div>';
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
filePath.textContent = data.path;
|
|
451
|
+
fileList.innerHTML = '';
|
|
452
|
+
|
|
453
|
+
// Parent dir link
|
|
454
|
+
if (dir !== '.') {
|
|
455
|
+
const parent = document.createElement('div');
|
|
456
|
+
parent.className = 'file-item dir';
|
|
457
|
+
parent.innerHTML = '<span class="icon">📁</span> ..';
|
|
458
|
+
parent.onclick = function() {
|
|
459
|
+
const p = dir.split('/').slice(0, -1).join('/') || '.';
|
|
460
|
+
loadFiles(p);
|
|
461
|
+
};
|
|
462
|
+
fileList.appendChild(parent);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
data.files.forEach(function(f) {
|
|
466
|
+
const item = document.createElement('div');
|
|
467
|
+
item.className = 'file-item' + (f.is_dir ? ' dir' : '');
|
|
468
|
+
const iconClass = getFileIcon(f.name, f.is_dir);
|
|
469
|
+
item.innerHTML = '<span class="icon"><i class="' + iconClass + '"></i></span> ' + f.name;
|
|
470
|
+
if (!f.is_dir) {
|
|
471
|
+
const size = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
|
|
472
|
+
item.title = size;
|
|
473
|
+
}
|
|
474
|
+
item.onclick = function() {
|
|
475
|
+
if (f.is_dir) {
|
|
476
|
+
loadFiles(dir + '/' + f.name);
|
|
477
|
+
} else {
|
|
478
|
+
openFile(dir + '/' + f.name);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
fileList.appendChild(item);
|
|
482
|
+
});
|
|
483
|
+
} catch (e) {
|
|
484
|
+
fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Failed to load files</div>';
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// File editor state
|
|
489
|
+
let currentEditorFile = '';
|
|
490
|
+
let editorOriginalContent = '';
|
|
491
|
+
let isEditing = false;
|
|
492
|
+
|
|
493
|
+
async function openFile(path) {
|
|
494
|
+
try {
|
|
495
|
+
const resp = await fetch(`/api/read-file?path=${encodeURIComponent(path)}`);
|
|
496
|
+
const data = await resp.json();
|
|
497
|
+
if (data.content !== undefined) {
|
|
498
|
+
sidebar.classList.remove('open');
|
|
499
|
+
overlay.classList.remove('show');
|
|
500
|
+
openEditor(path, data.content);
|
|
501
|
+
} else if (data.error) {
|
|
502
|
+
addSystemMessage('Error: ' + data.error);
|
|
503
|
+
}
|
|
504
|
+
} catch(e) {
|
|
505
|
+
addSystemMessage('Error reading file');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function openEditor(path, content) {
|
|
510
|
+
currentEditorFile = path;
|
|
511
|
+
editorOriginalContent = content;
|
|
512
|
+
isEditing = false;
|
|
513
|
+
|
|
514
|
+
document.getElementById('editorTitle').textContent = path;
|
|
515
|
+
document.getElementById('editorFileInfo').textContent = (content.length) + ' chars';
|
|
516
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
517
|
+
|
|
518
|
+
const readView = document.getElementById('editorReadView');
|
|
519
|
+
readView.textContent = content;
|
|
520
|
+
readView.style.display = 'block';
|
|
521
|
+
|
|
522
|
+
const textarea = document.getElementById('editorTextarea');
|
|
523
|
+
textarea.value = content;
|
|
524
|
+
textarea.style.display = 'none';
|
|
525
|
+
textarea.oninput = function() {
|
|
526
|
+
document.getElementById('editorModified').style.display = 'inline';
|
|
527
|
+
document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
531
|
+
document.getElementById('editorSaveBtn').disabled = true;
|
|
532
|
+
|
|
533
|
+
document.getElementById('editorOverlay').classList.add('show');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function closeEditor() {
|
|
537
|
+
document.getElementById('editorOverlay').classList.remove('show');
|
|
538
|
+
currentEditorFile = '';
|
|
539
|
+
editorOriginalContent = '';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function toggleEditorMode() {
|
|
543
|
+
const readView = document.getElementById('editorReadView');
|
|
544
|
+
const textarea = document.getElementById('editorTextarea');
|
|
545
|
+
const modeBtn = document.getElementById('editorModeBtn');
|
|
546
|
+
|
|
547
|
+
if (!isEditing) {
|
|
548
|
+
// Switch to edit mode
|
|
549
|
+
textarea.value = readView.textContent;
|
|
550
|
+
textarea.style.display = 'block';
|
|
551
|
+
readView.style.display = 'none';
|
|
552
|
+
textarea.focus();
|
|
553
|
+
isEditing = true;
|
|
554
|
+
modeBtn.innerHTML = '<i class="fas fa-eye"></i> View';
|
|
555
|
+
document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
|
|
556
|
+
} else {
|
|
557
|
+
// Switch back to view mode (discard unsaved changes)
|
|
558
|
+
readView.textContent = editorOriginalContent;
|
|
559
|
+
readView.style.display = 'block';
|
|
560
|
+
textarea.style.display = 'none';
|
|
561
|
+
isEditing = false;
|
|
562
|
+
modeBtn.innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
563
|
+
document.getElementById('editorSaveBtn').disabled = true;
|
|
564
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function saveEditorFile() {
|
|
569
|
+
const textarea = document.getElementById('editorTextarea');
|
|
570
|
+
const content = textarea.value;
|
|
571
|
+
const path = currentEditorFile;
|
|
572
|
+
|
|
573
|
+
if (!path || content === editorOriginalContent) return;
|
|
574
|
+
|
|
575
|
+
const saveBtn = document.getElementById('editorSaveBtn');
|
|
576
|
+
saveBtn.disabled = true;
|
|
577
|
+
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const resp = await fetch('/api/write-file', {
|
|
581
|
+
method: 'POST',
|
|
582
|
+
headers: { 'Content-Type': 'application/json' },
|
|
583
|
+
body: JSON.stringify({ path: path, content: content }),
|
|
584
|
+
});
|
|
585
|
+
const data = await resp.json();
|
|
586
|
+
if (data.status === 'written') {
|
|
587
|
+
editorOriginalContent = content;
|
|
588
|
+
isEditing = false;
|
|
589
|
+
document.getElementById('editorReadView').textContent = content;
|
|
590
|
+
document.getElementById('editorReadView').style.display = 'block';
|
|
591
|
+
textarea.style.display = 'none';
|
|
592
|
+
document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
593
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
594
|
+
document.getElementById('editorFileInfo').textContent = content.length + ' chars (saved)';
|
|
595
|
+
addSystemMessage('File saved: ' + path);
|
|
596
|
+
} else {
|
|
597
|
+
addSystemMessage('Save failed: ' + (data.error || 'unknown error'));
|
|
598
|
+
}
|
|
599
|
+
} catch(e) {
|
|
600
|
+
addSystemMessage('Save error: ' + e.message);
|
|
601
|
+
} finally {
|
|
602
|
+
saveBtn.innerHTML = '<i class="fas fa-check"></i> Save';
|
|
603
|
+
saveBtn.disabled = true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Keyboard shortcut: Ctrl+S to save in editor
|
|
608
|
+
document.addEventListener('keydown', function(e) {
|
|
609
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
610
|
+
const overlay = document.getElementById('editorOverlay');
|
|
611
|
+
if (overlay.classList.contains('show') && isEditing) {
|
|
612
|
+
e.preventDefault();
|
|
613
|
+
saveEditorFile();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Escape to close editor
|
|
617
|
+
if (e.key === 'Escape') {
|
|
618
|
+
const overlay = document.getElementById('editorOverlay');
|
|
619
|
+
if (overlay.classList.contains('show')) {
|
|
620
|
+
closeEditor();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Load tools + model info
|
|
626
|
+
|
|
627
|
+
// Load tools + model info
|
|
628
|
+
async function loadMeta() {
|
|
629
|
+
try {
|
|
630
|
+
const resp = await fetch('/api/tools');
|
|
631
|
+
const data = await resp.json();
|
|
632
|
+
if (data.tools) {
|
|
633
|
+
// Could show tool count somewhere
|
|
634
|
+
}
|
|
635
|
+
} catch(e) {}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Theme toggle
|
|
639
|
+
function toggleTheme() {
|
|
640
|
+
const isLight = document.body.classList.toggle('light');
|
|
641
|
+
const icon = document.querySelector('#themeToggle i');
|
|
642
|
+
icon.className = isLight ? 'fas fa-sun' : 'fas fa-moon';
|
|
643
|
+
localStorage.setItem('theme', isLight ? 'light' : 'dark');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Load saved theme
|
|
647
|
+
const savedTheme = localStorage.getItem('theme');
|
|
648
|
+
if (savedTheme === 'light') {
|
|
649
|
+
document.body.classList.add('light');
|
|
650
|
+
document.querySelector('#themeToggle i').className = 'fas fa-sun';
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Improve file item icons
|
|
654
|
+
function getFileIcon(name, isDir) {
|
|
655
|
+
if (isDir) return 'fa-solid fa-folder';
|
|
656
|
+
const ext = name.split('.').pop().toLowerCase();
|
|
657
|
+
const iconMap = {
|
|
658
|
+
'js': 'fa-brands fa-js',
|
|
659
|
+
'ts': 'fa-brands fa-js',
|
|
660
|
+
'jsx': 'fa-brands fa-react',
|
|
661
|
+
'tsx': 'fa-brands fa-react',
|
|
662
|
+
'py': 'fa-brands fa-python',
|
|
663
|
+
'html': 'fa-brands fa-html5',
|
|
664
|
+
'css': 'fa-brands fa-css3-alt',
|
|
665
|
+
'json': 'fa-solid fa-code',
|
|
666
|
+
'md': 'fa-solid fa-file-lines',
|
|
667
|
+
'txt': 'fa-solid fa-file-lines',
|
|
668
|
+
'yaml': 'fa-solid fa-file',
|
|
669
|
+
'yml': 'fa-solid fa-file',
|
|
670
|
+
'toml': 'fa-solid fa-file',
|
|
671
|
+
'env': 'fa-solid fa-gear',
|
|
672
|
+
'gitignore': 'fa-solid fa-eye-slash',
|
|
673
|
+
'jpg': 'fa-solid fa-image',
|
|
674
|
+
'png': 'fa-solid fa-image',
|
|
675
|
+
'svg': 'fa-solid fa-image',
|
|
676
|
+
'ico': 'fa-solid fa-image',
|
|
677
|
+
'woff2': 'fa-solid fa-font',
|
|
678
|
+
'woff': 'fa-solid fa-font',
|
|
679
|
+
'ttf': 'fa-solid fa-font',
|
|
680
|
+
};
|
|
681
|
+
return iconMap[ext] || 'fa-solid fa-file';
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Connect on page load
|
|
685
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
686
|
+
// Register service worker for PWA
|
|
687
|
+
if ('serviceWorker' in navigator) {
|
|
688
|
+
navigator.serviceWorker.register('/sw.js').catch(function() {});
|
|
689
|
+
}
|
|
690
|
+
connect();
|
|
691
|
+
loadFiles();
|
|
692
|
+
loadMeta();
|
|
693
|
+
inputEl.focus();
|
|
694
|
+
});
|