@mooncompany/uplink-chat 0.5.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.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
package/public/js/app.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// APP ENTRY POINT
|
|
3
|
+
// Imports all modules in correct dependency order.
|
|
4
|
+
// esbuild bundles this into public/dist/bundle.js
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
// === Foundation (no dependencies) ===
|
|
8
|
+
import './logger.js';
|
|
9
|
+
import './utils/constants.js';
|
|
10
|
+
import './utils/sanitize.js';
|
|
11
|
+
import './fetch-utils.js';
|
|
12
|
+
|
|
13
|
+
// === Core (depends on logger) ===
|
|
14
|
+
import './core.js';
|
|
15
|
+
import './encryption.js';
|
|
16
|
+
import './storage.js';
|
|
17
|
+
|
|
18
|
+
// === UI Rendering (depends on core, storage) ===
|
|
19
|
+
import './errors.js';
|
|
20
|
+
import './utils/sse-parser.js';
|
|
21
|
+
import './markdown.js';
|
|
22
|
+
import './message-renderer.js';
|
|
23
|
+
import './streaming-handler.js';
|
|
24
|
+
|
|
25
|
+
// === Audio & Queues ===
|
|
26
|
+
import './audio-queue.js';
|
|
27
|
+
import './offline-queue.js';
|
|
28
|
+
import './file-handler.js';
|
|
29
|
+
|
|
30
|
+
// === Event Bus (cross-module communication) ===
|
|
31
|
+
import './event-bus.js';
|
|
32
|
+
|
|
33
|
+
// === Core App (depends on rendering + storage) ===
|
|
34
|
+
import './onboarding.js';
|
|
35
|
+
import './ui.js';
|
|
36
|
+
import './chat.js';
|
|
37
|
+
import './connection-api.js';
|
|
38
|
+
import './connection.js';
|
|
39
|
+
|
|
40
|
+
// === Feature Modules (deferred in original, now bundled) ===
|
|
41
|
+
import './panels.js';
|
|
42
|
+
import './voice.js';
|
|
43
|
+
import './realtime-voice.js';
|
|
44
|
+
import './appearance-settings.js';
|
|
45
|
+
import './tts-settings.js';
|
|
46
|
+
import './stt-settings.js';
|
|
47
|
+
import './settings.js';
|
|
48
|
+
import './commands.js';
|
|
49
|
+
import './developer.js';
|
|
50
|
+
import './dashboard.js';
|
|
51
|
+
import './files.js';
|
|
52
|
+
import './themes.js';
|
|
53
|
+
import './premium.js';
|
|
54
|
+
import './theme-generator.js';
|
|
55
|
+
import './notifications.js';
|
|
56
|
+
import './shortcuts.js';
|
|
57
|
+
import './missed-messages.js';
|
|
58
|
+
import './timestamps.js';
|
|
59
|
+
import './gateway-chat.js';
|
|
60
|
+
import './agents-data.js';
|
|
61
|
+
import './agents-ui.js';
|
|
62
|
+
import './agents.js';
|
|
63
|
+
import './satellite-sync.js';
|
|
64
|
+
import './satellite-ui.js';
|
|
65
|
+
import './satellites.js';
|
|
66
|
+
import './message-actions.js';
|
|
67
|
+
import './context-tracker.js';
|
|
68
|
+
import './artifacts.js';
|
|
69
|
+
import './splitview.js';
|
|
70
|
+
import './split-resize.js';
|
|
71
|
+
import './split-chat.js';
|
|
72
|
+
import './update-notifier.js';
|
|
73
|
+
|
|
74
|
+
// === Bootstrap (ensures everything is ready) ===
|
|
75
|
+
import './bootstrap.js';
|
|
76
|
+
|
|
77
|
+
// NOTE: mobile-debug.js is intentionally excluded from the bundle.
|
|
78
|
+
// It is conditionally loaded only on non-localhost connections
|
|
79
|
+
// and is commented out in index.html by default.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// APPEARANCE SETTINGS MODULE
|
|
3
|
+
// Theme, text size, and visual display settings
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
// DOM elements
|
|
7
|
+
let themeSelect;
|
|
8
|
+
let textSizeSelect;
|
|
9
|
+
|
|
10
|
+
// Settings
|
|
11
|
+
let textSize = 'medium';
|
|
12
|
+
|
|
13
|
+
function init() {
|
|
14
|
+
themeSelect = document.getElementById('themeSelect');
|
|
15
|
+
textSizeSelect = document.getElementById('textSizeSelect');
|
|
16
|
+
|
|
17
|
+
loadSettings();
|
|
18
|
+
setupEvents();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadSettings() {
|
|
22
|
+
const storage = window.UplinkStorage;
|
|
23
|
+
if (!storage) return;
|
|
24
|
+
|
|
25
|
+
const saved = storage.loadSettings();
|
|
26
|
+
if (saved.textSize) textSize = saved.textSize;
|
|
27
|
+
if (saved.theme) applyTheme(saved.theme);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function applyTheme(theme) {
|
|
31
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
32
|
+
if (themeSelect) {
|
|
33
|
+
// Ensure 'custom' option exists if needed
|
|
34
|
+
if (theme === 'custom' && !themeSelect.querySelector('option[value="custom"]')) {
|
|
35
|
+
var opt = document.createElement('option');
|
|
36
|
+
opt.value = 'custom';
|
|
37
|
+
opt.textContent = '✦ Custom';
|
|
38
|
+
themeSelect.appendChild(opt);
|
|
39
|
+
}
|
|
40
|
+
themeSelect.value = theme;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setupEvents() {
|
|
45
|
+
// Theme change
|
|
46
|
+
themeSelect?.addEventListener('change', () => {
|
|
47
|
+
const theme = themeSelect.value;
|
|
48
|
+
// Use UplinkThemes if available (handles premium check + events)
|
|
49
|
+
if (window.UplinkThemes) {
|
|
50
|
+
window.UplinkThemes.apply(theme);
|
|
51
|
+
} else {
|
|
52
|
+
applyTheme(theme);
|
|
53
|
+
}
|
|
54
|
+
const storage = window.UplinkStorage;
|
|
55
|
+
if (storage) storage.saveSettings({ theme });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Text size change
|
|
59
|
+
textSizeSelect?.addEventListener('change', () => {
|
|
60
|
+
textSize = textSizeSelect.value;
|
|
61
|
+
applyTextSize();
|
|
62
|
+
saveSettings();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function applyTextSize() {
|
|
67
|
+
const messages = document.getElementById('messages');
|
|
68
|
+
if (messages) {
|
|
69
|
+
messages.classList.remove('text-small', 'text-medium', 'text-large');
|
|
70
|
+
messages.classList.add(`text-${textSize}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function saveSettings() {
|
|
75
|
+
const storage = window.UplinkStorage;
|
|
76
|
+
if (!storage) return;
|
|
77
|
+
|
|
78
|
+
storage.saveSettings({
|
|
79
|
+
textSize,
|
|
80
|
+
mode: window.UplinkCore?.mode || 'text'
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function applyState() {
|
|
85
|
+
// Update text size select
|
|
86
|
+
if (textSizeSelect) textSizeSelect.value = textSize;
|
|
87
|
+
|
|
88
|
+
// Sync theme select with current theme
|
|
89
|
+
const currentTheme = document.documentElement.getAttribute('data-theme') || 'midnight';
|
|
90
|
+
if (themeSelect) themeSelect.value = currentTheme;
|
|
91
|
+
|
|
92
|
+
applyTextSize();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Expose API
|
|
96
|
+
export const UplinkAppearanceSettings = {
|
|
97
|
+
init,
|
|
98
|
+
applyState,
|
|
99
|
+
applyTheme,
|
|
100
|
+
getTextSize: () => textSize,
|
|
101
|
+
setTextSize: (size) => {
|
|
102
|
+
textSize = size;
|
|
103
|
+
applyTextSize();
|
|
104
|
+
saveSettings();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
import { UplinkCore } from './core.js';
|
|
109
|
+
|
|
110
|
+
// Backward compat: assign to window
|
|
111
|
+
window.UplinkAppearanceSettings = UplinkAppearanceSettings;
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// ARTIFACTS MODULE
|
|
3
|
+
// Artifacts viewer panel for agent-generated documents
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
import { UplinkMarkdown } from './markdown.js';
|
|
8
|
+
|
|
9
|
+
// DOM elements
|
|
10
|
+
let artifactsBtn, artifactsPanel;
|
|
11
|
+
let artifactsList, artifactsReader, artifactsSearch;
|
|
12
|
+
let readerTitle, readerContent, readerBackBtn;
|
|
13
|
+
|
|
14
|
+
// State
|
|
15
|
+
let artifacts = [];
|
|
16
|
+
let currentArtifact = null;
|
|
17
|
+
|
|
18
|
+
// AbortController for event listeners cleanup
|
|
19
|
+
let eventsAbortController = null;
|
|
20
|
+
|
|
21
|
+
function init() {
|
|
22
|
+
artifactsBtn = document.getElementById('artifactsBtn');
|
|
23
|
+
artifactsPanel = document.getElementById('artifactsPanel');
|
|
24
|
+
artifactsList = document.getElementById('artifactsList');
|
|
25
|
+
artifactsReader = document.getElementById('artifactsReader');
|
|
26
|
+
artifactsSearch = document.getElementById('artifactsSearch');
|
|
27
|
+
readerTitle = document.getElementById('readerTitle');
|
|
28
|
+
readerContent = document.getElementById('readerContent');
|
|
29
|
+
readerBackBtn = document.getElementById('readerBackBtn');
|
|
30
|
+
|
|
31
|
+
if (!artifactsBtn || !artifactsPanel) {
|
|
32
|
+
console.warn('Artifacts: Elements not found, retrying...');
|
|
33
|
+
setTimeout(init, 100);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setupEvents();
|
|
38
|
+
|
|
39
|
+
// Register with panel manager
|
|
40
|
+
if (window.UplinkPanels && artifactsPanel) {
|
|
41
|
+
window.UplinkPanels.register('artifacts', {
|
|
42
|
+
element: artifactsPanel,
|
|
43
|
+
isOpen: () => artifactsPanel.classList.contains('visible'),
|
|
44
|
+
open: () => {
|
|
45
|
+
artifactsPanel.classList.add('visible');
|
|
46
|
+
onPanelOpen();
|
|
47
|
+
},
|
|
48
|
+
close: () => {
|
|
49
|
+
artifactsPanel.classList.remove('visible');
|
|
50
|
+
onPanelClose();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('Artifacts: Initialized');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setupEvents() {
|
|
59
|
+
// Abort previous event listeners
|
|
60
|
+
if (eventsAbortController) {
|
|
61
|
+
eventsAbortController.abort();
|
|
62
|
+
}
|
|
63
|
+
eventsAbortController = new AbortController();
|
|
64
|
+
const signal = eventsAbortController.signal;
|
|
65
|
+
|
|
66
|
+
// Toggle artifacts panel
|
|
67
|
+
artifactsBtn?.addEventListener('click', () => {
|
|
68
|
+
if (window.UplinkPanels) {
|
|
69
|
+
window.UplinkPanels.toggle('artifacts');
|
|
70
|
+
} else {
|
|
71
|
+
artifactsPanel?.classList.toggle('visible');
|
|
72
|
+
}
|
|
73
|
+
}, { signal });
|
|
74
|
+
|
|
75
|
+
// Close button
|
|
76
|
+
const closeBtn = document.getElementById('artifactsCloseBtn');
|
|
77
|
+
closeBtn?.addEventListener('click', () => {
|
|
78
|
+
if (window.UplinkPanels) {
|
|
79
|
+
window.UplinkPanels.close('artifacts');
|
|
80
|
+
} else {
|
|
81
|
+
artifactsPanel?.classList.remove('visible');
|
|
82
|
+
}
|
|
83
|
+
}, { signal });
|
|
84
|
+
|
|
85
|
+
// Back button
|
|
86
|
+
readerBackBtn?.addEventListener('click', showList, { signal });
|
|
87
|
+
|
|
88
|
+
// Download button
|
|
89
|
+
const downloadBtn = document.getElementById('readerDownloadBtn');
|
|
90
|
+
downloadBtn?.addEventListener('click', downloadCurrentArtifact, { signal });
|
|
91
|
+
|
|
92
|
+
// Search input
|
|
93
|
+
artifactsSearch?.addEventListener('input', filterArtifacts, { signal });
|
|
94
|
+
|
|
95
|
+
// Refresh button
|
|
96
|
+
const refreshBtn = document.getElementById('artifactsRefreshBtn');
|
|
97
|
+
refreshBtn?.addEventListener('click', loadArtifacts, { signal });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function onPanelOpen() {
|
|
101
|
+
// Load artifacts when panel opens
|
|
102
|
+
await loadArtifacts();
|
|
103
|
+
showList();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function onPanelClose() {
|
|
107
|
+
// Return focus to artifacts button
|
|
108
|
+
if (artifactsBtn) {
|
|
109
|
+
artifactsBtn.focus();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Load artifacts list from API
|
|
115
|
+
*/
|
|
116
|
+
async function loadArtifacts() {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch('/api/artifacts');
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`HTTP ${response.status}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
artifacts = await response.json();
|
|
124
|
+
renderList();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('Artifacts: Failed to load', error);
|
|
127
|
+
showError('Failed to load artifacts');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Render artifacts list
|
|
133
|
+
*/
|
|
134
|
+
function renderList() {
|
|
135
|
+
if (!artifactsList) return;
|
|
136
|
+
|
|
137
|
+
if (artifacts.length === 0) {
|
|
138
|
+
artifactsList.innerHTML = `
|
|
139
|
+
<div class="artifacts-empty">
|
|
140
|
+
<svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
141
|
+
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
142
|
+
</svg>
|
|
143
|
+
<p>No artifacts yet</p>
|
|
144
|
+
<small>Agent-generated documents will appear here</small>
|
|
145
|
+
</div>
|
|
146
|
+
`;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const searchTerm = artifactsSearch?.value.toLowerCase() || '';
|
|
151
|
+
const filteredArtifacts = searchTerm
|
|
152
|
+
? artifacts.filter(a => a.name.toLowerCase().includes(searchTerm))
|
|
153
|
+
: artifacts;
|
|
154
|
+
|
|
155
|
+
if (filteredArtifacts.length === 0) {
|
|
156
|
+
artifactsList.innerHTML = `
|
|
157
|
+
<div class="artifacts-empty">
|
|
158
|
+
<p>No matching artifacts</p>
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
artifactsList.innerHTML = filteredArtifacts.map(artifact => `
|
|
165
|
+
<div class="artifact-item" data-name="${escapeAttr(artifact.name)}">
|
|
166
|
+
<div class="artifact-icon">${getFileIcon(artifact.extension)}</div>
|
|
167
|
+
<div class="artifact-info">
|
|
168
|
+
<div class="artifact-name">${escapeHtml(artifact.name)}</div>
|
|
169
|
+
<div class="artifact-meta">
|
|
170
|
+
${formatFileSize(artifact.size)} · ${formatRelativeTime(artifact.modified)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
`).join('');
|
|
175
|
+
|
|
176
|
+
// Add click handlers
|
|
177
|
+
artifactsList.querySelectorAll('.artifact-item').forEach(item => {
|
|
178
|
+
item.addEventListener('click', () => {
|
|
179
|
+
const name = item.dataset.name;
|
|
180
|
+
const artifact = artifacts.find(a => a.name === name);
|
|
181
|
+
if (artifact) {
|
|
182
|
+
openArtifact(artifact);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Filter artifacts by search term
|
|
190
|
+
*/
|
|
191
|
+
function filterArtifacts() {
|
|
192
|
+
renderList();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Open artifact in reader view
|
|
197
|
+
*/
|
|
198
|
+
async function openArtifact(artifact) {
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(`/api/artifacts/${encodeURIComponent(artifact.name)}`);
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
throw new Error(`HTTP ${response.status}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const data = await response.json();
|
|
206
|
+
currentArtifact = data;
|
|
207
|
+
|
|
208
|
+
// Render in reader
|
|
209
|
+
if (readerTitle) {
|
|
210
|
+
readerTitle.textContent = data.name;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (readerContent) {
|
|
214
|
+
if (data.extension === '.md') {
|
|
215
|
+
// Render markdown
|
|
216
|
+
readerContent.innerHTML = UplinkMarkdown.render(data.content);
|
|
217
|
+
if (UplinkMarkdown.highlightCode) {
|
|
218
|
+
UplinkMarkdown.highlightCode(readerContent);
|
|
219
|
+
}
|
|
220
|
+
} else if (data.extension === '.html') {
|
|
221
|
+
// HTML files get a preview/source toggle
|
|
222
|
+
readerContent.innerHTML = `
|
|
223
|
+
<div class="artifacts-html-toggle">
|
|
224
|
+
<button class="artifacts-toggle-btn active" data-mode="preview">Preview</button>
|
|
225
|
+
<button class="artifacts-toggle-btn" data-mode="source">Source</button>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="artifacts-html-preview">
|
|
228
|
+
<iframe class="artifacts-iframe" sandbox="allow-same-origin" srcdoc="${escapeAttr(data.content)}"></iframe>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="artifacts-html-source" style="display: none;">
|
|
231
|
+
<pre><code>${escapeHtml(data.content)}</code></pre>
|
|
232
|
+
</div>
|
|
233
|
+
`;
|
|
234
|
+
// Wire up toggle buttons
|
|
235
|
+
readerContent.querySelectorAll('.artifacts-toggle-btn').forEach(btn => {
|
|
236
|
+
btn.addEventListener('click', () => {
|
|
237
|
+
const mode = btn.dataset.mode;
|
|
238
|
+
const preview = readerContent.querySelector('.artifacts-html-preview');
|
|
239
|
+
const source = readerContent.querySelector('.artifacts-html-source');
|
|
240
|
+
readerContent.querySelectorAll('.artifacts-toggle-btn').forEach(b => b.classList.remove('active'));
|
|
241
|
+
btn.classList.add('active');
|
|
242
|
+
if (mode === 'preview') {
|
|
243
|
+
preview.style.display = 'block';
|
|
244
|
+
source.style.display = 'none';
|
|
245
|
+
} else {
|
|
246
|
+
preview.style.display = 'none';
|
|
247
|
+
source.style.display = 'block';
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
// Plain text / code
|
|
253
|
+
readerContent.innerHTML = `<pre>${escapeHtml(data.content)}</pre>`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
showReader();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('Artifacts: Failed to open', error);
|
|
260
|
+
showError(`Failed to open ${artifact.name}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Show artifacts list view
|
|
266
|
+
*/
|
|
267
|
+
function showList() {
|
|
268
|
+
if (artifactsList) artifactsList.style.display = 'block';
|
|
269
|
+
if (artifactsReader) artifactsReader.style.display = 'none';
|
|
270
|
+
if (artifactsSearch) artifactsSearch.style.display = 'block';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Show artifact reader view
|
|
275
|
+
*/
|
|
276
|
+
function showReader() {
|
|
277
|
+
if (artifactsList) artifactsList.style.display = 'none';
|
|
278
|
+
if (artifactsReader) artifactsReader.style.display = 'block';
|
|
279
|
+
if (artifactsSearch) artifactsSearch.style.display = 'none';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Download the currently open artifact
|
|
284
|
+
*/
|
|
285
|
+
function downloadCurrentArtifact() {
|
|
286
|
+
if (!currentArtifact) return;
|
|
287
|
+
|
|
288
|
+
const blob = new Blob([currentArtifact.content], { type: 'text/plain;charset=utf-8' });
|
|
289
|
+
const url = URL.createObjectURL(blob);
|
|
290
|
+
const a = document.createElement('a');
|
|
291
|
+
a.href = url;
|
|
292
|
+
a.download = currentArtifact.name;
|
|
293
|
+
document.body.appendChild(a);
|
|
294
|
+
a.click();
|
|
295
|
+
document.body.removeChild(a);
|
|
296
|
+
URL.revokeObjectURL(url);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Show error message
|
|
301
|
+
*/
|
|
302
|
+
function showError(message) {
|
|
303
|
+
if (artifactsList) {
|
|
304
|
+
artifactsList.innerHTML = `
|
|
305
|
+
<div class="artifacts-error">
|
|
306
|
+
<p>${escapeHtml(message)}</p>
|
|
307
|
+
</div>
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get icon for file type
|
|
314
|
+
*/
|
|
315
|
+
function getFileIcon(extension) {
|
|
316
|
+
const icons = {
|
|
317
|
+
'.md': '📄',
|
|
318
|
+
'.txt': '📋',
|
|
319
|
+
'.html': '🌐',
|
|
320
|
+
'.json': '📊',
|
|
321
|
+
'.csv': '📈',
|
|
322
|
+
'.yml': '⚙️',
|
|
323
|
+
'.yaml': '⚙️',
|
|
324
|
+
'.xml': '📑',
|
|
325
|
+
'.log': '📝'
|
|
326
|
+
};
|
|
327
|
+
return icons[extension] || '📄';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Format file size
|
|
332
|
+
*/
|
|
333
|
+
function formatFileSize(bytes) {
|
|
334
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
335
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
336
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Format relative time
|
|
341
|
+
*/
|
|
342
|
+
function formatRelativeTime(isoString) {
|
|
343
|
+
const date = new Date(isoString);
|
|
344
|
+
const now = new Date();
|
|
345
|
+
const diffMs = now - date;
|
|
346
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
347
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
348
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
349
|
+
|
|
350
|
+
if (diffMins < 1) return 'Just now';
|
|
351
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
352
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
353
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
354
|
+
|
|
355
|
+
return date.toLocaleDateString();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Escape HTML
|
|
360
|
+
*/
|
|
361
|
+
function escapeHtml(text) {
|
|
362
|
+
const div = document.createElement('div');
|
|
363
|
+
div.textContent = text || '';
|
|
364
|
+
return div.innerHTML;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Escape attribute value
|
|
369
|
+
*/
|
|
370
|
+
function escapeAttr(text) {
|
|
371
|
+
return (text || '')
|
|
372
|
+
.replace(/&/g, '&')
|
|
373
|
+
.replace(/"/g, '"')
|
|
374
|
+
.replace(/'/g, ''')
|
|
375
|
+
.replace(/</g, '<')
|
|
376
|
+
.replace(/>/g, '>');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Cleanup function
|
|
381
|
+
*/
|
|
382
|
+
function destroy() {
|
|
383
|
+
if (eventsAbortController) {
|
|
384
|
+
eventsAbortController.abort();
|
|
385
|
+
eventsAbortController = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Open an artifact by filename — used for deep links from chat messages
|
|
391
|
+
* Opens the artifacts panel and navigates directly to the document
|
|
392
|
+
*/
|
|
393
|
+
async function openArtifactByName(filename) {
|
|
394
|
+
// Open the panel if not already open
|
|
395
|
+
if (window.UplinkPanels) {
|
|
396
|
+
window.UplinkPanels.open('artifacts');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Load artifacts if we haven't yet
|
|
400
|
+
if (artifacts.length === 0) {
|
|
401
|
+
await loadArtifacts();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Find the artifact
|
|
405
|
+
const artifact = artifacts.find(a => a.name === filename);
|
|
406
|
+
if (artifact) {
|
|
407
|
+
await openArtifact(artifact);
|
|
408
|
+
} else {
|
|
409
|
+
// Try loading fresh in case it was just created
|
|
410
|
+
await loadArtifacts();
|
|
411
|
+
const freshArtifact = artifacts.find(a => a.name === filename);
|
|
412
|
+
if (freshArtifact) {
|
|
413
|
+
await openArtifact(freshArtifact);
|
|
414
|
+
} else {
|
|
415
|
+
console.warn(`Artifacts: "${filename}" not found`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Export API
|
|
421
|
+
export const UplinkArtifacts = {
|
|
422
|
+
init,
|
|
423
|
+
loadArtifacts,
|
|
424
|
+
openArtifactByName,
|
|
425
|
+
destroy
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Backward compat: assign to window
|
|
429
|
+
window.UplinkArtifacts = UplinkArtifacts;
|
|
430
|
+
|
|
431
|
+
// Register and init
|
|
432
|
+
UplinkCore.registerModule('artifacts', init);
|