cct-cli 0.7.9.0__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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FATTY CAT — 1:1 CAT CLI Terminal Simulation Client
|
|
3
|
+
* Direct bridge to CAT Core via FastAPI Server
|
|
4
|
+
* Full Capability Integration (Tools, Modes, Commands, Providers, Memory, Extensions)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ─── STATE ─────────────────────────────────────────────────────────────
|
|
11
|
+
const state = {
|
|
12
|
+
workspaceId: 'current',
|
|
13
|
+
workspaceRoot: 'C:\\Users\\ADMIN\\Downloads\\compressed',
|
|
14
|
+
workspaceName: 'compressed',
|
|
15
|
+
activeMode: 'build',
|
|
16
|
+
modes: ['build', 'plan', 'research', 'debugger', 'agent', 'notebook'],
|
|
17
|
+
modeMeta: {
|
|
18
|
+
build: { label: 'Build', icon: '🔨', color: '#e0af68', bg: '#cca75d', desc: 'Autonomous coding, file editing, build commands & testing' },
|
|
19
|
+
plan: { label: 'Plan', icon: '🧭', color: '#9ece6a', bg: '#86b356', desc: 'High-level project roadmap, architecture planning & task breakdowns' },
|
|
20
|
+
research: { label: 'Research', icon: '🔍', color: '#ff69b4', bg: '#d45695', desc: 'Deep scientific web search, multi-query synthesis & citations' },
|
|
21
|
+
debugger: { label: 'Debugger', icon: '🪲', color: '#f7768e', bg: '#cf4661', desc: 'Diagnose runtime errors, stack traces, test failures & code fixes' },
|
|
22
|
+
agent: { label: 'Agent', icon: '⚙', color: '#bb9af7', bg: '#9d7ad9', desc: 'Multi-turn autonomous tool execution loop over CAT core' },
|
|
23
|
+
notebook: { label: 'Notebook', icon: '📘', color: '#7aa2f7', bg: '#587ec9', desc: 'Step-by-step chemistry derivations, formulas & numericals' }
|
|
24
|
+
},
|
|
25
|
+
provider: 'OLLAMA',
|
|
26
|
+
model: 'deepseek-r1',
|
|
27
|
+
permissionMode: 'ask',
|
|
28
|
+
permLabels: {
|
|
29
|
+
ask: { text: 'Ask Every Time', icon: '🔒' },
|
|
30
|
+
restricted: { text: 'Restricted', icon: '🛡' },
|
|
31
|
+
full: { text: 'Full Access', icon: '⚡' }
|
|
32
|
+
},
|
|
33
|
+
currentFile: null,
|
|
34
|
+
editorOpen: false,
|
|
35
|
+
splitPreview: false,
|
|
36
|
+
previewRunning: false,
|
|
37
|
+
turns: [],
|
|
38
|
+
recentProjects: [
|
|
39
|
+
{ name: 'compressed', path: 'C:\\Users\\ADMIN\\Downloads\\compressed' },
|
|
40
|
+
{ name: 'ADMIN', path: 'C:\\Users\\ADMIN' },
|
|
41
|
+
{ name: 'CAT_v0.7.9', path: 'C:\\Users\\ADMIN\\Downloads\\CAT_v0.7.9' },
|
|
42
|
+
{ name: 'Downloads', path: 'C:\\Users\\ADMIN\\Downloads' },
|
|
43
|
+
{ name: 'Scripts', path: 'C:\\Users\\ADMIN\\Downloads\\Scripts' }
|
|
44
|
+
],
|
|
45
|
+
sessionCount: 0,
|
|
46
|
+
commands: [],
|
|
47
|
+
filteredCommands: [],
|
|
48
|
+
paletteSelectedIndex: 0,
|
|
49
|
+
providers: [],
|
|
50
|
+
models: [],
|
|
51
|
+
selectedProvider: 'ollama',
|
|
52
|
+
selectedModel: 'deepseek-r1',
|
|
53
|
+
memoryFacts: [],
|
|
54
|
+
extensions: []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ─── API CLIENT ────────────────────────────────────────────────────────
|
|
58
|
+
const api = {
|
|
59
|
+
async request(method, path, body) {
|
|
60
|
+
const opts = {
|
|
61
|
+
method,
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
};
|
|
64
|
+
if (body) opts.body = JSON.stringify(body);
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(path, opts);
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
|
69
|
+
throw new Error(err.detail || 'HTTP ' + res.status);
|
|
70
|
+
}
|
|
71
|
+
return await res.json();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn('API call failed for ' + path, e);
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
get(path) { return this.request('GET', path); },
|
|
78
|
+
post(path, body) { return this.request('POST', path, body); },
|
|
79
|
+
del(path) { return this.request('DELETE', path); }
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ─── FATTY CONTROLLER ──────────────────────────────────────────────────
|
|
83
|
+
window.FATTY = {
|
|
84
|
+
async init() {
|
|
85
|
+
console.log('Initializing 1:1 Fatty CAT simulation with Full Feature Integration...');
|
|
86
|
+
this.bindDOM();
|
|
87
|
+
this.bindResizers();
|
|
88
|
+
|
|
89
|
+
// 1. Startup Welcome Lifecycle check (welcome_modal.py / Screenshot 2)
|
|
90
|
+
await this.checkStartup();
|
|
91
|
+
|
|
92
|
+
// 2. Load active workspace & file tree
|
|
93
|
+
await this.loadWorkspace();
|
|
94
|
+
|
|
95
|
+
// 3. Load permissions
|
|
96
|
+
await this.loadPermissions();
|
|
97
|
+
|
|
98
|
+
// 4. Load config & capabilities
|
|
99
|
+
await this.loadConfig();
|
|
100
|
+
|
|
101
|
+
// 5. Preload commands for Command Palette
|
|
102
|
+
await this.loadCommands();
|
|
103
|
+
|
|
104
|
+
// 6. Update line numbers for editor
|
|
105
|
+
this.updateLineNumbers();
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
bindDOM() {
|
|
109
|
+
const input = document.getElementById('cct-message-input');
|
|
110
|
+
if (input) {
|
|
111
|
+
input.addEventListener('keydown', (e) => {
|
|
112
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
this.sendMessage();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
input.addEventListener('input', () => {
|
|
118
|
+
input.style.height = 'auto';
|
|
119
|
+
input.style.height = Math.min(120, Math.max(22, input.scrollHeight)) + 'px';
|
|
120
|
+
// If user types '/' alone, suggest palette
|
|
121
|
+
if (input.value === '/') {
|
|
122
|
+
this.openCommandPalette();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const editor = document.getElementById('cct-code-editor');
|
|
128
|
+
if (editor) {
|
|
129
|
+
editor.addEventListener('input', () => {
|
|
130
|
+
this.updateLineNumbers();
|
|
131
|
+
});
|
|
132
|
+
editor.addEventListener('scroll', () => {
|
|
133
|
+
const gutter = document.getElementById('cct-line-numbers');
|
|
134
|
+
if (gutter) gutter.scrollTop = editor.scrollTop;
|
|
135
|
+
});
|
|
136
|
+
editor.addEventListener('keydown', (e) => {
|
|
137
|
+
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
this.saveCurrentFile();
|
|
140
|
+
} else if (e.key === 'Enter' && e.shiftKey) {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
// In Fatty CAT, Shift+Enter runs embedded web preview inside its own iframe (without popups)
|
|
143
|
+
this.saveCurrentFile();
|
|
144
|
+
if (!state.splitPreview) {
|
|
145
|
+
this.toggleSplitView();
|
|
146
|
+
} else {
|
|
147
|
+
this.runPreview();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Palette search input navigation
|
|
154
|
+
const paletteInput = document.getElementById('cct-palette-input');
|
|
155
|
+
if (paletteInput) {
|
|
156
|
+
paletteInput.addEventListener('input', (e) => {
|
|
157
|
+
this.filterPalette(e.target.value);
|
|
158
|
+
});
|
|
159
|
+
paletteInput.addEventListener('keydown', (e) => {
|
|
160
|
+
if (e.key === 'ArrowDown') {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
this.paletteNext();
|
|
163
|
+
} else if (e.key === 'ArrowUp') {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
this.palettePrev();
|
|
166
|
+
} else if (e.key === 'Enter') {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
this.executePaletteIndex();
|
|
169
|
+
} else if (e.key === 'Escape') {
|
|
170
|
+
this.closeAllModals();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Global Shortcuts: Main Menu, Ctrl+K, Ctrl+M, Ctrl+B, Escape
|
|
176
|
+
window.addEventListener('keydown', (e) => {
|
|
177
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
this.openCommandPalette();
|
|
180
|
+
} else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'P' || e.key === 'p')) {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
this.openCommandPalette();
|
|
183
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 'm' || e.key === 'M') && !e.shiftKey) {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
this.openModeSelector();
|
|
186
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 'b' || e.key === 'B') && !e.shiftKey) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
this.toggleSidebar();
|
|
189
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 'o' || e.key === 'O')) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
this.openFolderDialog();
|
|
192
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 'r' || e.key === 'R')) {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
this.openRecentWorkspacesModal();
|
|
195
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 'h' || e.key === 'H')) {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
this.openChatsModal();
|
|
198
|
+
} else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'M' || e.key === 'm')) {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
this.openMcpModal();
|
|
201
|
+
} else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'B' || e.key === 'b')) {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
this.openBackupProvidersModal();
|
|
204
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 't' || e.key === 'T')) {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
this.openThemesModal();
|
|
207
|
+
} else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'X' || e.key === 'x')) {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
this.openExtensionsModal();
|
|
210
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 'u' || e.key === 'U')) {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
this.openUserModal();
|
|
213
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 'q' || e.key === 'Q')) {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
this.openSignOutModal();
|
|
216
|
+
} else if (e.key === 'Escape') {
|
|
217
|
+
this.closeAllModals();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Outside click listener to dismiss main menu popup
|
|
222
|
+
document.addEventListener('click', (e) => {
|
|
223
|
+
const popup = document.getElementById('cct-main-menu-popup');
|
|
224
|
+
const brand = document.querySelector('.cct-header-left');
|
|
225
|
+
if (popup && popup.style.display === 'block') {
|
|
226
|
+
if (!popup.contains(e.target) && (!brand || !brand.contains(e.target))) {
|
|
227
|
+
popup.style.display = 'none';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
bindResizers() {
|
|
234
|
+
// Sidebar resizer
|
|
235
|
+
const sbResizer = document.getElementById('cct-sidebar-resizer');
|
|
236
|
+
const sidebar = document.getElementById('cct-sidebar');
|
|
237
|
+
if (sbResizer && sidebar) {
|
|
238
|
+
let isDragging = false;
|
|
239
|
+
sbResizer.addEventListener('mousedown', () => {
|
|
240
|
+
isDragging = true;
|
|
241
|
+
document.body.style.cursor = 'col-resize';
|
|
242
|
+
sbResizer.classList.add('dragging');
|
|
243
|
+
});
|
|
244
|
+
window.addEventListener('mousemove', (e) => {
|
|
245
|
+
if (!isDragging) return;
|
|
246
|
+
const newWidth = Math.max(180, Math.min(500, e.clientX));
|
|
247
|
+
sidebar.style.width = newWidth + 'px';
|
|
248
|
+
});
|
|
249
|
+
window.addEventListener('mouseup', () => {
|
|
250
|
+
isDragging = false;
|
|
251
|
+
document.body.style.cursor = '';
|
|
252
|
+
sbResizer.classList.remove('dragging');
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Editor resizer
|
|
257
|
+
const edResizer = document.getElementById('cct-editor-resizer');
|
|
258
|
+
const editorPane = document.getElementById('cct-editor-pane');
|
|
259
|
+
if (edResizer && editorPane) {
|
|
260
|
+
let isDragging = false;
|
|
261
|
+
edResizer.addEventListener('mousedown', () => {
|
|
262
|
+
isDragging = true;
|
|
263
|
+
document.body.style.cursor = 'col-resize';
|
|
264
|
+
edResizer.classList.add('dragging');
|
|
265
|
+
});
|
|
266
|
+
window.addEventListener('mousemove', (e) => {
|
|
267
|
+
if (!isDragging) return;
|
|
268
|
+
const newWidth = Math.max(220, Math.min(window.innerWidth - 350, window.innerWidth - e.clientX));
|
|
269
|
+
editorPane.style.width = newWidth + 'px';
|
|
270
|
+
});
|
|
271
|
+
window.addEventListener('mouseup', () => {
|
|
272
|
+
isDragging = false;
|
|
273
|
+
document.body.style.cursor = '';
|
|
274
|
+
edResizer.classList.remove('dragging');
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
280
|
+
// WORKSPACE & FILE TREE
|
|
281
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
282
|
+
async loadWorkspace(path) {
|
|
283
|
+
try {
|
|
284
|
+
let ws = await api.get('/api/workspace/current');
|
|
285
|
+
if (ws && ws.path) {
|
|
286
|
+
state.workspaceRoot = ws.path;
|
|
287
|
+
state.workspaceName = ws.name || 'compressed';
|
|
288
|
+
}
|
|
289
|
+
} catch (e) {
|
|
290
|
+
console.log('Using default workspace path');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Update header workspace name
|
|
294
|
+
const headWs = document.getElementById('cct-header-ws-name');
|
|
295
|
+
if (headWs) headWs.textContent = state.workspaceName;
|
|
296
|
+
|
|
297
|
+
// Update sidebar workspace name
|
|
298
|
+
const sideWs = document.getElementById('cct-ws-folder-name');
|
|
299
|
+
if (sideWs) sideWs.textContent = state.workspaceName;
|
|
300
|
+
|
|
301
|
+
// Fetch Tree
|
|
302
|
+
try {
|
|
303
|
+
const treeData = await api.get('/api/workspace/current/tree');
|
|
304
|
+
if (treeData) {
|
|
305
|
+
if (treeData.summary && treeData.summary.summary_line) {
|
|
306
|
+
const sub = document.getElementById('cct-ws-folder-sub');
|
|
307
|
+
if (sub) sub.textContent = treeData.summary.summary_line;
|
|
308
|
+
}
|
|
309
|
+
this.renderTree(treeData.children);
|
|
310
|
+
if (treeData.recent && treeData.recent.length) {
|
|
311
|
+
state.recentProjects = treeData.recent;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
this.renderFallbackTree();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.renderRecentProjects();
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
renderTree(nodes) {
|
|
322
|
+
const container = document.getElementById('cct-tree-container');
|
|
323
|
+
if (!container) return;
|
|
324
|
+
container.innerHTML = '';
|
|
325
|
+
|
|
326
|
+
const rootRow = document.createElement('div');
|
|
327
|
+
rootRow.className = 'cct-tree-node';
|
|
328
|
+
rootRow.innerHTML = `<span class="cct-tree-icon">📁</span> <span class="cct-tree-folder">${state.workspaceName}</span>`;
|
|
329
|
+
container.appendChild(rootRow);
|
|
330
|
+
|
|
331
|
+
const renderChildren = (children, prefix = ' ') => {
|
|
332
|
+
if (!children || !children.length) return;
|
|
333
|
+
children.forEach((child, idx) => {
|
|
334
|
+
const isLast = idx === children.length - 1;
|
|
335
|
+
const branch = isLast ? '└── ' : '├── ';
|
|
336
|
+
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
337
|
+
|
|
338
|
+
const row = document.createElement('div');
|
|
339
|
+
row.className = 'cct-tree-node';
|
|
340
|
+
row.dataset.path = child.path;
|
|
341
|
+
|
|
342
|
+
if (child.type === 'directory') {
|
|
343
|
+
row.innerHTML = `<span class="cct-tree-guide">${prefix}${branch}</span><span class="cct-tree-icon">📁</span> <span class="cct-tree-folder">${child.name}</span>`;
|
|
344
|
+
container.appendChild(row);
|
|
345
|
+
renderChildren(child.children, nextPrefix);
|
|
346
|
+
} else {
|
|
347
|
+
const isIndexHtml = child.name === 'index.html';
|
|
348
|
+
const fileIcon = child.ext === '.html' ? '📄' : (child.ext === '.js' ? '📄' : '📄');
|
|
349
|
+
row.innerHTML = `<span class="cct-tree-guide">${prefix}${branch}</span><span class="cct-tree-icon">${fileIcon}</span> <span class="cct-tree-name">${child.name}</span>`;
|
|
350
|
+
|
|
351
|
+
row.addEventListener('click', () => {
|
|
352
|
+
this.openFile(child.path, child.name);
|
|
353
|
+
});
|
|
354
|
+
container.appendChild(row);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
renderChildren(nodes);
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
renderFallbackTree() {
|
|
363
|
+
const fallbackNodes = [
|
|
364
|
+
{
|
|
365
|
+
name: 'projects',
|
|
366
|
+
type: 'directory',
|
|
367
|
+
path: 'projects',
|
|
368
|
+
children: [
|
|
369
|
+
{
|
|
370
|
+
name: 'calculator',
|
|
371
|
+
type: 'directory',
|
|
372
|
+
path: 'projects/calculator',
|
|
373
|
+
children: [
|
|
374
|
+
{ name: 'index.html', type: 'file', path: 'projects/calculator/index.html', ext: '.html' },
|
|
375
|
+
{ name: 'script.js', type: 'file', path: 'projects/calculator/script.js', ext: '.js' },
|
|
376
|
+
{ name: 'style.css', type: 'file', path: 'projects/calculator/style.css', ext: '.css' }
|
|
377
|
+
]
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
];
|
|
382
|
+
this.renderTree(fallbackNodes);
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
renderRecentProjects() {
|
|
386
|
+
const container = document.getElementById('cct-recent-container');
|
|
387
|
+
if (!container) return;
|
|
388
|
+
container.innerHTML = '';
|
|
389
|
+
|
|
390
|
+
state.recentProjects.slice(0, 5).forEach((p) => {
|
|
391
|
+
const row = document.createElement('div');
|
|
392
|
+
row.className = 'cct-recent-row';
|
|
393
|
+
row.innerHTML = `
|
|
394
|
+
<span class="cct-recent-name">📁 ${p.name}</span>
|
|
395
|
+
<span class="cct-recent-del" title="Remove from recent">✕</span>
|
|
396
|
+
`;
|
|
397
|
+
row.querySelector('.cct-recent-name').addEventListener('click', () => {
|
|
398
|
+
this.openRecentPath(p.path);
|
|
399
|
+
});
|
|
400
|
+
row.querySelector('.cct-recent-del').addEventListener('click', (e) => {
|
|
401
|
+
e.stopPropagation();
|
|
402
|
+
row.remove();
|
|
403
|
+
});
|
|
404
|
+
container.appendChild(row);
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
409
|
+
// FILE OPEN & EDITOR
|
|
410
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
411
|
+
async openFile(filePath, fileName) {
|
|
412
|
+
document.querySelectorAll('.cct-tree-node').forEach(n => {
|
|
413
|
+
if (n.dataset.path === filePath) {
|
|
414
|
+
n.classList.add('active-file');
|
|
415
|
+
} else {
|
|
416
|
+
n.classList.remove('active-file');
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
state.currentFile = { path: filePath, name: fileName };
|
|
421
|
+
const titleEl = document.getElementById('cct-editor-filename');
|
|
422
|
+
if (titleEl) titleEl.textContent = fileName;
|
|
423
|
+
|
|
424
|
+
let content = '';
|
|
425
|
+
try {
|
|
426
|
+
const res = await api.get(`/api/workspace/current/file?path=${encodeURIComponent(filePath)}`);
|
|
427
|
+
content = res.content || '';
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.warn('Could not read file from server, using sample code');
|
|
430
|
+
content = `<!DOCTYPE html>\n<html>\n<head><title>CAT Editor</title></head>\n<body>\n<h1>Hello from CAT CLI</h1>\n</body>\n</html>`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const textarea = document.getElementById('cct-code-editor');
|
|
434
|
+
if (textarea) {
|
|
435
|
+
textarea.value = content;
|
|
436
|
+
textarea.scrollTop = 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const editorPane = document.getElementById('cct-editor-pane');
|
|
440
|
+
const resizer = document.getElementById('cct-editor-resizer');
|
|
441
|
+
if (editorPane) editorPane.classList.add('open');
|
|
442
|
+
if (resizer) resizer.style.display = 'block';
|
|
443
|
+
state.editorOpen = true;
|
|
444
|
+
|
|
445
|
+
this.updateLineNumbers();
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
updateLineNumbers() {
|
|
449
|
+
const textarea = document.getElementById('cct-code-editor');
|
|
450
|
+
const gutter = document.getElementById('cct-line-numbers');
|
|
451
|
+
if (!textarea || !gutter) return;
|
|
452
|
+
|
|
453
|
+
const lines = textarea.value.split('\n').length;
|
|
454
|
+
let numHtml = '';
|
|
455
|
+
for (let i = 1; i <= Math.max(30, lines); i++) {
|
|
456
|
+
numHtml += `<div>${i}</div>`;
|
|
457
|
+
}
|
|
458
|
+
gutter.innerHTML = numHtml;
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
async saveCurrentFile() {
|
|
462
|
+
if (!state.currentFile) return;
|
|
463
|
+
const textarea = document.getElementById('cct-code-editor');
|
|
464
|
+
if (!textarea) return;
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
await api.post(`/api/workspace/current/file`, {
|
|
468
|
+
path: state.currentFile.path,
|
|
469
|
+
content: textarea.value
|
|
470
|
+
});
|
|
471
|
+
console.log('Saved file:', state.currentFile.path);
|
|
472
|
+
if (state.previewRunning) {
|
|
473
|
+
this.runPreview();
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
console.error('Error saving file:', e);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
closeEditor() {
|
|
481
|
+
const editorPane = document.getElementById('cct-editor-pane');
|
|
482
|
+
const resizer = document.getElementById('cct-editor-resizer');
|
|
483
|
+
if (editorPane) editorPane.classList.remove('open');
|
|
484
|
+
if (resizer) resizer.style.display = 'none';
|
|
485
|
+
state.editorOpen = false;
|
|
486
|
+
|
|
487
|
+
document.querySelectorAll('.cct-tree-node').forEach(n => {
|
|
488
|
+
n.classList.remove('active-file');
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
toggleSplitView() {
|
|
493
|
+
state.splitPreview = !state.splitPreview;
|
|
494
|
+
const preview = document.getElementById('cct-web-preview');
|
|
495
|
+
const codeBox = document.getElementById('cct-code-container');
|
|
496
|
+
const gutter = document.getElementById('cct-line-numbers');
|
|
497
|
+
|
|
498
|
+
if (state.splitPreview) {
|
|
499
|
+
if (codeBox) codeBox.style.display = 'none';
|
|
500
|
+
if (gutter) gutter.style.display = 'none';
|
|
501
|
+
if (preview) {
|
|
502
|
+
preview.classList.add('active');
|
|
503
|
+
this.runPreview();
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
if (codeBox) codeBox.style.display = 'block';
|
|
507
|
+
if (gutter) gutter.style.display = 'block';
|
|
508
|
+
if (preview) preview.classList.remove('active');
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
async runPreview() {
|
|
513
|
+
if (!state.currentFile) return;
|
|
514
|
+
const preview = document.getElementById('cct-web-preview');
|
|
515
|
+
const codeBox = document.getElementById('cct-code-container');
|
|
516
|
+
const gutter = document.getElementById('cct-line-numbers');
|
|
517
|
+
|
|
518
|
+
if (!state.splitPreview) {
|
|
519
|
+
state.splitPreview = true;
|
|
520
|
+
if (codeBox) codeBox.style.display = 'none';
|
|
521
|
+
if (gutter) gutter.style.display = 'none';
|
|
522
|
+
if (preview) preview.classList.add('active');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
state.previewRunning = true;
|
|
526
|
+
try {
|
|
527
|
+
const res = await api.post('/api/preview/start?workspace_id=' + encodeURIComponent(state.workspaceId || 'current'), {
|
|
528
|
+
file_path: state.currentFile.path
|
|
529
|
+
});
|
|
530
|
+
if (res && res.url && preview) {
|
|
531
|
+
preview.src = res.url;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
} catch (e) {
|
|
535
|
+
console.warn('Backend preview server fallback to iframe srcdoc', e);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const textarea = document.getElementById('cct-code-editor');
|
|
539
|
+
if (preview && textarea) {
|
|
540
|
+
preview.srcdoc = textarea.value;
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
545
|
+
// CHAT & CONVERSATION & AGENT EXECUTION
|
|
546
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
547
|
+
async sendMessage() {
|
|
548
|
+
const input = document.getElementById('cct-message-input');
|
|
549
|
+
if (!input) return;
|
|
550
|
+
const text = input.value.trim();
|
|
551
|
+
if (!text) return;
|
|
552
|
+
|
|
553
|
+
input.value = '';
|
|
554
|
+
input.style.height = 'auto';
|
|
555
|
+
|
|
556
|
+
// If slash command: dispatch directly
|
|
557
|
+
if (text.startsWith('/')) {
|
|
558
|
+
this.executeSlashCommand(text);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Switch to Chat Screen if first message
|
|
563
|
+
const welcome = document.getElementById('cct-welcome-view');
|
|
564
|
+
const turnsBox = document.getElementById('cct-chat-turns');
|
|
565
|
+
if (welcome) welcome.style.display = 'none';
|
|
566
|
+
if (turnsBox) turnsBox.style.display = 'flex';
|
|
567
|
+
|
|
568
|
+
// 1. Append User Bubble
|
|
569
|
+
this.appendUserMessage(text);
|
|
570
|
+
|
|
571
|
+
// 2. Append Assistant Loading Card
|
|
572
|
+
const cardEl = this.appendAssistantLoading();
|
|
573
|
+
|
|
574
|
+
// 3. Request AI Response or Agent Turn from server
|
|
575
|
+
const t0 = Date.now();
|
|
576
|
+
try {
|
|
577
|
+
let res;
|
|
578
|
+
const isAgentMode = state.activeMode === 'agent';
|
|
579
|
+
const isExplicitFileTask = /^\s*(create|write|delete|edit|refactor|generate)\s+(file|folder|dir|component|app|project)\b/i.test(text);
|
|
580
|
+
if (isAgentMode || (state.activeMode === 'build' && isExplicitFileTask)) {
|
|
581
|
+
// Use multi-turn agent for explicit file/tool tasks
|
|
582
|
+
res = await api.post('/api/agent', {
|
|
583
|
+
message: text,
|
|
584
|
+
mode: 'agent',
|
|
585
|
+
max_steps: 4,
|
|
586
|
+
project_path: state.workspaceRoot,
|
|
587
|
+
chat_id: state.chatId
|
|
588
|
+
});
|
|
589
|
+
} else {
|
|
590
|
+
// Fast AI chat for queries, discussions, explanations, and build mode code generation
|
|
591
|
+
res = await api.post('/api/chat', {
|
|
592
|
+
message: text,
|
|
593
|
+
mode: state.activeMode,
|
|
594
|
+
project_path: state.workspaceRoot,
|
|
595
|
+
chat_id: state.chatId
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (res && res.chat_id) {
|
|
599
|
+
state.chatId = res.chat_id;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const elapsed = Math.round((Date.now() - t0) / 1000);
|
|
603
|
+
const mins = Math.floor(elapsed / 60);
|
|
604
|
+
const secs = elapsed % 60;
|
|
605
|
+
const timeStr = `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
|
606
|
+
|
|
607
|
+
this.updateAssistantCard(cardEl, {
|
|
608
|
+
text: res.response || '(No response from AI)',
|
|
609
|
+
steps: res.steps || [],
|
|
610
|
+
provider: res.provider || state.provider,
|
|
611
|
+
model: res.model || state.model,
|
|
612
|
+
timeStr: res.time_str || timeStr,
|
|
613
|
+
tokens: res.tokens || Math.max(12, Math.round(text.length * 1.3)),
|
|
614
|
+
timings: res.timings || {
|
|
615
|
+
ttfb_ms: Math.max(120, elapsed * 800),
|
|
616
|
+
memory_ms: 37,
|
|
617
|
+
loop_ms: Math.max(140, elapsed * 1000)
|
|
618
|
+
},
|
|
619
|
+
calls: res.calls || (res.steps ? Math.max(1, res.steps.length) : 1)
|
|
620
|
+
});
|
|
621
|
+
} catch (e) {
|
|
622
|
+
this.updateAssistantCard(cardEl, {
|
|
623
|
+
text: '(Generation error: ' + (e.message || 'connection issue') + ')',
|
|
624
|
+
provider: state.provider,
|
|
625
|
+
model: state.model,
|
|
626
|
+
timeStr: '0:05',
|
|
627
|
+
tokens: 11,
|
|
628
|
+
timings: { ttfb_ms: 500, memory_ms: 37, loop_ms: 540 },
|
|
629
|
+
calls: 1
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
appendUserMessage(text) {
|
|
635
|
+
const turnsBox = document.getElementById('cct-chat-turns');
|
|
636
|
+
if (!turnsBox) return;
|
|
637
|
+
|
|
638
|
+
const now = new Date();
|
|
639
|
+
const timeStr = `${now.getHours()}:${now.getMinutes() < 10 ? '0' : ''}${now.getMinutes()}`;
|
|
640
|
+
|
|
641
|
+
const wrapper = document.createElement('div');
|
|
642
|
+
wrapper.className = 'cct-user-turn-wrapper';
|
|
643
|
+
wrapper.innerHTML = `
|
|
644
|
+
<div class="cct-user-bubble">
|
|
645
|
+
<div class="cct-user-text">${this.escapeHtml(text)}</div>
|
|
646
|
+
<div class="cct-user-meta">${timeStr} ✎</div>
|
|
647
|
+
</div>
|
|
648
|
+
`;
|
|
649
|
+
turnsBox.appendChild(wrapper);
|
|
650
|
+
this.scrollToBottom();
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
appendAssistantLoading() {
|
|
654
|
+
const turnsBox = document.getElementById('cct-chat-turns');
|
|
655
|
+
if (!turnsBox) return null;
|
|
656
|
+
|
|
657
|
+
const card = document.createElement('div');
|
|
658
|
+
card.className = 'cct-response-card';
|
|
659
|
+
const meta = state.modeMeta[state.activeMode] || state.modeMeta.build;
|
|
660
|
+
|
|
661
|
+
card.innerHTML = `
|
|
662
|
+
<div class="cct-card-header">${meta.icon} ${meta.label}</div>
|
|
663
|
+
<div class="cct-card-content">Thinking… ▌</div>
|
|
664
|
+
<div class="cct-card-meta">
|
|
665
|
+
<div class="cct-meta-line">Generating response from ${state.provider} ${state.model}…</div>
|
|
666
|
+
</div>
|
|
667
|
+
`;
|
|
668
|
+
turnsBox.appendChild(card);
|
|
669
|
+
this.scrollToBottom();
|
|
670
|
+
return card;
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
updateAssistantCard(cardEl, data) {
|
|
674
|
+
if (!cardEl) return;
|
|
675
|
+
const meta = state.modeMeta[state.activeMode] || state.modeMeta.build;
|
|
676
|
+
|
|
677
|
+
let stepsHtml = '';
|
|
678
|
+
if (data.steps && data.steps.length) {
|
|
679
|
+
stepsHtml = `<div class="cct-tools-container" style="margin-bottom: 8px;">` +
|
|
680
|
+
data.steps.map(s => {
|
|
681
|
+
const toolName = s.tool || 'tool';
|
|
682
|
+
const argsStr = typeof s.args === 'string' ? s.args : JSON.stringify(s.args || {});
|
|
683
|
+
return `
|
|
684
|
+
<div class="cct-tool-badge">
|
|
685
|
+
<span class="cct-tool-badge-icon">🔧</span>
|
|
686
|
+
<span class="cct-tool-badge-name">${this.escapeHtml(toolName)}</span>
|
|
687
|
+
<span class="cct-tool-badge-detail">${this.escapeHtml(argsStr.slice(0, 50))}</span>
|
|
688
|
+
</div>
|
|
689
|
+
`;
|
|
690
|
+
}).join('') +
|
|
691
|
+
`</div>`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
cardEl.innerHTML = `
|
|
695
|
+
<div class="cct-card-header">${meta.icon} ${meta.label}</div>
|
|
696
|
+
${stepsHtml}
|
|
697
|
+
<div class="cct-card-content">${this.formatContent(data.text)}</div>
|
|
698
|
+
<div class="cct-card-meta">
|
|
699
|
+
<div class="cct-meta-complete">✓ Response Complete</div>
|
|
700
|
+
<div class="cct-meta-line">Provider: ${data.provider} · Model: ${data.model} · Time: ${data.timeStr} · Tokens: ${data.tokens}</div>
|
|
701
|
+
<div class="cct-meta-timings">⏰ Real timings: TTFB ${data.timings.ttfb_ms}ms · memory ${data.timings.memory_ms}ms · loop ${data.timings.loop_ms}ms</div>
|
|
702
|
+
<div class="cct-meta-calls">🗂 ${data.calls} model call(s)</div>
|
|
703
|
+
</div>
|
|
704
|
+
`;
|
|
705
|
+
this.scrollToBottom();
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
async executeSlashCommand(cmdStr) {
|
|
709
|
+
const welcome = document.getElementById('cct-welcome-view');
|
|
710
|
+
const turnsBox = document.getElementById('cct-chat-turns');
|
|
711
|
+
if (welcome) welcome.style.display = 'none';
|
|
712
|
+
if (turnsBox) turnsBox.style.display = 'flex';
|
|
713
|
+
|
|
714
|
+
this.appendUserMessage(cmdStr);
|
|
715
|
+
const cardEl = this.appendAssistantLoading();
|
|
716
|
+
|
|
717
|
+
const base = cmdStr.split(' ')[0].toLowerCase();
|
|
718
|
+
if (base === '/clear') {
|
|
719
|
+
this.newChat();
|
|
720
|
+
return;
|
|
721
|
+
} else if (base === '/docs' || base === '/help') {
|
|
722
|
+
this.openDocs();
|
|
723
|
+
} else if (base === '/model') {
|
|
724
|
+
this.openModelPicker();
|
|
725
|
+
} else if (base === '/mode') {
|
|
726
|
+
this.openModeSelector();
|
|
727
|
+
} else if (base === '/memory' || base === '/m') {
|
|
728
|
+
this.openMemoryCenter();
|
|
729
|
+
} else if (base === '/extensions') {
|
|
730
|
+
this.openExtensionsManager();
|
|
731
|
+
} else if (base === '/workspace') {
|
|
732
|
+
this.openFolderDialog();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const res = await api.post('/api/command/dispatch', { command: cmdStr });
|
|
737
|
+
const out = res.output || 'Command executed.';
|
|
738
|
+
this.updateAssistantCard(cardEl, {
|
|
739
|
+
text: out,
|
|
740
|
+
provider: state.provider,
|
|
741
|
+
model: state.model,
|
|
742
|
+
timeStr: '0:01',
|
|
743
|
+
tokens: out.length,
|
|
744
|
+
timings: { ttfb_ms: 40, memory_ms: 10, loop_ms: 50 },
|
|
745
|
+
calls: 1
|
|
746
|
+
});
|
|
747
|
+
} catch (e) {
|
|
748
|
+
this.updateAssistantCard(cardEl, {
|
|
749
|
+
text: `Command error: ${e.message}`,
|
|
750
|
+
provider: state.provider,
|
|
751
|
+
model: state.model,
|
|
752
|
+
timeStr: '0:01',
|
|
753
|
+
tokens: 0,
|
|
754
|
+
timings: { ttfb_ms: 10, memory_ms: 5, loop_ms: 15 },
|
|
755
|
+
calls: 1
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
newChat() {
|
|
761
|
+
const welcome = document.getElementById('cct-welcome-view');
|
|
762
|
+
const turnsBox = document.getElementById('cct-chat-turns');
|
|
763
|
+
if (welcome) welcome.style.display = 'flex';
|
|
764
|
+
if (turnsBox) {
|
|
765
|
+
turnsBox.innerHTML = '';
|
|
766
|
+
turnsBox.style.display = 'none';
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
|
|
770
|
+
scrollToBottom() {
|
|
771
|
+
const conv = document.getElementById('cct-conversation');
|
|
772
|
+
if (conv) conv.scrollTop = conv.scrollHeight;
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
776
|
+
// COMMAND PALETTE (Ctrl+K / /)
|
|
777
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
778
|
+
async loadCommands() {
|
|
779
|
+
try {
|
|
780
|
+
const res = await api.get('/api/commands');
|
|
781
|
+
if (res && res.commands) {
|
|
782
|
+
state.commands = res.commands;
|
|
783
|
+
}
|
|
784
|
+
} catch (e) {
|
|
785
|
+
console.warn('Could not load commands from server');
|
|
786
|
+
state.commands = [
|
|
787
|
+
{ command: '/help', description: 'Show help & shortcuts' },
|
|
788
|
+
{ command: '/solve', description: 'Solve formula symbolically' },
|
|
789
|
+
{ command: '/calculator', description: 'Quick calculator' },
|
|
790
|
+
{ command: '/model', description: 'Switch AI model/provider' },
|
|
791
|
+
{ command: '/mode', description: 'Switch AI mode' },
|
|
792
|
+
{ command: '/memory', description: 'Memory center' },
|
|
793
|
+
{ command: '/extensions', description: 'Extensions manager' },
|
|
794
|
+
{ command: '/workspace', description: 'Manage workspace' },
|
|
795
|
+
{ command: '/clear', description: 'Reset conversation' },
|
|
796
|
+
{ command: '/atomsim', description: 'Live 2D Bohr atom simulation' },
|
|
797
|
+
{ command: '/orbitals', description: 'Live quantum orbital simulation' },
|
|
798
|
+
{ command: '/graph', description: 'Chemistry curves & graphs' },
|
|
799
|
+
{ command: '/research', description: 'Deep research' },
|
|
800
|
+
{ command: '/debug', description: 'Switch to debugger mode' },
|
|
801
|
+
{ command: '/install', description: 'Install packages' }
|
|
802
|
+
];
|
|
803
|
+
}
|
|
804
|
+
const promptSuggestions = [
|
|
805
|
+
{ command: 'what is a first order reaction', description: 'Quick conceptual answer · Kinetics', type: 'prompt' },
|
|
806
|
+
{ command: 'half life of a first order reaction', description: 'Numerical calculation · Kinetics', type: 'prompt' },
|
|
807
|
+
{ command: 'explain the arrhenius equation simply', description: 'Conceptual explanation · Physical Chem', type: 'prompt' },
|
|
808
|
+
{ command: 'solve ideal gas law with P=1, n=2, T=300', description: 'Symbolic solve + full notebook', type: 'prompt' },
|
|
809
|
+
{ command: 'derive the first order rate law step by step', description: 'Full derivation, notebook format', type: 'prompt' },
|
|
810
|
+
{ command: 'plot the arrhenius graph and export it', description: 'Interactive plot + PNG export', type: 'prompt' },
|
|
811
|
+
{ command: 'simulate an iron atom in 3d', description: 'Live 3D Bohr simulation', type: 'prompt' },
|
|
812
|
+
{ command: 'calculate molar mass of glucose', description: 'Step-by-step formula C6H12O6 mass', type: 'prompt' },
|
|
813
|
+
{ command: 'how to balance redox reactions', description: 'Ion-electron oxidation-reduction method', type: 'prompt' }
|
|
814
|
+
];
|
|
815
|
+
state.commands = [...(state.commands || []), ...promptSuggestions];
|
|
816
|
+
state.filteredCommands = [...state.commands];
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
openCommandPalette() {
|
|
820
|
+
const modal = document.getElementById('cct-command-palette-modal');
|
|
821
|
+
const input = document.getElementById('cct-palette-input');
|
|
822
|
+
if (!modal || !input) return;
|
|
823
|
+
|
|
824
|
+
modal.style.display = 'flex';
|
|
825
|
+
input.value = '';
|
|
826
|
+
this.filterPalette('');
|
|
827
|
+
setTimeout(() => input.focus(), 50);
|
|
828
|
+
},
|
|
829
|
+
|
|
830
|
+
filterPalette(query) {
|
|
831
|
+
const q = (query || '').toLowerCase().trim();
|
|
832
|
+
if (!q) {
|
|
833
|
+
state.filteredCommands = [...state.commands];
|
|
834
|
+
} else {
|
|
835
|
+
state.filteredCommands = state.commands.filter(c => {
|
|
836
|
+
const cmd = (c.command || '').toLowerCase();
|
|
837
|
+
const desc = (c.description || '').toLowerCase();
|
|
838
|
+
return cmd.includes(q) || desc.includes(q);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
state.paletteSelectedIndex = 0;
|
|
842
|
+
this.renderPalette();
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
renderPalette() {
|
|
846
|
+
const list = document.getElementById('cct-palette-list');
|
|
847
|
+
if (!list) return;
|
|
848
|
+
list.innerHTML = '';
|
|
849
|
+
|
|
850
|
+
if (!state.filteredCommands.length) {
|
|
851
|
+
list.innerHTML = '<div style="padding: 12px 16px; color: var(--term-text-faint);">No commands found matching query.</div>';
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
state.filteredCommands.slice(0, 40).forEach((c, idx) => {
|
|
856
|
+
const item = document.createElement('div');
|
|
857
|
+
item.className = 'cct-palette-item' + (idx === state.paletteSelectedIndex ? ' active' : '');
|
|
858
|
+
item.innerHTML = `
|
|
859
|
+
<div class="cct-palette-item-left">
|
|
860
|
+
<span class="cct-palette-cmd">${this.escapeHtml(c.command)}</span>
|
|
861
|
+
<span class="cct-palette-desc">${this.escapeHtml(c.description)}</span>
|
|
862
|
+
</div>
|
|
863
|
+
<span class="cct-palette-badge">cmd</span>
|
|
864
|
+
`;
|
|
865
|
+
item.addEventListener('click', () => {
|
|
866
|
+
this.executeCommandFromPalette(c.command);
|
|
867
|
+
});
|
|
868
|
+
list.appendChild(item);
|
|
869
|
+
});
|
|
870
|
+
},
|
|
871
|
+
|
|
872
|
+
paletteNext() {
|
|
873
|
+
if (!state.filteredCommands.length) return;
|
|
874
|
+
state.paletteSelectedIndex = (state.paletteSelectedIndex + 1) % Math.min(40, state.filteredCommands.length);
|
|
875
|
+
this.updatePaletteActiveItem();
|
|
876
|
+
},
|
|
877
|
+
|
|
878
|
+
palettePrev() {
|
|
879
|
+
if (!state.filteredCommands.length) return;
|
|
880
|
+
state.paletteSelectedIndex = (state.paletteSelectedIndex - 1 + Math.min(40, state.filteredCommands.length)) % Math.min(40, state.filteredCommands.length);
|
|
881
|
+
this.updatePaletteActiveItem();
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
updatePaletteActiveItem() {
|
|
885
|
+
const items = document.querySelectorAll('.cct-palette-item');
|
|
886
|
+
items.forEach((it, idx) => {
|
|
887
|
+
if (idx === state.paletteSelectedIndex) {
|
|
888
|
+
it.classList.add('active');
|
|
889
|
+
it.scrollIntoView({ block: 'nearest' });
|
|
890
|
+
} else {
|
|
891
|
+
it.classList.remove('active');
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
executePaletteIndex() {
|
|
897
|
+
const selected = state.filteredCommands[state.paletteSelectedIndex];
|
|
898
|
+
if (selected) {
|
|
899
|
+
this.executeCommandFromPalette(selected.command);
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
|
|
903
|
+
executeCommandFromPalette(cmdStr) {
|
|
904
|
+
this.closeAllModals();
|
|
905
|
+
this.executeSlashCommand(cmdStr);
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
909
|
+
// MODEL & PROVIDER PICKER
|
|
910
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
911
|
+
async openModelPicker() {
|
|
912
|
+
const modal = document.getElementById('cct-model-picker-modal');
|
|
913
|
+
if (!modal) return;
|
|
914
|
+
modal.style.display = 'flex';
|
|
915
|
+
|
|
916
|
+
await this.loadProviders();
|
|
917
|
+
},
|
|
918
|
+
|
|
919
|
+
async loadProviders() {
|
|
920
|
+
try {
|
|
921
|
+
const res = await api.get('/api/providers');
|
|
922
|
+
if (res && res.providers) {
|
|
923
|
+
state.providers = res.providers;
|
|
924
|
+
}
|
|
925
|
+
} catch (e) {
|
|
926
|
+
console.warn('Could not load providers list');
|
|
927
|
+
state.providers = [
|
|
928
|
+
{ id: 'ollama', name: 'Ollama (Local)' },
|
|
929
|
+
{ id: 'openai', name: 'OpenAI' },
|
|
930
|
+
{ id: 'anthropic', name: 'Anthropic Claude' },
|
|
931
|
+
{ id: 'gemini', name: 'Google Gemini' },
|
|
932
|
+
{ id: 'groq', name: 'Groq Cloud' },
|
|
933
|
+
{ id: 'deepseek', name: 'DeepSeek API' }
|
|
934
|
+
];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const countEl = document.getElementById('cct-provider-count');
|
|
938
|
+
if (countEl) countEl.textContent = state.providers.length;
|
|
939
|
+
|
|
940
|
+
this.renderProviders(state.providers);
|
|
941
|
+
this.selectProvider(state.provider.toLowerCase());
|
|
942
|
+
},
|
|
943
|
+
|
|
944
|
+
filterProviders(query) {
|
|
945
|
+
const q = (query || '').toLowerCase().trim();
|
|
946
|
+
const filtered = state.providers.filter(p => {
|
|
947
|
+
const id = (p.id || '').toLowerCase();
|
|
948
|
+
const name = (p.name || '').toLowerCase();
|
|
949
|
+
return id.includes(q) || name.includes(q);
|
|
950
|
+
});
|
|
951
|
+
this.renderProviders(filtered);
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
renderProviders(providers) {
|
|
955
|
+
const container = document.getElementById('cct-picker-providers');
|
|
956
|
+
if (!container) return;
|
|
957
|
+
container.innerHTML = '';
|
|
958
|
+
|
|
959
|
+
providers.slice(0, 60).forEach(p => {
|
|
960
|
+
const btn = document.createElement('button');
|
|
961
|
+
btn.className = 'cct-provider-btn' + (p.id === state.selectedProvider ? ' active' : '');
|
|
962
|
+
btn.textContent = p.name || p.id;
|
|
963
|
+
btn.addEventListener('click', () => {
|
|
964
|
+
this.selectProvider(p.id);
|
|
965
|
+
});
|
|
966
|
+
container.appendChild(btn);
|
|
967
|
+
});
|
|
968
|
+
},
|
|
969
|
+
|
|
970
|
+
async selectProvider(provId) {
|
|
971
|
+
state.selectedProvider = provId;
|
|
972
|
+
document.querySelectorAll('.cct-provider-btn').forEach(btn => {
|
|
973
|
+
btn.classList.toggle('active', btn.textContent.toLowerCase().includes(provId));
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
const modelContainer = document.getElementById('cct-picker-models');
|
|
977
|
+
if (!modelContainer) return;
|
|
978
|
+
modelContainer.innerHTML = '<div style="padding: 6px; color: var(--term-text-faint);">Loading models…</div>';
|
|
979
|
+
|
|
980
|
+
try {
|
|
981
|
+
const res = await api.get(`/api/models?provider=${encodeURIComponent(provId)}`);
|
|
982
|
+
const models = (res && res.models && res.models.length) ? res.models : [state.model, 'default'];
|
|
983
|
+
this.renderModels(models);
|
|
984
|
+
} catch (e) {
|
|
985
|
+
this.renderModels([state.model, 'default']);
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
|
|
989
|
+
renderModels(models) {
|
|
990
|
+
const container = document.getElementById('cct-picker-models');
|
|
991
|
+
if (!container) return;
|
|
992
|
+
container.innerHTML = '';
|
|
993
|
+
|
|
994
|
+
models.forEach(m => {
|
|
995
|
+
const row = document.createElement('div');
|
|
996
|
+
row.className = 'cct-model-row' + (m === state.selectedModel ? ' active' : '');
|
|
997
|
+
row.innerHTML = `
|
|
998
|
+
<span>${this.escapeHtml(m)}</span>
|
|
999
|
+
<span style="color: var(--term-text-faint); font-size: 11px;">Select</span>
|
|
1000
|
+
`;
|
|
1001
|
+
row.addEventListener('click', () => {
|
|
1002
|
+
state.selectedModel = m;
|
|
1003
|
+
document.querySelectorAll('.cct-model-row').forEach(r => r.classList.remove('active'));
|
|
1004
|
+
row.classList.add('active');
|
|
1005
|
+
});
|
|
1006
|
+
container.appendChild(row);
|
|
1007
|
+
});
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
async applySelectedModel() {
|
|
1011
|
+
state.provider = state.selectedProvider.toUpperCase();
|
|
1012
|
+
state.model = state.selectedModel;
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
await api.post('/api/models/switch', {
|
|
1016
|
+
provider: state.selectedProvider,
|
|
1017
|
+
model: state.selectedModel
|
|
1018
|
+
});
|
|
1019
|
+
} catch (e) {}
|
|
1020
|
+
|
|
1021
|
+
const modelBadge = document.getElementById('cct-model-badge');
|
|
1022
|
+
if (modelBadge) modelBadge.textContent = `${state.provider} ${state.model}`;
|
|
1023
|
+
|
|
1024
|
+
const welcomeModel = document.getElementById('cct-welcome-model');
|
|
1025
|
+
if (welcomeModel) welcomeModel.textContent = `${state.provider.toLowerCase()} ${state.model}`;
|
|
1026
|
+
|
|
1027
|
+
this.closeAllModals();
|
|
1028
|
+
},
|
|
1029
|
+
|
|
1030
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1031
|
+
// AI MODE SELECTOR (Ctrl+M)
|
|
1032
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1033
|
+
openModeSelector() {
|
|
1034
|
+
const modal = document.getElementById('cct-mode-selector-modal');
|
|
1035
|
+
const grid = document.getElementById('cct-mode-grid');
|
|
1036
|
+
if (!modal || !grid) return;
|
|
1037
|
+
|
|
1038
|
+
grid.innerHTML = '';
|
|
1039
|
+
state.modes.forEach(m => {
|
|
1040
|
+
const meta = state.modeMeta[m] || { label: m, icon: '●', color: '#fff', desc: '' };
|
|
1041
|
+
const card = document.createElement('div');
|
|
1042
|
+
card.className = 'cct-mode-card' + (m === state.activeMode ? ' active' : '');
|
|
1043
|
+
card.innerHTML = `
|
|
1044
|
+
<span class="cct-mode-card-icon" style="color: ${meta.color};">${meta.icon}</span>
|
|
1045
|
+
<div class="cct-mode-card-info">
|
|
1046
|
+
<span class="cct-mode-card-title" style="color: ${meta.color};">${meta.label}</span>
|
|
1047
|
+
<span class="cct-mode-card-desc">${meta.desc}</span>
|
|
1048
|
+
</div>
|
|
1049
|
+
`;
|
|
1050
|
+
card.addEventListener('click', () => {
|
|
1051
|
+
this.selectMode(m);
|
|
1052
|
+
});
|
|
1053
|
+
grid.appendChild(card);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
modal.style.display = 'flex';
|
|
1057
|
+
},
|
|
1058
|
+
|
|
1059
|
+
async selectMode(modeId) {
|
|
1060
|
+
state.activeMode = modeId;
|
|
1061
|
+
const meta = state.modeMeta[modeId] || state.modeMeta.build;
|
|
1062
|
+
|
|
1063
|
+
const badge = document.getElementById('cct-mode-badge');
|
|
1064
|
+
if (badge) {
|
|
1065
|
+
badge.textContent = `${meta.icon} ${meta.label}`;
|
|
1066
|
+
badge.style.color = meta.color;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
this.closeAllModals();
|
|
1070
|
+
|
|
1071
|
+
try {
|
|
1072
|
+
await api.post('/api/modes/switch', { mode: modeId });
|
|
1073
|
+
} catch (e) {}
|
|
1074
|
+
},
|
|
1075
|
+
|
|
1076
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1077
|
+
// MEMORY CENTER (/memory)
|
|
1078
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1079
|
+
async openMemoryCenter() {
|
|
1080
|
+
const modal = document.getElementById('cct-memory-modal');
|
|
1081
|
+
if (!modal) return;
|
|
1082
|
+
modal.style.display = 'flex';
|
|
1083
|
+
await this.loadMemoryFacts();
|
|
1084
|
+
},
|
|
1085
|
+
|
|
1086
|
+
async loadMemoryFacts() {
|
|
1087
|
+
const list = document.getElementById('cct-memory-list');
|
|
1088
|
+
if (!list) return;
|
|
1089
|
+
list.innerHTML = '<div style="padding: 8px; color: var(--term-text-faint);">Loading memory…</div>';
|
|
1090
|
+
|
|
1091
|
+
try {
|
|
1092
|
+
const res = await api.get('/api/memory/facts');
|
|
1093
|
+
state.memoryFacts = (res && res.facts) ? res.facts : [];
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
state.memoryFacts = [];
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
this.renderMemoryFacts();
|
|
1099
|
+
},
|
|
1100
|
+
|
|
1101
|
+
renderMemoryFacts() {
|
|
1102
|
+
const list = document.getElementById('cct-memory-list');
|
|
1103
|
+
if (!list) return;
|
|
1104
|
+
list.innerHTML = '';
|
|
1105
|
+
|
|
1106
|
+
if (!state.memoryFacts.length) {
|
|
1107
|
+
list.innerHTML = '<div style="padding: 8px; color: var(--term-text-faint);">No persistent facts recorded yet. Add one below.</div>';
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
state.memoryFacts.forEach((fact, idx) => {
|
|
1112
|
+
const card = document.createElement('div');
|
|
1113
|
+
card.className = 'cct-fact-card';
|
|
1114
|
+
const text = typeof fact === 'string' ? fact : (fact.fact || JSON.stringify(fact));
|
|
1115
|
+
card.innerHTML = `
|
|
1116
|
+
<span class="cct-fact-content">📌 ${this.escapeHtml(text)}</span>
|
|
1117
|
+
<span class="cct-fact-del" title="Delete fact">✕</span>
|
|
1118
|
+
`;
|
|
1119
|
+
card.querySelector('.cct-fact-del').addEventListener('click', () => {
|
|
1120
|
+
this.deleteMemoryFact(idx);
|
|
1121
|
+
});
|
|
1122
|
+
list.appendChild(card);
|
|
1123
|
+
});
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
async addMemoryFact() {
|
|
1127
|
+
const input = document.getElementById('cct-add-fact-input');
|
|
1128
|
+
if (!input || !input.value.trim()) return;
|
|
1129
|
+
const text = input.value.trim();
|
|
1130
|
+
input.value = '';
|
|
1131
|
+
|
|
1132
|
+
try {
|
|
1133
|
+
await api.post('/api/memory/fact', { fact: text });
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
console.error('Error adding fact:', e);
|
|
1136
|
+
}
|
|
1137
|
+
await this.loadMemoryFacts();
|
|
1138
|
+
},
|
|
1139
|
+
|
|
1140
|
+
async deleteMemoryFact(idx) {
|
|
1141
|
+
try {
|
|
1142
|
+
await api.del(`/api/memory/fact/${idx}`);
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
console.error('Error deleting fact:', e);
|
|
1145
|
+
}
|
|
1146
|
+
await this.loadMemoryFacts();
|
|
1147
|
+
},
|
|
1148
|
+
|
|
1149
|
+
async clearAllMemory() {
|
|
1150
|
+
if (!confirm('Are you sure you want to clear all persistent CAT memory?')) return;
|
|
1151
|
+
try {
|
|
1152
|
+
await api.post('/api/command/dispatch', { command: '/memory clear' });
|
|
1153
|
+
} catch (e) {}
|
|
1154
|
+
await this.loadMemoryFacts();
|
|
1155
|
+
},
|
|
1156
|
+
|
|
1157
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1158
|
+
// EXTENSIONS MANAGER (/extensions)
|
|
1159
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1160
|
+
async openExtensionsManager() {
|
|
1161
|
+
const modal = document.getElementById('cct-extensions-modal');
|
|
1162
|
+
if (!modal) return;
|
|
1163
|
+
modal.style.display = 'flex';
|
|
1164
|
+
await this.loadExtensions();
|
|
1165
|
+
},
|
|
1166
|
+
|
|
1167
|
+
async loadExtensions() {
|
|
1168
|
+
const list = document.getElementById('cct-extensions-list');
|
|
1169
|
+
if (!list) return;
|
|
1170
|
+
list.innerHTML = '<div style="padding: 8px; color: var(--term-text-faint);">Loading extensions…</div>';
|
|
1171
|
+
|
|
1172
|
+
try {
|
|
1173
|
+
const res = await api.get('/api/extensions');
|
|
1174
|
+
state.extensions = (res && res.extensions) ? res.extensions : [];
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
state.extensions = [
|
|
1177
|
+
{ id: 'gestures', name: 'Gestures & Camera Control', enabled: true, description: 'Hand-gesture camera control pipeline' },
|
|
1178
|
+
{ id: 'vision', name: 'Vision Agent & Canvas Annotations', enabled: false, description: 'Visual UI analysis & cursor tracking' },
|
|
1179
|
+
{ id: 'personalize', name: 'AI Personalization', enabled: true, description: 'Adaptive system prompt & user preferences' },
|
|
1180
|
+
{ id: 'customization', name: 'Customization Engine', enabled: true, description: 'Themes, sound effects & keybinding customizations' }
|
|
1181
|
+
];
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
this.renderExtensions();
|
|
1185
|
+
},
|
|
1186
|
+
|
|
1187
|
+
renderExtensions() {
|
|
1188
|
+
const list = document.getElementById('cct-extensions-list');
|
|
1189
|
+
if (!list) return;
|
|
1190
|
+
list.innerHTML = '';
|
|
1191
|
+
|
|
1192
|
+
state.extensions.forEach(ext => {
|
|
1193
|
+
const card = document.createElement('div');
|
|
1194
|
+
card.className = 'cct-ext-card';
|
|
1195
|
+
const icon = ext.icon || '🧩';
|
|
1196
|
+
const isEnabled = ext.enabled || ext.state === 'INSTALLED_ENABLED';
|
|
1197
|
+
card.innerHTML = `
|
|
1198
|
+
<div class="cct-ext-info">
|
|
1199
|
+
<span class="cct-ext-title">${this.escapeHtml(icon)} ${this.escapeHtml(ext.name || ext.display_name || ext.id)}</span>
|
|
1200
|
+
<span class="cct-ext-desc">${this.escapeHtml(ext.description || '')}</span>
|
|
1201
|
+
<span style="font-size: 11px; color: var(--term-text-faint); margin-top: 3px; display: block;">${this.escapeHtml(ext.publisher || 'Custom')} · v${this.escapeHtml(ext.version || '1.0.0')} · ${this.escapeHtml(ext.category || 'General')}</span>
|
|
1202
|
+
</div>
|
|
1203
|
+
<button class="cct-ext-toggle-btn ${isEnabled ? 'active' : ''}">
|
|
1204
|
+
${isEnabled ? 'Enabled' : 'Disabled'}
|
|
1205
|
+
</button>
|
|
1206
|
+
`;
|
|
1207
|
+
card.querySelector('.cct-ext-toggle-btn').addEventListener('click', () => {
|
|
1208
|
+
this.toggleExtension(ext.id);
|
|
1209
|
+
});
|
|
1210
|
+
list.appendChild(card);
|
|
1211
|
+
});
|
|
1212
|
+
},
|
|
1213
|
+
|
|
1214
|
+
async toggleExtension(extId) {
|
|
1215
|
+
try {
|
|
1216
|
+
await api.post('/api/extensions/toggle', { id: extId });
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
console.error('Error toggling extension:', e);
|
|
1219
|
+
}
|
|
1220
|
+
await this.loadExtensions();
|
|
1221
|
+
},
|
|
1222
|
+
|
|
1223
|
+
openCreateExtensionModal() {
|
|
1224
|
+
const modal = document.getElementById('cct-create-ext-modal');
|
|
1225
|
+
if (!modal) return;
|
|
1226
|
+
const err = document.getElementById('cct-create-ext-error');
|
|
1227
|
+
if (err) { err.style.display = 'none'; err.textContent = ''; }
|
|
1228
|
+
const nameInput = document.getElementById('cct-create-ext-name');
|
|
1229
|
+
const idInput = document.getElementById('cct-create-ext-id');
|
|
1230
|
+
const descInput = document.getElementById('cct-create-ext-desc');
|
|
1231
|
+
if (nameInput) nameInput.value = '';
|
|
1232
|
+
if (idInput) idInput.value = '';
|
|
1233
|
+
if (descInput) descInput.value = '';
|
|
1234
|
+
modal.style.display = 'flex';
|
|
1235
|
+
if (nameInput) nameInput.focus();
|
|
1236
|
+
},
|
|
1237
|
+
|
|
1238
|
+
closeCreateExtensionModal(e) {
|
|
1239
|
+
if (e && e.target && e.target !== e.currentTarget && !e.target.classList.contains('cct-modal-close-btn')) return;
|
|
1240
|
+
const modal = document.getElementById('cct-create-ext-modal');
|
|
1241
|
+
if (modal) modal.style.display = 'none';
|
|
1242
|
+
},
|
|
1243
|
+
|
|
1244
|
+
async submitCreateExtension() {
|
|
1245
|
+
const nameInput = document.getElementById('cct-create-ext-name');
|
|
1246
|
+
const idInput = document.getElementById('cct-create-ext-id');
|
|
1247
|
+
const iconInput = document.getElementById('cct-create-ext-icon');
|
|
1248
|
+
const descInput = document.getElementById('cct-create-ext-desc');
|
|
1249
|
+
const catInput = document.getElementById('cct-create-ext-cat');
|
|
1250
|
+
const err = document.getElementById('cct-create-ext-error');
|
|
1251
|
+
const submitBtn = document.getElementById('cct-create-ext-submit');
|
|
1252
|
+
|
|
1253
|
+
const name = nameInput ? nameInput.value.trim() : '';
|
|
1254
|
+
let extId = idInput ? idInput.value.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_') : '';
|
|
1255
|
+
if (!name) {
|
|
1256
|
+
if (err) { err.textContent = 'Please enter an extension name.'; err.style.display = 'block'; }
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (!extId) {
|
|
1260
|
+
extId = name.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (submitBtn) {
|
|
1264
|
+
submitBtn.disabled = true;
|
|
1265
|
+
submitBtn.textContent = 'Creating…';
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
const payload = {
|
|
1270
|
+
id: extId,
|
|
1271
|
+
name: name,
|
|
1272
|
+
icon: (iconInput && iconInput.value.trim()) || '⚡',
|
|
1273
|
+
description: (descInput && descInput.value.trim()) || '',
|
|
1274
|
+
category: (catInput && catInput.value) || 'Custom',
|
|
1275
|
+
publisher: 'You (Custom)',
|
|
1276
|
+
};
|
|
1277
|
+
const res = await api.post('/api/extensions/create', payload);
|
|
1278
|
+
if (res && res.success) {
|
|
1279
|
+
this.closeCreateExtensionModal();
|
|
1280
|
+
await this.loadExtensions();
|
|
1281
|
+
} else {
|
|
1282
|
+
if (err) {
|
|
1283
|
+
err.textContent = (res && res.extension && res.extension.message) || 'Failed to create extension.';
|
|
1284
|
+
err.style.display = 'block';
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
if (err) {
|
|
1289
|
+
err.textContent = e.message || 'Network error creating extension.';
|
|
1290
|
+
err.style.display = 'block';
|
|
1291
|
+
}
|
|
1292
|
+
} finally {
|
|
1293
|
+
if (submitBtn) {
|
|
1294
|
+
submitBtn.disabled = false;
|
|
1295
|
+
submitBtn.textContent = 'Create & Install';
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
|
|
1300
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1301
|
+
// PERMISSIONS
|
|
1302
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1303
|
+
async loadPermissions() {
|
|
1304
|
+
try {
|
|
1305
|
+
const res = await api.get('/api/permissions');
|
|
1306
|
+
if (res && res.mode) {
|
|
1307
|
+
state.permissionMode = res.mode;
|
|
1308
|
+
}
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
console.log('Using default permission mode');
|
|
1311
|
+
}
|
|
1312
|
+
this.updatePermissionUI();
|
|
1313
|
+
},
|
|
1314
|
+
|
|
1315
|
+
updatePermissionUI() {
|
|
1316
|
+
const pillIcon = document.getElementById('cct-perm-icon');
|
|
1317
|
+
const pillText = document.getElementById('cct-perm-text');
|
|
1318
|
+
const label = state.permLabels[state.permissionMode] || state.permLabels.ask;
|
|
1319
|
+
|
|
1320
|
+
if (pillIcon) pillIcon.textContent = label.icon;
|
|
1321
|
+
if (pillText) pillText.textContent = label.text;
|
|
1322
|
+
},
|
|
1323
|
+
|
|
1324
|
+
togglePermissionModal() {
|
|
1325
|
+
const modal = document.getElementById('cct-perm-modal');
|
|
1326
|
+
if (modal) {
|
|
1327
|
+
modal.style.display = modal.style.display === 'none' ? 'flex' : 'none';
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1330
|
+
|
|
1331
|
+
async setPermissionMode(mode) {
|
|
1332
|
+
state.permissionMode = mode;
|
|
1333
|
+
this.updatePermissionUI();
|
|
1334
|
+
this.closeAllModals();
|
|
1335
|
+
|
|
1336
|
+
try {
|
|
1337
|
+
await api.post('/api/permissions/mode', { mode });
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
console.error('Error changing permission mode:', e);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Append audit log line if in chat
|
|
1343
|
+
const turnsBox = document.getElementById('cct-chat-turns');
|
|
1344
|
+
if (turnsBox && turnsBox.style.display !== 'none') {
|
|
1345
|
+
const audit = document.createElement('div');
|
|
1346
|
+
audit.className = 'cct-audit-log';
|
|
1347
|
+
const label = state.permLabels[mode] || state.permLabels.ask;
|
|
1348
|
+
audit.innerHTML = `
|
|
1349
|
+
<div class="cct-audit-line">Access: ${label.icon} ${label.text} Scope:</div>
|
|
1350
|
+
<div class="cct-audit-scope">${state.workspaceRoot}</div>
|
|
1351
|
+
<div class="cct-audit-line">Permission mode: ${label.icon} ${label.text}</div>
|
|
1352
|
+
`;
|
|
1353
|
+
turnsBox.appendChild(audit);
|
|
1354
|
+
this.scrollToBottom();
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
|
|
1358
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1359
|
+
// SETTINGS & CONFIG
|
|
1360
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1361
|
+
async loadConfig() {
|
|
1362
|
+
try {
|
|
1363
|
+
const st = await api.get('/api/status');
|
|
1364
|
+
if (st) {
|
|
1365
|
+
if (st.provider && st.provider !== 'none') state.provider = st.provider.toUpperCase();
|
|
1366
|
+
if (st.model && st.model !== 'none') state.model = st.model;
|
|
1367
|
+
}
|
|
1368
|
+
} catch (e) {}
|
|
1369
|
+
|
|
1370
|
+
const modelBadge = document.getElementById('cct-model-badge');
|
|
1371
|
+
if (modelBadge) modelBadge.textContent = `${state.provider} ${state.model}`;
|
|
1372
|
+
|
|
1373
|
+
const welcomeModel = document.getElementById('cct-welcome-model');
|
|
1374
|
+
if (welcomeModel) welcomeModel.textContent = `${state.provider.toLowerCase()} ${state.model}`;
|
|
1375
|
+
},
|
|
1376
|
+
|
|
1377
|
+
openSettings() {
|
|
1378
|
+
const modal = document.getElementById('cct-settings-modal');
|
|
1379
|
+
const pInp = document.getElementById('cct-cfg-provider');
|
|
1380
|
+
const mInp = document.getElementById('cct-cfg-model');
|
|
1381
|
+
if (pInp) pInp.value = state.provider.toLowerCase();
|
|
1382
|
+
if (mInp) mInp.value = state.model;
|
|
1383
|
+
if (modal) modal.style.display = 'flex';
|
|
1384
|
+
},
|
|
1385
|
+
|
|
1386
|
+
async saveSettings() {
|
|
1387
|
+
const pInp = document.getElementById('cct-cfg-provider');
|
|
1388
|
+
const mInp = document.getElementById('cct-cfg-model');
|
|
1389
|
+
|
|
1390
|
+
if (pInp && pInp.value) state.provider = pInp.value.toUpperCase();
|
|
1391
|
+
if (mInp && mInp.value) state.model = mInp.value;
|
|
1392
|
+
|
|
1393
|
+
try {
|
|
1394
|
+
await api.post('/api/models/switch', {
|
|
1395
|
+
provider: state.provider.toLowerCase(),
|
|
1396
|
+
model: state.model
|
|
1397
|
+
});
|
|
1398
|
+
} catch (e) {}
|
|
1399
|
+
|
|
1400
|
+
this.closeAllModals();
|
|
1401
|
+
await this.loadConfig();
|
|
1402
|
+
},
|
|
1403
|
+
|
|
1404
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1405
|
+
// MAIN MENU & STARTUP LIFECYCLE
|
|
1406
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1407
|
+
async toggleNavMenu(e) {
|
|
1408
|
+
if (e) e.stopPropagation();
|
|
1409
|
+
const popup = document.getElementById('cct-main-menu-popup');
|
|
1410
|
+
if (!popup) return;
|
|
1411
|
+
if (popup.style.display === 'block') {
|
|
1412
|
+
popup.style.display = 'none';
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
await this.loadNavMenu();
|
|
1416
|
+
popup.style.display = 'block';
|
|
1417
|
+
},
|
|
1418
|
+
|
|
1419
|
+
async loadNavMenu() {
|
|
1420
|
+
const container = document.getElementById('cct-main-menu-items');
|
|
1421
|
+
if (!container) return;
|
|
1422
|
+
container.innerHTML = '<div style="padding: 8px 12px; color: var(--term-text-muted); font-size: 11px;">Loading menu...</div>';
|
|
1423
|
+
|
|
1424
|
+
try {
|
|
1425
|
+
const data = await api.get('/api/menu');
|
|
1426
|
+
const items = data.items || [];
|
|
1427
|
+
container.innerHTML = '';
|
|
1428
|
+
|
|
1429
|
+
items.forEach(item => {
|
|
1430
|
+
if (item.is_separator) {
|
|
1431
|
+
const sep = document.createElement('div');
|
|
1432
|
+
sep.className = 'cct-menu-sep';
|
|
1433
|
+
container.appendChild(sep);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (item.is_section_header) {
|
|
1437
|
+
const sec = document.createElement('div');
|
|
1438
|
+
sec.className = 'cct-menu-section';
|
|
1439
|
+
sec.textContent = item.label;
|
|
1440
|
+
container.appendChild(sec);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (item.is_placeholder) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const row = document.createElement('div');
|
|
1448
|
+
row.className = 'cct-menu-item';
|
|
1449
|
+
row.innerHTML = `
|
|
1450
|
+
<div class="cct-menu-item-left">
|
|
1451
|
+
<span class="cct-menu-icon">${item.icon || '•'}</span>
|
|
1452
|
+
<span>${item.label}</span>
|
|
1453
|
+
</div>
|
|
1454
|
+
${item.shortcut ? `<span class="cct-menu-shortcut">${item.shortcut}</span>` : ''}
|
|
1455
|
+
`;
|
|
1456
|
+
row.addEventListener('click', (ev) => {
|
|
1457
|
+
ev.stopPropagation();
|
|
1458
|
+
document.getElementById('cct-main-menu-popup').style.display = 'none';
|
|
1459
|
+
this.handleMenuAction(item.action);
|
|
1460
|
+
});
|
|
1461
|
+
container.appendChild(row);
|
|
1462
|
+
});
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
container.innerHTML = '<div style="padding: 8px 12px; color: var(--color-red); font-size: 11px;">Failed to load menu</div>';
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
|
|
1468
|
+
handleMenuAction(action) {
|
|
1469
|
+
switch (action) {
|
|
1470
|
+
case 'open_folder':
|
|
1471
|
+
this.openFolderDialog();
|
|
1472
|
+
break;
|
|
1473
|
+
case 'recent_workspaces':
|
|
1474
|
+
this.openRecentWorkspacesModal();
|
|
1475
|
+
break;
|
|
1476
|
+
case 'chats':
|
|
1477
|
+
this.openChatsModal();
|
|
1478
|
+
break;
|
|
1479
|
+
case 'mcp_servers':
|
|
1480
|
+
this.openMcpModal();
|
|
1481
|
+
break;
|
|
1482
|
+
case 'backup_providers':
|
|
1483
|
+
this.openBackupProvidersModal();
|
|
1484
|
+
break;
|
|
1485
|
+
case 'themes':
|
|
1486
|
+
this.openThemesModal();
|
|
1487
|
+
break;
|
|
1488
|
+
case 'extensions':
|
|
1489
|
+
this.openExtensionsModal();
|
|
1490
|
+
break;
|
|
1491
|
+
case 'user':
|
|
1492
|
+
this.openUserModal();
|
|
1493
|
+
break;
|
|
1494
|
+
case 'signout':
|
|
1495
|
+
this.openSignOutModal();
|
|
1496
|
+
break;
|
|
1497
|
+
case 'cust_customization':
|
|
1498
|
+
case 'cust_personalize':
|
|
1499
|
+
this.openPersonalizeModal();
|
|
1500
|
+
break;
|
|
1501
|
+
case 'cust_gestures':
|
|
1502
|
+
this.openGesturesModal();
|
|
1503
|
+
break;
|
|
1504
|
+
default:
|
|
1505
|
+
console.log('Action selected:', action);
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
|
|
1510
|
+
async checkStartup() {
|
|
1511
|
+
try {
|
|
1512
|
+
const st = await api.get('/api/startup/status');
|
|
1513
|
+
if (st && !st.has_seen) {
|
|
1514
|
+
const modal = document.getElementById('cct-welcome-modal');
|
|
1515
|
+
const grid = document.getElementById('cct-highlights-grid');
|
|
1516
|
+
if (grid && st.highlights) {
|
|
1517
|
+
grid.innerHTML = st.highlights.map(h => `
|
|
1518
|
+
<div class="cct-highlight-card">
|
|
1519
|
+
<span class="cct-highlight-icon">${h.icon}</span>
|
|
1520
|
+
<div>
|
|
1521
|
+
<div class="cct-highlight-title">${h.name}</div>
|
|
1522
|
+
<div class="cct-highlight-desc">${h.desc}</div>
|
|
1523
|
+
</div>
|
|
1524
|
+
</div>
|
|
1525
|
+
`).join('');
|
|
1526
|
+
}
|
|
1527
|
+
if (modal) modal.style.display = 'flex';
|
|
1528
|
+
|
|
1529
|
+
// Staged boot animation
|
|
1530
|
+
const bar = document.getElementById('cct-boot-progress-bar');
|
|
1531
|
+
const status = document.getElementById('cct-boot-status');
|
|
1532
|
+
if (bar && status) {
|
|
1533
|
+
bar.style.width = '15%';
|
|
1534
|
+
status.textContent = '● Initializing provider subsystems...';
|
|
1535
|
+
setTimeout(() => {
|
|
1536
|
+
bar.style.width = '55%';
|
|
1537
|
+
status.textContent = '● Loading memory & workspace index...';
|
|
1538
|
+
}, 300);
|
|
1539
|
+
setTimeout(() => {
|
|
1540
|
+
bar.style.width = '85%';
|
|
1541
|
+
status.textContent = '● Checking security sandbox & permissions...';
|
|
1542
|
+
}, 600);
|
|
1543
|
+
setTimeout(() => {
|
|
1544
|
+
bar.style.width = '100%';
|
|
1545
|
+
status.textContent = '● System ready. All subsystems initialized.';
|
|
1546
|
+
}, 900);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
} catch (e) {
|
|
1550
|
+
console.warn('Startup status check error:', e);
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
|
|
1554
|
+
async dismissWelcomeModal() {
|
|
1555
|
+
const modal = document.getElementById('cct-welcome-modal');
|
|
1556
|
+
const dontShow = document.getElementById('cct-welcome-dont-show');
|
|
1557
|
+
if (dontShow && dontShow.checked) {
|
|
1558
|
+
try {
|
|
1559
|
+
await api.post('/api/startup/dismiss');
|
|
1560
|
+
} catch (e) {}
|
|
1561
|
+
}
|
|
1562
|
+
if (modal) modal.style.display = 'none';
|
|
1563
|
+
},
|
|
1564
|
+
|
|
1565
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1566
|
+
// OPEN FOLDER (METHOD A: BROWSE & METHOD B: ENTER PATH)
|
|
1567
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1568
|
+
currentBrowsePath: null,
|
|
1569
|
+
parentBrowsePath: null,
|
|
1570
|
+
|
|
1571
|
+
switchFolderTab(tab) {
|
|
1572
|
+
const bTab = document.getElementById('tab-folder-browse');
|
|
1573
|
+
const pTab = document.getElementById('tab-folder-path');
|
|
1574
|
+
const bBox = document.getElementById('cct-folder-tab-browse');
|
|
1575
|
+
const pBox = document.getElementById('cct-folder-tab-path');
|
|
1576
|
+
if (tab === 'browse') {
|
|
1577
|
+
if (bTab) bTab.classList.add('active');
|
|
1578
|
+
if (pTab) pTab.classList.remove('active');
|
|
1579
|
+
if (bBox) bBox.style.display = 'block';
|
|
1580
|
+
if (pBox) pBox.style.display = 'none';
|
|
1581
|
+
} else {
|
|
1582
|
+
if (bTab) bTab.classList.remove('active');
|
|
1583
|
+
if (pTab) pTab.classList.add('active');
|
|
1584
|
+
if (bBox) bBox.style.display = 'none';
|
|
1585
|
+
if (pBox) pBox.style.display = 'block';
|
|
1586
|
+
const inp = document.getElementById('cct-folder-input');
|
|
1587
|
+
if (inp && (!inp.value || inp.value === '')) {
|
|
1588
|
+
inp.value = this.currentBrowsePath || state.workspaceRoot;
|
|
1589
|
+
}
|
|
1590
|
+
if (inp) this.onFolderInputChanged(inp.value);
|
|
1591
|
+
}
|
|
1592
|
+
},
|
|
1593
|
+
|
|
1594
|
+
async openFolderDialog() {
|
|
1595
|
+
const modal = document.getElementById('cct-folder-modal');
|
|
1596
|
+
this.switchFolderTab('browse');
|
|
1597
|
+
await this.loadFsBrowse(state.workspaceRoot);
|
|
1598
|
+
if (modal) modal.style.display = 'flex';
|
|
1599
|
+
},
|
|
1600
|
+
|
|
1601
|
+
async loadFsBrowse(path) {
|
|
1602
|
+
const pathDisplay = document.getElementById('cct-fs-current-path');
|
|
1603
|
+
const drivesRow = document.getElementById('cct-fs-drives-row');
|
|
1604
|
+
const folderList = document.getElementById('cct-fs-folder-list');
|
|
1605
|
+
if (folderList) folderList.innerHTML = '<div style="padding: 8px; color: var(--term-text-muted);">Loading folders...</div>';
|
|
1606
|
+
|
|
1607
|
+
try {
|
|
1608
|
+
const url = path ? `/api/fs/browse?path=${encodeURIComponent(path)}` : '/api/fs/browse';
|
|
1609
|
+
const res = await api.get(url);
|
|
1610
|
+
this.currentBrowsePath = res.current_path;
|
|
1611
|
+
this.parentBrowsePath = res.parent_path;
|
|
1612
|
+
|
|
1613
|
+
if (pathDisplay) {
|
|
1614
|
+
pathDisplay.textContent = res.current_path;
|
|
1615
|
+
pathDisplay.title = res.current_path;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
const input = document.getElementById('cct-folder-input');
|
|
1619
|
+
if (input) input.value = res.current_path;
|
|
1620
|
+
|
|
1621
|
+
// Render drives
|
|
1622
|
+
if (drivesRow && res.drives) {
|
|
1623
|
+
drivesRow.innerHTML = res.drives.map(d => `
|
|
1624
|
+
<button class="cct-fs-drive-btn ${res.current_path.startsWith(d) ? 'active' : ''}" onclick="FATTY.loadFsBrowse('${d.replace(/\\/g, '\\\\')}')">
|
|
1625
|
+
${d}
|
|
1626
|
+
</button>
|
|
1627
|
+
`).join('');
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Render folders
|
|
1631
|
+
if (folderList) {
|
|
1632
|
+
folderList.innerHTML = '';
|
|
1633
|
+
if (!res.folders || res.folders.length === 0) {
|
|
1634
|
+
folderList.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted); font-size: 11.5px;">No subdirectories found</div>';
|
|
1635
|
+
} else {
|
|
1636
|
+
res.folders.forEach(f => {
|
|
1637
|
+
const item = document.createElement('div');
|
|
1638
|
+
item.className = 'cct-fs-folder-item';
|
|
1639
|
+
item.innerHTML = `
|
|
1640
|
+
<span>📁 ${f.name}</span>
|
|
1641
|
+
${f.protected ? '<span style="color: var(--color-amber); font-size: 10px;">🔒 protected</span>' : ''}
|
|
1642
|
+
`;
|
|
1643
|
+
item.addEventListener('click', () => {
|
|
1644
|
+
this.loadFsBrowse(f.path);
|
|
1645
|
+
});
|
|
1646
|
+
folderList.appendChild(item);
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
if (folderList) folderList.innerHTML = `<div style="padding: 8px; color: var(--color-red);">Error: ${err.message}</div>`;
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
|
|
1655
|
+
browseFolderUp() {
|
|
1656
|
+
if (this.parentBrowsePath) {
|
|
1657
|
+
this.loadFsBrowse(this.parentBrowsePath);
|
|
1658
|
+
}
|
|
1659
|
+
},
|
|
1660
|
+
|
|
1661
|
+
folderValidateTimer: null,
|
|
1662
|
+
onFolderInputChanged(val) {
|
|
1663
|
+
clearTimeout(this.folderValidateTimer);
|
|
1664
|
+
const msg = document.getElementById('cct-folder-validation-msg');
|
|
1665
|
+
if (!val || !val.trim()) {
|
|
1666
|
+
if (msg) {
|
|
1667
|
+
msg.className = 'cct-validation-msg';
|
|
1668
|
+
msg.textContent = 'Enter folder path to validate...';
|
|
1669
|
+
}
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
this.folderValidateTimer = setTimeout(async () => {
|
|
1673
|
+
try {
|
|
1674
|
+
const res = await api.get(`/api/fs/validate?path=${encodeURIComponent(val.trim())}`);
|
|
1675
|
+
if (msg) {
|
|
1676
|
+
msg.className = `cct-validation-msg ${res.valid ? 'valid' : 'invalid'}`;
|
|
1677
|
+
msg.textContent = res.message;
|
|
1678
|
+
}
|
|
1679
|
+
} catch (e) {
|
|
1680
|
+
if (msg) {
|
|
1681
|
+
msg.className = 'cct-validation-msg invalid';
|
|
1682
|
+
msg.textContent = 'Validation error: ' + e.message;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}, 250);
|
|
1686
|
+
},
|
|
1687
|
+
|
|
1688
|
+
async confirmOpenFolder() {
|
|
1689
|
+
const isPathTab = document.getElementById('cct-folder-tab-path').style.display !== 'none';
|
|
1690
|
+
let targetPath = this.currentBrowsePath;
|
|
1691
|
+
if (isPathTab) {
|
|
1692
|
+
const inp = document.getElementById('cct-folder-input');
|
|
1693
|
+
if (inp && inp.value.trim()) targetPath = inp.value.trim();
|
|
1694
|
+
}
|
|
1695
|
+
if (!targetPath) return;
|
|
1696
|
+
|
|
1697
|
+
try {
|
|
1698
|
+
const res = await api.post('/api/workspace/open', { path: targetPath });
|
|
1699
|
+
state.workspaceRoot = res.path || targetPath;
|
|
1700
|
+
state.workspaceName = res.name || targetPath.split(/[\\/]/).filter(Boolean).pop() || targetPath;
|
|
1701
|
+
this.closeAllModals();
|
|
1702
|
+
await this.loadWorkspace(targetPath);
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
alert('Could not open folder: ' + err.message);
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
|
|
1708
|
+
openRecentPath(pathOrName) {
|
|
1709
|
+
const match = state.recentProjects.find(p => p.name === pathOrName || p.path === pathOrName);
|
|
1710
|
+
const target = match ? match.path : pathOrName;
|
|
1711
|
+
document.getElementById('cct-folder-input').value = target;
|
|
1712
|
+
this.currentBrowsePath = target;
|
|
1713
|
+
this.confirmOpenFolder();
|
|
1714
|
+
},
|
|
1715
|
+
|
|
1716
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1717
|
+
// RECENT WORKSPACES MODAL
|
|
1718
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1719
|
+
async openRecentWorkspacesModal() {
|
|
1720
|
+
const modal = document.getElementById('cct-recent-modal');
|
|
1721
|
+
const list = document.getElementById('cct-recent-modal-list');
|
|
1722
|
+
if (list) list.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">Loading recent workspaces...</div>';
|
|
1723
|
+
if (modal) modal.style.display = 'flex';
|
|
1724
|
+
|
|
1725
|
+
try {
|
|
1726
|
+
const data = await api.get('/api/workspace/recent');
|
|
1727
|
+
const items = data.workspaces || [];
|
|
1728
|
+
if (list) {
|
|
1729
|
+
list.innerHTML = '';
|
|
1730
|
+
if (items.length === 0) {
|
|
1731
|
+
list.innerHTML = '<div style="padding: 12px; color: var(--term-text-muted);">No recent workspaces recorded.</div>';
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
items.forEach(w => {
|
|
1735
|
+
const row = document.createElement('div');
|
|
1736
|
+
row.className = 'cct-card-row';
|
|
1737
|
+
const existsTag = w.exists ? '<span style="color: var(--color-green); font-size: 10px;">✓ Exists</span>' : '<span style="color: var(--color-red); font-size: 10px;">⚠ Missing</span>';
|
|
1738
|
+
const pinIcon = w.pinned ? '📌' : '📍';
|
|
1739
|
+
row.innerHTML = `
|
|
1740
|
+
<div class="cct-card-row-info">
|
|
1741
|
+
<div class="cct-card-row-title">
|
|
1742
|
+
<span>📁 ${w.name}</span>
|
|
1743
|
+
${existsTag}
|
|
1744
|
+
${w.pinned ? '<span style="font-size: 11px;">📌</span>' : ''}
|
|
1745
|
+
</div>
|
|
1746
|
+
<div class="cct-card-row-sub">${w.path}</div>
|
|
1747
|
+
</div>
|
|
1748
|
+
<div class="cct-card-row-actions">
|
|
1749
|
+
<button class="cct-icon-btn" onclick="FATTY.togglePinRecent('${w.path.replace(/\\/g, '\\\\')}', ${!w.pinned})" title="${w.pinned ? 'Unpin' : 'Pin to top'}">${pinIcon}</button>
|
|
1750
|
+
<button class="cct-icon-btn" onclick="FATTY.removeRecentWorkspace('${w.path.replace(/\\/g, '\\\\')}')" title="Remove from list">✕</button>
|
|
1751
|
+
<button class="cct-modal-btn primary" style="padding: 3px 8px; font-size: 11px;" onclick="FATTY.openDirectWorkspace('${w.path.replace(/\\/g, '\\\\')}')">Open</button>
|
|
1752
|
+
</div>
|
|
1753
|
+
`;
|
|
1754
|
+
list.appendChild(row);
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
} catch (err) {
|
|
1758
|
+
if (list) list.innerHTML = `<div style="padding: 10px; color: var(--color-red);">${err.message}</div>`;
|
|
1759
|
+
}
|
|
1760
|
+
},
|
|
1761
|
+
|
|
1762
|
+
async togglePinRecent(path, pin) {
|
|
1763
|
+
await api.post('/api/workspace/recent/pin', { path, pinned: pin });
|
|
1764
|
+
this.openRecentWorkspacesModal();
|
|
1765
|
+
},
|
|
1766
|
+
|
|
1767
|
+
async removeRecentWorkspace(path) {
|
|
1768
|
+
await api.post('/api/workspace/recent/remove', { path });
|
|
1769
|
+
this.openRecentWorkspacesModal();
|
|
1770
|
+
},
|
|
1771
|
+
|
|
1772
|
+
async clearRecentWorkspaces() {
|
|
1773
|
+
await api.post('/api/workspace/recent/clear');
|
|
1774
|
+
this.openRecentWorkspacesModal();
|
|
1775
|
+
},
|
|
1776
|
+
|
|
1777
|
+
async openDirectWorkspace(path) {
|
|
1778
|
+
this.closeAllModals();
|
|
1779
|
+
await api.post('/api/workspace/open', { path });
|
|
1780
|
+
state.workspaceRoot = path;
|
|
1781
|
+
state.workspaceName = path.split(/[\\/]/).filter(Boolean).pop() || path;
|
|
1782
|
+
await this.loadWorkspace(path);
|
|
1783
|
+
},
|
|
1784
|
+
|
|
1785
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1786
|
+
// SAVED CHATS MODAL (chat_store.py)
|
|
1787
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1788
|
+
async openChatsModal() {
|
|
1789
|
+
const modal = document.getElementById('cct-chats-modal');
|
|
1790
|
+
const list = document.getElementById('cct-chats-modal-list');
|
|
1791
|
+
if (list) list.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">Loading chat sessions...</div>';
|
|
1792
|
+
if (modal) modal.style.display = 'flex';
|
|
1793
|
+
|
|
1794
|
+
try {
|
|
1795
|
+
const data = await api.get('/api/chats');
|
|
1796
|
+
const chats = data.chats || [];
|
|
1797
|
+
if (list) {
|
|
1798
|
+
list.innerHTML = '';
|
|
1799
|
+
if (chats.length === 0) {
|
|
1800
|
+
list.innerHTML = '<div style="padding: 12px; color: var(--term-text-muted);">No saved chats yet. Start typing to create one!</div>';
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
chats.forEach(c => {
|
|
1804
|
+
const row = document.createElement('div');
|
|
1805
|
+
row.className = 'cct-card-row';
|
|
1806
|
+
const isActive = state.chatId === c.id;
|
|
1807
|
+
row.innerHTML = `
|
|
1808
|
+
<div class="cct-card-row-info">
|
|
1809
|
+
<div class="cct-card-row-title">
|
|
1810
|
+
<span>💬 ${c.name || 'Untitled Chat'}</span>
|
|
1811
|
+
${isActive ? '<span style="color: var(--color-blue); font-size: 10px;">● ACTIVE</span>' : ''}
|
|
1812
|
+
</div>
|
|
1813
|
+
<div class="cct-card-row-sub">${c.message_count || 0} messages · Updated ${c.updated_at ? new Date(c.updated_at * 1000).toLocaleTimeString() : 'Recently'}</div>
|
|
1814
|
+
</div>
|
|
1815
|
+
<div class="cct-card-row-actions">
|
|
1816
|
+
<button class="cct-icon-btn" onclick="FATTY.renameChatSession('${c.id}', '${(c.name || '').replace(/'/g, "\\'")}')">Rename</button>
|
|
1817
|
+
<button class="cct-icon-btn" onclick="FATTY.deleteChatSession('${c.id}')" title="Delete">✕</button>
|
|
1818
|
+
<button class="cct-modal-btn primary" style="padding: 3px 8px; font-size: 11px;" onclick="FATTY.switchChatSession('${c.id}')">Switch</button>
|
|
1819
|
+
</div>
|
|
1820
|
+
`;
|
|
1821
|
+
list.appendChild(row);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
} catch (err) {
|
|
1825
|
+
if (list) list.innerHTML = `<div style="padding: 10px; color: var(--color-red);">${err.message}</div>`;
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
|
|
1829
|
+
async createNewChatSession() {
|
|
1830
|
+
const name = prompt('New chat title:', 'Chat ' + new Date().toLocaleTimeString());
|
|
1831
|
+
if (!name) return;
|
|
1832
|
+
try {
|
|
1833
|
+
const res = await api.post('/api/chats', { name, project_path: state.workspaceRoot });
|
|
1834
|
+
if (res && res.id) {
|
|
1835
|
+
this.switchChatSession(res.id);
|
|
1836
|
+
}
|
|
1837
|
+
} catch (e) {
|
|
1838
|
+
alert('Could not create chat: ' + e.message);
|
|
1839
|
+
}
|
|
1840
|
+
},
|
|
1841
|
+
|
|
1842
|
+
async renameChatSession(id, oldName) {
|
|
1843
|
+
const name = prompt('Rename chat:', oldName);
|
|
1844
|
+
if (!name || name === oldName) return;
|
|
1845
|
+
await api.post(`/api/chats/${id}/rename`, { name });
|
|
1846
|
+
this.openChatsModal();
|
|
1847
|
+
},
|
|
1848
|
+
|
|
1849
|
+
async deleteChatSession(id) {
|
|
1850
|
+
if (!confirm('Are you sure you want to delete this chat session?')) return;
|
|
1851
|
+
await api.del(`/api/chats/${id}`);
|
|
1852
|
+
if (state.chatId === id) {
|
|
1853
|
+
state.chatId = null;
|
|
1854
|
+
this.newChat();
|
|
1855
|
+
}
|
|
1856
|
+
this.openChatsModal();
|
|
1857
|
+
},
|
|
1858
|
+
|
|
1859
|
+
async switchChatSession(id) {
|
|
1860
|
+
try {
|
|
1861
|
+
const chat = await api.get(`/api/chats/${id}`);
|
|
1862
|
+
state.chatId = id;
|
|
1863
|
+
this.closeAllModals();
|
|
1864
|
+
|
|
1865
|
+
// Switch main screen to chat turns
|
|
1866
|
+
const welcome = document.getElementById('cct-welcome-view');
|
|
1867
|
+
const turnsBox = document.getElementById('cct-chat-turns');
|
|
1868
|
+
if (welcome) welcome.style.display = 'none';
|
|
1869
|
+
if (turnsBox) {
|
|
1870
|
+
turnsBox.style.display = 'flex';
|
|
1871
|
+
turnsBox.innerHTML = '';
|
|
1872
|
+
const messages = chat.messages || [];
|
|
1873
|
+
messages.forEach(m => {
|
|
1874
|
+
if (m.role === 'user') {
|
|
1875
|
+
this.appendUserMessage(m.content);
|
|
1876
|
+
} else if (m.role === 'assistant') {
|
|
1877
|
+
const card = this.appendAssistantLoading();
|
|
1878
|
+
this.updateAssistantCard(card, {
|
|
1879
|
+
text: m.content,
|
|
1880
|
+
model: state.model,
|
|
1881
|
+
mode: state.activeMode,
|
|
1882
|
+
time: '0:01',
|
|
1883
|
+
tools_used: []
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
alert('Could not load chat: ' + err.message);
|
|
1890
|
+
}
|
|
1891
|
+
},
|
|
1892
|
+
|
|
1893
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1894
|
+
// MCP SERVERS MODAL (mcp.py)
|
|
1895
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1896
|
+
async openMcpModal() {
|
|
1897
|
+
const modal = document.getElementById('cct-mcp-modal');
|
|
1898
|
+
const list = document.getElementById('cct-mcp-servers-list');
|
|
1899
|
+
if (list) list.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">Loading MCP servers...</div>';
|
|
1900
|
+
if (modal) modal.style.display = 'flex';
|
|
1901
|
+
|
|
1902
|
+
try {
|
|
1903
|
+
const data = await api.get('/api/mcp/servers');
|
|
1904
|
+
const servers = data.servers || [];
|
|
1905
|
+
if (list) {
|
|
1906
|
+
list.innerHTML = '';
|
|
1907
|
+
if (servers.length === 0) {
|
|
1908
|
+
list.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted); font-size: 11.5px;">No MCP servers configured yet. Add one below!</div>';
|
|
1909
|
+
} else {
|
|
1910
|
+
servers.forEach(s => {
|
|
1911
|
+
const row = document.createElement('div');
|
|
1912
|
+
row.className = 'cct-card-row';
|
|
1913
|
+
const isConn = s.connected;
|
|
1914
|
+
row.innerHTML = `
|
|
1915
|
+
<div class="cct-card-row-info">
|
|
1916
|
+
<div class="cct-card-row-title">
|
|
1917
|
+
<span style="color: ${isConn ? 'var(--color-green)' : 'var(--term-text-faint)'};">●</span>
|
|
1918
|
+
<span>${s.name || s.id}</span>
|
|
1919
|
+
<span style="font-size: 10px; color: var(--term-text-muted);">(${s.command || 'custom'})</span>
|
|
1920
|
+
</div>
|
|
1921
|
+
<div class="cct-card-row-sub">${(s.args || []).join(' ')}</div>
|
|
1922
|
+
</div>
|
|
1923
|
+
<div class="cct-card-row-actions">
|
|
1924
|
+
<button class="cct-icon-btn" onclick="FATTY.toggleMcpServer('${s.id}', ${!isConn})">${isConn ? 'Disconnect' : 'Connect'}</button>
|
|
1925
|
+
<button class="cct-icon-btn" onclick="FATTY.removeMcpServer('${s.id}')">✕</button>
|
|
1926
|
+
</div>
|
|
1927
|
+
`;
|
|
1928
|
+
list.appendChild(row);
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
} catch (e) {
|
|
1933
|
+
if (list) list.innerHTML = `<div style="padding: 10px; color: var(--color-red);">${e.message}</div>`;
|
|
1934
|
+
}
|
|
1935
|
+
},
|
|
1936
|
+
|
|
1937
|
+
async toggleMcpServer(id, connect) {
|
|
1938
|
+
try {
|
|
1939
|
+
if (connect) {
|
|
1940
|
+
await api.post(`/api/mcp/servers/${id}/connect`);
|
|
1941
|
+
} else {
|
|
1942
|
+
await api.post(`/api/mcp/servers/${id}/disconnect`);
|
|
1943
|
+
}
|
|
1944
|
+
} catch (e) {}
|
|
1945
|
+
this.openMcpModal();
|
|
1946
|
+
},
|
|
1947
|
+
|
|
1948
|
+
async removeMcpServer(id) {
|
|
1949
|
+
await api.del(`/api/mcp/servers/${id}`);
|
|
1950
|
+
this.openMcpModal();
|
|
1951
|
+
},
|
|
1952
|
+
|
|
1953
|
+
async saveMcpServer() {
|
|
1954
|
+
const name = document.getElementById('cct-mcp-name').value.trim();
|
|
1955
|
+
const cmd = document.getElementById('cct-mcp-cmd').value.trim();
|
|
1956
|
+
const argsRaw = document.getElementById('cct-mcp-args').value.trim();
|
|
1957
|
+
if (!name || !cmd) {
|
|
1958
|
+
alert('Server name and command are required.');
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
const args = argsRaw ? argsRaw.split(/\s+/) : [];
|
|
1962
|
+
await api.post('/api/mcp/servers', { id: name.toLowerCase(), name, command: cmd, args });
|
|
1963
|
+
document.getElementById('cct-mcp-name').value = '';
|
|
1964
|
+
document.getElementById('cct-mcp-cmd').value = '';
|
|
1965
|
+
document.getElementById('cct-mcp-args').value = '';
|
|
1966
|
+
this.openMcpModal();
|
|
1967
|
+
},
|
|
1968
|
+
|
|
1969
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1970
|
+
// BACKUP PROVIDERS MODAL (Automatic Failover Chain)
|
|
1971
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1972
|
+
backupChain: [],
|
|
1973
|
+
async openBackupProvidersModal() {
|
|
1974
|
+
const modal = document.getElementById('cct-backup-modal');
|
|
1975
|
+
const list = document.getElementById('cct-backup-chain-list');
|
|
1976
|
+
if (list) list.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">Loading failover chain...</div>';
|
|
1977
|
+
if (modal) modal.style.display = 'flex';
|
|
1978
|
+
|
|
1979
|
+
try {
|
|
1980
|
+
const data = await api.get('/api/backup-providers');
|
|
1981
|
+
this.backupChain = data.providers || [];
|
|
1982
|
+
this.renderBackupChain();
|
|
1983
|
+
} catch (e) {
|
|
1984
|
+
if (list) list.innerHTML = `<div style="padding: 10px; color: var(--color-red);">${e.message}</div>`;
|
|
1985
|
+
}
|
|
1986
|
+
},
|
|
1987
|
+
|
|
1988
|
+
renderBackupChain() {
|
|
1989
|
+
const list = document.getElementById('cct-backup-chain-list');
|
|
1990
|
+
if (!list) return;
|
|
1991
|
+
list.innerHTML = '';
|
|
1992
|
+
if (this.backupChain.length === 0) {
|
|
1993
|
+
list.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">No backup providers configured.</div>';
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
this.backupChain.forEach((p, idx) => {
|
|
1997
|
+
const row = document.createElement('div');
|
|
1998
|
+
row.className = 'cct-card-row';
|
|
1999
|
+
row.innerHTML = `
|
|
2000
|
+
<div class="cct-card-row-info">
|
|
2001
|
+
<div class="cct-card-row-title">
|
|
2002
|
+
<span style="color: var(--color-cyan); font-weight: bold;">#${idx + 1}</span>
|
|
2003
|
+
<span>📡 ${p.toUpperCase()}</span>
|
|
2004
|
+
</div>
|
|
2005
|
+
</div>
|
|
2006
|
+
<div class="cct-card-row-actions">
|
|
2007
|
+
<button class="cct-icon-btn" onclick="FATTY.moveBackupChain(${idx}, -1)" ${idx === 0 ? 'disabled' : ''}>▲</button>
|
|
2008
|
+
<button class="cct-icon-btn" onclick="FATTY.moveBackupChain(${idx}, 1)" ${idx === this.backupChain.length - 1 ? 'disabled' : ''}>▼</button>
|
|
2009
|
+
<button class="cct-icon-btn" onclick="FATTY.testBackupProvider('${p}')">Test</button>
|
|
2010
|
+
<button class="cct-icon-btn" onclick="FATTY.removeBackupFromChain(${idx})">✕</button>
|
|
2011
|
+
</div>
|
|
2012
|
+
`;
|
|
2013
|
+
list.appendChild(row);
|
|
2014
|
+
});
|
|
2015
|
+
},
|
|
2016
|
+
|
|
2017
|
+
moveBackupChain(index, delta) {
|
|
2018
|
+
const target = index + delta;
|
|
2019
|
+
if (target < 0 || target >= this.backupChain.length) return;
|
|
2020
|
+
const temp = this.backupChain[index];
|
|
2021
|
+
this.backupChain[index] = this.backupChain[target];
|
|
2022
|
+
this.backupChain[target] = temp;
|
|
2023
|
+
this.renderBackupChain();
|
|
2024
|
+
},
|
|
2025
|
+
|
|
2026
|
+
removeBackupFromChain(index) {
|
|
2027
|
+
this.backupChain.splice(index, 1);
|
|
2028
|
+
this.renderBackupChain();
|
|
2029
|
+
},
|
|
2030
|
+
|
|
2031
|
+
async testBackupProvider(name) {
|
|
2032
|
+
try {
|
|
2033
|
+
const res = await api.post('/api/verify', { provider: name });
|
|
2034
|
+
alert(`Provider [${name}] connectivity: ${res.ok ? 'SUCCESS' : 'FAILED'}\n${res.message || ''}`);
|
|
2035
|
+
} catch (e) {
|
|
2036
|
+
alert(`Test error for [${name}]: ${e.message}`);
|
|
2037
|
+
}
|
|
2038
|
+
},
|
|
2039
|
+
|
|
2040
|
+
async saveBackupChain() {
|
|
2041
|
+
try {
|
|
2042
|
+
await api.post('/api/backup-providers', { providers: this.backupChain });
|
|
2043
|
+
alert('Backup failover chain saved successfully.');
|
|
2044
|
+
this.closeAllModals();
|
|
2045
|
+
} catch (e) {
|
|
2046
|
+
alert('Could not save chain: ' + e.message);
|
|
2047
|
+
}
|
|
2048
|
+
},
|
|
2049
|
+
|
|
2050
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2051
|
+
// THEMES MODAL & LIVE CSS INJECTION (theme.py + theme_css.py)
|
|
2052
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2053
|
+
async openThemesModal() {
|
|
2054
|
+
const modal = document.getElementById('cct-themes-modal');
|
|
2055
|
+
const grid = document.getElementById('cct-themes-grid');
|
|
2056
|
+
if (grid) grid.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">Loading themes...</div>';
|
|
2057
|
+
if (modal) modal.style.display = 'flex';
|
|
2058
|
+
|
|
2059
|
+
try {
|
|
2060
|
+
const data = await api.get('/api/themes');
|
|
2061
|
+
const themes = data.themes || [];
|
|
2062
|
+
if (grid) {
|
|
2063
|
+
grid.innerHTML = '';
|
|
2064
|
+
themes.forEach(th => {
|
|
2065
|
+
const card = document.createElement('div');
|
|
2066
|
+
card.className = `cct-theme-card ${th.active ? 'active' : ''}`;
|
|
2067
|
+
card.innerHTML = `
|
|
2068
|
+
<div class="cct-theme-name">${th.label || th.name}</div>
|
|
2069
|
+
<div class="cct-theme-badge">${th.is_light ? '☀ Light Theme' : '🌙 Dark Theme'}</div>
|
|
2070
|
+
${th.active ? '<span style="font-size: 10px; color: var(--color-blue); font-weight: bold;">● Active Theme</span>' : ''}
|
|
2071
|
+
`;
|
|
2072
|
+
card.addEventListener('click', () => {
|
|
2073
|
+
this.applyTheme(th.name);
|
|
2074
|
+
});
|
|
2075
|
+
grid.appendChild(card);
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
} catch (e) {
|
|
2079
|
+
if (grid) grid.innerHTML = `<div style="padding: 10px; color: var(--color-red);">${e.message}</div>`;
|
|
2080
|
+
}
|
|
2081
|
+
},
|
|
2082
|
+
|
|
2083
|
+
async applyTheme(themeName) {
|
|
2084
|
+
try {
|
|
2085
|
+
const res = await api.post('/api/themes/select', { theme: themeName });
|
|
2086
|
+
if (res && res.css_variables) {
|
|
2087
|
+
for (const [key, val] of Object.entries(res.css_variables)) {
|
|
2088
|
+
document.documentElement.style.setProperty(`--${key}`, val);
|
|
2089
|
+
if (key === 'background') document.documentElement.style.setProperty('--term-bg', val);
|
|
2090
|
+
if (key === 'surface') document.documentElement.style.setProperty('--term-surface', val);
|
|
2091
|
+
if (key === 'primary') document.documentElement.style.setProperty('--color-blue', val);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (res) {
|
|
2095
|
+
if (res.is_light) {
|
|
2096
|
+
document.documentElement.classList.add('cct-light-theme');
|
|
2097
|
+
} else {
|
|
2098
|
+
document.documentElement.classList.remove('cct-light-theme');
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
this.openThemesModal();
|
|
2102
|
+
} catch (e) {
|
|
2103
|
+
console.warn('Theme apply error:', e);
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
|
|
2107
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2108
|
+
// USER PROFILE & IDENTITY MODAL (fomoji_auth.py + identity.py)
|
|
2109
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2110
|
+
async openUserModal() {
|
|
2111
|
+
const modal = document.getElementById('cct-user-modal');
|
|
2112
|
+
const content = document.getElementById('cct-user-modal-content');
|
|
2113
|
+
if (content) content.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">Loading user profile...</div>';
|
|
2114
|
+
if (modal) modal.style.display = 'flex';
|
|
2115
|
+
|
|
2116
|
+
try {
|
|
2117
|
+
const prof = await api.get('/api/user/profile');
|
|
2118
|
+
if (content) {
|
|
2119
|
+
content.innerHTML = `
|
|
2120
|
+
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 14px;">
|
|
2121
|
+
<div style="font-size: 32px;">🐱</div>
|
|
2122
|
+
<div>
|
|
2123
|
+
<div style="font-size: 15px; font-weight: bold; color: var(--term-text);">${prof.user.name}</div>
|
|
2124
|
+
<div style="font-size: 11.5px; color: var(--term-text-muted);">${prof.user.email} · Role: ${prof.user.role}</div>
|
|
2125
|
+
<div style="font-size: 11px; color: ${prof.authenticated ? 'var(--color-green)' : 'var(--color-amber)'}; margin-top: 2px;">
|
|
2126
|
+
● ${prof.authenticated ? 'Authenticated via Fomoji' : 'Local Guest Mode'}
|
|
2127
|
+
</div>
|
|
2128
|
+
</div>
|
|
2129
|
+
</div>
|
|
2130
|
+
|
|
2131
|
+
<div class="cct-profile-grid">
|
|
2132
|
+
<div class="cct-profile-metric">
|
|
2133
|
+
<div class="cct-profile-metric-title">Active AI Model</div>
|
|
2134
|
+
<div class="cct-profile-metric-val" style="font-size: 12px;">${prof.active_provider.toUpperCase()} / ${prof.active_model}</div>
|
|
2135
|
+
</div>
|
|
2136
|
+
<div class="cct-profile-metric">
|
|
2137
|
+
<div class="cct-profile-metric-title">App Version</div>
|
|
2138
|
+
<div class="cct-profile-metric-val" style="font-size: 12px;">${prof.app.short_name} v${prof.app.version}</div>
|
|
2139
|
+
</div>
|
|
2140
|
+
<div class="cct-profile-metric">
|
|
2141
|
+
<div class="cct-profile-metric-title">Durable Memory Facts</div>
|
|
2142
|
+
<div class="cct-profile-metric-val">${prof.memory_facts_count}</div>
|
|
2143
|
+
</div>
|
|
2144
|
+
<div class="cct-profile-metric">
|
|
2145
|
+
<div class="cct-profile-metric-title">Saved Sessions</div>
|
|
2146
|
+
<div class="cct-profile-metric-val">${prof.saved_chats_count}</div>
|
|
2147
|
+
</div>
|
|
2148
|
+
</div>
|
|
2149
|
+
|
|
2150
|
+
<div style="margin-top: 14px; font-size: 11px; color: var(--term-text-faint); line-height: 1.4;">
|
|
2151
|
+
${prof.app.tagline}<br>
|
|
2152
|
+
Creator: ${prof.app.creator} · Tech stack: ${prof.app.tech_stack}
|
|
2153
|
+
</div>
|
|
2154
|
+
`;
|
|
2155
|
+
}
|
|
2156
|
+
} catch (e) {
|
|
2157
|
+
if (content) content.innerHTML = `<div style="padding: 10px; color: var(--color-red);">${e.message}</div>`;
|
|
2158
|
+
}
|
|
2159
|
+
},
|
|
2160
|
+
|
|
2161
|
+
openSignOutModal() {
|
|
2162
|
+
const modal = document.getElementById('cct-signout-modal');
|
|
2163
|
+
if (modal) modal.style.display = 'flex';
|
|
2164
|
+
},
|
|
2165
|
+
|
|
2166
|
+
async confirmSignOut() {
|
|
2167
|
+
try {
|
|
2168
|
+
await api.post('/api/auth/logout');
|
|
2169
|
+
alert('Signed out successfully.');
|
|
2170
|
+
} catch (e) {}
|
|
2171
|
+
this.closeAllModals();
|
|
2172
|
+
await this.loadConfig();
|
|
2173
|
+
},
|
|
2174
|
+
|
|
2175
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2176
|
+
// GESTURES MODAL
|
|
2177
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2178
|
+
async openGesturesModal() {
|
|
2179
|
+
const modal = document.getElementById('cct-gestures-modal');
|
|
2180
|
+
const list = document.getElementById('cct-gestures-list');
|
|
2181
|
+
if (list) list.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">Loading gestures...</div>';
|
|
2182
|
+
if (modal) modal.style.display = 'flex';
|
|
2183
|
+
|
|
2184
|
+
try {
|
|
2185
|
+
const data = await api.get('/api/gestures');
|
|
2186
|
+
const gestures = data.gestures || [];
|
|
2187
|
+
if (list) {
|
|
2188
|
+
list.innerHTML = '';
|
|
2189
|
+
if (gestures.length === 0) {
|
|
2190
|
+
list.innerHTML = '<div style="padding: 10px; color: var(--term-text-muted);">No gestures registered.</div>';
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
gestures.forEach(g => {
|
|
2194
|
+
const row = document.createElement('div');
|
|
2195
|
+
row.className = 'cct-card-row';
|
|
2196
|
+
row.innerHTML = `
|
|
2197
|
+
<div class="cct-card-row-info">
|
|
2198
|
+
<div class="cct-card-row-title">
|
|
2199
|
+
<span>✋ ${g.name || g.id}</span>
|
|
2200
|
+
</div>
|
|
2201
|
+
<div class="cct-card-row-sub">${g.description || 'Smart terminal gesture'}</div>
|
|
2202
|
+
</div>
|
|
2203
|
+
<div class="cct-card-row-actions">
|
|
2204
|
+
<button class="cct-icon-btn ${g.enabled ? 'active' : ''}" onclick="FATTY.toggleGesture('${g.id}', ${!g.enabled})">
|
|
2205
|
+
${g.enabled ? 'Enabled' : 'Disabled'}
|
|
2206
|
+
</button>
|
|
2207
|
+
</div>
|
|
2208
|
+
`;
|
|
2209
|
+
list.appendChild(row);
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
} catch (e) {
|
|
2213
|
+
if (list) list.innerHTML = `<div style="padding: 10px; color: var(--color-red);">${e.message}</div>`;
|
|
2214
|
+
}
|
|
2215
|
+
},
|
|
2216
|
+
|
|
2217
|
+
async toggleGesture(id, enable) {
|
|
2218
|
+
await api.post('/api/gestures/toggle', { id, enabled: enable });
|
|
2219
|
+
this.openGesturesModal();
|
|
2220
|
+
},
|
|
2221
|
+
|
|
2222
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2223
|
+
// PERSONALIZE & AI CUSTOMIZATION MODAL
|
|
2224
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2225
|
+
personalizeData: null,
|
|
2226
|
+
renderPersonaCards(profiles, activeId) {
|
|
2227
|
+
const cardsBox = document.getElementById('cct-persona-cards');
|
|
2228
|
+
if (!cardsBox) return;
|
|
2229
|
+
cardsBox.innerHTML = (profiles || []).map(p => {
|
|
2230
|
+
const isActive = p.id === activeId;
|
|
2231
|
+
const icon = p.id.includes('builder') ? '🏗️' : p.id.includes('code') ? '⚡' : p.id.includes('sci') ? '🔬' : p.id.includes('review') ? '🛡️' : '💡';
|
|
2232
|
+
return `
|
|
2233
|
+
<div class="cct-theme-card ${isActive ? 'active' : ''}" style="padding: 10px; cursor: pointer; border-radius: 0px !important;" onclick="FATTY.selectPersonaCard('${p.id}')">
|
|
2234
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
2235
|
+
<span style="font-size: 18px;">${icon}</span>
|
|
2236
|
+
<div style="overflow: hidden;">
|
|
2237
|
+
<div style="font-weight: bold; font-size: 12.5px; color: ${isActive ? 'var(--accent-build)' : 'var(--term-text)'};">${p.name}</div>
|
|
2238
|
+
<div style="font-size: 10.5px; color: var(--term-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${p.description || ''}</div>
|
|
2239
|
+
</div>
|
|
2240
|
+
</div>
|
|
2241
|
+
</div>
|
|
2242
|
+
`;
|
|
2243
|
+
}).join('');
|
|
2244
|
+
},
|
|
2245
|
+
|
|
2246
|
+
selectPersonaCard(id) {
|
|
2247
|
+
const select = document.getElementById('cct-persona-select');
|
|
2248
|
+
if (select) select.value = id;
|
|
2249
|
+
if (this.personalizeData) {
|
|
2250
|
+
this.personalizeData.active_id = id;
|
|
2251
|
+
this.renderPersonaCards(this.personalizeData.profiles || [], id);
|
|
2252
|
+
}
|
|
2253
|
+
this.onPersonaSelected(id);
|
|
2254
|
+
},
|
|
2255
|
+
|
|
2256
|
+
async openPersonalizeModal() {
|
|
2257
|
+
const modal = document.getElementById('cct-personalize-modal');
|
|
2258
|
+
const select = document.getElementById('cct-persona-select');
|
|
2259
|
+
const promptInp = document.getElementById('cct-custom-prompt');
|
|
2260
|
+
const slider = document.getElementById('cct-temp-slider');
|
|
2261
|
+
const tempVal = document.getElementById('cct-temp-val');
|
|
2262
|
+
|
|
2263
|
+
if (modal) modal.style.display = 'flex';
|
|
2264
|
+
|
|
2265
|
+
// 1. If we have cached data, render immediately with 0ms lag
|
|
2266
|
+
if (this.personalizeData) {
|
|
2267
|
+
const data = this.personalizeData;
|
|
2268
|
+
const profiles = data.profiles || [];
|
|
2269
|
+
this.renderPersonaCards(profiles, data.active_id);
|
|
2270
|
+
if (select) {
|
|
2271
|
+
select.innerHTML = profiles.map(p => `
|
|
2272
|
+
<option value="${p.id}" ${p.id === data.active_id ? 'selected' : ''}>${p.name}</option>
|
|
2273
|
+
`).join('');
|
|
2274
|
+
select.value = data.active_id || 'default';
|
|
2275
|
+
}
|
|
2276
|
+
if (promptInp && !promptInp.value) promptInp.value = data.prompt_customization || '';
|
|
2277
|
+
if (slider) {
|
|
2278
|
+
slider.value = data.temperature !== undefined ? data.temperature : 0.7;
|
|
2279
|
+
if (tempVal) tempVal.textContent = parseFloat(slider.value).toFixed(2);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// 2. Fetch latest data in background and update smoothly
|
|
2284
|
+
try {
|
|
2285
|
+
const data = await api.get('/api/personalize');
|
|
2286
|
+
this.personalizeData = data;
|
|
2287
|
+
const profiles = data.profiles || [];
|
|
2288
|
+
this.renderPersonaCards(profiles, data.active_id);
|
|
2289
|
+
if (select) {
|
|
2290
|
+
select.innerHTML = profiles.map(p => `
|
|
2291
|
+
<option value="${p.id}" ${p.id === data.active_id ? 'selected' : ''}>${p.name}</option>
|
|
2292
|
+
`).join('');
|
|
2293
|
+
select.value = data.active_id || 'default';
|
|
2294
|
+
}
|
|
2295
|
+
if (promptInp) promptInp.value = data.prompt_customization || '';
|
|
2296
|
+
if (slider) {
|
|
2297
|
+
slider.value = data.temperature !== undefined ? data.temperature : 0.7;
|
|
2298
|
+
if (tempVal) tempVal.textContent = parseFloat(slider.value).toFixed(2);
|
|
2299
|
+
}
|
|
2300
|
+
} catch (e) {
|
|
2301
|
+
console.warn('Personalize load error:', e);
|
|
2302
|
+
}
|
|
2303
|
+
},
|
|
2304
|
+
|
|
2305
|
+
onPersonaSelected(id) {
|
|
2306
|
+
if (!this.personalizeData) return;
|
|
2307
|
+
const match = (this.personalizeData.profiles || []).find(p => p.id === id);
|
|
2308
|
+
if (match && match.prompt) {
|
|
2309
|
+
const promptInp = document.getElementById('cct-custom-prompt');
|
|
2310
|
+
if (promptInp) promptInp.value = match.prompt;
|
|
2311
|
+
}
|
|
2312
|
+
},
|
|
2313
|
+
|
|
2314
|
+
async savePersonalization() {
|
|
2315
|
+
const select = document.getElementById('cct-persona-select');
|
|
2316
|
+
const promptInp = document.getElementById('cct-custom-prompt');
|
|
2317
|
+
const slider = document.getElementById('cct-temp-slider');
|
|
2318
|
+
|
|
2319
|
+
const personaId = select ? select.value : 'default';
|
|
2320
|
+
const prompt = promptInp ? promptInp.value : '';
|
|
2321
|
+
const temp = slider ? parseFloat(slider.value) : 0.7;
|
|
2322
|
+
|
|
2323
|
+
try {
|
|
2324
|
+
await api.post('/api/personalize/active', { id: personaId });
|
|
2325
|
+
await api.post('/api/personalize/save', {
|
|
2326
|
+
id: personaId,
|
|
2327
|
+
prompt,
|
|
2328
|
+
temperature: temp
|
|
2329
|
+
});
|
|
2330
|
+
alert('Personalization settings saved.');
|
|
2331
|
+
this.closeAllModals();
|
|
2332
|
+
} catch (e) {
|
|
2333
|
+
alert('Save error: ' + e.message);
|
|
2334
|
+
}
|
|
2335
|
+
},
|
|
2336
|
+
|
|
2337
|
+
openDocs() {
|
|
2338
|
+
const modal = document.getElementById('cct-docs-modal');
|
|
2339
|
+
if (modal) modal.style.display = 'flex';
|
|
2340
|
+
},
|
|
2341
|
+
|
|
2342
|
+
triggerAttach() {
|
|
2343
|
+
const fileInput = document.getElementById('cct-file-input');
|
|
2344
|
+
if (fileInput) fileInput.click();
|
|
2345
|
+
},
|
|
2346
|
+
|
|
2347
|
+
handleFileAttach(e) {
|
|
2348
|
+
const file = e.target.files && e.target.files[0];
|
|
2349
|
+
if (!file) return;
|
|
2350
|
+
const input = document.getElementById('cct-message-input');
|
|
2351
|
+
if (input) {
|
|
2352
|
+
input.value += ` [Attached: ${file.name}]`;
|
|
2353
|
+
}
|
|
2354
|
+
},
|
|
2355
|
+
|
|
2356
|
+
toggleSidebar() {
|
|
2357
|
+
const sb = document.getElementById('cct-sidebar');
|
|
2358
|
+
if (!sb) return;
|
|
2359
|
+
sb.style.display = sb.style.display === 'none' ? 'flex' : 'none';
|
|
2360
|
+
},
|
|
2361
|
+
|
|
2362
|
+
closeAllModals(e) {
|
|
2363
|
+
if (e && e.target && !e.target.classList.contains('cct-modal-backdrop')) {
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
document.querySelectorAll('.cct-modal-backdrop').forEach(m => {
|
|
2367
|
+
m.style.display = 'none';
|
|
2368
|
+
});
|
|
2369
|
+
const menu = document.getElementById('cct-main-menu-popup');
|
|
2370
|
+
if (menu) menu.style.display = 'none';
|
|
2371
|
+
},
|
|
2372
|
+
|
|
2373
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2374
|
+
// UTILS
|
|
2375
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2376
|
+
escapeHtml(str) {
|
|
2377
|
+
return (str || '')
|
|
2378
|
+
.replace(/&/g, '&')
|
|
2379
|
+
.replace(/</g, '<')
|
|
2380
|
+
.replace(/>/g, '>');
|
|
2381
|
+
},
|
|
2382
|
+
|
|
2383
|
+
formatContent(str) {
|
|
2384
|
+
if (!str) return '';
|
|
2385
|
+
if (str.includes('```')) {
|
|
2386
|
+
const parts = str.split(/(```[\s\S]*?```)/g);
|
|
2387
|
+
return parts.map(p => {
|
|
2388
|
+
if (p.startsWith('```')) {
|
|
2389
|
+
const clean = p.replace(/^```[a-zA-Z0-9]*\n?/, '').replace(/```$/, '');
|
|
2390
|
+
return `<pre><code>${this.escapeHtml(clean)}</code></pre>`;
|
|
2391
|
+
}
|
|
2392
|
+
return `<p>${this.escapeHtml(p).replace(/\n/g, '<br>')}</p>`;
|
|
2393
|
+
}).join('');
|
|
2394
|
+
}
|
|
2395
|
+
return `<p>${this.escapeHtml(str).replace(/\n/g, '<br>')}</p>`;
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
|
|
2399
|
+
// Start app on DOMContentLoaded
|
|
2400
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2401
|
+
window.FATTY.init();
|
|
2402
|
+
});
|
|
2403
|
+
})();
|