openvoiceui 1.0.0
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.
- package/.env.example +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- package/tts_providers/supertonic_tts.py +399 -0
|
@@ -0,0 +1,854 @@
|
|
|
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">
|
|
6
|
+
<title>File Explorer</title>
|
|
7
|
+
<style>
|
|
8
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
9
|
+
html,body{width:100%!important;height:100%!important;overflow:hidden!important;font-family:Tahoma,'Segoe UI',sans-serif!important;font-size:12px;color:#222!important;background:#111!important;padding:0!important;display:flex;flex-direction:column}
|
|
10
|
+
|
|
11
|
+
/* ── OS Container (matches desktop.html) ── */
|
|
12
|
+
#os-container{
|
|
13
|
+
position:absolute;
|
|
14
|
+
top:40px;left:40px;right:40px;bottom:40px;
|
|
15
|
+
border-radius:12px;
|
|
16
|
+
overflow:hidden;
|
|
17
|
+
box-shadow:0 0 40px rgba(0,0,0,0.6);
|
|
18
|
+
display:flex;flex-direction:column;
|
|
19
|
+
background:#244893;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ── Window area (file explorer fills this) ── */
|
|
23
|
+
#window-area{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
|
24
|
+
|
|
25
|
+
/* ── Title bar ── */
|
|
26
|
+
#title-bar{
|
|
27
|
+
height:28px;flex-shrink:0;
|
|
28
|
+
background:linear-gradient(to bottom, #3A6EA5 0%, #0A246A 100%)!important;
|
|
29
|
+
display:flex;align-items:center;padding:0 8px;gap:8px;
|
|
30
|
+
color:#fff!important;font-size:12px;font-weight:bold;
|
|
31
|
+
border-bottom:1px solid #002266;
|
|
32
|
+
}
|
|
33
|
+
#title-bar .tb-icon{font-size:16px}
|
|
34
|
+
#title-bar .tb-title{flex:1}
|
|
35
|
+
|
|
36
|
+
/* ── Toolbar ── */
|
|
37
|
+
#toolbar{
|
|
38
|
+
height:28px;flex-shrink:0;
|
|
39
|
+
background:#ECE9D8!important;border-bottom:1px solid #ADB2B5;
|
|
40
|
+
display:flex;align-items:center;padding:0 4px;gap:2px;color:#222!important;
|
|
41
|
+
}
|
|
42
|
+
.nav-btn{
|
|
43
|
+
height:22px;padding:0 6px;background:transparent;
|
|
44
|
+
border:1px solid transparent;cursor:pointer;font-size:11px;
|
|
45
|
+
display:flex;align-items:center;gap:3px;border-radius:2px;white-space:nowrap;
|
|
46
|
+
color:#222!important;
|
|
47
|
+
}
|
|
48
|
+
.nav-btn:hover{background:#E0DFD5;border-color:#ADB2B5}
|
|
49
|
+
.nav-btn:active{background:#C8C7C0}
|
|
50
|
+
.nav-btn:disabled{opacity:0.4;cursor:default;pointer-events:none}
|
|
51
|
+
.tb-sep{width:1px;height:18px;background:#ADB2B5;margin:0 2px;flex-shrink:0}
|
|
52
|
+
#addr-wrap{flex:1;display:flex;align-items:center;gap:4px;padding:0 4px}
|
|
53
|
+
#addr-label{color:#666!important;font-size:11px;white-space:nowrap}
|
|
54
|
+
#addr-bar{
|
|
55
|
+
flex:1;height:20px;background:#fff!important;border:1px solid #7F9DB9;
|
|
56
|
+
padding:0 6px;display:flex;align-items:center;overflow:hidden;
|
|
57
|
+
}
|
|
58
|
+
.bc-part{color:#003399!important;cursor:pointer;white-space:nowrap;font-size:11px}
|
|
59
|
+
.bc-part:hover{text-decoration:underline}
|
|
60
|
+
.bc-sep{color:#666!important;margin:0 2px;font-size:11px}
|
|
61
|
+
#search-box{
|
|
62
|
+
width:130px;height:20px;border:1px solid #7F9DB9;
|
|
63
|
+
padding:0 6px;font-size:11px;font-family:Tahoma,sans-serif;
|
|
64
|
+
background:#fff!important;color:#222!important;
|
|
65
|
+
}
|
|
66
|
+
#search-box::placeholder{color:#aaa!important}
|
|
67
|
+
|
|
68
|
+
/* ── Main layout ── */
|
|
69
|
+
#main{display:flex;flex:1;overflow:hidden}
|
|
70
|
+
|
|
71
|
+
/* ── Sidebar ── */
|
|
72
|
+
#sidebar{
|
|
73
|
+
width:190px;flex-shrink:0;
|
|
74
|
+
background:#F5F4EE!important;border-right:1px solid #ADB2B5;
|
|
75
|
+
display:flex;flex-direction:column;color:#222!important;
|
|
76
|
+
overflow:hidden;
|
|
77
|
+
}
|
|
78
|
+
/* Top zone: Quick Access/Media/Agent — never squishes, scrolls independently if needed */
|
|
79
|
+
#sb-top{flex-shrink:0;overflow-y:auto;max-height:60%}
|
|
80
|
+
/* Bottom zone: Folders tree — takes remaining space, scrolls on its own */
|
|
81
|
+
#sb-bottom{flex:1;min-height:80px;overflow-y:auto;border-top:2px solid #C8C7C0}
|
|
82
|
+
.sb-section{
|
|
83
|
+
font-size:10px;font-weight:bold;color:#316AC5!important;
|
|
84
|
+
padding:6px 10px 3px;text-transform:uppercase;letter-spacing:0.5px;
|
|
85
|
+
border-bottom:1px solid #E0DFD8;margin-top:2px;flex-shrink:0;
|
|
86
|
+
}
|
|
87
|
+
.sb-item{
|
|
88
|
+
padding:4px 8px 4px 14px;cursor:pointer;
|
|
89
|
+
display:flex;align-items:center;gap:7px;
|
|
90
|
+
font-size:11px;color:#222!important;white-space:nowrap;overflow:hidden;
|
|
91
|
+
}
|
|
92
|
+
.sb-item:hover{background:rgba(49,106,197,0.12)}
|
|
93
|
+
.sb-item.active{background:#316AC5;color:#fff!important}
|
|
94
|
+
.sb-item .sb-icon{font-size:15px;flex-shrink:0}
|
|
95
|
+
.sb-item .sb-label{overflow:hidden;text-overflow:ellipsis}
|
|
96
|
+
|
|
97
|
+
/* ── Tree ── */
|
|
98
|
+
.tree-node{user-select:none}
|
|
99
|
+
.tn-row{
|
|
100
|
+
padding:3px 4px 3px 6px;display:flex;align-items:center;gap:2px;
|
|
101
|
+
cursor:pointer;font-size:11px;color:#222!important;
|
|
102
|
+
}
|
|
103
|
+
.tn-row:hover{background:rgba(49,106,197,0.12)}
|
|
104
|
+
.tn-row.active{background:#316AC5;color:#fff!important}
|
|
105
|
+
.tn-toggle{
|
|
106
|
+
width:12px;height:12px;flex-shrink:0;
|
|
107
|
+
display:flex;align-items:center;justify-content:center;
|
|
108
|
+
font-size:9px;color:#666!important;
|
|
109
|
+
}
|
|
110
|
+
.tn-row.active .tn-toggle{color:#cce!important}
|
|
111
|
+
.tn-icon{font-size:13px;flex-shrink:0}
|
|
112
|
+
.tn-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
113
|
+
.tn-children{display:none}
|
|
114
|
+
.tn-children.open{display:block}
|
|
115
|
+
|
|
116
|
+
/* ── Right pane ── */
|
|
117
|
+
#right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background:#fff!important;color:#222!important}
|
|
118
|
+
|
|
119
|
+
/* ── Column headers ── */
|
|
120
|
+
#col-hdr{
|
|
121
|
+
display:flex;flex-shrink:0;
|
|
122
|
+
background:#F0EFE7!important;border-bottom:1px solid #ADB2B5;
|
|
123
|
+
}
|
|
124
|
+
.ch{
|
|
125
|
+
padding:3px 8px;font-size:11px;font-weight:bold;
|
|
126
|
+
border-right:1px solid #ADB2B5;cursor:pointer;user-select:none;
|
|
127
|
+
white-space:nowrap;overflow:hidden;color:#222!important;
|
|
128
|
+
}
|
|
129
|
+
.ch:hover{background:#E0DFD8}
|
|
130
|
+
.ch-name{flex:1;min-width:160px}
|
|
131
|
+
.ch-type{width:80px}
|
|
132
|
+
.ch-size{width:68px;text-align:right}
|
|
133
|
+
.ch-date{width:106px}
|
|
134
|
+
|
|
135
|
+
/* ── File list ── */
|
|
136
|
+
#file-list-area{flex:1;overflow-y:auto;background:#fff!important}
|
|
137
|
+
#file-table{width:100%;border-collapse:collapse}
|
|
138
|
+
#file-table tbody tr{border-bottom:1px solid #F2F1EC;cursor:pointer}
|
|
139
|
+
#file-table tbody tr:nth-child(even){background:#F8F8F4!important}
|
|
140
|
+
#file-table tbody tr:hover{background:#EEF3FD!important}
|
|
141
|
+
#file-table tbody tr.sel{background:#316AC5!important;color:#fff!important}
|
|
142
|
+
#file-table td{padding:3px 8px;font-size:11px;white-space:nowrap;color:#222!important}
|
|
143
|
+
#file-table tbody tr.sel td{color:#fff!important}
|
|
144
|
+
.fi-cell{display:flex;align-items:center;gap:7px}
|
|
145
|
+
.fi-icon{font-size:16px;flex-shrink:0}
|
|
146
|
+
.fi-name{overflow:hidden;text-overflow:ellipsis;max-width:280px}
|
|
147
|
+
.fi-size{text-align:right;color:#555!important}
|
|
148
|
+
.fi-date{color:#555!important}
|
|
149
|
+
tr.sel .fi-size,tr.sel .fi-date{color:#cce!important}
|
|
150
|
+
|
|
151
|
+
/* ── Preview pane ── */
|
|
152
|
+
#preview-wrap{flex-shrink:0;display:none;flex-direction:column;height:300px;border-top:2px solid #316AC5}
|
|
153
|
+
#preview-wrap.open{display:flex}
|
|
154
|
+
#preview-hdr{
|
|
155
|
+
background:#F0EFE7!important;border-bottom:1px solid #ADB2B5;
|
|
156
|
+
padding:3px 8px;display:flex;align-items:center;gap:8px;
|
|
157
|
+
flex-shrink:0;font-size:11px;color:#222!important;
|
|
158
|
+
}
|
|
159
|
+
#preview-hdr .ph-name{flex:1;font-weight:bold;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
160
|
+
#preview-hdr .ph-size{color:#666!important;white-space:nowrap}
|
|
161
|
+
#ph-close{border:1px solid #ADB2B5;background:#F0EFE7!important;cursor:pointer;padding:1px 10px;font-size:11px;color:#222!important}
|
|
162
|
+
#ph-close:hover{background:#E0DFD8!important}
|
|
163
|
+
#preview-body{flex:1;overflow:auto;padding:14px 18px;background:#fff!important;color:#222!important}
|
|
164
|
+
#preview-body.raw{font-family:Consolas,'Courier New',monospace;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-all;color:#222!important}
|
|
165
|
+
#preview-body.img-preview{display:flex;align-items:center;justify-content:center;background:#f0f0f0!important;padding:8px}
|
|
166
|
+
#preview-body.img-preview img{max-width:100%;max-height:100%;object-fit:contain;border:1px solid #ddd;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
|
|
167
|
+
#preview-body.img-preview .img-error{color:#c00!important;font-size:12px;text-align:center}
|
|
168
|
+
#preview-body.audio-preview{display:flex;align-items:center;justify-content:center;padding:20px}
|
|
169
|
+
#preview-body.audio-preview audio{width:90%;max-width:400px}
|
|
170
|
+
#preview-body.video-preview{display:flex;align-items:center;justify-content:center;background:#000!important;padding:0}
|
|
171
|
+
#preview-body.video-preview video{max-width:100%;max-height:100%}
|
|
172
|
+
|
|
173
|
+
/* ── Markdown styles ── */
|
|
174
|
+
#preview-body.md{font-family:Tahoma,'Segoe UI',sans-serif;font-size:12px;line-height:1.6;color:#222!important}
|
|
175
|
+
#preview-body.md h1{font-size:17px;border-bottom:2px solid #316AC5;padding-bottom:5px;margin:6px 0 10px;color:#0A246A!important}
|
|
176
|
+
#preview-body.md h2{font-size:14px;border-bottom:1px solid #ddd;padding-bottom:3px;margin:12px 0 6px;color:#1a3a6a!important}
|
|
177
|
+
#preview-body.md h3{font-size:12px;margin:10px 0 4px;color:#244893!important}
|
|
178
|
+
#preview-body.md h4,#preview-body.md h5,#preview-body.md h6{font-size:11px;margin:6px 0 2px;font-weight:bold}
|
|
179
|
+
#preview-body.md p{margin-bottom:6px}
|
|
180
|
+
#preview-body.md ul,#preview-body.md ol{padding-left:22px;margin:4px 0 8px}
|
|
181
|
+
#preview-body.md li{margin-bottom:2px}
|
|
182
|
+
#preview-body.md code{background:#f0f0f0;border:1px solid #ddd;padding:0 3px;border-radius:2px;font-family:Consolas,'Courier New',monospace;font-size:10px;color:#333!important}
|
|
183
|
+
#preview-body.md pre{background:#f5f5f5!important;border:1px solid #ddd;padding:10px;overflow-x:auto;margin:6px 0;border-radius:3px}
|
|
184
|
+
#preview-body.md pre code{background:none;border:none;padding:0;font-size:11px}
|
|
185
|
+
#preview-body.md blockquote{border-left:3px solid #316AC5;padding-left:10px;color:#555!important;margin:6px 0}
|
|
186
|
+
#preview-body.md a{color:#003399!important}
|
|
187
|
+
#preview-body.md hr{border:none;border-top:1px solid #ddd;margin:10px 0}
|
|
188
|
+
#preview-body.md strong{font-weight:bold}
|
|
189
|
+
#preview-body.md em{font-style:italic}
|
|
190
|
+
#preview-body.md del{text-decoration:line-through;color:#888!important}
|
|
191
|
+
#preview-body.md table{border-collapse:collapse;font-size:11px;margin:6px 0}
|
|
192
|
+
#preview-body.md th{background:#F0EFE7!important;border:1px solid #bbb;padding:4px 8px;text-align:left;color:#222!important}
|
|
193
|
+
#preview-body.md td{border:1px solid #ddd;padding:3px 8px;color:#222!important}
|
|
194
|
+
#preview-body.md .md-img{color:#888!important;font-style:italic}
|
|
195
|
+
|
|
196
|
+
/* ── Status bar ── */
|
|
197
|
+
#status-bar{
|
|
198
|
+
height:22px;flex-shrink:0;
|
|
199
|
+
background:#ECE9D8!important;border-top:1px solid #ADB2B5;
|
|
200
|
+
display:flex;align-items:center;padding:0 10px;gap:12px;font-size:11px;color:#333!important;
|
|
201
|
+
}
|
|
202
|
+
#st-count{flex:1}
|
|
203
|
+
#st-path{color:#666!important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:320px}
|
|
204
|
+
|
|
205
|
+
/* ── Taskbar (matches desktop.html) ── */
|
|
206
|
+
#taskbar{
|
|
207
|
+
height:30px;flex-shrink:0;
|
|
208
|
+
background:linear-gradient(to bottom, #3A6EA5 0%, #245DAB 3%, #245DAB 97%, #18438B 100%)!important;
|
|
209
|
+
display:flex;align-items:center;
|
|
210
|
+
border-top:1px solid #5B9BD5;
|
|
211
|
+
color:#fff!important;
|
|
212
|
+
}
|
|
213
|
+
#start-btn{
|
|
214
|
+
border:none;cursor:pointer;display:flex;align-items:center;gap:5px;
|
|
215
|
+
height:100%;padding:0 10px;
|
|
216
|
+
background:linear-gradient(to bottom, #3C9A40 0%, #328E36 50%, #277D2A 100%)!important;
|
|
217
|
+
color:#fff!important;font-size:12px;font-weight:bold;
|
|
218
|
+
border-right:1px solid #18438B;
|
|
219
|
+
border-radius:0 8px 8px 0;
|
|
220
|
+
}
|
|
221
|
+
#start-btn:hover{filter:brightness(1.1)}
|
|
222
|
+
#start-btn:active{filter:brightness(0.9)}
|
|
223
|
+
.win-flag{display:inline-grid;grid-template-columns:1fr 1fr;width:14px;height:14px;gap:1px}
|
|
224
|
+
.win-flag span{border-radius:1px}
|
|
225
|
+
#taskbar-active{
|
|
226
|
+
flex:1;display:flex;align-items:center;padding:0 8px;gap:4px;
|
|
227
|
+
font-size:11px;color:#fff!important;
|
|
228
|
+
}
|
|
229
|
+
#taskbar-active .active-btn{
|
|
230
|
+
height:22px;border:1px solid rgba(255,255,255,0.3);
|
|
231
|
+
background:rgba(255,255,255,0.15);color:#fff!important;
|
|
232
|
+
font-size:11px;padding:0 10px;cursor:default;
|
|
233
|
+
display:flex;align-items:center;gap:4px;border-radius:2px;
|
|
234
|
+
font-weight:bold;
|
|
235
|
+
}
|
|
236
|
+
#tray{display:flex;align-items:center;gap:6px;padding:0 8px;font-size:11px;color:#fff!important;height:100%}
|
|
237
|
+
#clock{white-space:nowrap}
|
|
238
|
+
|
|
239
|
+
/* ── Sidebar condensed mode (titles only when tree is large) ── */
|
|
240
|
+
#sb-top.condensed .sb-item{display:none}
|
|
241
|
+
#sb-top.condensed #qa-sites{display:none}
|
|
242
|
+
|
|
243
|
+
/* ── Utility ── */
|
|
244
|
+
.empty-state{padding:40px;text-align:center;color:#888!important;font-size:12px}
|
|
245
|
+
.loading-state{padding:24px;text-align:center;color:#316AC5!important;font-size:11px}
|
|
246
|
+
.err-state{padding:24px;text-align:center;color:#c00!important;font-size:11px}
|
|
247
|
+
.unavail-state{padding:40px;text-align:center;color:#888!important}
|
|
248
|
+
.unavail-state h3{font-size:14px;color:#555!important;margin-bottom:8px}
|
|
249
|
+
</style>
|
|
250
|
+
</head>
|
|
251
|
+
<body>
|
|
252
|
+
|
|
253
|
+
<!-- OS Container with padding -->
|
|
254
|
+
<div id="os-container">
|
|
255
|
+
|
|
256
|
+
<!-- File Explorer Window -->
|
|
257
|
+
<div id="window-area">
|
|
258
|
+
<!-- Title bar -->
|
|
259
|
+
<div id="title-bar">
|
|
260
|
+
<span class="tb-icon">📁</span>
|
|
261
|
+
<span class="tb-title">File Explorer</span>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Toolbar -->
|
|
265
|
+
<div id="toolbar">
|
|
266
|
+
<button class="nav-btn" id="btn-back" onclick="navBack()" disabled>◀ Back</button>
|
|
267
|
+
<button class="nav-btn" id="btn-fwd" onclick="navFwd()" disabled>Forward ▶</button>
|
|
268
|
+
<button class="nav-btn" id="btn-up" onclick="navUp()">↑ Up</button>
|
|
269
|
+
<div class="tb-sep"></div>
|
|
270
|
+
<div id="addr-wrap">
|
|
271
|
+
<span id="addr-label">Address:</span>
|
|
272
|
+
<div id="addr-bar"><div id="breadcrumb"></div></div>
|
|
273
|
+
</div>
|
|
274
|
+
<input type="text" id="search-box" placeholder="Search..." oninput="filterFiles(this.value)">
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<!-- Main -->
|
|
278
|
+
<div id="main">
|
|
279
|
+
<!-- Sidebar -->
|
|
280
|
+
<div id="sidebar">
|
|
281
|
+
<!-- Top zone: Quick Access / Media / Agent — condenses to icons when tree is large -->
|
|
282
|
+
<div id="sb-top">
|
|
283
|
+
<div class="sb-section">Quick Access</div>
|
|
284
|
+
<div class="sb-item" onclick="navigate('')" data-qa="root"><span class="sb-icon">🖥</span><span class="sb-label">My Files</span></div>
|
|
285
|
+
<div class="sb-item" onclick="navigate('Websites')" data-qa="Websites"><span class="sb-icon">🌐</span><span class="sb-label">Websites</span></div>
|
|
286
|
+
<div class="sb-item" onclick="navigate('Uploads')" data-qa="Uploads"><span class="sb-icon">📁</span><span class="sb-label">Uploads</span></div>
|
|
287
|
+
<div class="sb-item" onclick="navigate('Canvas')" data-qa="Canvas"><span class="sb-icon">🎨</span><span class="sb-label">Canvas Pages</span></div>
|
|
288
|
+
<div id="qa-sites"></div>
|
|
289
|
+
<div class="sb-section">Media</div>
|
|
290
|
+
<div class="sb-item" onclick="navigate('Music')" data-qa="Music"><span class="sb-icon">🎵</span><span class="sb-label">Music</span></div>
|
|
291
|
+
<div class="sb-item" onclick="navigate('AI-Music')" data-qa="AI-Music"><span class="sb-icon">🎶</span><span class="sb-label">AI Music</span></div>
|
|
292
|
+
<div class="sb-item" onclick="navigate('Transcripts')" data-qa="Transcripts"><span class="sb-icon">📜</span><span class="sb-label">Transcripts</span></div>
|
|
293
|
+
<div class="sb-section">Agent</div>
|
|
294
|
+
<div class="sb-item" onclick="navigate('Agent/memory')" data-qa="Agent/memory"><span class="sb-icon">🧠</span><span class="sb-label">Memory</span></div>
|
|
295
|
+
<div class="sb-item" onclick="navigate('Agent/business')" data-qa="Agent/business"><span class="sb-icon">💼</span><span class="sb-label">Business</span></div>
|
|
296
|
+
<div class="sb-item" onclick="navigate('Agent')" data-qa="Agent"><span class="sb-icon">🤖</span><span class="sb-label">All Agent Files</span></div>
|
|
297
|
+
</div>
|
|
298
|
+
<!-- Bottom zone: Folders tree — takes remaining space, scrolls independently -->
|
|
299
|
+
<div id="sb-bottom">
|
|
300
|
+
<div class="sb-section">Folders</div>
|
|
301
|
+
<div id="tree"></div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<!-- Right pane -->
|
|
306
|
+
<div id="right-pane">
|
|
307
|
+
<!-- Column headers -->
|
|
308
|
+
<div id="col-hdr">
|
|
309
|
+
<div class="ch ch-name" onclick="sortBy('name')">Name <span id="sort-name"></span></div>
|
|
310
|
+
<div class="ch ch-type" onclick="sortBy('type')">Type <span id="sort-type"></span></div>
|
|
311
|
+
<div class="ch ch-size" onclick="sortBy('size')">Size <span id="sort-size"></span></div>
|
|
312
|
+
<div class="ch ch-date" onclick="sortBy('modified')">Date Modified <span id="sort-modified"></span></div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<!-- File list -->
|
|
316
|
+
<div id="file-list-area">
|
|
317
|
+
<table id="file-table">
|
|
318
|
+
<tbody id="file-tbody"></tbody>
|
|
319
|
+
</table>
|
|
320
|
+
<div id="list-msg" class="loading-state">Loading...</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<!-- Preview pane -->
|
|
324
|
+
<div id="preview-wrap">
|
|
325
|
+
<div id="preview-hdr">
|
|
326
|
+
<span class="ph-name" id="ph-name"></span>
|
|
327
|
+
<span class="ph-size" id="ph-size"></span>
|
|
328
|
+
<button id="ph-close" onclick="closePreview()">✕</button>
|
|
329
|
+
</div>
|
|
330
|
+
<div id="preview-body"></div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<!-- Status bar -->
|
|
336
|
+
<div id="status-bar">
|
|
337
|
+
<span id="st-count">Ready</span>
|
|
338
|
+
<span id="st-path"></span>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<!-- Taskbar -->
|
|
343
|
+
<div id="taskbar">
|
|
344
|
+
<button id="start-btn" onclick="goDesktop()">
|
|
345
|
+
<span class="win-flag"><span style="background:#F25022"></span><span style="background:#7FBA00"></span><span style="background:#00A4EF"></span><span style="background:#FFB900"></span></span>
|
|
346
|
+
Start
|
|
347
|
+
</button>
|
|
348
|
+
<div id="taskbar-active">
|
|
349
|
+
<div class="active-btn">📁 File Explorer</div>
|
|
350
|
+
</div>
|
|
351
|
+
<div id="tray">
|
|
352
|
+
<span id="clock"></span>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<script>
|
|
358
|
+
/* STATE */
|
|
359
|
+
let currentPath = '';
|
|
360
|
+
let history = [];
|
|
361
|
+
let histIdx = -1;
|
|
362
|
+
let currentEntries = [];
|
|
363
|
+
let sortCol = 'name';
|
|
364
|
+
let sortDir = 1;
|
|
365
|
+
let treeExpanded = new Set();
|
|
366
|
+
let selectedRow = null;
|
|
367
|
+
let searchQuery = '';
|
|
368
|
+
|
|
369
|
+
/* CLOCK */
|
|
370
|
+
function updateClock() {
|
|
371
|
+
const d = new Date();
|
|
372
|
+
let h = d.getHours(), m = d.getMinutes();
|
|
373
|
+
const ampm = h >= 12 ? 'PM' : 'AM';
|
|
374
|
+
h = h % 12 || 12;
|
|
375
|
+
document.getElementById('clock').textContent = h + ':' + (m < 10 ? '0' : '') + m + ' ' + ampm;
|
|
376
|
+
}
|
|
377
|
+
updateClock();
|
|
378
|
+
setInterval(updateClock, 30000);
|
|
379
|
+
|
|
380
|
+
/* NAVIGATION */
|
|
381
|
+
function navigate(path, addHistory = true) {
|
|
382
|
+
if (addHistory) {
|
|
383
|
+
if (histIdx < history.length - 1) history = history.slice(0, histIdx + 1);
|
|
384
|
+
history.push(path);
|
|
385
|
+
histIdx = history.length - 1;
|
|
386
|
+
}
|
|
387
|
+
currentPath = path;
|
|
388
|
+
selectedRow = null;
|
|
389
|
+
searchQuery = '';
|
|
390
|
+
document.getElementById('search-box').value = '';
|
|
391
|
+
document.getElementById('btn-back').disabled = histIdx <= 0;
|
|
392
|
+
document.getElementById('btn-fwd').disabled = histIdx >= history.length - 1;
|
|
393
|
+
document.getElementById('btn-up').disabled = !path;
|
|
394
|
+
updateBreadcrumb(path);
|
|
395
|
+
loadDir(path);
|
|
396
|
+
updateSidebarActive(path);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function navBack() { if (histIdx > 0) { histIdx--; navigate(history[histIdx], false); } }
|
|
400
|
+
function navFwd() { if (histIdx < history.length - 1) { histIdx++; navigate(history[histIdx], false); } }
|
|
401
|
+
function navUp() {
|
|
402
|
+
if (!currentPath) return;
|
|
403
|
+
const parts = currentPath.split('/').filter(Boolean);
|
|
404
|
+
parts.pop();
|
|
405
|
+
navigate(parts.join('/'));
|
|
406
|
+
}
|
|
407
|
+
function goDesktop() {
|
|
408
|
+
window.parent.postMessage({type:'canvas-action', action:'navigate', pageId:'desktop'}, '*');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* BREADCRUMB */
|
|
412
|
+
function updateBreadcrumb(path) {
|
|
413
|
+
const bc = document.getElementById('breadcrumb');
|
|
414
|
+
const parts = path ? path.split('/').filter(Boolean) : [];
|
|
415
|
+
let html = '<span class="bc-part" onclick="navigate(\'\')">My Files</span>';
|
|
416
|
+
let built = '';
|
|
417
|
+
for (let i = 0; i < parts.length; i++) {
|
|
418
|
+
built += (built ? '/' : '') + parts[i];
|
|
419
|
+
const p = built;
|
|
420
|
+
html += '<span class="bc-sep">\u203A</span>';
|
|
421
|
+
html += '<span class="bc-part" onclick="navigate(\'' + esc(p) + '\')">' + escHtml(parts[i]) + '</span>';
|
|
422
|
+
}
|
|
423
|
+
bc.innerHTML = html;
|
|
424
|
+
document.getElementById('st-path').textContent = 'My Files' + (path ? '/' + path : '');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* LOAD DIRECTORY */
|
|
428
|
+
async function loadDir(path) {
|
|
429
|
+
showListMsg('loading', 'Loading...');
|
|
430
|
+
try {
|
|
431
|
+
const r = await fetch('/api/workspace/browse?path=' + encodeURIComponent(path));
|
|
432
|
+
const ct = r.headers.get('content-type') || '';
|
|
433
|
+
if (!ct.includes('json')) {
|
|
434
|
+
showListMsg('err', r.status === 403 ? 'Permission denied' : 'Folder not available (HTTP ' + r.status + ')');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const data = await r.json();
|
|
438
|
+
if (data.unavailable) { showUnavail(); return; }
|
|
439
|
+
if (data.error) { showListMsg('err', 'Error: ' + data.error); return; }
|
|
440
|
+
currentEntries = data.entries || [];
|
|
441
|
+
renderFileList();
|
|
442
|
+
} catch(e) {
|
|
443
|
+
showListMsg('err', 'Failed to load: ' + e.message);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function showListMsg(type, msg) {
|
|
448
|
+
document.getElementById('file-tbody').innerHTML = '';
|
|
449
|
+
const el = document.getElementById('list-msg');
|
|
450
|
+
el.style.display = 'block';
|
|
451
|
+
el.className = type === 'loading' ? 'loading-state' : (type === 'err' ? 'err-state' : 'empty-state');
|
|
452
|
+
el.textContent = msg;
|
|
453
|
+
}
|
|
454
|
+
function hideListMsg() { document.getElementById('list-msg').style.display = 'none'; }
|
|
455
|
+
|
|
456
|
+
function showUnavail() {
|
|
457
|
+
document.getElementById('file-tbody').innerHTML = '';
|
|
458
|
+
const el = document.getElementById('list-msg');
|
|
459
|
+
el.style.display = 'block';
|
|
460
|
+
el.className = 'unavail-state';
|
|
461
|
+
el.innerHTML = '<h3>Workspace Not Mounted</h3><p>The workspace volume is not yet connected to this container.</p><p style="margin-top:8px;font-size:10px;color:#aaa">Container restart with updated compose file required.</p>';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* RENDER FILE LIST */
|
|
465
|
+
function renderFileList() {
|
|
466
|
+
let entries = currentEntries.slice();
|
|
467
|
+
if (searchQuery) {
|
|
468
|
+
const q = searchQuery.toLowerCase();
|
|
469
|
+
entries = entries.filter(e => e.name.toLowerCase().includes(q));
|
|
470
|
+
}
|
|
471
|
+
entries.sort((a, b) => {
|
|
472
|
+
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
|
|
473
|
+
let av, bv;
|
|
474
|
+
if (sortCol === 'name') { av = a.name.toLowerCase(); bv = b.name.toLowerCase(); }
|
|
475
|
+
else if (sortCol === 'size') { av = a.size; bv = b.size; }
|
|
476
|
+
else if (sortCol === 'modified') { av = a.modified; bv = b.modified; }
|
|
477
|
+
else if (sortCol === 'type') { av = a.ext || ''; bv = b.ext || ''; }
|
|
478
|
+
else { av = a.name.toLowerCase(); bv = b.name.toLowerCase(); }
|
|
479
|
+
return av < bv ? -sortDir : av > bv ? sortDir : 0;
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
['name','type','size','modified'].forEach(c => {
|
|
483
|
+
const el = document.getElementById('sort-' + c);
|
|
484
|
+
if (el) el.textContent = sortCol === c ? (sortDir === 1 ? ' \u25B2' : ' \u25BC') : '';
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const tbody = document.getElementById('file-tbody');
|
|
488
|
+
tbody.innerHTML = '';
|
|
489
|
+
|
|
490
|
+
if (entries.length === 0) {
|
|
491
|
+
showListMsg('empty', searchQuery ? 'No results for "' + searchQuery + '"' : 'This folder is empty.');
|
|
492
|
+
document.getElementById('st-count').textContent = '0 items';
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
hideListMsg();
|
|
496
|
+
|
|
497
|
+
const dirCount = entries.filter(e => e.type === 'dir').length;
|
|
498
|
+
const fileCount = entries.filter(e => e.type === 'file').length;
|
|
499
|
+
document.getElementById('st-count').textContent =
|
|
500
|
+
(dirCount ? dirCount + ' folder' + (dirCount !== 1 ? 's' : '') : '') +
|
|
501
|
+
(dirCount && fileCount ? ', ' : '') +
|
|
502
|
+
(fileCount ? fileCount + ' file' + (fileCount !== 1 ? 's' : '') : '');
|
|
503
|
+
|
|
504
|
+
entries.forEach(entry => {
|
|
505
|
+
const tr = document.createElement('tr');
|
|
506
|
+
tr.dataset.name = entry.name;
|
|
507
|
+
tr.dataset.type = entry.type;
|
|
508
|
+
const icon = getIcon(entry);
|
|
509
|
+
const typeLabel = entry.type === 'dir'
|
|
510
|
+
? (entry.children !== undefined ? 'Folder (' + entry.children + ')' : 'Folder')
|
|
511
|
+
: getTypeLabel(entry.ext);
|
|
512
|
+
const sizeStr = entry.type === 'file' ? formatSize(entry.size) : '';
|
|
513
|
+
const dateStr = formatDate(entry.modified);
|
|
514
|
+
tr.innerHTML =
|
|
515
|
+
'<td><div class="fi-cell"><span class="fi-icon">' + icon + '</span><span class="fi-name">' + escHtml(entry.name) + '</span></div></td>' +
|
|
516
|
+
'<td class="fi-type">' + escHtml(typeLabel) + '</td>' +
|
|
517
|
+
'<td class="fi-size">' + escHtml(sizeStr) + '</td>' +
|
|
518
|
+
'<td class="fi-date">' + escHtml(dateStr) + '</td>';
|
|
519
|
+
tr.addEventListener('click', function() { selectRow(tr, entry); });
|
|
520
|
+
tr.addEventListener('dblclick', function() { openEntry(entry); });
|
|
521
|
+
tbody.appendChild(tr);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function selectRow(tr, entry) {
|
|
526
|
+
document.querySelectorAll('#file-tbody tr.sel').forEach(function(r) { r.classList.remove('sel'); });
|
|
527
|
+
tr.classList.add('sel');
|
|
528
|
+
selectedRow = entry;
|
|
529
|
+
if (entry.type === 'file') previewFile(currentPath ? currentPath + '/' + entry.name : entry.name, entry);
|
|
530
|
+
}
|
|
531
|
+
function openEntry(entry) {
|
|
532
|
+
if (entry.type === 'dir') navigate(currentPath ? currentPath + '/' + entry.name : entry.name);
|
|
533
|
+
}
|
|
534
|
+
function filterFiles(q) { searchQuery = q.trim(); renderFileList(); }
|
|
535
|
+
function sortBy(col) {
|
|
536
|
+
if (sortCol === col) sortDir = -sortDir;
|
|
537
|
+
else { sortCol = col; sortDir = 1; }
|
|
538
|
+
renderFileList();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/* FILE PREVIEW */
|
|
542
|
+
var IMAGE_EXTS = ['.jpg','.jpeg','.png','.gif','.webp','.svg','.ico','.bmp'];
|
|
543
|
+
var AUDIO_EXTS = ['.mp3','.wav','.ogg','.m4a'];
|
|
544
|
+
var VIDEO_EXTS = ['.mp4','.webm','.mov'];
|
|
545
|
+
|
|
546
|
+
async function previewFile(path, entry) {
|
|
547
|
+
var wrap = document.getElementById('preview-wrap');
|
|
548
|
+
var body = document.getElementById('preview-body');
|
|
549
|
+
wrap.classList.add('open');
|
|
550
|
+
document.getElementById('ph-name').textContent = entry.name;
|
|
551
|
+
document.getElementById('ph-size').textContent = formatSize(entry.size);
|
|
552
|
+
body.className = '';
|
|
553
|
+
body.innerHTML = '';
|
|
554
|
+
body.textContent = 'Loading...';
|
|
555
|
+
|
|
556
|
+
var ext = (entry.ext || '').toLowerCase();
|
|
557
|
+
var rawUrl = '/api/workspace/raw?path=' + encodeURIComponent(path);
|
|
558
|
+
|
|
559
|
+
// Image preview
|
|
560
|
+
if (IMAGE_EXTS.indexOf(ext) >= 0) {
|
|
561
|
+
body.className = 'img-preview';
|
|
562
|
+
body.innerHTML = '';
|
|
563
|
+
var img = document.createElement('img');
|
|
564
|
+
img.src = rawUrl;
|
|
565
|
+
img.alt = entry.name;
|
|
566
|
+
img.onerror = function() {
|
|
567
|
+
body.innerHTML = '<div class="img-error">Failed to load image</div>';
|
|
568
|
+
};
|
|
569
|
+
body.appendChild(img);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Audio preview
|
|
574
|
+
if (AUDIO_EXTS.indexOf(ext) >= 0) {
|
|
575
|
+
body.className = 'audio-preview';
|
|
576
|
+
body.innerHTML = '<audio controls src="' + escHtml(rawUrl) + '">Audio not supported</audio>';
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Video preview
|
|
581
|
+
if (VIDEO_EXTS.indexOf(ext) >= 0) {
|
|
582
|
+
body.className = 'video-preview';
|
|
583
|
+
body.innerHTML = '<video controls src="' + escHtml(rawUrl) + '">Video not supported</video>';
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// PDF — link to open in new tab
|
|
588
|
+
if (ext === '.pdf') {
|
|
589
|
+
body.className = 'raw';
|
|
590
|
+
body.innerHTML = '<a href="' + escHtml(rawUrl) + '" target="_blank" style="color:#003399;font-size:13px">Open PDF in new tab</a>';
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Text file preview
|
|
595
|
+
try {
|
|
596
|
+
var r = await fetch('/api/workspace/file?path=' + encodeURIComponent(path));
|
|
597
|
+
var ct = r.headers.get('content-type') || '';
|
|
598
|
+
if (!ct.includes('json')) {
|
|
599
|
+
body.className = 'raw';
|
|
600
|
+
body.textContent = r.status === 403 ? 'Permission denied' : 'File not available (HTTP ' + r.status + ')';
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
var data = await r.json();
|
|
604
|
+
if (data.too_large) {
|
|
605
|
+
body.className = 'raw';
|
|
606
|
+
body.textContent = 'File too large for preview (' + Math.round(data.size / 1024) + ' KB). Max is 500 KB.';
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (data.binary) { body.className = 'raw'; body.textContent = 'Binary file -- no preview available.'; return; }
|
|
610
|
+
if (data.error) { body.className = 'raw'; body.textContent = 'Error: ' + data.error; return; }
|
|
611
|
+
if (ext === '.md') { body.className = 'md'; body.innerHTML = renderMarkdown(data.content); }
|
|
612
|
+
else if (ext === '.json') {
|
|
613
|
+
body.className = 'raw';
|
|
614
|
+
try { body.textContent = JSON.stringify(JSON.parse(data.content), null, 2); }
|
|
615
|
+
catch(e2) { body.textContent = data.content; }
|
|
616
|
+
} else { body.className = 'raw'; body.textContent = data.content; }
|
|
617
|
+
} catch(e) { body.className = 'raw'; body.textContent = 'Failed to load: ' + e.message; }
|
|
618
|
+
}
|
|
619
|
+
function closePreview() {
|
|
620
|
+
document.getElementById('preview-wrap').classList.remove('open');
|
|
621
|
+
document.querySelectorAll('#file-tbody tr.sel').forEach(function(r) { r.classList.remove('sel'); });
|
|
622
|
+
selectedRow = null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* SIDEBAR TREE */
|
|
626
|
+
async function buildTree() {
|
|
627
|
+
const treeEl = document.getElementById('tree');
|
|
628
|
+
treeEl.innerHTML = '<div class="loading-state" style="padding:8px">Loading...</div>';
|
|
629
|
+
const data = await fetchTree('');
|
|
630
|
+
treeEl.innerHTML = '';
|
|
631
|
+
data.dirs.forEach(function(d) { treeEl.appendChild(buildTreeNode(d, 0)); });
|
|
632
|
+
}
|
|
633
|
+
async function fetchTree(path) {
|
|
634
|
+
try {
|
|
635
|
+
const r = await fetch('/api/workspace/tree?path=' + encodeURIComponent(path));
|
|
636
|
+
const ct = r.headers.get('content-type') || '';
|
|
637
|
+
if (!ct.includes('json')) return {dirs:[]};
|
|
638
|
+
return await r.json();
|
|
639
|
+
} catch(e) { return {dirs:[]}; }
|
|
640
|
+
}
|
|
641
|
+
function buildTreeNode(dir, depth) {
|
|
642
|
+
const node = document.createElement('div');
|
|
643
|
+
node.className = 'tree-node';
|
|
644
|
+
const row = document.createElement('div');
|
|
645
|
+
row.className = 'tn-row';
|
|
646
|
+
row.style.paddingLeft = (8 + depth * 12) + 'px';
|
|
647
|
+
row.dataset.path = dir.path;
|
|
648
|
+
const toggle = document.createElement('span');
|
|
649
|
+
toggle.className = 'tn-toggle';
|
|
650
|
+
toggle.textContent = dir.has_subdirs ? '\u25B6' : ' ';
|
|
651
|
+
const icon = document.createElement('span');
|
|
652
|
+
icon.className = 'tn-icon';
|
|
653
|
+
icon.textContent = '\uD83D\uDCC1';
|
|
654
|
+
const label = document.createElement('span');
|
|
655
|
+
label.className = 'tn-label';
|
|
656
|
+
label.textContent = dir.name;
|
|
657
|
+
row.appendChild(toggle);
|
|
658
|
+
row.appendChild(icon);
|
|
659
|
+
row.appendChild(label);
|
|
660
|
+
node.appendChild(row);
|
|
661
|
+
const children = document.createElement('div');
|
|
662
|
+
children.className = 'tn-children';
|
|
663
|
+
node.appendChild(children);
|
|
664
|
+
row.addEventListener('click', async function(e) {
|
|
665
|
+
e.stopPropagation();
|
|
666
|
+
navigate(dir.path);
|
|
667
|
+
if (!dir.has_subdirs) return;
|
|
668
|
+
if (treeExpanded.has(dir.path)) {
|
|
669
|
+
treeExpanded.delete(dir.path);
|
|
670
|
+
children.classList.remove('open');
|
|
671
|
+
toggle.textContent = '\u25B6';
|
|
672
|
+
} else {
|
|
673
|
+
treeExpanded.add(dir.path);
|
|
674
|
+
toggle.textContent = '\u25BC';
|
|
675
|
+
if (!children.children.length) {
|
|
676
|
+
children.innerHTML = '<div class="loading-state" style="padding:4px 4px 4px 24px;font-size:10px">...</div>';
|
|
677
|
+
const data = await fetchTree(dir.path);
|
|
678
|
+
children.innerHTML = '';
|
|
679
|
+
data.dirs.forEach(function(sub) { children.appendChild(buildTreeNode(sub, depth + 1)); });
|
|
680
|
+
if (!children.children.length) toggle.textContent = ' ';
|
|
681
|
+
}
|
|
682
|
+
children.classList.add('open');
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
return node;
|
|
686
|
+
}
|
|
687
|
+
function updateSidebarActive(path) {
|
|
688
|
+
document.querySelectorAll('.sb-item').forEach(function(el) {
|
|
689
|
+
const qa = el.dataset.qa;
|
|
690
|
+
if (!qa) { el.classList.remove('active'); return; }
|
|
691
|
+
if (qa === 'root') el.classList.toggle('active', path === '');
|
|
692
|
+
else el.classList.toggle('active', path === qa || path.startsWith(qa + '/'));
|
|
693
|
+
});
|
|
694
|
+
document.querySelectorAll('.tn-row').forEach(function(el) {
|
|
695
|
+
el.classList.toggle('active', el.dataset.path === path);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/* QUICK ACCESS — auto-discover website image/ai folders */
|
|
700
|
+
async function buildQuickAccess() {
|
|
701
|
+
var container = document.getElementById('qa-sites');
|
|
702
|
+
try {
|
|
703
|
+
var r = await fetch('/api/workspace/browse?path=Websites');
|
|
704
|
+
var ct = r.headers.get('content-type') || '';
|
|
705
|
+
if (!ct.includes('json')) return;
|
|
706
|
+
var data = await r.json();
|
|
707
|
+
if (data.unavailable || data.error || !data.entries) return;
|
|
708
|
+
var sites = data.entries.filter(function(e) { return e.type === 'dir' && !e.name.endsWith('.old') && !e.name.endsWith('.old-starter'); });
|
|
709
|
+
if (!sites.length) return;
|
|
710
|
+
var html = '';
|
|
711
|
+
for (var i = 0; i < Math.min(sites.length, 4); i++) {
|
|
712
|
+
var s = sites[i];
|
|
713
|
+
var shortName = s.name.replace(/-website$/, '').replace(/-web-\d+$/, '').replace(/-/g, ' ');
|
|
714
|
+
// Add images shortcut
|
|
715
|
+
html += '<div class="sb-item" onclick="navigate(\'' + esc('Websites/' + s.name + '/public/images') + '\')" style="padding-left:18px"><span class="sb-icon">\uD83D\uDDBC\uFE0F</span><span class="sb-label">' + escHtml(shortName) + ' images</span></div>';
|
|
716
|
+
// Add /ai/ shortcut
|
|
717
|
+
html += '<div class="sb-item" onclick="navigate(\'' + esc('Websites/' + s.name + '/ai') + '\')" style="padding-left:18px"><span class="sb-icon">\uD83E\uDD16</span><span class="sb-label">' + escHtml(shortName) + ' /ai</span></div>';
|
|
718
|
+
}
|
|
719
|
+
container.innerHTML = html;
|
|
720
|
+
} catch(e) {}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* MARKDOWN RENDERER */
|
|
724
|
+
function renderMarkdown(text) {
|
|
725
|
+
var lines = text.split('\n');
|
|
726
|
+
var html = '';
|
|
727
|
+
var inCode = false, codeLang = '', codeLines = [];
|
|
728
|
+
var inList = false, listType = '';
|
|
729
|
+
for (var i = 0; i < lines.length; i++) {
|
|
730
|
+
var line = lines[i];
|
|
731
|
+
if (line.startsWith('```')) {
|
|
732
|
+
if (inCode) {
|
|
733
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
734
|
+
html += '<pre><code>' + escHtml(codeLines.join('\n')) + '</code></pre>';
|
|
735
|
+
inCode = false; codeLines = []; codeLang = '';
|
|
736
|
+
} else {
|
|
737
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
738
|
+
inCode = true; codeLang = line.slice(3).trim();
|
|
739
|
+
}
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (inCode) { codeLines.push(line); continue; }
|
|
743
|
+
var ulMatch = line.match(/^(\s*)[-*+]\s(.+)$/);
|
|
744
|
+
var olMatch = line.match(/^(\s*)\d+\.\s(.+)$/);
|
|
745
|
+
if (!ulMatch && !olMatch && inList) { html += '</' + listType + '>'; inList = false; }
|
|
746
|
+
if (/^---+$|^\*\*\*+$|^___+$/.test(line.trim())) { html += '<hr>'; continue; }
|
|
747
|
+
var hm = line.match(/^(#{1,6})\s(.+)$/);
|
|
748
|
+
if (hm) { html += '<h' + hm[1].length + '>' + inlineMd(hm[2]) + '</h' + hm[1].length + '>'; continue; }
|
|
749
|
+
if (line.startsWith('> ')) { html += '<blockquote>' + inlineMd(line.slice(2)) + '</blockquote>'; continue; }
|
|
750
|
+
if (ulMatch) {
|
|
751
|
+
if (!inList) { html += '<ul>'; inList = true; listType = 'ul'; }
|
|
752
|
+
html += '<li>' + inlineMd(ulMatch[2]) + '</li>'; continue;
|
|
753
|
+
}
|
|
754
|
+
if (olMatch) {
|
|
755
|
+
if (!inList) { html += '<ol>'; inList = true; listType = 'ol'; }
|
|
756
|
+
html += '<li>' + inlineMd(olMatch[2]) + '</li>'; continue;
|
|
757
|
+
}
|
|
758
|
+
if (line.startsWith('|') && line.endsWith('|')) {
|
|
759
|
+
if (/^\|[-| :]+\|$/.test(line)) continue;
|
|
760
|
+
var cells = line.slice(1, -1).split('|').map(function(c) { return c.trim(); });
|
|
761
|
+
if (!html.endsWith('</tr>') && !html.endsWith('<table>')) {
|
|
762
|
+
if (!html.endsWith('</thead>')) html += '<table><thead><tr>' + cells.map(function(c) { return '<th>' + inlineMd(c) + '</th>'; }).join('') + '</tr></thead><tbody>';
|
|
763
|
+
} else {
|
|
764
|
+
html += '<tr>' + cells.map(function(c) { return '<td>' + inlineMd(c) + '</td>'; }).join('') + '</tr>';
|
|
765
|
+
}
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (html.includes('<tbody>') && !html.endsWith('</table>') && !line.startsWith('|')) html += '</tbody></table>';
|
|
769
|
+
if (line.trim() === '') { html += '<br>'; continue; }
|
|
770
|
+
html += '<p>' + inlineMd(line) + '</p>';
|
|
771
|
+
}
|
|
772
|
+
if (inList) html += '</' + listType + '>';
|
|
773
|
+
if (inCode) html += '<pre><code>' + escHtml(codeLines.join('\n')) + '</code></pre>';
|
|
774
|
+
return html;
|
|
775
|
+
}
|
|
776
|
+
function inlineMd(text) {
|
|
777
|
+
return text
|
|
778
|
+
.replace(/`([^`]+)`/g, function(_, c) { return '<code>' + escHtml(c) + '</code>'; })
|
|
779
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
780
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
781
|
+
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
782
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
783
|
+
.replace(/_([^_]+)_/g, '<em>$1</em>')
|
|
784
|
+
.replace(/~~(.+?)~~/g, '<del>$1</del>')
|
|
785
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '<span class="md-img">[img: $1]</span>')
|
|
786
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/* ICON & FORMAT HELPERS */
|
|
790
|
+
function getIcon(entry) {
|
|
791
|
+
if (entry.type === 'dir') return '\uD83D\uDCC1';
|
|
792
|
+
var ext = (entry.ext || '').toLowerCase();
|
|
793
|
+
if (ext === '.md') return '\uD83D\uDCDD';
|
|
794
|
+
if (ext === '.json') return '\uD83D\uDDC2\uFE0F';
|
|
795
|
+
if (ext === '.csv') return '\uD83D\uDCCA';
|
|
796
|
+
if (ext === '.py') return '\uD83D\uDC0D';
|
|
797
|
+
if (ext === '.js' || ext === '.ts') return '\u2699\uFE0F';
|
|
798
|
+
if (ext === '.html') return '\uD83C\uDF10';
|
|
799
|
+
if (ext === '.sh') return '\u26A1';
|
|
800
|
+
if (ext === '.pdf') return '\uD83D\uDCC4';
|
|
801
|
+
if (['.jpg','.jpeg','.png','.gif','.webp','.svg'].indexOf(ext) >= 0) return '\uD83D\uDDBC\uFE0F';
|
|
802
|
+
if (['.mp3','.wav','.ogg','.m4a'].indexOf(ext) >= 0) return '\uD83C\uDFB5';
|
|
803
|
+
if (['.mp4','.webm','.mov'].indexOf(ext) >= 0) return '\uD83C\uDFAC';
|
|
804
|
+
if (['.zip','.tar','.gz','.bz2'].indexOf(ext) >= 0) return '\uD83D\uDCE6';
|
|
805
|
+
return '\uD83D\uDCC4';
|
|
806
|
+
}
|
|
807
|
+
function getTypeLabel(ext) {
|
|
808
|
+
if (!ext) return 'File';
|
|
809
|
+
var labels = {'.md':'Markdown','.txt':'Text','.json':'JSON','.csv':'CSV','.py':'Python',
|
|
810
|
+
'.js':'JavaScript','.ts':'TypeScript','.html':'HTML','.sh':'Shell Script','.pdf':'PDF',
|
|
811
|
+
'.jpg':'JPEG Image','.jpeg':'JPEG Image','.png':'PNG Image','.gif':'GIF Image',
|
|
812
|
+
'.mp3':'Audio','.mp4':'Video','.zip':'Archive','.yaml':'YAML','.yml':'YAML',
|
|
813
|
+
'.log':'Log File','.sql':'SQL','.toml':'TOML','.xml':'XML'};
|
|
814
|
+
return labels[ext] || (ext.slice(1).toUpperCase() + ' File');
|
|
815
|
+
}
|
|
816
|
+
function formatSize(bytes) {
|
|
817
|
+
if (!bytes) return '';
|
|
818
|
+
if (bytes < 1024) return bytes + ' B';
|
|
819
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
820
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
821
|
+
}
|
|
822
|
+
function formatDate(ts) {
|
|
823
|
+
if (!ts) return '';
|
|
824
|
+
var d = new Date(ts * 1000);
|
|
825
|
+
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
826
|
+
return months[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear();
|
|
827
|
+
}
|
|
828
|
+
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
829
|
+
function esc(s) { return String(s).replace(/'/g,"\\'"); }
|
|
830
|
+
|
|
831
|
+
/* SIDEBAR CONDENSED MODE — toggle icon-only when tree needs to scroll */
|
|
832
|
+
function updateSidebarCondensed() {
|
|
833
|
+
var sbTop = document.getElementById('sb-top');
|
|
834
|
+
var sbBottom = document.getElementById('sb-bottom');
|
|
835
|
+
var tree = document.getElementById('tree');
|
|
836
|
+
if (!sbTop || !sbBottom || !tree) return;
|
|
837
|
+
// Condense when tree content height exceeds the available bottom zone height
|
|
838
|
+
sbTop.classList.toggle('condensed', tree.scrollHeight > sbBottom.clientHeight);
|
|
839
|
+
}
|
|
840
|
+
new MutationObserver(updateSidebarCondensed).observe(
|
|
841
|
+
document.getElementById('tree'),
|
|
842
|
+
{ childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
/* INIT */
|
|
846
|
+
async function init() {
|
|
847
|
+
await Promise.all([buildTree(), buildQuickAccess()]);
|
|
848
|
+
updateSidebarCondensed();
|
|
849
|
+
navigate('');
|
|
850
|
+
}
|
|
851
|
+
init();
|
|
852
|
+
</script>
|
|
853
|
+
</body>
|
|
854
|
+
</html>
|