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,1965 @@
|
|
|
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, viewport-fit=cover">
|
|
6
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
8
|
+
<meta name="apple-mobile-web-app-title" content="LuckyD Code">
|
|
9
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
10
|
+
<meta name="theme-color" content="#0d1117">
|
|
11
|
+
<link rel="manifest" href="/manifest.json">
|
|
12
|
+
<link rel="apple-touch-icon" href="/icon-192.png">
|
|
13
|
+
<link rel="icon" href="/icon-192.png" type="image/png">
|
|
14
|
+
<title>LuckyD Code</title>
|
|
15
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
|
16
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
17
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.2/marked.min.js"></script>
|
|
18
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
19
|
+
<style>
|
|
20
|
+
/* === Auto-generated by build_html.py v202604281724 === */
|
|
21
|
+
/* === Auto-generated by build_html.py v202604281723 === */
|
|
22
|
+
/* === Auto-generated by build_html.py v202604281723 === */
|
|
23
|
+
:root {
|
|
24
|
+
--bg: #1a1a2e;
|
|
25
|
+
--surface: #16213e;
|
|
26
|
+
--surface2: #1f2b47;
|
|
27
|
+
--border: #2a3a5e;
|
|
28
|
+
--text: #e0e0e0;
|
|
29
|
+
--text-dim: #8892b0;
|
|
30
|
+
--accent: #4fc3f7;
|
|
31
|
+
--accent2: #81c784;
|
|
32
|
+
--accent3: #ffb74d;
|
|
33
|
+
--danger: #ef5350;
|
|
34
|
+
--msg-user: #1e3a5f;
|
|
35
|
+
--msg-assistant: #1a1a2e;
|
|
36
|
+
--sidebar-w: 280px;
|
|
37
|
+
--header-h: 52px;
|
|
38
|
+
--radius: 8px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Light theme overrides */
|
|
42
|
+
.light {
|
|
43
|
+
--bg: #f5f5f5;
|
|
44
|
+
--surface: #ffffff;
|
|
45
|
+
--surface2: #e8e8e8;
|
|
46
|
+
--border: #d0d0d0;
|
|
47
|
+
--text: #1a1a2e;
|
|
48
|
+
--text-dim: #666;
|
|
49
|
+
--accent: #1976d2;
|
|
50
|
+
--accent2: #388e3c;
|
|
51
|
+
--accent3: #f57c00;
|
|
52
|
+
--danger: #d32f2f;
|
|
53
|
+
--msg-user: #e3f2fd;
|
|
54
|
+
--msg-assistant: #ffffff;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
58
|
+
|
|
59
|
+
body {
|
|
60
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
61
|
+
background: var(--bg);
|
|
62
|
+
color: var(--text);
|
|
63
|
+
height: 100vh;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Layout */
|
|
68
|
+
.app { display: flex; height: 100vh; }
|
|
69
|
+
|
|
70
|
+
/* Sidebar */
|
|
71
|
+
.sidebar {
|
|
72
|
+
width: var(--sidebar-w);
|
|
73
|
+
background: var(--surface);
|
|
74
|
+
border-right: 1px solid var(--border);
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
transition: transform .3s;
|
|
79
|
+
}
|
|
80
|
+
.sidebar-header {
|
|
81
|
+
padding: 14px 16px;
|
|
82
|
+
border-bottom: 1px solid var(--border);
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
font-size: 14px;
|
|
85
|
+
color: var(--text-dim);
|
|
86
|
+
display: flex;
|
|
87
|
+
justify-content: space-between;
|
|
88
|
+
align-items: center;
|
|
89
|
+
}
|
|
90
|
+
.sidebar-header button {
|
|
91
|
+
background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 14px;
|
|
92
|
+
}
|
|
93
|
+
.sidebar-content {
|
|
94
|
+
flex: 1;
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
padding: 8px;
|
|
97
|
+
}
|
|
98
|
+
.file-item {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
padding: 6px 10px;
|
|
103
|
+
border-radius: 4px;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
font-size: 13px;
|
|
106
|
+
color: var(--text-dim);
|
|
107
|
+
transition: background .15s;
|
|
108
|
+
}
|
|
109
|
+
.file-item:hover { background: var(--surface2); color: var(--text); }
|
|
110
|
+
.file-item .icon { font-size: 14px; }
|
|
111
|
+
.file-item.dir { color: var(--accent); }
|
|
112
|
+
.file-path { padding: 6px 10px; font-size: 11px; color: var(--text-dim); border-top: 1px solid var(--border); word-break: break-all; }
|
|
113
|
+
|
|
114
|
+
.sidebar-toggle {
|
|
115
|
+
display: none;
|
|
116
|
+
background: none;
|
|
117
|
+
border: none;
|
|
118
|
+
color: var(--text);
|
|
119
|
+
font-size: 20px;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
padding: 8px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Main area */
|
|
125
|
+
.main {
|
|
126
|
+
flex: 1;
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
min-width: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Header */
|
|
133
|
+
.header {
|
|
134
|
+
height: var(--header-h);
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
padding: 0 16px;
|
|
138
|
+
border-bottom: 1px solid var(--border);
|
|
139
|
+
gap: 12px;
|
|
140
|
+
flex-shrink: 0;
|
|
141
|
+
}
|
|
142
|
+
.header h1 {
|
|
143
|
+
font-size: 16px;
|
|
144
|
+
font-weight: 600;
|
|
145
|
+
}
|
|
146
|
+
.header .model-badge {
|
|
147
|
+
font-size: 11px;
|
|
148
|
+
color: var(--accent);
|
|
149
|
+
background: rgba(79,195,247,.15);
|
|
150
|
+
padding: 2px 8px;
|
|
151
|
+
border-radius: 10px;
|
|
152
|
+
}
|
|
153
|
+
.header-actions {
|
|
154
|
+
margin-left: auto;
|
|
155
|
+
display: flex;
|
|
156
|
+
gap: 8px;
|
|
157
|
+
}
|
|
158
|
+
.header-actions button {
|
|
159
|
+
background: none; border: none; color: var(--text-dim); cursor: pointer;
|
|
160
|
+
padding: 4px 8px; border-radius: 4px; font-size: 13px;
|
|
161
|
+
}
|
|
162
|
+
.header-actions button:hover { background: var(--surface2); color: var(--text); }
|
|
163
|
+
|
|
164
|
+
/* Messages */
|
|
165
|
+
.messages {
|
|
166
|
+
flex: 1;
|
|
167
|
+
overflow-y: auto;
|
|
168
|
+
padding: 16px;
|
|
169
|
+
display: flex;
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
gap: 12px;
|
|
172
|
+
}
|
|
173
|
+
.message {
|
|
174
|
+
max-width: 85%;
|
|
175
|
+
padding: 12px 16px;
|
|
176
|
+
border-radius: var(--radius);
|
|
177
|
+
line-height: 1.5;
|
|
178
|
+
font-size: 14px;
|
|
179
|
+
animation: fadeIn .2s;
|
|
180
|
+
}
|
|
181
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
182
|
+
|
|
183
|
+
.message.user {
|
|
184
|
+
align-self: flex-end;
|
|
185
|
+
background: var(--msg-user);
|
|
186
|
+
border-bottom-right-radius: 2px;
|
|
187
|
+
}
|
|
188
|
+
.message.assistant {
|
|
189
|
+
align-self: flex-start;
|
|
190
|
+
background: var(--msg-assistant);
|
|
191
|
+
border: 1px solid var(--border);
|
|
192
|
+
border-bottom-left-radius: 2px;
|
|
193
|
+
}
|
|
194
|
+
.message.tool-call {
|
|
195
|
+
align-self: flex-start;
|
|
196
|
+
background: rgba(255,183,77,.08);
|
|
197
|
+
border: 1px solid rgba(255,183,77,.2);
|
|
198
|
+
font-size: 13px;
|
|
199
|
+
color: var(--accent3);
|
|
200
|
+
width: 100%;
|
|
201
|
+
max-width: 100%;
|
|
202
|
+
}
|
|
203
|
+
.message.tool-call .tool-name { font-weight: 600; }
|
|
204
|
+
.message.tool-call .tool-result { color: var(--text-dim); margin-top: 4px; font-family: monospace; font-size: 12px; }
|
|
205
|
+
.message.error {
|
|
206
|
+
align-self: center;
|
|
207
|
+
background: rgba(239,83,80,.1);
|
|
208
|
+
border: 1px solid rgba(239,83,80,.3);
|
|
209
|
+
color: var(--danger);
|
|
210
|
+
font-size: 13px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Message content markdown */
|
|
214
|
+
.message p { margin-bottom: 8px; }
|
|
215
|
+
.message p:last-child { margin-bottom: 0; }
|
|
216
|
+
.message ul, .message ol { margin: 4px 0 8px 20px; }
|
|
217
|
+
.message li { margin-bottom: 2px; }
|
|
218
|
+
.message code {
|
|
219
|
+
background: rgba(255,255,255,.08);
|
|
220
|
+
padding: 2px 6px;
|
|
221
|
+
border-radius: 3px;
|
|
222
|
+
font-size: 13px;
|
|
223
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
224
|
+
}
|
|
225
|
+
.message pre {
|
|
226
|
+
margin: 8px 0;
|
|
227
|
+
border-radius: 6px;
|
|
228
|
+
overflow-x: auto;
|
|
229
|
+
}
|
|
230
|
+
.message pre code {
|
|
231
|
+
background: none;
|
|
232
|
+
padding: 0;
|
|
233
|
+
font-size: 13px;
|
|
234
|
+
}
|
|
235
|
+
.message h1, .message h2, .message h3, .message h4 {
|
|
236
|
+
margin: 12px 0 6px;
|
|
237
|
+
color: var(--accent);
|
|
238
|
+
}
|
|
239
|
+
.message h1 { font-size: 18px; }
|
|
240
|
+
.message h2 { font-size: 16px; }
|
|
241
|
+
.message h3 { font-size: 15px; }
|
|
242
|
+
.message blockquote {
|
|
243
|
+
border-left: 3px solid var(--accent);
|
|
244
|
+
padding-left: 12px;
|
|
245
|
+
color: var(--text-dim);
|
|
246
|
+
margin: 8px 0;
|
|
247
|
+
}
|
|
248
|
+
.message table {
|
|
249
|
+
border-collapse: collapse;
|
|
250
|
+
margin: 8px 0;
|
|
251
|
+
font-size: 13px;
|
|
252
|
+
}
|
|
253
|
+
.message th, .message td {
|
|
254
|
+
border: 1px solid var(--border);
|
|
255
|
+
padding: 6px 10px;
|
|
256
|
+
text-align: left;
|
|
257
|
+
}
|
|
258
|
+
.message th { background: var(--surface2); }
|
|
259
|
+
|
|
260
|
+
/* Typing indicator */
|
|
261
|
+
.typing {
|
|
262
|
+
align-self: flex-start;
|
|
263
|
+
display: flex;
|
|
264
|
+
gap: 4px;
|
|
265
|
+
padding: 12px 16px;
|
|
266
|
+
background: var(--msg-assistant);
|
|
267
|
+
border: 1px solid var(--border);
|
|
268
|
+
border-radius: var(--radius);
|
|
269
|
+
}
|
|
270
|
+
.typing span {
|
|
271
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
272
|
+
background: var(--text-dim);
|
|
273
|
+
animation: typingDot 1.4s infinite;
|
|
274
|
+
}
|
|
275
|
+
.typing span:nth-child(2) { animation-delay: .2s; }
|
|
276
|
+
.typing span:nth-child(3) { animation-delay: .4s; }
|
|
277
|
+
@keyframes typingDot { 0%,60%,100% { opacity: .3; transform: scale(1); } 30% { opacity: 1; transform: scale(1.2); } }
|
|
278
|
+
|
|
279
|
+
/* Input area */
|
|
280
|
+
.input-area {
|
|
281
|
+
padding: 12px 16px;
|
|
282
|
+
border-top: 1px solid var(--border);
|
|
283
|
+
background: var(--surface);
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
}
|
|
286
|
+
.input-row {
|
|
287
|
+
display: flex;
|
|
288
|
+
gap: 8px;
|
|
289
|
+
align-items: flex-end;
|
|
290
|
+
}
|
|
291
|
+
.input-row textarea {
|
|
292
|
+
flex: 1;
|
|
293
|
+
background: var(--surface2);
|
|
294
|
+
border: 1px solid var(--border);
|
|
295
|
+
border-radius: var(--radius);
|
|
296
|
+
color: var(--text);
|
|
297
|
+
padding: 10px 14px;
|
|
298
|
+
font-size: 14px;
|
|
299
|
+
font-family: inherit;
|
|
300
|
+
resize: none;
|
|
301
|
+
outline: none;
|
|
302
|
+
min-height: 42px;
|
|
303
|
+
max-height: 150px;
|
|
304
|
+
line-height: 1.4;
|
|
305
|
+
}
|
|
306
|
+
.input-row textarea:focus { border-color: var(--accent); }
|
|
307
|
+
.input-row textarea::placeholder { color: var(--text-dim); }
|
|
308
|
+
|
|
309
|
+
.input-row button {
|
|
310
|
+
background: var(--accent);
|
|
311
|
+
border: none;
|
|
312
|
+
color: #0d1117;
|
|
313
|
+
width: 42px;
|
|
314
|
+
height: 42px;
|
|
315
|
+
border-radius: 50%;
|
|
316
|
+
cursor: pointer;
|
|
317
|
+
font-size: 18px;
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
justify-content: center;
|
|
321
|
+
transition: background .15s, opacity .15s;
|
|
322
|
+
flex-shrink: 0;
|
|
323
|
+
}
|
|
324
|
+
.input-row button:hover { background: #39b0e8; }
|
|
325
|
+
.input-row button:disabled { opacity: .4; cursor: not-allowed; }
|
|
326
|
+
.input-row button.voice { background: var(--surface2); color: var(--text); }
|
|
327
|
+
.input-row button.voice:hover { background: var(--border); }
|
|
328
|
+
.input-row button.voice.listening { background: var(--danger); color: white; animation: pulse 1s infinite; }
|
|
329
|
+
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(239,83,80,.4); } 50% { box-shadow: 0 0 0 8px rgba(239,83,80,0); } }
|
|
330
|
+
|
|
331
|
+
.input-hint {
|
|
332
|
+
font-size: 11px;
|
|
333
|
+
color: var(--text-dim);
|
|
334
|
+
margin-top: 6px;
|
|
335
|
+
text-align: center;
|
|
336
|
+
}
|
|
337
|
+
.input-hint kbd {
|
|
338
|
+
background: var(--surface2);
|
|
339
|
+
padding: 1px 5px;
|
|
340
|
+
border-radius: 3px;
|
|
341
|
+
font-size: 10px;
|
|
342
|
+
font-family: inherit;
|
|
343
|
+
border: 1px solid var(--border);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* Scrollbar */
|
|
347
|
+
::-webkit-scrollbar { width: 6px; }
|
|
348
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
349
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
350
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
|
|
351
|
+
|
|
352
|
+
/* Mobile responsive */
|
|
353
|
+
@media (max-width: 768px) {
|
|
354
|
+
.sidebar {
|
|
355
|
+
position: fixed;
|
|
356
|
+
left: 0;
|
|
357
|
+
top: 0;
|
|
358
|
+
bottom: 0;
|
|
359
|
+
z-index: 100;
|
|
360
|
+
transform: translateX(-100%);
|
|
361
|
+
}
|
|
362
|
+
.sidebar.open { transform: translateX(0); }
|
|
363
|
+
.sidebar-toggle { display: block; }
|
|
364
|
+
.message { max-width: 95%; }
|
|
365
|
+
.overlay {
|
|
366
|
+
display: none;
|
|
367
|
+
position: fixed;
|
|
368
|
+
inset: 0;
|
|
369
|
+
background: rgba(0,0,0,.5);
|
|
370
|
+
z-index: 99;
|
|
371
|
+
}
|
|
372
|
+
.overlay.show { display: block; }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* Copy button for code blocks */
|
|
376
|
+
.copy-btn {
|
|
377
|
+
position: absolute;
|
|
378
|
+
top: 6px;
|
|
379
|
+
right: 6px;
|
|
380
|
+
background: rgba(255,255,255,.1);
|
|
381
|
+
border: 1px solid var(--border);
|
|
382
|
+
color: var(--text-dim);
|
|
383
|
+
padding: 4px 8px;
|
|
384
|
+
border-radius: 4px;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
font-size: 12px;
|
|
387
|
+
opacity: 0;
|
|
388
|
+
transition: opacity .2s;
|
|
389
|
+
}
|
|
390
|
+
pre:hover .copy-btn { opacity: 1; }
|
|
391
|
+
.copy-btn:hover { background: rgba(255,255,255,.2); color: var(--text); }
|
|
392
|
+
|
|
393
|
+
/* File editor */
|
|
394
|
+
.editor-overlay {
|
|
395
|
+
display: none;
|
|
396
|
+
position: fixed;
|
|
397
|
+
inset: 0;
|
|
398
|
+
background: rgba(0,0,0,.6);
|
|
399
|
+
z-index: 200;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: center;
|
|
402
|
+
padding: 20px;
|
|
403
|
+
}
|
|
404
|
+
.editor-overlay.show { display: flex; }
|
|
405
|
+
.editor-panel {
|
|
406
|
+
background: var(--surface);
|
|
407
|
+
border: 1px solid var(--border);
|
|
408
|
+
border-radius: var(--radius);
|
|
409
|
+
width: 100%;
|
|
410
|
+
max-width: 800px;
|
|
411
|
+
max-height: 85vh;
|
|
412
|
+
display: flex;
|
|
413
|
+
flex-direction: column;
|
|
414
|
+
box-shadow: 0 8px 32px rgba(0,0,0,.4);
|
|
415
|
+
}
|
|
416
|
+
.editor-header {
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: space-between;
|
|
420
|
+
padding: 12px 16px;
|
|
421
|
+
border-bottom: 1px solid var(--border);
|
|
422
|
+
}
|
|
423
|
+
.editor-header h3 { font-size: 14px; font-weight: 600; word-break: break-all; }
|
|
424
|
+
.editor-actions { display: flex; gap: 6px; }
|
|
425
|
+
.editor-actions button {
|
|
426
|
+
background: var(--surface2);
|
|
427
|
+
border: 1px solid var(--border);
|
|
428
|
+
color: var(--text);
|
|
429
|
+
padding: 6px 14px;
|
|
430
|
+
border-radius: 4px;
|
|
431
|
+
cursor: pointer;
|
|
432
|
+
font-size: 13px;
|
|
433
|
+
transition: background .15s;
|
|
434
|
+
}
|
|
435
|
+
.editor-actions button:hover { background: var(--border); }
|
|
436
|
+
.editor-actions button.save {
|
|
437
|
+
background: var(--accent);
|
|
438
|
+
color: #0d1117;
|
|
439
|
+
border-color: var(--accent);
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
}
|
|
442
|
+
.editor-actions button.save:hover { opacity: .85; }
|
|
443
|
+
.editor-actions button.save:disabled { opacity: .4; cursor: not-allowed; }
|
|
444
|
+
.editor-body {
|
|
445
|
+
flex: 1;
|
|
446
|
+
overflow-y: auto;
|
|
447
|
+
padding: 0;
|
|
448
|
+
}
|
|
449
|
+
.editor-body textarea {
|
|
450
|
+
width: 100%;
|
|
451
|
+
min-height: 300px;
|
|
452
|
+
max-height: 65vh;
|
|
453
|
+
border: none;
|
|
454
|
+
background: var(--bg);
|
|
455
|
+
color: var(--text);
|
|
456
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
457
|
+
font-size: 13px;
|
|
458
|
+
line-height: 1.5;
|
|
459
|
+
padding: 16px;
|
|
460
|
+
resize: vertical;
|
|
461
|
+
outline: none;
|
|
462
|
+
tab-size: 2;
|
|
463
|
+
}
|
|
464
|
+
.editor-body .readonly-view {
|
|
465
|
+
padding: 16px;
|
|
466
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
467
|
+
font-size: 13px;
|
|
468
|
+
line-height: 1.5;
|
|
469
|
+
white-space: pre-wrap;
|
|
470
|
+
overflow-x: auto;
|
|
471
|
+
color: var(--text);
|
|
472
|
+
}
|
|
473
|
+
.editor-footer {
|
|
474
|
+
padding: 10px 16px;
|
|
475
|
+
border-top: 1px solid var(--border);
|
|
476
|
+
font-size: 12px;
|
|
477
|
+
color: var(--text-dim);
|
|
478
|
+
display: flex;
|
|
479
|
+
justify-content: space-between;
|
|
480
|
+
}
|
|
481
|
+
.editor-footer .modified { color: var(--accent3); }
|
|
482
|
+
|
|
483
|
+
/* Connection status */
|
|
484
|
+
.status-dot {
|
|
485
|
+
width: 8px;
|
|
486
|
+
height: 8px;
|
|
487
|
+
border-radius: 50%;
|
|
488
|
+
display: inline-block;
|
|
489
|
+
margin-right: 6px;
|
|
490
|
+
}
|
|
491
|
+
.status-dot.connected { background: var(--accent2); }
|
|
492
|
+
.status-dot.disconnected { background: var(--danger); }
|
|
493
|
+
.status-dot.connecting { background: var(--accent3); }
|
|
494
|
+
</style>
|
|
495
|
+
</head>
|
|
496
|
+
<body>
|
|
497
|
+
<div class="app">
|
|
498
|
+
<!-- Sidebar backdrop for mobile -->
|
|
499
|
+
<div class="overlay" id="sidebarOverlay"></div>
|
|
500
|
+
|
|
501
|
+
<!-- Sidebar -->
|
|
502
|
+
<aside class="sidebar" id="sidebar">
|
|
503
|
+
<div class="sidebar-header">
|
|
504
|
+
Files
|
|
505
|
+
<button onclick="loadFiles()" title="Refresh">↺</button>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="sidebar-content" id="fileList">
|
|
508
|
+
<div style="color:var(--text-dim);padding:16px;text-align:center;font-size:13px;">Loading files...</div>
|
|
509
|
+
</div>
|
|
510
|
+
<div class="file-path" id="filePath">.</div>
|
|
511
|
+
</aside>
|
|
512
|
+
|
|
513
|
+
<!-- Main -->
|
|
514
|
+
<div class="main">
|
|
515
|
+
<!-- Header -->
|
|
516
|
+
<div class="header">
|
|
517
|
+
<button class="sidebar-toggle" id="sidebarToggle" onclick="toggleSidebar()">☰</button>
|
|
518
|
+
<h1>LuckyD Code</h1>
|
|
519
|
+
<span class="model-badge" id="modelBadge">luckyd-chat</span>
|
|
520
|
+
<div class="header-actions">
|
|
521
|
+
<span class="status-dot disconnected" id="statusDot"></span>
|
|
522
|
+
<button id="speakToggle" onclick="toggleSpeak()" title="Voice output"><i class="fas fa-volume-up"></i></button>
|
|
523
|
+
<button id="autoLoopToggle" onclick="toggleAutoLoop()" title="Auto-listen"><i class="fas fa-sync-alt"></i></button>
|
|
524
|
+
<button id="themeToggle" onclick="toggleTheme()" title="Toggle theme"><i class="fas fa-moon"></i></button>
|
|
525
|
+
<button onclick="clearChat()" title="Clear conversation"><i class="fas fa-trash-alt"></i></button>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<!-- Messages -->
|
|
530
|
+
<div class="messages" id="messages"></div>
|
|
531
|
+
|
|
532
|
+
<!-- Input -->
|
|
533
|
+
<div class="input-area">
|
|
534
|
+
<div class="input-row">
|
|
535
|
+
<button class="voice" id="voiceBtn" onclick="toggleVoice()" title="Voice input">🎙</button>
|
|
536
|
+
<textarea id="input" rows="1" placeholder="Type a message..." maxlength="10000"></textarea>
|
|
537
|
+
<button id="sendBtn" onclick="sendMessage()" title="Send">➤</button>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="input-hint">
|
|
540
|
+
<kbd>Enter</kbd> to send • <kbd>Shift+Enter</kbd> for newline
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<!-- File editor overlay -->
|
|
547
|
+
<div class="editor-overlay" id="editorOverlay">
|
|
548
|
+
<div class="editor-panel">
|
|
549
|
+
<div class="editor-header">
|
|
550
|
+
<h3 id="editorTitle">File</h3>
|
|
551
|
+
<div class="editor-actions">
|
|
552
|
+
<button onclick="toggleEditorMode()" id="editorModeBtn"><i class="fas fa-edit"></i> Edit</button>
|
|
553
|
+
<button class="save" onclick="saveEditorFile()" id="editorSaveBtn" disabled><i class="fas fa-check"></i> Save</button>
|
|
554
|
+
<button onclick="closeEditor()"><i class="fas fa-times"></i> Close</button>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
<div class="editor-body">
|
|
558
|
+
<div class="readonly-view" id="editorReadView"></div>
|
|
559
|
+
<textarea id="editorTextarea" style="display:none;"></textarea>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="editor-footer">
|
|
562
|
+
<span id="editorFileInfo"></span>
|
|
563
|
+
<span id="editorModified" class="modified" style="display:none;">Modified</span>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<script>
|
|
569
|
+
// Configure marked
|
|
570
|
+
marked.setOptions({
|
|
571
|
+
highlight: function(code, lang) {
|
|
572
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
573
|
+
try { return hljs.highlight(code, { language: lang }).value; } catch(e) {}
|
|
574
|
+
}
|
|
575
|
+
try { return hljs.highlightAuto(code).value; } catch(e) {}
|
|
576
|
+
return code;
|
|
577
|
+
},
|
|
578
|
+
breaks: true,
|
|
579
|
+
gfm: true,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// State
|
|
583
|
+
let ws = null;
|
|
584
|
+
let isProcessing = false;
|
|
585
|
+
let currentAssistantMsg = null;
|
|
586
|
+
let currentAssistantDiv = null;
|
|
587
|
+
let currentToolMsg = null;
|
|
588
|
+
let reconnectTimer = null;
|
|
589
|
+
let reconnectAttempts = 0;
|
|
590
|
+
let lastSendTime = 0;
|
|
591
|
+
let messageQueue = [];
|
|
592
|
+
let renderTimeout = null;
|
|
593
|
+
|
|
594
|
+
// DOM refs
|
|
595
|
+
const messagesEl = document.getElementById('messages');
|
|
596
|
+
const inputEl = document.getElementById('input');
|
|
597
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
598
|
+
const voiceBtn = document.getElementById('voiceBtn');
|
|
599
|
+
const statusDot = document.getElementById('statusDot');
|
|
600
|
+
const fileList = document.getElementById('fileList');
|
|
601
|
+
const filePath = document.getElementById('filePath');
|
|
602
|
+
const sidebar = document.getElementById('sidebar');
|
|
603
|
+
const overlay = document.getElementById('sidebarOverlay');
|
|
604
|
+
|
|
605
|
+
// Auto-resize textarea
|
|
606
|
+
inputEl.addEventListener('input', function() {
|
|
607
|
+
this.style.height = 'auto';
|
|
608
|
+
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Enter to send, Shift+Enter for newline, Ctrl+Enter also sends
|
|
612
|
+
inputEl.addEventListener('keydown', function(e) {
|
|
613
|
+
if ((e.key === 'Enter' && !e.shiftKey) || (e.key === 'Enter' && e.ctrlKey)) {
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
sendMessage();
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// WebSocket connection with exponential backoff
|
|
620
|
+
function connect() {
|
|
621
|
+
if (ws && ws.readyState === WebSocket.OPEN) return;
|
|
622
|
+
|
|
623
|
+
statusDot.className = 'status-dot connecting';
|
|
624
|
+
|
|
625
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
626
|
+
const url = `${protocol}//${location.host}/ws`;
|
|
627
|
+
|
|
628
|
+
ws = new WebSocket(url);
|
|
629
|
+
|
|
630
|
+
ws.onopen = function() {
|
|
631
|
+
statusDot.className = 'status-dot connected';
|
|
632
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
633
|
+
reconnectAttempts = 0;
|
|
634
|
+
removeSystemMessage('disconnected');
|
|
635
|
+
addSystemMessage('Connected');
|
|
636
|
+
|
|
637
|
+
// Flush queued messages
|
|
638
|
+
while (messageQueue.length > 0) {
|
|
639
|
+
var q = messageQueue.shift();
|
|
640
|
+
ws.send(q);
|
|
641
|
+
}
|
|
642
|
+
// Re-enable input if stuck
|
|
643
|
+
if (isProcessing) {
|
|
644
|
+
isProcessing = false;
|
|
645
|
+
setInputEnabled(true);
|
|
646
|
+
removeTyping();
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
ws.onclose = function() {
|
|
651
|
+
statusDot.className = 'status-dot disconnected';
|
|
652
|
+
if (!reconnectTimer) {
|
|
653
|
+
reconnectAttempts++;
|
|
654
|
+
var delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
655
|
+
addSystemMessage('Reconnecting in ' + Math.round(delay / 1000) + 's...');
|
|
656
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
ws.onerror = function() {
|
|
661
|
+
statusDot.className = 'status-dot disconnected';
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
ws.onmessage = function(event) {
|
|
665
|
+
try {
|
|
666
|
+
var msg = JSON.parse(event.data);
|
|
667
|
+
handleMessage(msg);
|
|
668
|
+
} catch (e) {
|
|
669
|
+
console.error('Parse error:', e);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Message handler
|
|
675
|
+
function handleMessage(msg) {
|
|
676
|
+
switch (msg.type) {
|
|
677
|
+
case 'text':
|
|
678
|
+
if (!currentAssistantMsg) {
|
|
679
|
+
currentAssistantDiv = createMessageDiv('assistant');
|
|
680
|
+
currentAssistantMsg = {div: currentAssistantDiv, text: ''};
|
|
681
|
+
removeTyping();
|
|
682
|
+
}
|
|
683
|
+
currentAssistantMsg.text += msg.content;
|
|
684
|
+
currentAssistantMsg.div.innerHTML = marked.parse(currentAssistantMsg.text);
|
|
685
|
+
// Debounced syntax highlighting
|
|
686
|
+
if (renderTimeout) clearTimeout(renderTimeout);
|
|
687
|
+
renderTimeout = setTimeout(function() {
|
|
688
|
+
currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
|
|
689
|
+
hljs.highlightElement(b);
|
|
690
|
+
});
|
|
691
|
+
}, 200);
|
|
692
|
+
break;
|
|
693
|
+
|
|
694
|
+
case 'tool':
|
|
695
|
+
// Tool call start
|
|
696
|
+
removeTyping();
|
|
697
|
+
if (currentAssistantMsg) {
|
|
698
|
+
currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
|
|
699
|
+
hljs.highlightElement(b);
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
currentToolMsg = createMessageDiv('tool-call');
|
|
703
|
+
currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span> Running...`;
|
|
704
|
+
break;
|
|
705
|
+
|
|
706
|
+
case 'tool_result':
|
|
707
|
+
if (currentToolMsg) {
|
|
708
|
+
currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span><div class="tool-result">${escapeHtml(msg.content)}</div>`;
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
|
|
712
|
+
case 'error':
|
|
713
|
+
removeTyping();
|
|
714
|
+
createMessageDiv('error').textContent = msg.content;
|
|
715
|
+
break;
|
|
716
|
+
|
|
717
|
+
case 'done':
|
|
718
|
+
isProcessing = false;
|
|
719
|
+
// Speak the response and auto-loop
|
|
720
|
+
const spokenText = currentAssistantMsg ? currentAssistantMsg.text : '';
|
|
721
|
+
currentAssistantMsg = null;
|
|
722
|
+
currentAssistantDiv = null;
|
|
723
|
+
currentToolMsg = null;
|
|
724
|
+
setInputEnabled(true);
|
|
725
|
+
if (renderTimeout) clearTimeout(renderTimeout);
|
|
726
|
+
renderTimeout = null;
|
|
727
|
+
if (spokenText) {
|
|
728
|
+
speakText(spokenText).then(() => startAutoLoop());
|
|
729
|
+
} else {
|
|
730
|
+
startAutoLoop();
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
733
|
+
|
|
734
|
+
case 'cleared':
|
|
735
|
+
messagesEl.innerHTML = '';
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
scrollToBottom();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function createMessageDiv(role) {
|
|
743
|
+
const div = document.createElement('div');
|
|
744
|
+
div.className = `message ${role}`;
|
|
745
|
+
messagesEl.appendChild(div);
|
|
746
|
+
return div;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function renderAndScroll(el) {
|
|
750
|
+
const text = el.textContent || el.innerText;
|
|
751
|
+
el.innerHTML = marked.parse(text);
|
|
752
|
+
el.querySelectorAll('pre code').forEach(function(b) {
|
|
753
|
+
hljs.highlightElement(b);
|
|
754
|
+
// Add copy button
|
|
755
|
+
const pre = b.parentElement;
|
|
756
|
+
if (pre && !pre.querySelector('.copy-btn')) {
|
|
757
|
+
const btn = document.createElement('button');
|
|
758
|
+
btn.className = 'copy-btn';
|
|
759
|
+
btn.innerHTML = '<i class="fas fa-copy"></i>';
|
|
760
|
+
btn.onclick = function() {
|
|
761
|
+
navigator.clipboard.writeText(b.textContent).then(function() {
|
|
762
|
+
btn.innerHTML = '<i class="fas fa-check"></i>';
|
|
763
|
+
setTimeout(function() { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 2000);
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
pre.style.position = 'relative';
|
|
767
|
+
pre.appendChild(btn);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Escape HTML for tool results
|
|
773
|
+
function escapeHtml(text) {
|
|
774
|
+
const d = document.createElement('div');
|
|
775
|
+
d.textContent = text;
|
|
776
|
+
return d.innerHTML;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function removeTyping() {
|
|
780
|
+
const typing = document.querySelector('.typing');
|
|
781
|
+
if (typing) typing.remove();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function scrollToBottom() {
|
|
785
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function addSystemMessage(text) {
|
|
789
|
+
const div = document.createElement('div');
|
|
790
|
+
div.style.cssText = 'text-align:center;font-size:12px;color:var(--text-dim);padding:4px;';
|
|
791
|
+
div.textContent = text;
|
|
792
|
+
messagesEl.appendChild(div);
|
|
793
|
+
scrollToBottom();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Send message
|
|
797
|
+
function removeSystemMessage(id) {
|
|
798
|
+
var el = document.querySelector('.system-msg-' + id);
|
|
799
|
+
if (el) el.remove();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function sendMessage() {
|
|
803
|
+
var text = inputEl.value.trim();
|
|
804
|
+
if (!text || isProcessing) return;
|
|
805
|
+
|
|
806
|
+
// Rate limiting: max 1 message per 500ms
|
|
807
|
+
var now = Date.now();
|
|
808
|
+
if (now - lastSendTime < 500) {
|
|
809
|
+
addSystemMessage('Please wait...');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
814
|
+
addSystemMessage('Not connected. Message queued.', 'disconnected');
|
|
815
|
+
messageQueue.push(JSON.stringify({ type: 'message', content: text }));
|
|
816
|
+
connect();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Input length limit
|
|
821
|
+
if (text.length > 10000) {
|
|
822
|
+
addSystemMessage('Message too long (max 10000 characters)');
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Add user message
|
|
827
|
+
var userDiv = createMessageDiv('user');
|
|
828
|
+
userDiv.textContent = text;
|
|
829
|
+
scrollToBottom();
|
|
830
|
+
|
|
831
|
+
inputEl.value = '';
|
|
832
|
+
inputEl.style.height = 'auto';
|
|
833
|
+
|
|
834
|
+
lastSendTime = now;
|
|
835
|
+
isProcessing = true;
|
|
836
|
+
setInputEnabled(false);
|
|
837
|
+
|
|
838
|
+
// Add typing indicator
|
|
839
|
+
var typing = document.createElement('div');
|
|
840
|
+
typing.className = 'typing';
|
|
841
|
+
typing.innerHTML = '<span></span><span></span><span></span>';
|
|
842
|
+
messagesEl.appendChild(typing);
|
|
843
|
+
scrollToBottom();
|
|
844
|
+
|
|
845
|
+
// Reset accumulators
|
|
846
|
+
currentAssistantMsg = null;
|
|
847
|
+
currentToolMsg = null;
|
|
848
|
+
|
|
849
|
+
ws.send(JSON.stringify({ type: 'message', content: text }));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function setInputEnabled(enabled) {
|
|
853
|
+
inputEl.disabled = !enabled;
|
|
854
|
+
sendBtn.disabled = !enabled;
|
|
855
|
+
if (enabled) inputEl.focus();
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Clear chat
|
|
859
|
+
function clearChat() {
|
|
860
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
861
|
+
ws.send(JSON.stringify({ type: 'clear' }));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Voice input
|
|
865
|
+
let recognition = null;
|
|
866
|
+
let isListening = false;
|
|
867
|
+
|
|
868
|
+
function toggleVoice() {
|
|
869
|
+
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
|
870
|
+
addSystemMessage('Voice input not supported in this browser');
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (isListening) {
|
|
875
|
+
recognition.stop();
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
880
|
+
recognition = new SpeechRecognition();
|
|
881
|
+
recognition.lang = 'en-US';
|
|
882
|
+
recognition.interimResults = true;
|
|
883
|
+
recognition.continuous = true;
|
|
884
|
+
|
|
885
|
+
recognition.onstart = function() {
|
|
886
|
+
isListening = true;
|
|
887
|
+
voiceBtn.classList.add('listening');
|
|
888
|
+
voiceBtn.title = 'Listening...';
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
recognition.onresult = function(e) {
|
|
892
|
+
let transcript = '';
|
|
893
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
894
|
+
transcript += e.results[i][0].transcript;
|
|
895
|
+
}
|
|
896
|
+
inputEl.value = transcript;
|
|
897
|
+
inputEl.style.height = 'auto';
|
|
898
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
recognition.onend = function() {
|
|
902
|
+
isListening = false;
|
|
903
|
+
voiceBtn.classList.remove('listening');
|
|
904
|
+
voiceBtn.title = 'Voice input';
|
|
905
|
+
// Auto-send if we got something
|
|
906
|
+
if (inputEl.value.trim()) {
|
|
907
|
+
sendMessage();
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
recognition.onerror = function(e) {
|
|
912
|
+
isListening = false;
|
|
913
|
+
voiceBtn.classList.remove('listening');
|
|
914
|
+
voiceBtn.title = 'Voice input';
|
|
915
|
+
if (e.error !== 'no-speech') {
|
|
916
|
+
addSystemMessage('Voice error: ' + e.error);
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
recognition.start();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Voice output (TTS)
|
|
924
|
+
let speakEnabled = true;
|
|
925
|
+
let autoLoopEnabled = false;
|
|
926
|
+
|
|
927
|
+
function toggleSpeak() {
|
|
928
|
+
speakEnabled = !speakEnabled;
|
|
929
|
+
const btn = document.getElementById('speakToggle');
|
|
930
|
+
if (speakEnabled) {
|
|
931
|
+
btn.innerHTML = '<i class="fas fa-volume-up"></i>';
|
|
932
|
+
btn.style.color = '';
|
|
933
|
+
} else {
|
|
934
|
+
btn.innerHTML = '<i class="fas fa-volume-mute"></i>';
|
|
935
|
+
btn.style.color = 'var(--danger)';
|
|
936
|
+
window.speechSynthesis.cancel();
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function toggleAutoLoop() {
|
|
941
|
+
autoLoopEnabled = !autoLoopEnabled;
|
|
942
|
+
const btn = document.getElementById('autoLoopToggle');
|
|
943
|
+
if (autoLoopEnabled) {
|
|
944
|
+
btn.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i>';
|
|
945
|
+
btn.style.color = 'var(--accent)';
|
|
946
|
+
speakEnabled = true;
|
|
947
|
+
document.getElementById('speakToggle').innerHTML = '<i class="fas fa-volume-up"></i>';
|
|
948
|
+
document.getElementById('speakToggle').style.color = '';
|
|
949
|
+
} else {
|
|
950
|
+
btn.innerHTML = '<i class="fas fa-sync-alt"></i>';
|
|
951
|
+
btn.style.color = '';
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function speakText(text) {
|
|
956
|
+
if (!speakEnabled || !window.speechSynthesis) return Promise.resolve();
|
|
957
|
+
// Clean text for speech: remove markdown, code blocks, etc.
|
|
958
|
+
let clean = text
|
|
959
|
+
.replace(/```[\s\S]*?```/g, ' code block omitted ')
|
|
960
|
+
.replace(/`([^`]+)`/g, ' $1 ')
|
|
961
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
962
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
963
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
964
|
+
.replace(/#{1,6}\s*/g, '')
|
|
965
|
+
.replace(/[-*]\s/g, '')
|
|
966
|
+
.replace(/\n{2}/g, '. ')
|
|
967
|
+
.replace(/\n/g, ' ')
|
|
968
|
+
.trim();
|
|
969
|
+
|
|
970
|
+
if (!clean) return Promise.resolve();
|
|
971
|
+
|
|
972
|
+
return new Promise((resolve) => {
|
|
973
|
+
const utterance = new SpeechSynthesisUtterance(clean);
|
|
974
|
+
utterance.lang = 'en-US';
|
|
975
|
+
utterance.rate = 1.1;
|
|
976
|
+
utterance.pitch = 1.0;
|
|
977
|
+
utterance.volume = 1.0;
|
|
978
|
+
utterance.onend = resolve;
|
|
979
|
+
utterance.onerror = resolve;
|
|
980
|
+
window.speechSynthesis.speak(utterance);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function startAutoLoop() {
|
|
985
|
+
if (!autoLoopEnabled) return;
|
|
986
|
+
// Small delay then re-activate mic
|
|
987
|
+
setTimeout(() => {
|
|
988
|
+
if (autoLoopEnabled && !isListening && document.visibilityState === 'visible') {
|
|
989
|
+
toggleVoice();
|
|
990
|
+
}
|
|
991
|
+
}, 800);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Sidebar toggle
|
|
995
|
+
function toggleSidebar() {
|
|
996
|
+
sidebar.classList.toggle('open');
|
|
997
|
+
overlay.classList.toggle('show');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
overlay.addEventListener('click', function() {
|
|
1001
|
+
sidebar.classList.remove('open');
|
|
1002
|
+
overlay.classList.remove('show');
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// File browser
|
|
1006
|
+
async function loadFiles(dir) {
|
|
1007
|
+
if (!dir) dir = '.';
|
|
1008
|
+
try {
|
|
1009
|
+
fileList.innerHTML = '<div style="color:var(--text-dim);padding:16px;text-align:center;font-size:13px;">Loading...</div>';
|
|
1010
|
+
const resp = await fetch(`/api/files?dir=${encodeURIComponent(dir)}`);
|
|
1011
|
+
const data = await resp.json();
|
|
1012
|
+
|
|
1013
|
+
if (data.error) {
|
|
1014
|
+
fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Error: ' + data.error + '</div>';
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
filePath.textContent = data.path;
|
|
1019
|
+
fileList.innerHTML = '';
|
|
1020
|
+
|
|
1021
|
+
// Parent dir link
|
|
1022
|
+
if (dir !== '.') {
|
|
1023
|
+
const parent = document.createElement('div');
|
|
1024
|
+
parent.className = 'file-item dir';
|
|
1025
|
+
parent.innerHTML = '<span class="icon">📁</span> ..';
|
|
1026
|
+
parent.onclick = function() {
|
|
1027
|
+
const p = dir.split('/').slice(0, -1).join('/') || '.';
|
|
1028
|
+
loadFiles(p);
|
|
1029
|
+
};
|
|
1030
|
+
fileList.appendChild(parent);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
data.files.forEach(function(f) {
|
|
1034
|
+
const item = document.createElement('div');
|
|
1035
|
+
item.className = 'file-item' + (f.is_dir ? ' dir' : '');
|
|
1036
|
+
const iconClass = getFileIcon(f.name, f.is_dir);
|
|
1037
|
+
item.innerHTML = '<span class="icon"><i class="' + iconClass + '"></i></span> ' + f.name;
|
|
1038
|
+
if (!f.is_dir) {
|
|
1039
|
+
const size = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
|
|
1040
|
+
item.title = size;
|
|
1041
|
+
}
|
|
1042
|
+
item.onclick = function() {
|
|
1043
|
+
if (f.is_dir) {
|
|
1044
|
+
loadFiles(dir + '/' + f.name);
|
|
1045
|
+
} else {
|
|
1046
|
+
openFile(dir + '/' + f.name);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
fileList.appendChild(item);
|
|
1050
|
+
});
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Failed to load files</div>';
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// File editor state
|
|
1057
|
+
let currentEditorFile = '';
|
|
1058
|
+
let editorOriginalContent = '';
|
|
1059
|
+
let isEditing = false;
|
|
1060
|
+
|
|
1061
|
+
async function openFile(path) {
|
|
1062
|
+
try {
|
|
1063
|
+
const resp = await fetch(`/api/read-file?path=${encodeURIComponent(path)}`);
|
|
1064
|
+
const data = await resp.json();
|
|
1065
|
+
if (data.content !== undefined) {
|
|
1066
|
+
sidebar.classList.remove('open');
|
|
1067
|
+
overlay.classList.remove('show');
|
|
1068
|
+
openEditor(path, data.content);
|
|
1069
|
+
} else if (data.error) {
|
|
1070
|
+
addSystemMessage('Error: ' + data.error);
|
|
1071
|
+
}
|
|
1072
|
+
} catch(e) {
|
|
1073
|
+
addSystemMessage('Error reading file');
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function openEditor(path, content) {
|
|
1078
|
+
currentEditorFile = path;
|
|
1079
|
+
editorOriginalContent = content;
|
|
1080
|
+
isEditing = false;
|
|
1081
|
+
|
|
1082
|
+
document.getElementById('editorTitle').textContent = path;
|
|
1083
|
+
document.getElementById('editorFileInfo').textContent = (content.length) + ' chars';
|
|
1084
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
1085
|
+
|
|
1086
|
+
const readView = document.getElementById('editorReadView');
|
|
1087
|
+
readView.textContent = content;
|
|
1088
|
+
readView.style.display = 'block';
|
|
1089
|
+
|
|
1090
|
+
const textarea = document.getElementById('editorTextarea');
|
|
1091
|
+
textarea.value = content;
|
|
1092
|
+
textarea.style.display = 'none';
|
|
1093
|
+
textarea.oninput = function() {
|
|
1094
|
+
document.getElementById('editorModified').style.display = 'inline';
|
|
1095
|
+
document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
1099
|
+
document.getElementById('editorSaveBtn').disabled = true;
|
|
1100
|
+
|
|
1101
|
+
document.getElementById('editorOverlay').classList.add('show');
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function closeEditor() {
|
|
1105
|
+
document.getElementById('editorOverlay').classList.remove('show');
|
|
1106
|
+
currentEditorFile = '';
|
|
1107
|
+
editorOriginalContent = '';
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function toggleEditorMode() {
|
|
1111
|
+
const readView = document.getElementById('editorReadView');
|
|
1112
|
+
const textarea = document.getElementById('editorTextarea');
|
|
1113
|
+
const modeBtn = document.getElementById('editorModeBtn');
|
|
1114
|
+
|
|
1115
|
+
if (!isEditing) {
|
|
1116
|
+
// Switch to edit mode
|
|
1117
|
+
textarea.value = readView.textContent;
|
|
1118
|
+
textarea.style.display = 'block';
|
|
1119
|
+
readView.style.display = 'none';
|
|
1120
|
+
textarea.focus();
|
|
1121
|
+
isEditing = true;
|
|
1122
|
+
modeBtn.innerHTML = '<i class="fas fa-eye"></i> View';
|
|
1123
|
+
document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
|
|
1124
|
+
} else {
|
|
1125
|
+
// Switch back to view mode (discard unsaved changes)
|
|
1126
|
+
readView.textContent = editorOriginalContent;
|
|
1127
|
+
readView.style.display = 'block';
|
|
1128
|
+
textarea.style.display = 'none';
|
|
1129
|
+
isEditing = false;
|
|
1130
|
+
modeBtn.innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
1131
|
+
document.getElementById('editorSaveBtn').disabled = true;
|
|
1132
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async function saveEditorFile() {
|
|
1137
|
+
const textarea = document.getElementById('editorTextarea');
|
|
1138
|
+
const content = textarea.value;
|
|
1139
|
+
const path = currentEditorFile;
|
|
1140
|
+
|
|
1141
|
+
if (!path || content === editorOriginalContent) return;
|
|
1142
|
+
|
|
1143
|
+
const saveBtn = document.getElementById('editorSaveBtn');
|
|
1144
|
+
saveBtn.disabled = true;
|
|
1145
|
+
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
|
|
1146
|
+
|
|
1147
|
+
try {
|
|
1148
|
+
const resp = await fetch('/api/write-file', {
|
|
1149
|
+
method: 'POST',
|
|
1150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1151
|
+
body: JSON.stringify({ path: path, content: content }),
|
|
1152
|
+
});
|
|
1153
|
+
const data = await resp.json();
|
|
1154
|
+
if (data.status === 'written') {
|
|
1155
|
+
editorOriginalContent = content;
|
|
1156
|
+
isEditing = false;
|
|
1157
|
+
document.getElementById('editorReadView').textContent = content;
|
|
1158
|
+
document.getElementById('editorReadView').style.display = 'block';
|
|
1159
|
+
textarea.style.display = 'none';
|
|
1160
|
+
document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
1161
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
1162
|
+
document.getElementById('editorFileInfo').textContent = content.length + ' chars (saved)';
|
|
1163
|
+
addSystemMessage('File saved: ' + path);
|
|
1164
|
+
} else {
|
|
1165
|
+
addSystemMessage('Save failed: ' + (data.error || 'unknown error'));
|
|
1166
|
+
}
|
|
1167
|
+
} catch(e) {
|
|
1168
|
+
addSystemMessage('Save error: ' + e.message);
|
|
1169
|
+
} finally {
|
|
1170
|
+
saveBtn.innerHTML = '<i class="fas fa-check"></i> Save';
|
|
1171
|
+
saveBtn.disabled = true;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Keyboard shortcut: Ctrl+S to save in editor
|
|
1176
|
+
document.addEventListener('keydown', function(e) {
|
|
1177
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
1178
|
+
const overlay = document.getElementById('editorOverlay');
|
|
1179
|
+
if (overlay.classList.contains('show') && isEditing) {
|
|
1180
|
+
e.preventDefault();
|
|
1181
|
+
saveEditorFile();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// Escape to close editor
|
|
1185
|
+
if (e.key === 'Escape') {
|
|
1186
|
+
const overlay = document.getElementById('editorOverlay');
|
|
1187
|
+
if (overlay.classList.contains('show')) {
|
|
1188
|
+
closeEditor();
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// Load tools + model info
|
|
1194
|
+
|
|
1195
|
+
// Load tools + model info
|
|
1196
|
+
async function loadMeta() {
|
|
1197
|
+
try {
|
|
1198
|
+
const resp = await fetch('/api/tools');
|
|
1199
|
+
const data = await resp.json();
|
|
1200
|
+
if (data.tools) {
|
|
1201
|
+
// Could show tool count somewhere
|
|
1202
|
+
}
|
|
1203
|
+
} catch(e) {}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Theme toggle
|
|
1207
|
+
function toggleTheme() {
|
|
1208
|
+
const isLight = document.body.classList.toggle('light');
|
|
1209
|
+
const icon = document.querySelector('#themeToggle i');
|
|
1210
|
+
icon.className = isLight ? 'fas fa-sun' : 'fas fa-moon';
|
|
1211
|
+
localStorage.setItem('theme', isLight ? 'light' : 'dark');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Load saved theme
|
|
1215
|
+
const savedTheme = localStorage.getItem('theme');
|
|
1216
|
+
if (savedTheme === 'light') {
|
|
1217
|
+
document.body.classList.add('light');
|
|
1218
|
+
document.querySelector('#themeToggle i').className = 'fas fa-sun';
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Improve file item icons
|
|
1222
|
+
function getFileIcon(name, isDir) {
|
|
1223
|
+
if (isDir) return 'fa-solid fa-folder';
|
|
1224
|
+
const ext = name.split('.').pop().toLowerCase();
|
|
1225
|
+
const iconMap = {
|
|
1226
|
+
'js': 'fa-brands fa-js',
|
|
1227
|
+
'ts': 'fa-brands fa-js',
|
|
1228
|
+
'jsx': 'fa-brands fa-react',
|
|
1229
|
+
'tsx': 'fa-brands fa-react',
|
|
1230
|
+
'py': 'fa-brands fa-python',
|
|
1231
|
+
'html': 'fa-brands fa-html5',
|
|
1232
|
+
'css': 'fa-brands fa-css3-alt',
|
|
1233
|
+
'json': 'fa-solid fa-code',
|
|
1234
|
+
'md': 'fa-solid fa-file-lines',
|
|
1235
|
+
'txt': 'fa-solid fa-file-lines',
|
|
1236
|
+
'yaml': 'fa-solid fa-file',
|
|
1237
|
+
'yml': 'fa-solid fa-file',
|
|
1238
|
+
'toml': 'fa-solid fa-file',
|
|
1239
|
+
'env': 'fa-solid fa-gear',
|
|
1240
|
+
'gitignore': 'fa-solid fa-eye-slash',
|
|
1241
|
+
'jpg': 'fa-solid fa-image',
|
|
1242
|
+
'png': 'fa-solid fa-image',
|
|
1243
|
+
'svg': 'fa-solid fa-image',
|
|
1244
|
+
'ico': 'fa-solid fa-image',
|
|
1245
|
+
'woff2': 'fa-solid fa-font',
|
|
1246
|
+
'woff': 'fa-solid fa-font',
|
|
1247
|
+
'ttf': 'fa-solid fa-font',
|
|
1248
|
+
};
|
|
1249
|
+
return iconMap[ext] || 'fa-solid fa-file';
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Connect on page load
|
|
1253
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1254
|
+
// Register service worker for PWA
|
|
1255
|
+
if ('serviceWorker' in navigator) {
|
|
1256
|
+
navigator.serviceWorker.register('/sw.js').catch(function() {});
|
|
1257
|
+
}
|
|
1258
|
+
connect();
|
|
1259
|
+
loadFiles();
|
|
1260
|
+
loadMeta();
|
|
1261
|
+
inputEl.focus();
|
|
1262
|
+
});
|
|
1263
|
+
</script>
|
|
1264
|
+
<script>
|
|
1265
|
+
// === end ===
|
|
1266
|
+
console.log('LuckyD Code Web UI loaded');
|
|
1267
|
+
</script>
|
|
1268
|
+
</body>
|
|
1269
|
+
</html>
|
|
1270
|
+
<!-- duplicate block removed by fix --><!--
|
|
1271
|
+
highlight: function(code, lang) {
|
|
1272
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
1273
|
+
try { return hljs.highlight(code, { language: lang }).value; } catch(e) {}
|
|
1274
|
+
}
|
|
1275
|
+
try { return hljs.highlightAuto(code).value; } catch(e) {}
|
|
1276
|
+
return code;
|
|
1277
|
+
},
|
|
1278
|
+
breaks: true,
|
|
1279
|
+
gfm: true,
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
// State
|
|
1283
|
+
let ws = null;
|
|
1284
|
+
let isProcessing = false;
|
|
1285
|
+
let currentAssistantMsg = null;
|
|
1286
|
+
let currentAssistantDiv = null;
|
|
1287
|
+
let currentToolMsg = null;
|
|
1288
|
+
let reconnectTimer = null;
|
|
1289
|
+
let reconnectAttempts = 0;
|
|
1290
|
+
let lastSendTime = 0;
|
|
1291
|
+
let messageQueue = [];
|
|
1292
|
+
let renderTimeout = null;
|
|
1293
|
+
|
|
1294
|
+
// DOM refs
|
|
1295
|
+
const messagesEl = document.getElementById('messages');
|
|
1296
|
+
const inputEl = document.getElementById('input');
|
|
1297
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
1298
|
+
const voiceBtn = document.getElementById('voiceBtn');
|
|
1299
|
+
const statusDot = document.getElementById('statusDot');
|
|
1300
|
+
const fileList = document.getElementById('fileList');
|
|
1301
|
+
const filePath = document.getElementById('filePath');
|
|
1302
|
+
const sidebar = document.getElementById('sidebar');
|
|
1303
|
+
const overlay = document.getElementById('sidebarOverlay');
|
|
1304
|
+
|
|
1305
|
+
// Auto-resize textarea
|
|
1306
|
+
inputEl.addEventListener('input', function() {
|
|
1307
|
+
this.style.height = 'auto';
|
|
1308
|
+
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
// Enter to send, Shift+Enter for newline, Ctrl+Enter also sends
|
|
1312
|
+
inputEl.addEventListener('keydown', function(e) {
|
|
1313
|
+
if ((e.key === 'Enter' && !e.shiftKey) || (e.key === 'Enter' && e.ctrlKey)) {
|
|
1314
|
+
e.preventDefault();
|
|
1315
|
+
sendMessage();
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// WebSocket connection with exponential backoff
|
|
1320
|
+
function connect() {
|
|
1321
|
+
if (ws && ws.readyState === WebSocket.OPEN) return;
|
|
1322
|
+
|
|
1323
|
+
statusDot.className = 'status-dot connecting';
|
|
1324
|
+
|
|
1325
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1326
|
+
const url = `${protocol}//${location.host}/ws`;
|
|
1327
|
+
|
|
1328
|
+
ws = new WebSocket(url);
|
|
1329
|
+
|
|
1330
|
+
ws.onopen = function() {
|
|
1331
|
+
statusDot.className = 'status-dot connected';
|
|
1332
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1333
|
+
reconnectAttempts = 0;
|
|
1334
|
+
removeSystemMessage('disconnected');
|
|
1335
|
+
addSystemMessage('Connected');
|
|
1336
|
+
|
|
1337
|
+
// Flush queued messages
|
|
1338
|
+
while (messageQueue.length > 0) {
|
|
1339
|
+
var q = messageQueue.shift();
|
|
1340
|
+
ws.send(q);
|
|
1341
|
+
}
|
|
1342
|
+
// Re-enable input if stuck
|
|
1343
|
+
if (isProcessing) {
|
|
1344
|
+
isProcessing = false;
|
|
1345
|
+
setInputEnabled(true);
|
|
1346
|
+
removeTyping();
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
ws.onclose = function() {
|
|
1351
|
+
statusDot.className = 'status-dot disconnected';
|
|
1352
|
+
if (!reconnectTimer) {
|
|
1353
|
+
reconnectAttempts++;
|
|
1354
|
+
var delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
1355
|
+
addSystemMessage('Reconnecting in ' + Math.round(delay / 1000) + 's...');
|
|
1356
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
ws.onerror = function() {
|
|
1361
|
+
statusDot.className = 'status-dot disconnected';
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
ws.onmessage = function(event) {
|
|
1365
|
+
try {
|
|
1366
|
+
var msg = JSON.parse(event.data);
|
|
1367
|
+
handleMessage(msg);
|
|
1368
|
+
} catch (e) {
|
|
1369
|
+
console.error('Parse error:', e);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Message handler
|
|
1375
|
+
function handleMessage(msg) {
|
|
1376
|
+
switch (msg.type) {
|
|
1377
|
+
case 'text':
|
|
1378
|
+
if (!currentAssistantMsg) {
|
|
1379
|
+
currentAssistantDiv = createMessageDiv('assistant');
|
|
1380
|
+
currentAssistantMsg = {div: currentAssistantDiv, text: ''};
|
|
1381
|
+
removeTyping();
|
|
1382
|
+
}
|
|
1383
|
+
currentAssistantMsg.text += msg.content;
|
|
1384
|
+
currentAssistantMsg.div.innerHTML = marked.parse(currentAssistantMsg.text);
|
|
1385
|
+
// Debounced syntax highlighting
|
|
1386
|
+
if (renderTimeout) clearTimeout(renderTimeout);
|
|
1387
|
+
renderTimeout = setTimeout(function() {
|
|
1388
|
+
currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
|
|
1389
|
+
hljs.highlightElement(b);
|
|
1390
|
+
});
|
|
1391
|
+
}, 200);
|
|
1392
|
+
break;
|
|
1393
|
+
|
|
1394
|
+
case 'tool':
|
|
1395
|
+
// Tool call start
|
|
1396
|
+
removeTyping();
|
|
1397
|
+
if (currentAssistantMsg) {
|
|
1398
|
+
currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
|
|
1399
|
+
hljs.highlightElement(b);
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
currentToolMsg = createMessageDiv('tool-call');
|
|
1403
|
+
currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span> Running...`;
|
|
1404
|
+
break;
|
|
1405
|
+
|
|
1406
|
+
case 'tool_result':
|
|
1407
|
+
if (currentToolMsg) {
|
|
1408
|
+
currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span><div class="tool-result">${escapeHtml(msg.content)}</div>`;
|
|
1409
|
+
}
|
|
1410
|
+
break;
|
|
1411
|
+
|
|
1412
|
+
case 'error':
|
|
1413
|
+
removeTyping();
|
|
1414
|
+
createMessageDiv('error').textContent = msg.content;
|
|
1415
|
+
break;
|
|
1416
|
+
|
|
1417
|
+
case 'done':
|
|
1418
|
+
isProcessing = false;
|
|
1419
|
+
// Speak the response and auto-loop
|
|
1420
|
+
const spokenText = currentAssistantMsg ? currentAssistantMsg.text : '';
|
|
1421
|
+
currentAssistantMsg = null;
|
|
1422
|
+
currentAssistantDiv = null;
|
|
1423
|
+
currentToolMsg = null;
|
|
1424
|
+
setInputEnabled(true);
|
|
1425
|
+
if (renderTimeout) clearTimeout(renderTimeout);
|
|
1426
|
+
renderTimeout = null;
|
|
1427
|
+
if (spokenText) {
|
|
1428
|
+
speakText(spokenText).then(() => startAutoLoop());
|
|
1429
|
+
} else {
|
|
1430
|
+
startAutoLoop();
|
|
1431
|
+
}
|
|
1432
|
+
break;
|
|
1433
|
+
|
|
1434
|
+
case 'cleared':
|
|
1435
|
+
messagesEl.innerHTML = '';
|
|
1436
|
+
break;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
scrollToBottom();
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function createMessageDiv(role) {
|
|
1443
|
+
const div = document.createElement('div');
|
|
1444
|
+
div.className = `message ${role}`;
|
|
1445
|
+
messagesEl.appendChild(div);
|
|
1446
|
+
return div;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function renderAndScroll(el) {
|
|
1450
|
+
const text = el.textContent || el.innerText;
|
|
1451
|
+
el.innerHTML = marked.parse(text);
|
|
1452
|
+
el.querySelectorAll('pre code').forEach(function(b) {
|
|
1453
|
+
hljs.highlightElement(b);
|
|
1454
|
+
// Add copy button
|
|
1455
|
+
const pre = b.parentElement;
|
|
1456
|
+
if (pre && !pre.querySelector('.copy-btn')) {
|
|
1457
|
+
const btn = document.createElement('button');
|
|
1458
|
+
btn.className = 'copy-btn';
|
|
1459
|
+
btn.innerHTML = '<i class="fas fa-copy"></i>';
|
|
1460
|
+
btn.onclick = function() {
|
|
1461
|
+
navigator.clipboard.writeText(b.textContent).then(function() {
|
|
1462
|
+
btn.innerHTML = '<i class="fas fa-check"></i>';
|
|
1463
|
+
setTimeout(function() { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 2000);
|
|
1464
|
+
});
|
|
1465
|
+
};
|
|
1466
|
+
pre.style.position = 'relative';
|
|
1467
|
+
pre.appendChild(btn);
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Escape HTML for tool results
|
|
1473
|
+
function escapeHtml(text) {
|
|
1474
|
+
const d = document.createElement('div');
|
|
1475
|
+
d.textContent = text;
|
|
1476
|
+
return d.innerHTML;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function removeTyping() {
|
|
1480
|
+
const typing = document.querySelector('.typing');
|
|
1481
|
+
if (typing) typing.remove();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function scrollToBottom() {
|
|
1485
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function addSystemMessage(text) {
|
|
1489
|
+
const div = document.createElement('div');
|
|
1490
|
+
div.style.cssText = 'text-align:center;font-size:12px;color:var(--text-dim);padding:4px;';
|
|
1491
|
+
div.textContent = text;
|
|
1492
|
+
messagesEl.appendChild(div);
|
|
1493
|
+
scrollToBottom();
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Send message
|
|
1497
|
+
function removeSystemMessage(id) {
|
|
1498
|
+
var el = document.querySelector('.system-msg-' + id);
|
|
1499
|
+
if (el) el.remove();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function sendMessage() {
|
|
1503
|
+
var text = inputEl.value.trim();
|
|
1504
|
+
if (!text || isProcessing) return;
|
|
1505
|
+
|
|
1506
|
+
// Rate limiting: max 1 message per 500ms
|
|
1507
|
+
var now = Date.now();
|
|
1508
|
+
if (now - lastSendTime < 500) {
|
|
1509
|
+
addSystemMessage('Please wait...');
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
1514
|
+
addSystemMessage('Not connected. Message queued.', 'disconnected');
|
|
1515
|
+
messageQueue.push(JSON.stringify({ type: 'message', content: text }));
|
|
1516
|
+
connect();
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Input length limit
|
|
1521
|
+
if (text.length > 10000) {
|
|
1522
|
+
addSystemMessage('Message too long (max 10000 characters)');
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Add user message
|
|
1527
|
+
var userDiv = createMessageDiv('user');
|
|
1528
|
+
userDiv.textContent = text;
|
|
1529
|
+
scrollToBottom();
|
|
1530
|
+
|
|
1531
|
+
inputEl.value = '';
|
|
1532
|
+
inputEl.style.height = 'auto';
|
|
1533
|
+
|
|
1534
|
+
lastSendTime = now;
|
|
1535
|
+
isProcessing = true;
|
|
1536
|
+
setInputEnabled(false);
|
|
1537
|
+
|
|
1538
|
+
// Add typing indicator
|
|
1539
|
+
var typing = document.createElement('div');
|
|
1540
|
+
typing.className = 'typing';
|
|
1541
|
+
typing.innerHTML = '<span></span><span></span><span></span>';
|
|
1542
|
+
messagesEl.appendChild(typing);
|
|
1543
|
+
scrollToBottom();
|
|
1544
|
+
|
|
1545
|
+
// Reset accumulators
|
|
1546
|
+
currentAssistantMsg = null;
|
|
1547
|
+
currentToolMsg = null;
|
|
1548
|
+
|
|
1549
|
+
ws.send(JSON.stringify({ type: 'message', content: text }));
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function setInputEnabled(enabled) {
|
|
1553
|
+
inputEl.disabled = !enabled;
|
|
1554
|
+
sendBtn.disabled = !enabled;
|
|
1555
|
+
if (enabled) inputEl.focus();
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Clear chat
|
|
1559
|
+
function clearChat() {
|
|
1560
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
1561
|
+
ws.send(JSON.stringify({ type: 'clear' }));
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Voice input
|
|
1565
|
+
let recognition = null;
|
|
1566
|
+
let isListening = false;
|
|
1567
|
+
|
|
1568
|
+
function toggleVoice() {
|
|
1569
|
+
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
|
1570
|
+
addSystemMessage('Voice input not supported in this browser');
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (isListening) {
|
|
1575
|
+
recognition.stop();
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1580
|
+
recognition = new SpeechRecognition();
|
|
1581
|
+
recognition.lang = 'en-US';
|
|
1582
|
+
recognition.interimResults = true;
|
|
1583
|
+
recognition.continuous = true;
|
|
1584
|
+
|
|
1585
|
+
recognition.onstart = function() {
|
|
1586
|
+
isListening = true;
|
|
1587
|
+
voiceBtn.classList.add('listening');
|
|
1588
|
+
voiceBtn.title = 'Listening...';
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
recognition.onresult = function(e) {
|
|
1592
|
+
let transcript = '';
|
|
1593
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
1594
|
+
transcript += e.results[i][0].transcript;
|
|
1595
|
+
}
|
|
1596
|
+
inputEl.value = transcript;
|
|
1597
|
+
inputEl.style.height = 'auto';
|
|
1598
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
recognition.onend = function() {
|
|
1602
|
+
isListening = false;
|
|
1603
|
+
voiceBtn.classList.remove('listening');
|
|
1604
|
+
voiceBtn.title = 'Voice input';
|
|
1605
|
+
// Auto-send if we got something
|
|
1606
|
+
if (inputEl.value.trim()) {
|
|
1607
|
+
sendMessage();
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
recognition.onerror = function(e) {
|
|
1612
|
+
isListening = false;
|
|
1613
|
+
voiceBtn.classList.remove('listening');
|
|
1614
|
+
voiceBtn.title = 'Voice input';
|
|
1615
|
+
if (e.error !== 'no-speech') {
|
|
1616
|
+
addSystemMessage('Voice error: ' + e.error);
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
recognition.start();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Voice output (TTS)
|
|
1624
|
+
let speakEnabled = true;
|
|
1625
|
+
let autoLoopEnabled = false;
|
|
1626
|
+
|
|
1627
|
+
function toggleSpeak() {
|
|
1628
|
+
speakEnabled = !speakEnabled;
|
|
1629
|
+
const btn = document.getElementById('speakToggle');
|
|
1630
|
+
if (speakEnabled) {
|
|
1631
|
+
btn.innerHTML = '<i class="fas fa-volume-up"></i>';
|
|
1632
|
+
btn.style.color = '';
|
|
1633
|
+
} else {
|
|
1634
|
+
btn.innerHTML = '<i class="fas fa-volume-mute"></i>';
|
|
1635
|
+
btn.style.color = 'var(--danger)';
|
|
1636
|
+
window.speechSynthesis.cancel();
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function toggleAutoLoop() {
|
|
1641
|
+
autoLoopEnabled = !autoLoopEnabled;
|
|
1642
|
+
const btn = document.getElementById('autoLoopToggle');
|
|
1643
|
+
if (autoLoopEnabled) {
|
|
1644
|
+
btn.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i>';
|
|
1645
|
+
btn.style.color = 'var(--accent)';
|
|
1646
|
+
speakEnabled = true;
|
|
1647
|
+
document.getElementById('speakToggle').innerHTML = '<i class="fas fa-volume-up"></i>';
|
|
1648
|
+
document.getElementById('speakToggle').style.color = '';
|
|
1649
|
+
} else {
|
|
1650
|
+
btn.innerHTML = '<i class="fas fa-sync-alt"></i>';
|
|
1651
|
+
btn.style.color = '';
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function speakText(text) {
|
|
1656
|
+
if (!speakEnabled || !window.speechSynthesis) return Promise.resolve();
|
|
1657
|
+
// Clean text for speech: remove markdown, code blocks, etc.
|
|
1658
|
+
let clean = text
|
|
1659
|
+
.replace(/```[\s\S]*?```/g, ' code block omitted ')
|
|
1660
|
+
.replace(/`([^`]+)`/g, ' $1 ')
|
|
1661
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
1662
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
1663
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
1664
|
+
.replace(/#{1,6}\s*/g, '')
|
|
1665
|
+
.replace(/[-*]\s/g, '')
|
|
1666
|
+
.replace(/\n{2}/g, '. ')
|
|
1667
|
+
.replace(/\n/g, ' ')
|
|
1668
|
+
.trim();
|
|
1669
|
+
|
|
1670
|
+
if (!clean) return Promise.resolve();
|
|
1671
|
+
|
|
1672
|
+
return new Promise((resolve) => {
|
|
1673
|
+
const utterance = new SpeechSynthesisUtterance(clean);
|
|
1674
|
+
utterance.lang = 'en-US';
|
|
1675
|
+
utterance.rate = 1.1;
|
|
1676
|
+
utterance.pitch = 1.0;
|
|
1677
|
+
utterance.volume = 1.0;
|
|
1678
|
+
utterance.onend = resolve;
|
|
1679
|
+
utterance.onerror = resolve;
|
|
1680
|
+
window.speechSynthesis.speak(utterance);
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function startAutoLoop() {
|
|
1685
|
+
if (!autoLoopEnabled) return;
|
|
1686
|
+
// Small delay then re-activate mic
|
|
1687
|
+
setTimeout(() => {
|
|
1688
|
+
if (autoLoopEnabled && !isListening && document.visibilityState === 'visible') {
|
|
1689
|
+
toggleVoice();
|
|
1690
|
+
}
|
|
1691
|
+
}, 800);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Sidebar toggle
|
|
1695
|
+
function toggleSidebar() {
|
|
1696
|
+
sidebar.classList.toggle('open');
|
|
1697
|
+
overlay.classList.toggle('show');
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
overlay.addEventListener('click', function() {
|
|
1701
|
+
sidebar.classList.remove('open');
|
|
1702
|
+
overlay.classList.remove('show');
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
// File browser
|
|
1706
|
+
async function loadFiles(dir) {
|
|
1707
|
+
if (!dir) dir = '.';
|
|
1708
|
+
try {
|
|
1709
|
+
fileList.innerHTML = '<div style="color:var(--text-dim);padding:16px;text-align:center;font-size:13px;">Loading...</div>';
|
|
1710
|
+
const resp = await fetch(`/api/files?dir=${encodeURIComponent(dir)}`);
|
|
1711
|
+
const data = await resp.json();
|
|
1712
|
+
|
|
1713
|
+
if (data.error) {
|
|
1714
|
+
fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Error: ' + data.error + '</div>';
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
filePath.textContent = data.path;
|
|
1719
|
+
fileList.innerHTML = '';
|
|
1720
|
+
|
|
1721
|
+
// Parent dir link
|
|
1722
|
+
if (dir !== '.') {
|
|
1723
|
+
const parent = document.createElement('div');
|
|
1724
|
+
parent.className = 'file-item dir';
|
|
1725
|
+
parent.innerHTML = '<span class="icon">📁</span> ..';
|
|
1726
|
+
parent.onclick = function() {
|
|
1727
|
+
const p = dir.split('/').slice(0, -1).join('/') || '.';
|
|
1728
|
+
loadFiles(p);
|
|
1729
|
+
};
|
|
1730
|
+
fileList.appendChild(parent);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
data.files.forEach(function(f) {
|
|
1734
|
+
const item = document.createElement('div');
|
|
1735
|
+
item.className = 'file-item' + (f.is_dir ? ' dir' : '');
|
|
1736
|
+
const iconClass = getFileIcon(f.name, f.is_dir);
|
|
1737
|
+
item.innerHTML = '<span class="icon"><i class="' + iconClass + '"></i></span> ' + f.name;
|
|
1738
|
+
if (!f.is_dir) {
|
|
1739
|
+
const size = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
|
|
1740
|
+
item.title = size;
|
|
1741
|
+
}
|
|
1742
|
+
item.onclick = function() {
|
|
1743
|
+
if (f.is_dir) {
|
|
1744
|
+
loadFiles(dir + '/' + f.name);
|
|
1745
|
+
} else {
|
|
1746
|
+
openFile(dir + '/' + f.name);
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
fileList.appendChild(item);
|
|
1750
|
+
});
|
|
1751
|
+
} catch (e) {
|
|
1752
|
+
fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Failed to load files</div>';
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// File editor state
|
|
1757
|
+
let currentEditorFile = '';
|
|
1758
|
+
let editorOriginalContent = '';
|
|
1759
|
+
let isEditing = false;
|
|
1760
|
+
|
|
1761
|
+
async function openFile(path) {
|
|
1762
|
+
try {
|
|
1763
|
+
const resp = await fetch(`/api/read-file?path=${encodeURIComponent(path)}`);
|
|
1764
|
+
const data = await resp.json();
|
|
1765
|
+
if (data.content !== undefined) {
|
|
1766
|
+
sidebar.classList.remove('open');
|
|
1767
|
+
overlay.classList.remove('show');
|
|
1768
|
+
openEditor(path, data.content);
|
|
1769
|
+
} else if (data.error) {
|
|
1770
|
+
addSystemMessage('Error: ' + data.error);
|
|
1771
|
+
}
|
|
1772
|
+
} catch(e) {
|
|
1773
|
+
addSystemMessage('Error reading file');
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function openEditor(path, content) {
|
|
1778
|
+
currentEditorFile = path;
|
|
1779
|
+
editorOriginalContent = content;
|
|
1780
|
+
isEditing = false;
|
|
1781
|
+
|
|
1782
|
+
document.getElementById('editorTitle').textContent = path;
|
|
1783
|
+
document.getElementById('editorFileInfo').textContent = (content.length) + ' chars';
|
|
1784
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
1785
|
+
|
|
1786
|
+
const readView = document.getElementById('editorReadView');
|
|
1787
|
+
readView.textContent = content;
|
|
1788
|
+
readView.style.display = 'block';
|
|
1789
|
+
|
|
1790
|
+
const textarea = document.getElementById('editorTextarea');
|
|
1791
|
+
textarea.value = content;
|
|
1792
|
+
textarea.style.display = 'none';
|
|
1793
|
+
textarea.oninput = function() {
|
|
1794
|
+
document.getElementById('editorModified').style.display = 'inline';
|
|
1795
|
+
document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
1799
|
+
document.getElementById('editorSaveBtn').disabled = true;
|
|
1800
|
+
|
|
1801
|
+
document.getElementById('editorOverlay').classList.add('show');
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function closeEditor() {
|
|
1805
|
+
document.getElementById('editorOverlay').classList.remove('show');
|
|
1806
|
+
currentEditorFile = '';
|
|
1807
|
+
editorOriginalContent = '';
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function toggleEditorMode() {
|
|
1811
|
+
const readView = document.getElementById('editorReadView');
|
|
1812
|
+
const textarea = document.getElementById('editorTextarea');
|
|
1813
|
+
const modeBtn = document.getElementById('editorModeBtn');
|
|
1814
|
+
|
|
1815
|
+
if (!isEditing) {
|
|
1816
|
+
// Switch to edit mode
|
|
1817
|
+
textarea.value = readView.textContent;
|
|
1818
|
+
textarea.style.display = 'block';
|
|
1819
|
+
readView.style.display = 'none';
|
|
1820
|
+
textarea.focus();
|
|
1821
|
+
isEditing = true;
|
|
1822
|
+
modeBtn.innerHTML = '<i class="fas fa-eye"></i> View';
|
|
1823
|
+
document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
|
|
1824
|
+
} else {
|
|
1825
|
+
// Switch back to view mode (discard unsaved changes)
|
|
1826
|
+
readView.textContent = editorOriginalContent;
|
|
1827
|
+
readView.style.display = 'block';
|
|
1828
|
+
textarea.style.display = 'none';
|
|
1829
|
+
isEditing = false;
|
|
1830
|
+
modeBtn.innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
1831
|
+
document.getElementById('editorSaveBtn').disabled = true;
|
|
1832
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
async function saveEditorFile() {
|
|
1837
|
+
const textarea = document.getElementById('editorTextarea');
|
|
1838
|
+
const content = textarea.value;
|
|
1839
|
+
const path = currentEditorFile;
|
|
1840
|
+
|
|
1841
|
+
if (!path || content === editorOriginalContent) return;
|
|
1842
|
+
|
|
1843
|
+
const saveBtn = document.getElementById('editorSaveBtn');
|
|
1844
|
+
saveBtn.disabled = true;
|
|
1845
|
+
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
|
|
1846
|
+
|
|
1847
|
+
try {
|
|
1848
|
+
const resp = await fetch('/api/write-file', {
|
|
1849
|
+
method: 'POST',
|
|
1850
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1851
|
+
body: JSON.stringify({ path: path, content: content }),
|
|
1852
|
+
});
|
|
1853
|
+
const data = await resp.json();
|
|
1854
|
+
if (data.status === 'written') {
|
|
1855
|
+
editorOriginalContent = content;
|
|
1856
|
+
isEditing = false;
|
|
1857
|
+
document.getElementById('editorReadView').textContent = content;
|
|
1858
|
+
document.getElementById('editorReadView').style.display = 'block';
|
|
1859
|
+
textarea.style.display = 'none';
|
|
1860
|
+
document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
|
|
1861
|
+
document.getElementById('editorModified').style.display = 'none';
|
|
1862
|
+
document.getElementById('editorFileInfo').textContent = content.length + ' chars (saved)';
|
|
1863
|
+
addSystemMessage('File saved: ' + path);
|
|
1864
|
+
} else {
|
|
1865
|
+
addSystemMessage('Save failed: ' + (data.error || 'unknown error'));
|
|
1866
|
+
}
|
|
1867
|
+
} catch(e) {
|
|
1868
|
+
addSystemMessage('Save error: ' + e.message);
|
|
1869
|
+
} finally {
|
|
1870
|
+
saveBtn.innerHTML = '<i class="fas fa-check"></i> Save';
|
|
1871
|
+
saveBtn.disabled = true;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Keyboard shortcut: Ctrl+S to save in editor
|
|
1876
|
+
document.addEventListener('keydown', function(e) {
|
|
1877
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
1878
|
+
const overlay = document.getElementById('editorOverlay');
|
|
1879
|
+
if (overlay.classList.contains('show') && isEditing) {
|
|
1880
|
+
e.preventDefault();
|
|
1881
|
+
saveEditorFile();
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
// Escape to close editor
|
|
1885
|
+
if (e.key === 'Escape') {
|
|
1886
|
+
const overlay = document.getElementById('editorOverlay');
|
|
1887
|
+
if (overlay.classList.contains('show')) {
|
|
1888
|
+
closeEditor();
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
// Load tools + model info
|
|
1894
|
+
|
|
1895
|
+
// Load tools + model info
|
|
1896
|
+
async function loadMeta() {
|
|
1897
|
+
try {
|
|
1898
|
+
const resp = await fetch('/api/tools');
|
|
1899
|
+
const data = await resp.json();
|
|
1900
|
+
if (data.tools) {
|
|
1901
|
+
// Could show tool count somewhere
|
|
1902
|
+
}
|
|
1903
|
+
} catch(e) {}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// Theme toggle
|
|
1907
|
+
function toggleTheme() {
|
|
1908
|
+
const isLight = document.body.classList.toggle('light');
|
|
1909
|
+
const icon = document.querySelector('#themeToggle i');
|
|
1910
|
+
icon.className = isLight ? 'fas fa-sun' : 'fas fa-moon';
|
|
1911
|
+
localStorage.setItem('theme', isLight ? 'light' : 'dark');
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// Load saved theme
|
|
1915
|
+
const savedTheme = localStorage.getItem('theme');
|
|
1916
|
+
if (savedTheme === 'light') {
|
|
1917
|
+
document.body.classList.add('light');
|
|
1918
|
+
document.querySelector('#themeToggle i').className = 'fas fa-sun';
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Improve file item icons
|
|
1922
|
+
function getFileIcon(name, isDir) {
|
|
1923
|
+
if (isDir) return 'fa-solid fa-folder';
|
|
1924
|
+
const ext = name.split('.').pop().toLowerCase();
|
|
1925
|
+
const iconMap = {
|
|
1926
|
+
'js': 'fa-brands fa-js',
|
|
1927
|
+
'ts': 'fa-brands fa-js',
|
|
1928
|
+
'jsx': 'fa-brands fa-react',
|
|
1929
|
+
'tsx': 'fa-brands fa-react',
|
|
1930
|
+
'py': 'fa-brands fa-python',
|
|
1931
|
+
'html': 'fa-brands fa-html5',
|
|
1932
|
+
'css': 'fa-brands fa-css3-alt',
|
|
1933
|
+
'json': 'fa-solid fa-code',
|
|
1934
|
+
'md': 'fa-solid fa-file-lines',
|
|
1935
|
+
'txt': 'fa-solid fa-file-lines',
|
|
1936
|
+
'yaml': 'fa-solid fa-file',
|
|
1937
|
+
'yml': 'fa-solid fa-file',
|
|
1938
|
+
'toml': 'fa-solid fa-file',
|
|
1939
|
+
'env': 'fa-solid fa-gear',
|
|
1940
|
+
'gitignore': 'fa-solid fa-eye-slash',
|
|
1941
|
+
'jpg': 'fa-solid fa-image',
|
|
1942
|
+
'png': 'fa-solid fa-image',
|
|
1943
|
+
'svg': 'fa-solid fa-image',
|
|
1944
|
+
'ico': 'fa-solid fa-image',
|
|
1945
|
+
'woff2': 'fa-solid fa-font',
|
|
1946
|
+
'woff': 'fa-solid fa-font',
|
|
1947
|
+
'ttf': 'fa-solid fa-font',
|
|
1948
|
+
};
|
|
1949
|
+
return iconMap[ext] || 'fa-solid fa-file';
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Connect on page load
|
|
1953
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1954
|
+
// Register service worker for PWA
|
|
1955
|
+
if ('serviceWorker' in navigator) {
|
|
1956
|
+
navigator.serviceWorker.register('/sw.js').catch(function() {});
|
|
1957
|
+
}
|
|
1958
|
+
connect();
|
|
1959
|
+
loadFiles();
|
|
1960
|
+
loadMeta();
|
|
1961
|
+
inputEl.focus();
|
|
1962
|
+
});
|
|
1963
|
+
</script>
|
|
1964
|
+
</body>
|
|
1965
|
+
</html>
|