opc-agent 3.0.1 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +404 -74
- package/README.zh-CN.md +82 -0
- package/dist/channels/dingtalk.d.ts +17 -0
- package/dist/channels/dingtalk.js +38 -0
- package/dist/channels/googlechat.d.ts +14 -0
- package/dist/channels/googlechat.js +37 -0
- package/dist/channels/imessage.d.ts +13 -0
- package/dist/channels/imessage.js +28 -0
- package/dist/channels/irc.d.ts +20 -0
- package/dist/channels/irc.js +71 -0
- package/dist/channels/line.d.ts +14 -0
- package/dist/channels/line.js +28 -0
- package/dist/channels/matrix.d.ts +15 -0
- package/dist/channels/matrix.js +28 -0
- package/dist/channels/mattermost.d.ts +18 -0
- package/dist/channels/mattermost.js +49 -0
- package/dist/channels/msteams.d.ts +14 -0
- package/dist/channels/msteams.js +28 -0
- package/dist/channels/nostr.d.ts +14 -0
- package/dist/channels/nostr.js +28 -0
- package/dist/channels/qq.d.ts +15 -0
- package/dist/channels/qq.js +28 -0
- package/dist/channels/signal.d.ts +14 -0
- package/dist/channels/signal.js +28 -0
- package/dist/channels/sms.d.ts +15 -0
- package/dist/channels/sms.js +28 -0
- package/dist/channels/twitch.d.ts +17 -0
- package/dist/channels/twitch.js +59 -0
- package/dist/channels/voice-call.d.ts +27 -0
- package/dist/channels/voice-call.js +82 -0
- package/dist/channels/whatsapp.d.ts +14 -0
- package/dist/channels/whatsapp.js +28 -0
- package/dist/cli/chat.d.ts +2 -0
- package/dist/cli/chat.js +134 -0
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.js +303 -0
- package/dist/cli.js +142 -6
- package/dist/core/api-server.d.ts +25 -0
- package/dist/core/api-server.js +286 -0
- package/dist/core/audio.d.ts +50 -0
- package/dist/core/audio.js +68 -0
- package/dist/core/context-discovery.d.ts +16 -0
- package/dist/core/context-discovery.js +107 -0
- package/dist/core/context-refs.d.ts +29 -0
- package/dist/core/context-refs.js +162 -0
- package/dist/core/gateway.d.ts +53 -0
- package/dist/core/gateway.js +80 -0
- package/dist/core/heartbeat.d.ts +19 -0
- package/dist/core/heartbeat.js +50 -0
- package/dist/core/hooks.d.ts +28 -0
- package/dist/core/hooks.js +82 -0
- package/dist/core/ide-bridge.d.ts +53 -0
- package/dist/core/ide-bridge.js +97 -0
- package/dist/core/node-network.d.ts +23 -0
- package/dist/core/node-network.js +77 -0
- package/dist/core/profiles.d.ts +27 -0
- package/dist/core/profiles.js +131 -0
- package/dist/core/sandbox.d.ts +25 -0
- package/dist/core/sandbox.js +84 -1
- package/dist/core/session-manager.d.ts +33 -0
- package/dist/core/session-manager.js +157 -0
- package/dist/core/vision.d.ts +45 -0
- package/dist/core/vision.js +177 -0
- package/dist/hub/brain-seed.d.ts +14 -0
- package/dist/hub/brain-seed.js +77 -0
- package/dist/hub/client.d.ts +25 -0
- package/dist/hub/client.js +44 -0
- package/dist/index.d.ts +66 -1
- package/dist/index.js +95 -3
- package/dist/memory/context-compressor.d.ts +43 -0
- package/dist/memory/context-compressor.js +167 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -1
- package/dist/memory/user-profiler.d.ts +50 -0
- package/dist/memory/user-profiler.js +201 -0
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +54 -1
- package/dist/scheduler/cron-engine.d.ts +41 -0
- package/dist/scheduler/cron-engine.js +200 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +7 -0
- package/dist/schema/oad.d.ts +12 -12
- package/dist/security/approvals.d.ts +53 -0
- package/dist/security/approvals.js +115 -0
- package/dist/security/elevated.d.ts +41 -0
- package/dist/security/elevated.js +89 -0
- package/dist/security/index.d.ts +6 -0
- package/dist/security/index.js +7 -1
- package/dist/security/secrets.d.ts +34 -0
- package/dist/security/secrets.js +115 -0
- package/dist/skills/builtin/index.d.ts +6 -0
- package/dist/skills/builtin/index.js +402 -0
- package/dist/skills/marketplace.d.ts +30 -0
- package/dist/skills/marketplace.js +142 -0
- package/dist/skills/types.d.ts +34 -0
- package/dist/skills/types.js +16 -0
- package/dist/studio/server.d.ts +25 -0
- package/dist/studio/server.js +780 -0
- package/dist/studio/templates-data.d.ts +21 -0
- package/dist/studio/templates-data.js +148 -0
- package/dist/studio-ui/index.html +2502 -1073
- package/dist/tools/builtin/browser.d.ts +47 -0
- package/dist/tools/builtin/browser.js +284 -0
- package/dist/tools/builtin/home-assistant.d.ts +12 -0
- package/dist/tools/builtin/home-assistant.js +126 -0
- package/dist/tools/builtin/index.d.ts +7 -1
- package/dist/tools/builtin/index.js +23 -2
- package/dist/tools/builtin/rl-tools.d.ts +13 -0
- package/dist/tools/builtin/rl-tools.js +228 -0
- package/dist/tools/builtin/vision.d.ts +6 -0
- package/dist/tools/builtin/vision.js +61 -0
- package/dist/tools/builtin/web-search.d.ts +9 -0
- package/dist/tools/builtin/web-search.js +150 -0
- package/dist/tools/document-processor.d.ts +39 -0
- package/dist/tools/document-processor.js +188 -0
- package/dist/tools/image-generator.d.ts +42 -0
- package/dist/tools/image-generator.js +136 -0
- package/dist/tools/web-scraper.d.ts +20 -0
- package/dist/tools/web-scraper.js +148 -0
- package/dist/tools/web-search.d.ts +51 -0
- package/dist/tools/web-search.js +152 -0
- package/install.ps1 +154 -0
- package/install.sh +164 -0
- package/package.json +63 -52
- package/src/channels/dingtalk.ts +46 -0
- package/src/channels/googlechat.ts +42 -0
- package/src/channels/imessage.ts +32 -0
- package/src/channels/irc.ts +82 -0
- package/src/channels/line.ts +33 -0
- package/src/channels/matrix.ts +34 -0
- package/src/channels/mattermost.ts +57 -0
- package/src/channels/msteams.ts +33 -0
- package/src/channels/nostr.ts +33 -0
- package/src/channels/qq.ts +34 -0
- package/src/channels/signal.ts +33 -0
- package/src/channels/sms.ts +34 -0
- package/src/channels/twitch.ts +65 -0
- package/src/channels/voice-call.ts +100 -0
- package/src/channels/whatsapp.ts +33 -0
- package/src/cli/chat.ts +99 -0
- package/src/cli/setup.ts +314 -0
- package/src/cli.ts +148 -6
- package/src/core/api-server.ts +277 -0
- package/src/core/audio.ts +98 -0
- package/src/core/context-discovery.ts +85 -0
- package/src/core/context-refs.ts +140 -0
- package/src/core/gateway.ts +106 -0
- package/src/core/heartbeat.ts +51 -0
- package/src/core/hooks.ts +105 -0
- package/src/core/ide-bridge.ts +133 -0
- package/src/core/node-network.ts +86 -0
- package/src/core/profiles.ts +122 -0
- package/src/core/sandbox.ts +100 -0
- package/src/core/session-manager.ts +137 -0
- package/src/core/vision.ts +180 -0
- package/src/hub/brain-seed.ts +54 -0
- package/src/hub/client.ts +60 -0
- package/src/index.ts +86 -1
- package/src/memory/context-compressor.ts +189 -0
- package/src/memory/index.ts +4 -0
- package/src/memory/user-profiler.ts +215 -0
- package/src/providers/index.ts +64 -1
- package/src/scheduler/cron-engine.ts +191 -0
- package/src/scheduler/index.ts +2 -0
- package/src/security/approvals.ts +143 -0
- package/src/security/elevated.ts +105 -0
- package/src/security/index.ts +6 -0
- package/src/security/secrets.ts +129 -0
- package/src/skills/builtin/index.ts +408 -0
- package/src/skills/marketplace.ts +113 -0
- package/src/skills/types.ts +42 -0
- package/src/studio/server.ts +1591 -791
- package/src/studio/templates-data.ts +178 -0
- package/src/studio-ui/index.html +2502 -1073
- package/src/tools/builtin/browser.ts +299 -0
- package/src/tools/builtin/home-assistant.ts +116 -0
- package/src/tools/builtin/index.ts +37 -28
- package/src/tools/builtin/rl-tools.ts +243 -0
- package/src/tools/builtin/vision.ts +64 -0
- package/src/tools/builtin/web-search.ts +126 -0
- package/src/tools/document-processor.ts +213 -0
- package/src/tools/image-generator.ts +150 -0
- package/src/tools/web-scraper.ts +179 -0
- package/src/tools/web-search.ts +180 -0
- package/tests/api-server.test.ts +148 -0
- package/tests/approvals.test.ts +89 -0
- package/tests/audio.test.ts +40 -0
- package/tests/browser.test.ts +179 -0
- package/tests/builtin-tools.test.ts +83 -83
- package/tests/channels-extra.test.ts +45 -0
- package/tests/context-compressor.test.ts +172 -0
- package/tests/context-refs.test.ts +121 -0
- package/tests/cron-engine.test.ts +101 -0
- package/tests/document-processor.test.ts +69 -0
- package/tests/e2e-nocode.test.ts +442 -0
- package/tests/elevated.test.ts +69 -0
- package/tests/gateway.test.ts +63 -71
- package/tests/home-assistant.test.ts +40 -0
- package/tests/hooks.test.ts +79 -0
- package/tests/ide-bridge.test.ts +38 -0
- package/tests/image-generator.test.ts +84 -0
- package/tests/node-network.test.ts +74 -0
- package/tests/profiles.test.ts +61 -0
- package/tests/rl-tools.test.ts +93 -0
- package/tests/sandbox-manager.test.ts +46 -0
- package/tests/secrets.test.ts +107 -0
- package/tests/settings-api.test.ts +148 -0
- package/tests/setup.test.ts +73 -0
- package/tests/studio.test.ts +402 -229
- package/tests/tools/builtin-extended.test.ts +138 -138
- package/tests/user-profiler.test.ts +169 -0
- package/tests/v090-features.test.ts +254 -0
- package/tests/vision.test.ts +61 -0
- package/tests/voice-call.test.ts +47 -0
- package/tests/voice-interaction.test.ts +38 -0
- package/tests/web-search.test.ts +155 -0
package/src/studio-ui/index.html
CHANGED
|
@@ -5,1274 +5,2703 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>OPC Studio</title>
|
|
7
7
|
<style>
|
|
8
|
-
/* === Global === */
|
|
9
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
9
|
:root {
|
|
11
|
-
--bg: #0a0a0a;
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--text: #e5e5e5;
|
|
16
|
-
--text-muted: #737373;
|
|
17
|
-
--accent: #3b82f6;
|
|
18
|
-
--accent-hover: #2563eb;
|
|
19
|
-
--green: #22c55e;
|
|
20
|
-
--red: #ef4444;
|
|
21
|
-
--yellow: #eab308;
|
|
22
|
-
--purple: #a855f7;
|
|
10
|
+
--bg: #0a0a0a; --bg-card: #141414; --bg-hover: #1a1a1a; --bg-input: #1a1a1a;
|
|
11
|
+
--border: #262626; --text: #e5e5e5; --text-muted: #737373; --text-dim: #525252;
|
|
12
|
+
--accent: #3b82f6; --accent-hover: #2563eb; --accent-light: rgba(59,130,246,0.1);
|
|
13
|
+
--green: #22c55e; --red: #ef4444; --yellow: #eab308; --purple: #a855f7;
|
|
23
14
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
24
15
|
--mono: 'SF Mono', 'Fira Code', monospace;
|
|
25
|
-
--radius:
|
|
16
|
+
--radius: 10px; --radius-lg: 16px;
|
|
26
17
|
}
|
|
27
18
|
body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
a { color: var(--accent); text-decoration: none; }
|
|
20
|
+
button { font-family: var(--font); cursor: pointer; border: none; }
|
|
21
|
+
input, select, textarea { font-family: var(--font); }
|
|
22
|
+
|
|
23
|
+
/* Layout */
|
|
30
24
|
.app { display: flex; min-height: 100vh; }
|
|
31
|
-
|
|
32
|
-
/* Sidebar */
|
|
33
25
|
.sidebar {
|
|
34
|
-
width:
|
|
35
|
-
padding: 16px; display: flex; flex-direction: column; position: fixed; height: 100vh;
|
|
26
|
+
width: 220px; background: var(--bg-card); border-right: 1px solid var(--border);
|
|
27
|
+
padding: 16px 12px; display: flex; flex-direction: column; position: fixed; height: 100vh; z-index: 100;
|
|
36
28
|
}
|
|
37
|
-
.sidebar-logo { font-size:
|
|
29
|
+
.sidebar-logo { font-size: 17px; font-weight: 700; padding: 10px 12px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
|
|
38
30
|
.sidebar-logo span { color: var(--accent); }
|
|
39
|
-
.sidebar-nav { flex: 1; }
|
|
40
31
|
.nav-item {
|
|
41
32
|
display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius);
|
|
42
33
|
cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-size: 14px; margin-bottom: 2px;
|
|
43
34
|
}
|
|
44
35
|
.nav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
45
|
-
.nav-item.active { background: var(--
|
|
46
|
-
.nav-item .icon { width:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
.nav-item.active { background: var(--accent-light); color: var(--accent); font-weight: 500; }
|
|
37
|
+
.nav-item .icon { width: 20px; text-align: center; font-size: 16px; }
|
|
38
|
+
.main { flex: 1; margin-left: 220px; min-height: 100vh; }
|
|
39
|
+
|
|
40
|
+
/* Mobile */
|
|
41
|
+
.mobile-header { display: none; background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 12px 16px; position: sticky; top: 0; z-index: 50; }
|
|
42
|
+
.mobile-header button { background: none; border: none; color: var(--text); font-size: 20px; }
|
|
43
|
+
@media (max-width: 768px) {
|
|
44
|
+
.sidebar { transform: translateX(-100%); transition: transform 0.2s; width: 260px; }
|
|
45
|
+
.sidebar.open { transform: translateX(0); }
|
|
46
|
+
.mobile-header { display: flex; align-items: center; justify-content: space-between; }
|
|
47
|
+
.main { margin-left: 0; }
|
|
48
|
+
.sidebar-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 99; }
|
|
49
|
+
.sidebar-overlay.show { display: block; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Page container */
|
|
53
|
+
.page { display: none; padding: 32px; max-width: 1200px; margin: 0 auto; }
|
|
54
|
+
.page.active { display: block; }
|
|
55
|
+
.page-title { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
|
|
56
|
+
.page-subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 24px; }
|
|
57
|
+
|
|
56
58
|
/* Cards */
|
|
57
|
-
.card {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
/* Stats */
|
|
65
|
-
.stat { text-align: center; padding: 16px; }
|
|
66
|
-
.stat-value { font-size: 32px; font-weight: 700; color: var(--accent); }
|
|
67
|
-
.stat-label { font-size: 12px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
68
|
-
|
|
69
|
-
/* Status badge */
|
|
70
|
-
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; }
|
|
71
|
-
.badge-green { background: rgba(34,197,94,0.1); color: var(--green); }
|
|
72
|
-
.badge-red { background: rgba(239,68,68,0.1); color: var(--red); }
|
|
73
|
-
.badge-yellow { background: rgba(234,179,8,0.1); color: var(--yellow); }
|
|
74
|
-
.badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
75
|
-
|
|
76
|
-
/* Table */
|
|
77
|
-
.table { width: 100%; border-collapse: collapse; }
|
|
78
|
-
.table th { text-align: left; padding: 10px 12px; font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
|
|
79
|
-
.table td { padding: 10px 12px; font-size: 14px; border-bottom: 1px solid var(--border); }
|
|
80
|
-
.table tr:hover { background: var(--bg-hover); }
|
|
81
|
-
|
|
82
|
-
/* Chat */
|
|
83
|
-
.chat-container { height: 500px; display: flex; flex-direction: column; }
|
|
84
|
-
.chat-messages { flex: 1; overflow-y: auto; padding: 16px; }
|
|
85
|
-
.chat-input-row { display: flex; gap: 8px; padding: 16px; border-top: 1px solid var(--border); }
|
|
86
|
-
.chat-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; color: var(--text); font-size: 14px; outline: none; }
|
|
87
|
-
.chat-input:focus { border-color: var(--accent); }
|
|
88
|
-
.chat-send { background: var(--accent); color: white; border: none; border-radius: var(--radius); padding: 10px 20px; font-weight: 500; cursor: pointer; }
|
|
89
|
-
.chat-send:hover { background: var(--accent-hover); }
|
|
90
|
-
.message { margin-bottom: 16px; }
|
|
91
|
-
.message-user { text-align: right; }
|
|
92
|
-
.message-user .bubble { background: var(--accent); color: white; display: inline-block; padding: 10px 14px; border-radius: 14px 14px 4px 14px; max-width: 70%; text-align: left; }
|
|
93
|
-
.message-agent .bubble { background: var(--bg-hover); display: inline-block; padding: 10px 14px; border-radius: 14px 14px 14px 4px; max-width: 70%; }
|
|
94
|
-
.message-label { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
|
|
95
|
-
|
|
96
|
-
/* Config editor */
|
|
97
|
-
.editor { width: 100%; min-height: 400px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; color: var(--text); font-family: var(--mono); font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
|
|
98
|
-
.editor:focus { border-color: var(--accent); }
|
|
99
|
-
|
|
100
|
-
/* Button */
|
|
101
|
-
.btn { padding: 8px 16px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); transition: all 0.15s; }
|
|
102
|
-
.btn:hover { background: var(--bg-hover); }
|
|
103
|
-
.btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
|
|
59
|
+
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; transition: border-color 0.15s; }
|
|
60
|
+
.card:hover { border-color: #333; }
|
|
61
|
+
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
|
|
62
|
+
|
|
63
|
+
/* Buttons */
|
|
64
|
+
.btn { padding: 10px 20px; border-radius: var(--radius); font-size: 14px; font-weight: 500; transition: all 0.15s; display: inline-flex; align-items: center; gap: 8px; }
|
|
65
|
+
.btn-primary { background: var(--accent); color: white; }
|
|
104
66
|
.btn-primary:hover { background: var(--accent-hover); }
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
.
|
|
116
|
-
.
|
|
117
|
-
.
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
/* Search */
|
|
121
|
-
.search-bar {
|
|
122
|
-
.search-input {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.
|
|
127
|
-
|
|
67
|
+
.btn-secondary { background: var(--bg-hover); color: var(--text); border: 1px solid var(--border); }
|
|
68
|
+
.btn-secondary:hover { border-color: #444; }
|
|
69
|
+
.btn-danger { background: rgba(239,68,68,0.1); color: var(--red); }
|
|
70
|
+
.btn-danger:hover { background: rgba(239,68,68,0.2); }
|
|
71
|
+
.btn-sm { padding: 6px 12px; font-size: 13px; }
|
|
72
|
+
.btn-lg { padding: 14px 28px; font-size: 16px; }
|
|
73
|
+
|
|
74
|
+
/* Form elements */
|
|
75
|
+
.input { width: 100%; padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; outline: none; transition: border-color 0.15s; }
|
|
76
|
+
.input:focus { border-color: var(--accent); }
|
|
77
|
+
.input::placeholder { color: var(--text-dim); }
|
|
78
|
+
.label { display: block; font-size: 13px; font-weight: 500; color: var(--text-muted); margin-bottom: 6px; }
|
|
79
|
+
.form-group { margin-bottom: 20px; }
|
|
80
|
+
select.input { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23737373' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px; }
|
|
81
|
+
|
|
82
|
+
/* Search bar */
|
|
83
|
+
.search-bar { position: relative; margin-bottom: 20px; }
|
|
84
|
+
.search-bar .input { padding-left: 40px; }
|
|
85
|
+
.search-bar::before { content: '🔍'; position: absolute; left: 14px; top: 50%; transform: translateY(-50%); font-size: 14px; }
|
|
86
|
+
|
|
87
|
+
/* Tags / Chips */
|
|
88
|
+
.chip { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; background: var(--bg-hover); color: var(--text-muted); border: 1px solid var(--border); cursor: pointer; transition: all 0.15s; }
|
|
89
|
+
.chip:hover, .chip.active { background: var(--accent-light); color: var(--accent); border-color: var(--accent); }
|
|
90
|
+
.chip-group { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
|
|
91
|
+
|
|
92
|
+
/* Template card */
|
|
93
|
+
.tpl-card { cursor: pointer; }
|
|
94
|
+
.tpl-card .tpl-icon { font-size: 32px; margin-bottom: 12px; }
|
|
95
|
+
.tpl-card .tpl-name { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
|
|
96
|
+
.tpl-card .tpl-desc { font-size: 13px; color: var(--text-muted); margin-bottom: 12px; line-height: 1.5; }
|
|
97
|
+
.tpl-card .tpl-tags { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
98
|
+
.tpl-card .tpl-tag { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: var(--bg-hover); color: var(--text-dim); }
|
|
99
|
+
|
|
100
|
+
/* Wizard */
|
|
101
|
+
.wizard { max-width: 640px; margin: 0 auto; }
|
|
102
|
+
.wizard-steps { display: flex; justify-content: center; gap: 8px; margin-bottom: 32px; }
|
|
103
|
+
.wizard-step { display: flex; align-items: center; gap: 8px; color: var(--text-dim); font-size: 13px; }
|
|
104
|
+
.wizard-step.active { color: var(--accent); }
|
|
105
|
+
.wizard-step.done { color: var(--green); }
|
|
106
|
+
.wizard-step .step-num { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; border: 2px solid var(--border); }
|
|
107
|
+
.wizard-step.active .step-num { border-color: var(--accent); background: var(--accent-light); }
|
|
108
|
+
.wizard-step.done .step-num { border-color: var(--green); background: rgba(34,197,94,0.1); }
|
|
109
|
+
.wizard-step .step-line { width: 40px; height: 2px; background: var(--border); }
|
|
110
|
+
.wizard-panel { display: none; }
|
|
111
|
+
.wizard-panel.active { display: block; }
|
|
112
|
+
|
|
113
|
+
/* Agent card on dashboard */
|
|
114
|
+
.agent-card { cursor: pointer; position: relative; }
|
|
115
|
+
.agent-card .agent-icon { font-size: 36px; margin-bottom: 12px; }
|
|
116
|
+
.agent-card .agent-name { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
|
|
117
|
+
.agent-card .agent-template { font-size: 12px; color: var(--text-dim); margin-bottom: 8px; }
|
|
118
|
+
.agent-card .agent-stats { display: flex; gap: 16px; font-size: 12px; color: var(--text-muted); }
|
|
119
|
+
.agent-card .agent-actions { position: absolute; top: 16px; right: 16px; display: flex; gap: 4px; opacity: 0; transition: opacity 0.15s; }
|
|
120
|
+
.agent-card:hover .agent-actions { opacity: 1; }
|
|
121
|
+
.agent-card .agent-actions button { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; font-size: 12px; color: var(--text-muted); }
|
|
122
|
+
.agent-card .agent-actions button:hover { color: var(--text); border-color: #444; }
|
|
123
|
+
|
|
124
|
+
/* Chat UI */
|
|
125
|
+
.chat-container { display: none; flex-direction: column; height: calc(100vh - 0px); }
|
|
126
|
+
.chat-container.active { display: flex; }
|
|
127
|
+
.chat-header { padding: 16px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; background: var(--bg-card); }
|
|
128
|
+
.chat-header .chat-icon { font-size: 28px; }
|
|
129
|
+
.chat-header .chat-name { font-size: 16px; font-weight: 600; }
|
|
130
|
+
.chat-header .chat-status { font-size: 12px; color: var(--text-muted); }
|
|
131
|
+
.chat-header .chat-back { background: none; border: none; color: var(--text-muted); font-size: 18px; cursor: pointer; margin-right: 4px; }
|
|
132
|
+
.chat-messages { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 16px; }
|
|
133
|
+
.msg { max-width: 75%; display: flex; flex-direction: column; gap: 4px; }
|
|
134
|
+
.msg.user { align-self: flex-end; }
|
|
135
|
+
.msg.assistant { align-self: flex-start; }
|
|
136
|
+
.msg-bubble { padding: 12px 16px; border-radius: 16px; font-size: 14px; line-height: 1.6; white-space: pre-wrap; }
|
|
137
|
+
.msg.user .msg-bubble { background: var(--accent); color: white; border-bottom-right-radius: 4px; }
|
|
138
|
+
.msg.assistant .msg-bubble { background: var(--bg-card); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
|
|
139
|
+
.msg-time { font-size: 11px; color: var(--text-dim); }
|
|
140
|
+
.msg.user .msg-time { text-align: right; }
|
|
141
|
+
.typing-indicator { display: none; align-self: flex-start; }
|
|
142
|
+
.typing-indicator.show { display: flex; }
|
|
143
|
+
.typing-indicator .dots { display: flex; gap: 4px; padding: 12px 16px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 16px; }
|
|
144
|
+
.typing-indicator .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim); animation: bounce 1.4s infinite ease-in-out; }
|
|
145
|
+
.typing-indicator .dot:nth-child(2) { animation-delay: 0.2s; }
|
|
146
|
+
.typing-indicator .dot:nth-child(3) { animation-delay: 0.4s; }
|
|
147
|
+
@keyframes bounce { 0%,80%,100% { transform: scale(0.6); } 40% { transform: scale(1); } }
|
|
148
|
+
.chat-input-bar { padding: 16px 24px; border-top: 1px solid var(--border); background: var(--bg-card); display: flex; gap: 12px; align-items: center; }
|
|
149
|
+
.chat-input-bar .input { flex: 1; border-radius: 24px; padding: 12px 20px; }
|
|
150
|
+
.chat-input-bar .btn { border-radius: 24px; padding: 12px 24px; }
|
|
151
|
+
|
|
152
|
+
/* Memory timeline */
|
|
153
|
+
.timeline { position: relative; padding-left: 24px; }
|
|
154
|
+
.timeline::before { content: ''; position: absolute; left: 8px; top: 0; bottom: 0; width: 2px; background: var(--border); }
|
|
155
|
+
.timeline-item { position: relative; margin-bottom: 24px; }
|
|
156
|
+
.timeline-item::before { content: ''; position: absolute; left: -20px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: var(--accent); border: 2px solid var(--bg); }
|
|
157
|
+
.timeline-date { font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }
|
|
158
|
+
.timeline-content { font-size: 14px; color: var(--text-muted); line-height: 1.5; }
|
|
159
|
+
|
|
160
|
+
/* Empty state */
|
|
161
|
+
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
|
|
162
|
+
.empty-state .empty-icon { font-size: 48px; margin-bottom: 16px; }
|
|
163
|
+
.empty-state .empty-title { font-size: 18px; font-weight: 600; color: var(--text); margin-bottom: 8px; }
|
|
164
|
+
.empty-state .empty-desc { font-size: 14px; margin-bottom: 24px; }
|
|
165
|
+
|
|
166
|
+
/* Confirm dialog */
|
|
167
|
+
.dialog-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 200; align-items: center; justify-content: center; }
|
|
168
|
+
.dialog-overlay.show { display: flex; }
|
|
169
|
+
.dialog { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 24px; max-width: 400px; width: 90%; }
|
|
170
|
+
.dialog-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
|
171
|
+
.dialog-desc { font-size: 14px; color: var(--text-muted); margin-bottom: 20px; }
|
|
172
|
+
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
173
|
+
|
|
174
|
+
/* Settings layout */
|
|
175
|
+
.settings-layout { display: flex; gap: 0; min-height: calc(100vh - 64px); }
|
|
176
|
+
.settings-nav { width: 200px; background: var(--bg-card); border-right: 1px solid var(--border); padding: 16px 8px; flex-shrink: 0; }
|
|
177
|
+
.settings-nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius); cursor: pointer; color: var(--text-muted); font-size: 14px; margin-bottom: 2px; transition: all 0.15s; }
|
|
178
|
+
.settings-nav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
179
|
+
.settings-nav-item.active { background: var(--accent-light); color: var(--accent); font-weight: 500; }
|
|
180
|
+
.settings-content { flex: 1; padding: 32px; max-width: 900px; }
|
|
181
|
+
.settings-panel { display: none; }
|
|
182
|
+
.settings-panel.active { display: block; }
|
|
183
|
+
@media (max-width: 768px) {
|
|
184
|
+
.settings-layout { flex-direction: column; }
|
|
185
|
+
.settings-nav { width: 100%; display: flex; overflow-x: auto; padding: 8px; gap: 4px; border-right: none; border-bottom: 1px solid var(--border); }
|
|
186
|
+
.settings-nav-item { white-space: nowrap; font-size: 13px; padding: 8px 12px; }
|
|
187
|
+
.settings-content { padding: 16px; }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Tabs */
|
|
191
|
+
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 1px solid var(--border); }
|
|
192
|
+
.tab { padding: 10px 20px; cursor: pointer; color: var(--text-muted); font-size: 14px; border-bottom: 2px solid transparent; transition: all 0.15s; }
|
|
193
|
+
.tab:hover { color: var(--text); }
|
|
194
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 500; }
|
|
195
|
+
.tab-panel { display: none; }
|
|
196
|
+
.tab-panel.active { display: block; }
|
|
197
|
+
|
|
198
|
+
/* Status dot */
|
|
199
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
200
|
+
.status-dot.green { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
201
|
+
.status-dot.red { background: var(--red); box-shadow: 0 0 6px var(--red); }
|
|
202
|
+
.status-dot.yellow { background: var(--yellow); box-shadow: 0 0 6px var(--yellow); }
|
|
203
|
+
|
|
204
|
+
/* Channel card */
|
|
205
|
+
.channel-card { display: flex; align-items: center; gap: 16px; cursor: pointer; }
|
|
206
|
+
.channel-card .ch-icon { font-size: 28px; }
|
|
207
|
+
.channel-card .ch-info { flex: 1; }
|
|
208
|
+
.channel-card .ch-name { font-size: 15px; font-weight: 600; }
|
|
209
|
+
.channel-card .ch-status { font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
|
|
210
|
+
|
|
211
|
+
/* Stat card */
|
|
212
|
+
.stat-card { text-align: center; }
|
|
213
|
+
.stat-value { font-size: 28px; font-weight: 700; color: var(--accent); }
|
|
214
|
+
.stat-label { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
|
|
215
|
+
|
|
216
|
+
/* Log viewer */
|
|
217
|
+
.log-viewer { background: #0d0d0d; border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-family: var(--mono); font-size: 12px; color: var(--text-muted); max-height: 400px; overflow-y: auto; white-space: pre-wrap; line-height: 1.6; }
|
|
218
|
+
|
|
219
|
+
/* Module iframe */
|
|
220
|
+
.module-frame-container { position: relative; border-radius: var(--radius-lg); overflow: hidden; border: 1px solid var(--border); }
|
|
221
|
+
.module-frame-container iframe { width: 100%; height: 600px; border: none; background: var(--bg); }
|
|
222
|
+
.module-frame-fallback { text-align: center; padding: 48px 24px; }
|
|
223
|
+
.module-frame-fallback .mf-icon { font-size: 48px; margin-bottom: 16px; }
|
|
224
|
+
|
|
225
|
+
/* Ollama tutorial */
|
|
226
|
+
.tutorial-steps { counter-reset: step; }
|
|
227
|
+
.tutorial-step { display: flex; gap: 16px; margin-bottom: 20px; align-items: flex-start; }
|
|
228
|
+
.tutorial-step::before { counter-increment: step; content: counter(step); width: 32px; height: 32px; border-radius: 50%; background: var(--accent); color: white; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; flex-shrink: 0; }
|
|
229
|
+
.tutorial-step-content { flex: 1; }
|
|
230
|
+
.tutorial-step-content h4 { font-size: 15px; margin-bottom: 4px; }
|
|
231
|
+
.tutorial-step-content p { font-size: 13px; color: var(--text-muted); line-height: 1.5; }
|
|
232
|
+
.tutorial-step-content code { background: var(--bg-hover); padding: 2px 8px; border-radius: 4px; font-family: var(--mono); font-size: 13px; }
|
|
233
|
+
|
|
234
|
+
/* Provider card */
|
|
235
|
+
.provider-card { cursor: pointer; transition: all 0.15s; }
|
|
236
|
+
.provider-card:hover { border-color: var(--accent); }
|
|
237
|
+
.provider-card.configured { border-color: var(--green); }
|
|
238
|
+
.provider-card .pv-header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
|
239
|
+
.provider-card .pv-name { font-size: 15px; font-weight: 600; }
|
|
240
|
+
.provider-card .pv-status { font-size: 12px; }
|
|
241
|
+
|
|
242
|
+
/* Bar chart */
|
|
243
|
+
.bar-chart { display: flex; align-items: flex-end; gap: 4px; height: 120px; padding-top: 8px; }
|
|
244
|
+
.bar-chart .bar { flex: 1; background: var(--accent); border-radius: 3px 3px 0 0; min-height: 4px; transition: height 0.3s; position: relative; }
|
|
245
|
+
.bar-chart .bar:hover { opacity: 0.8; }
|
|
246
|
+
.bar-chart .bar-label { position: absolute; bottom: -20px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-dim); white-space: nowrap; }
|
|
247
|
+
|
|
128
248
|
/* Scrollbar */
|
|
129
249
|
::-webkit-scrollbar { width: 6px; }
|
|
130
250
|
::-webkit-scrollbar-track { background: transparent; }
|
|
131
|
-
::-webkit-scrollbar-thumb { background:
|
|
132
|
-
|
|
133
|
-
/*
|
|
134
|
-
.
|
|
135
|
-
.
|
|
136
|
-
|
|
251
|
+
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
|
252
|
+
|
|
253
|
+
/* Settings layout */
|
|
254
|
+
.settings-layout { display: flex; gap: 24px; align-items: flex-start; }
|
|
255
|
+
.settings-subnav { width: 190px; flex-shrink: 0; }
|
|
256
|
+
.snav-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius); cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-size: 14px; margin-bottom: 2px; }
|
|
257
|
+
.snav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
258
|
+
.snav-item.active { background: var(--accent-light); color: var(--accent); font-weight: 500; }
|
|
259
|
+
.settings-content { flex: 1; min-width: 0; }
|
|
260
|
+
.settings-section { display: none; }
|
|
261
|
+
.settings-section.active { display: block; }
|
|
262
|
+
.tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
|
|
263
|
+
.tab { padding: 10px 20px; font-size: 14px; font-weight: 500; cursor: pointer; color: var(--text-muted); border-bottom: 2px solid transparent; margin-bottom: -1px; transition: all 0.15s; }
|
|
264
|
+
.tab:hover { color: var(--text); }
|
|
265
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
266
|
+
.tab-panel { display: none; }
|
|
267
|
+
.tab-panel.active { display: block; }
|
|
268
|
+
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; vertical-align: middle; flex-shrink: 0; }
|
|
269
|
+
.status-dot.green { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
270
|
+
.status-dot.red { background: var(--red); }
|
|
271
|
+
.status-dot.yellow { background: var(--yellow); animation: sdpulse 1.5s ease-in-out infinite; }
|
|
272
|
+
@keyframes sdpulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
273
|
+
.channel-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
|
|
274
|
+
.channel-card { cursor: pointer; text-align: center; padding: 20px 12px; }
|
|
275
|
+
.channel-card:hover { border-color: var(--accent); }
|
|
276
|
+
.channel-card.connected { border-color: var(--green); }
|
|
277
|
+
.log-viewer { background: #080808; border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; font-family: var(--mono); font-size: 12px; line-height: 1.7; overflow-y: auto; max-height: 280px; color: #86efac; white-space: pre-wrap; word-break: break-all; }
|
|
278
|
+
.bar-chart-wrap { display: flex; align-items: flex-end; gap: 6px; height: 80px; }
|
|
279
|
+
.bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; height: 100%; justify-content: flex-end; }
|
|
280
|
+
.bar-fill { width: 100%; background: var(--accent); border-radius: 3px 3px 0 0; min-height: 2px; transition: height 0.4s ease; }
|
|
281
|
+
.bar-lbl { font-size: 10px; color: var(--text-dim); }
|
|
282
|
+
.iframe-wrap { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
|
283
|
+
.iframe-wrap iframe { width: 100%; height: 580px; border: none; display: block; background: var(--bg); }
|
|
284
|
+
.help-text { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
|
|
285
|
+
@media (max-width: 768px) {
|
|
286
|
+
.settings-layout { flex-direction: column; }
|
|
287
|
+
.settings-subnav { width: 100%; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
288
|
+
.snav-item { padding: 8px 10px; font-size: 12px; flex-direction: column; gap: 4px; text-align: center; min-width: 70px; }
|
|
289
|
+
}
|
|
137
290
|
</style>
|
|
138
291
|
</head>
|
|
139
292
|
<body>
|
|
140
293
|
<div class="app">
|
|
141
294
|
<!-- Sidebar -->
|
|
295
|
+
<div class="sidebar-overlay" onclick="toggleSidebar(false)"></div>
|
|
142
296
|
<nav class="sidebar">
|
|
143
|
-
<div class="sidebar-logo">⚡ <span>OPC
|
|
297
|
+
<div class="sidebar-logo">⚡ <span>OPC Studio</span></div>
|
|
144
298
|
<div class="sidebar-nav">
|
|
145
|
-
<div class="nav-item active" data-page="dashboard"
|
|
146
|
-
|
|
147
|
-
<div class="nav-item" data-page="config"><span class="icon">⚙️</span> Config</div>
|
|
148
|
-
<div class="nav-item" data-page="memory"><span class="icon">🧠</span> Memory</div>
|
|
149
|
-
<div class="nav-item" data-page="skills"><span class="icon">🛠</span> Skills</div>
|
|
150
|
-
<div class="nav-item" data-page="tools"><span class="icon">🔧</span> Tools</div>
|
|
151
|
-
<div class="nav-item" data-page="channels"><span class="icon">📡</span> Channels</div>
|
|
152
|
-
<div class="nav-item" data-page="workflows"><span class="icon">🔀</span> Workflows</div>
|
|
153
|
-
<div class="nav-item" data-page="jobs"><span class="icon">⏰</span> Jobs</div>
|
|
154
|
-
<div class="nav-item" data-page="plugins"><span class="icon">🔌</span> Plugins</div>
|
|
155
|
-
<div class="nav-item" data-page="protocols"><span class="icon">📡</span> Protocols</div>
|
|
156
|
-
<div class="nav-item" data-page="doctor"><span class="icon">🩺</span> Doctor</div>
|
|
157
|
-
<div class="nav-item" data-page="evals"><span class="icon">🧪</span> Evals</div>
|
|
158
|
-
<div class="nav-item" data-page="telemetry"><span class="icon">📈</span> Telemetry</div>
|
|
159
|
-
<div class="nav-item" data-page="logs"><span class="icon">📜</span> Logs</div>
|
|
160
|
-
<div class="nav-item" data-page="playground"><span class="icon">🎮</span> Playground</div>
|
|
161
|
-
<div style="padding: 8px 12px; margin-top: 16px; font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px;">Modules</div>
|
|
162
|
-
<div class="nav-item" data-page="modules"><span class="icon">🔌</span> Modules</div>
|
|
163
|
-
<div class="nav-item" data-page="brain-module"><span class="icon">🧠</span> DeepBrain</div>
|
|
164
|
-
<div class="nav-item" data-page="kits-module"><span class="icon">📊</span> AgentKits</div>
|
|
165
|
-
<div class="nav-item" data-page="workstation-module"><span class="icon">👤</span> Workstation</div>
|
|
166
|
-
</div>
|
|
167
|
-
<div style="padding: 8px 12px; font-size: 11px; color: var(--text-muted);">OPC Agent v2.1</div>
|
|
168
|
-
</nav>
|
|
169
|
-
|
|
170
|
-
<!-- Main Content -->
|
|
171
|
-
<main class="main">
|
|
172
|
-
<!-- Dashboard -->
|
|
173
|
-
<div class="page active" id="page-dashboard">
|
|
174
|
-
<div class="page-header">
|
|
175
|
-
<div class="page-title">Dashboard</div>
|
|
176
|
-
<div class="page-subtitle">Agent overview and health status</div>
|
|
299
|
+
<div class="nav-item active" data-page="dashboard" onclick="navigate('dashboard')">
|
|
300
|
+
<span class="icon">🏠</span> Dashboard
|
|
177
301
|
</div>
|
|
178
|
-
<div class="
|
|
179
|
-
<
|
|
180
|
-
<div class="card stat"><div class="stat-value" id="agent-model">—</div><div class="stat-label">Model</div></div>
|
|
181
|
-
<div class="card stat"><div class="stat-value" id="agent-channels">—</div><div class="stat-label">Channels</div></div>
|
|
182
|
-
<div class="card stat"><div class="stat-value" id="agent-skills">—</div><div class="stat-label">Skills</div></div>
|
|
302
|
+
<div class="nav-item" data-page="chat" onclick="openLastChat()">
|
|
303
|
+
<span class="icon">💬</span> Chat
|
|
183
304
|
</div>
|
|
184
|
-
<div class="
|
|
185
|
-
<
|
|
186
|
-
<div id="agent-status-detail">Loading...</div>
|
|
305
|
+
<div class="nav-item" data-page="templates" onclick="navigate('templates')">
|
|
306
|
+
<span class="icon">👤</span> Templates
|
|
187
307
|
</div>
|
|
188
|
-
<div class="
|
|
189
|
-
<
|
|
190
|
-
<div id="memory-stats">Loading...</div>
|
|
308
|
+
<div class="nav-item" data-page="skills" onclick="navigate('skills')">
|
|
309
|
+
<span class="icon">🧩</span> Skills Market
|
|
191
310
|
</div>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<!-- Chat -->
|
|
195
|
-
<div class="page" id="page-chat">
|
|
196
|
-
<div class="page-header">
|
|
197
|
-
<div class="page-title">Chat</div>
|
|
198
|
-
<div class="page-subtitle">Test your agent in real-time</div>
|
|
311
|
+
<div class="nav-item" data-page="create" onclick="navigate('create')">
|
|
312
|
+
<span class="icon">✨</span> Create Agent
|
|
199
313
|
</div>
|
|
200
|
-
<div class="
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
314
|
+
<div class="nav-item" data-page="settings" onclick="currentSettingsTab='models';navigate('settings')">
|
|
315
|
+
<span class="icon">🤖</span> Models
|
|
316
|
+
</div>
|
|
317
|
+
<div class="nav-item" data-page="settings" onclick="currentSettingsTab='channels';navigate('settings')">
|
|
318
|
+
<span class="icon">📡</span> Channels
|
|
319
|
+
</div>
|
|
320
|
+
<div class="nav-item" data-page="settings" onclick="currentSettingsTab='memory';navigate('settings')">
|
|
321
|
+
<span class="icon">🧠</span> Memory
|
|
208
322
|
</div>
|
|
323
|
+
<div class="nav-item" data-page="settings" onclick="navigate('settings')">
|
|
324
|
+
<span class="icon">⚙️</span> Settings
|
|
325
|
+
</div>
|
|
326
|
+
<div class="nav-item" data-page="schedules" onclick="navigate('schedules')">
|
|
327
|
+
<span class="icon">⏰</span> Schedules
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
<div style="padding: 12px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-dim);">
|
|
331
|
+
OPC Studio v2.0
|
|
209
332
|
</div>
|
|
333
|
+
</nav>
|
|
210
334
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
335
|
+
<!-- Mobile Header -->
|
|
336
|
+
<div class="mobile-header">
|
|
337
|
+
<button onclick="toggleSidebar(true)">☰</button>
|
|
338
|
+
<span style="font-weight:600;">⚡ OPC Studio</span>
|
|
339
|
+
<span></span>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<!-- Main Content -->
|
|
343
|
+
<div class="main">
|
|
344
|
+
<!-- Dashboard Page -->
|
|
345
|
+
<div class="page active" id="page-dashboard">
|
|
346
|
+
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;margin-bottom:24px;">
|
|
347
|
+
<div>
|
|
348
|
+
<h1 class="page-title">My Agents</h1>
|
|
349
|
+
<p class="page-subtitle">Manage and chat with your AI agents</p>
|
|
221
350
|
</div>
|
|
222
|
-
<
|
|
351
|
+
<button class="btn btn-primary" onclick="navigate('create')">✨ Create New Agent</button>
|
|
352
|
+
</div>
|
|
353
|
+
<!-- Health Status Section -->
|
|
354
|
+
<div id="health-section" style="margin-bottom:24px;"></div>
|
|
355
|
+
<div id="agents-list" class="card-grid"></div>
|
|
356
|
+
<div id="agents-empty" class="empty-state" style="display:none;">
|
|
357
|
+
<div class="empty-icon">🤖</div>
|
|
358
|
+
<div class="empty-title">No agents yet</div>
|
|
359
|
+
<div class="empty-desc">Create your first AI agent in just 3 steps — no coding required!</div>
|
|
360
|
+
<button class="btn btn-primary btn-lg" onclick="navigate('create')">✨ Create My First Agent</button>
|
|
223
361
|
</div>
|
|
224
362
|
</div>
|
|
225
363
|
|
|
226
|
-
<!--
|
|
227
|
-
<div class="page" id="page-
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
<div class="page-subtitle">DeepBrain knowledge pages</div>
|
|
231
|
-
</div>
|
|
364
|
+
<!-- Templates Page -->
|
|
365
|
+
<div class="page" id="page-templates">
|
|
366
|
+
<h1 class="page-title">Template Market</h1>
|
|
367
|
+
<p class="page-subtitle">Browse 100+ ready-to-use agent templates across 19 industries</p>
|
|
232
368
|
<div class="search-bar">
|
|
233
|
-
<input class="
|
|
234
|
-
<button class="btn" onclick="searchMemory()">Search</button>
|
|
369
|
+
<input class="input" id="tpl-search" placeholder="Search templates..." oninput="filterTemplates()">
|
|
235
370
|
</div>
|
|
236
|
-
<div class="
|
|
371
|
+
<div class="chip-group" id="industry-chips"></div>
|
|
372
|
+
<div class="card-grid" id="templates-grid"></div>
|
|
237
373
|
</div>
|
|
238
374
|
|
|
239
|
-
<!-- Skills -->
|
|
375
|
+
<!-- Skills Marketplace Page -->
|
|
240
376
|
<div class="page" id="page-skills">
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
377
|
+
<h1 class="page-title">🧩 Skill Market</h1>
|
|
378
|
+
<p class="page-subtitle">One-click install new capabilities for your agent — no coding required</p>
|
|
379
|
+
<div class="search-bar" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;">
|
|
380
|
+
<input class="input" id="skills-search" placeholder="Search skills..." oninput="filterSkills()" style="flex:1;min-width:200px;">
|
|
381
|
+
<div class="chip-group" id="skill-category-chips" style="margin:0;"></div>
|
|
244
382
|
</div>
|
|
245
|
-
<div class="card" id="skills-
|
|
383
|
+
<div class="card-grid" id="skills-grid" style="margin-top:16px;"></div>
|
|
246
384
|
</div>
|
|
247
385
|
|
|
248
|
-
<!--
|
|
249
|
-
<div class="page" id="page-
|
|
250
|
-
<div class="
|
|
251
|
-
<div class="
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
386
|
+
<!-- Create Wizard Page -->
|
|
387
|
+
<div class="page" id="page-create">
|
|
388
|
+
<div class="wizard">
|
|
389
|
+
<div class="wizard-steps">
|
|
390
|
+
<div class="wizard-step active" id="ws-1"><div class="step-num">1</div><span>Choose Template</span></div>
|
|
391
|
+
<div class="wizard-step" id="ws-2"><div class="step-line"></div><div class="step-num">2</div><span>Configure</span></div>
|
|
392
|
+
<div class="wizard-step" id="ws-3"><div class="step-line"></div><div class="step-num">3</div><span>Confirm</span></div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<!-- Step 1: Choose Template -->
|
|
396
|
+
<div class="wizard-panel active" id="wp-1">
|
|
397
|
+
<h2 style="font-size:20px;margin-bottom:8px;">Choose a Template</h2>
|
|
398
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">Pick a role for your agent. Don't worry, you can customize it later.</p>
|
|
399
|
+
<div class="search-bar">
|
|
400
|
+
<input class="input" id="wizard-tpl-search" placeholder="Search templates..." oninput="filterWizardTemplates()">
|
|
401
|
+
</div>
|
|
402
|
+
<div class="chip-group" id="wizard-industry-chips"></div>
|
|
403
|
+
<div class="card-grid" id="wizard-tpl-grid" style="max-height:400px;overflow-y:auto;"></div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<!-- Step 2: Configure -->
|
|
407
|
+
<div class="wizard-panel" id="wp-2">
|
|
408
|
+
<h2 style="font-size:20px;margin-bottom:8px;">Configure Your Agent</h2>
|
|
409
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:24px;">Give your agent a name and tell it about your business.</p>
|
|
410
|
+
<div class="form-group">
|
|
411
|
+
<label class="label">Agent Name *</label>
|
|
412
|
+
<input class="input" id="agent-name" placeholder="e.g. My Sales Coach">
|
|
413
|
+
</div>
|
|
414
|
+
<div class="form-group">
|
|
415
|
+
<label class="label">Company / Business Description</label>
|
|
416
|
+
<textarea class="input" id="agent-desc" rows="3" placeholder="Brief description of your business so the agent can better help you..."></textarea>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="form-group">
|
|
419
|
+
<label class="label">AI Model</label>
|
|
420
|
+
<select class="input" id="agent-model">
|
|
421
|
+
<option value="gpt-4o-mini">GPT-4o Mini (Fast & Affordable) ⭐ Recommended</option>
|
|
422
|
+
<option value="gpt-4o">GPT-4o (Most Capable)</option>
|
|
423
|
+
<option value="claude-sonnet-4">Claude Sonnet (Balanced)</option>
|
|
424
|
+
<option value="claude-haiku">Claude Haiku (Fast)</option>
|
|
425
|
+
<option value="gemini-2.0-flash">Gemini 2.0 Flash (Google)</option>
|
|
426
|
+
<option value="deepseek-v3">DeepSeek V3 (Open Source)</option>
|
|
427
|
+
</select>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="form-group">
|
|
430
|
+
<label class="label">Language Preference</label>
|
|
431
|
+
<select class="input" id="agent-lang">
|
|
432
|
+
<option value="en">English</option>
|
|
433
|
+
<option value="zh">中文</option>
|
|
434
|
+
<option value="auto">Auto-detect</option>
|
|
435
|
+
</select>
|
|
436
|
+
</div>
|
|
437
|
+
<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:24px;">
|
|
438
|
+
<button class="btn btn-secondary" onclick="wizardBack()">← Back</button>
|
|
439
|
+
<button class="btn btn-primary" onclick="wizardNext()">Next →</button>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
256
442
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
443
|
+
<!-- Step 3: Confirm -->
|
|
444
|
+
<div class="wizard-panel" id="wp-3">
|
|
445
|
+
<h2 style="font-size:20px;margin-bottom:8px;">Review & Create</h2>
|
|
446
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:24px;">Everything looks good? Let's bring your agent to life!</p>
|
|
447
|
+
<div class="card" id="confirm-card" style="margin-bottom:24px;"></div>
|
|
448
|
+
<div style="display:flex;gap:12px;justify-content:flex-end;">
|
|
449
|
+
<button class="btn btn-secondary" onclick="wizardBack()">← Back</button>
|
|
450
|
+
<button class="btn btn-primary btn-lg" onclick="createAgent()" id="create-btn">🚀 Create Agent</button>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
262
453
|
</div>
|
|
263
|
-
<div class="card" id="channels-list">Loading...</div>
|
|
264
454
|
</div>
|
|
265
455
|
|
|
266
|
-
<!--
|
|
267
|
-
<div class="
|
|
268
|
-
<div class="
|
|
456
|
+
<!-- Chat Page (full height, no padding) -->
|
|
457
|
+
<div class="chat-container" id="page-chat">
|
|
458
|
+
<div class="chat-header">
|
|
459
|
+
<button class="chat-back" onclick="navigate('dashboard')">←</button>
|
|
460
|
+
<span class="chat-icon" id="chat-agent-icon">🤖</span>
|
|
269
461
|
<div>
|
|
270
|
-
<div class="
|
|
271
|
-
<div class="
|
|
462
|
+
<div class="chat-name" id="chat-agent-name">Agent</div>
|
|
463
|
+
<div class="chat-status" id="chat-agent-status">Online</div>
|
|
272
464
|
</div>
|
|
273
|
-
<div style="display:flex;gap:8px;
|
|
274
|
-
<select id="
|
|
275
|
-
|
|
276
|
-
</
|
|
277
|
-
<button class="btn" onclick="
|
|
278
|
-
<button class="btn btn-primary" onclick="dagEditor.save()">💾 Save</button>
|
|
279
|
-
<button class="btn" onclick="dagEditor.exportJSON()">📤 Export</button>
|
|
280
|
-
<button class="btn" onclick="dagEditor.importJSON()">📥 Import</button>
|
|
281
|
-
<button class="btn" style="background:var(--green);border-color:var(--green);color:#000" onclick="dagEditor.run()">▶ Run</button>
|
|
465
|
+
<div style="margin-left:auto;display:flex;gap:8px;align-items:center;">
|
|
466
|
+
<select id="chat-agent-select" class="input" style="width:auto;padding:6px 10px;font-size:13px;border-radius:8px;" onchange="switchChatAgent(this.value)"></select>
|
|
467
|
+
<span id="streaming-indicator" style="display:none;font-size:16px;" title="Thinking...">⏳</span>
|
|
468
|
+
<button class="btn btn-sm btn-secondary" onclick="clearChat()">🗑 Clear</button>
|
|
469
|
+
<button class="btn btn-sm btn-secondary" onclick="openMemory()">🧠 Memory</button>
|
|
282
470
|
</div>
|
|
283
471
|
</div>
|
|
284
|
-
<div
|
|
285
|
-
<div class="
|
|
286
|
-
<div
|
|
287
|
-
<div class="dag-palette-item" draggable="true" data-type="input" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'input')">📥 Input</div>
|
|
288
|
-
<div class="dag-palette-item" draggable="true" data-type="agent" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'agent')">🤖 Agent</div>
|
|
289
|
-
<div class="dag-palette-item" draggable="true" data-type="tool" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'tool')">🔧 Tool</div>
|
|
290
|
-
<div class="dag-palette-item" draggable="true" data-type="condition" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'condition')">❓ Condition</div>
|
|
291
|
-
<div class="dag-palette-item" draggable="true" data-type="loop" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'loop')">🔁 Loop</div>
|
|
292
|
-
<div class="dag-palette-item" draggable="true" data-type="parallel" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'parallel')">⚡ Parallel</div>
|
|
293
|
-
<div class="dag-palette-item" draggable="true" data-type="output" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'output')">📤 Output</div>
|
|
294
|
-
<hr style="border-color:var(--border);margin:12px 0">
|
|
295
|
-
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">Actions</div>
|
|
296
|
-
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.undo()">↩ Undo</button>
|
|
297
|
-
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.redo()">↪ Redo</button>
|
|
298
|
-
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.deleteSelected()">🗑 Delete</button>
|
|
299
|
-
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.zoomIn()">🔍+ Zoom In</button>
|
|
300
|
-
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.zoomOut()">🔍- Zoom Out</button>
|
|
301
|
-
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.fitView()">⊞ Fit</button>
|
|
302
|
-
</div>
|
|
303
|
-
<div style="flex:1;position:relative">
|
|
304
|
-
<canvas id="dag-canvas" style="width:100%;height:600px;border-radius:8px;border:1px solid var(--border);background:#0d0d0d;cursor:default"></canvas>
|
|
472
|
+
<div class="chat-messages" id="chat-messages">
|
|
473
|
+
<div class="msg assistant">
|
|
474
|
+
<div class="msg-bubble" id="chat-welcome">Hello! How can I help you today?</div>
|
|
305
475
|
</div>
|
|
306
476
|
</div>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<div class="card-title">Node Properties</div>
|
|
310
|
-
<div id="dag-props-content"></div>
|
|
477
|
+
<div class="typing-indicator" id="typing-indicator">
|
|
478
|
+
<div class="dots"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
|
|
311
479
|
</div>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
<
|
|
315
|
-
<
|
|
480
|
+
<div class="chat-input-bar">
|
|
481
|
+
<input type="file" id="doc-upload-input" style="display:none" accept=".pdf,.txt,.md,.docx,.csv,.json" onchange="handleDocUpload(this)">
|
|
482
|
+
<button class="btn" onclick="document.getElementById('doc-upload-input').click()" title="Upload document" style="padding:12px;font-size:18px;background:transparent;border:1px solid var(--border);border-radius:24px;cursor:pointer;">📎</button>
|
|
483
|
+
<button class="btn" id="voice-btn" onclick="toggleVoiceInput()" title="Voice input (click to start/stop)" style="padding:12px;font-size:18px;background:transparent;border:1px solid var(--border);border-radius:24px;cursor:pointer;">🎤</button>
|
|
484
|
+
<input class="input" id="chat-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendMessage()">
|
|
485
|
+
<button class="btn btn-primary" onclick="sendMessage()">Send</button>
|
|
316
486
|
</div>
|
|
317
487
|
</div>
|
|
318
488
|
|
|
319
|
-
<!--
|
|
320
|
-
<div class="page" id="page-
|
|
321
|
-
<
|
|
322
|
-
|
|
323
|
-
|
|
489
|
+
<!-- Settings Page -->
|
|
490
|
+
<div class="page" id="page-settings">
|
|
491
|
+
<h1 class="page-title">⚙️ Settings</h1>
|
|
492
|
+
<p class="page-subtitle">配置您的 AI Agent 平台</p>
|
|
493
|
+
<div class="settings-layout">
|
|
494
|
+
<!-- Sub-nav -->
|
|
495
|
+
<div class="settings-subnav">
|
|
496
|
+
<div class="snav-item active" data-section="models" onclick="showSettingsSection('models')"><span>🤖</span> 模型配置</div>
|
|
497
|
+
<div class="snav-item" data-section="channels" onclick="showSettingsSection('channels')"><span>📡</span> 渠道配置</div>
|
|
498
|
+
<div class="snav-item" data-section="memory" onclick="showSettingsSection('memory')"><span>🧠</span> 记忆管理</div>
|
|
499
|
+
<div class="snav-item" data-section="role" onclick="showSettingsSection('role')"><span>👤</span> 角色编辑</div>
|
|
500
|
+
<div class="snav-item" data-section="status" onclick="showSettingsSection('status')"><span>📊</span> 运行状态</div>
|
|
501
|
+
<div class="snav-item" data-section="usage" onclick="showSettingsSection('usage')"><span>💰</span> 用量统计</div>
|
|
502
|
+
</div>
|
|
503
|
+
<!-- Content -->
|
|
504
|
+
<div class="settings-content">
|
|
505
|
+
|
|
506
|
+
<!-- SECTION: Models -->
|
|
507
|
+
<div class="settings-section active" id="section-models">
|
|
508
|
+
<h2 style="font-size:18px;font-weight:700;margin-bottom:4px;">🤖 模型配置</h2>
|
|
509
|
+
<p style="color:var(--text-muted);font-size:13px;margin-bottom:20px;">选择让 Agent 思考和对话的 AI 大脑</p>
|
|
510
|
+
<div class="tabs">
|
|
511
|
+
<div class="tab active" id="tab-btn-local" onclick="switchModelTab('local')">本地模型</div>
|
|
512
|
+
<div class="tab" id="tab-btn-cloud" onclick="switchModelTab('cloud')">☁️ 云端 API</div>
|
|
513
|
+
</div>
|
|
514
|
+
<!-- Local tab -->
|
|
515
|
+
<div class="tab-panel active" id="tab-local">
|
|
516
|
+
<div id="ollama-checking" style="display:flex;align-items:center;gap:8px;color:var(--text-muted);font-size:14px;padding:16px 0;">
|
|
517
|
+
<span class="status-dot yellow"></span> 正在检测本地 Ollama...
|
|
518
|
+
</div>
|
|
519
|
+
<div id="ollama-running" style="display:none;">
|
|
520
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
|
521
|
+
<div style="display:flex;align-items:center;gap:8px;font-size:14px;font-weight:500;">
|
|
522
|
+
<span class="status-dot green"></span> Ollama 运行中
|
|
523
|
+
</div>
|
|
524
|
+
<button class="btn btn-secondary btn-sm" onclick="refreshLocalModels()">🔄 刷新</button>
|
|
525
|
+
</div>
|
|
526
|
+
<div id="local-models-list" style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px;"></div>
|
|
527
|
+
<div class="card" style="background:rgba(59,130,246,0.06);border-color:rgba(59,130,246,0.3);">
|
|
528
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:6px;">💡 推荐模型组合</div>
|
|
529
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:10px;">对话:<strong>qwen2.5:7b</strong> · 嵌入:<strong>nomic-embed-text</strong></div>
|
|
530
|
+
<button class="btn btn-primary btn-sm" onclick="showPullCommand()">查看安装命令</button>
|
|
531
|
+
</div>
|
|
532
|
+
<div id="pull-cmd" style="display:none;margin-top:12px;">
|
|
533
|
+
<div style="background:#080808;padding:10px 14px;border-radius:var(--radius);font-family:var(--mono);font-size:12px;color:#86efac;line-height:1.8;">ollama pull qwen2.5:7b<br>ollama pull nomic-embed-text</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
<div id="ollama-offline" style="display:none;">
|
|
537
|
+
<div class="card">
|
|
538
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
|
539
|
+
<span class="status-dot red"></span>
|
|
540
|
+
<span style="font-size:14px;font-weight:600;">Ollama 未检测到</span>
|
|
541
|
+
</div>
|
|
542
|
+
<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px;">在本地运行 AI 模型,完全免费、无需联网、数据不外泄</p>
|
|
543
|
+
<div style="display:flex;flex-direction:column;gap:14px;">
|
|
544
|
+
<div style="display:flex;gap:12px;">
|
|
545
|
+
<div style="width:26px;height:26px;min-width:26px;background:var(--accent);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:white;">1</div>
|
|
546
|
+
<div>
|
|
547
|
+
<div style="font-size:14px;font-weight:500;margin-bottom:2px;">下载 Ollama</div>
|
|
548
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">访问官网下载安装包(约 200MB,免费)</div>
|
|
549
|
+
<a href="https://ollama.com/download" target="_blank" class="btn btn-secondary btn-sm">打开下载页面 ↗</a>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
<div style="display:flex;gap:12px;">
|
|
553
|
+
<div style="width:26px;height:26px;min-width:26px;background:var(--accent);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:white;">2</div>
|
|
554
|
+
<div>
|
|
555
|
+
<div style="font-size:14px;font-weight:500;margin-bottom:2px;">安装并启动</div>
|
|
556
|
+
<div style="font-size:12px;color:var(--text-muted);">按向导安装,Ollama 会自动在后台运行</div>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
<div style="display:flex;gap:12px;">
|
|
560
|
+
<div style="width:26px;height:26px;min-width:26px;background:var(--accent);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:white;">3</div>
|
|
561
|
+
<div>
|
|
562
|
+
<div style="font-size:14px;font-weight:500;margin-bottom:2px;">拉取推荐模型</div>
|
|
563
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">在终端运行(首次约 5GB,之后永久免费)</div>
|
|
564
|
+
<div style="background:#080808;padding:8px 12px;border-radius:var(--radius);font-family:var(--mono);font-size:12px;color:#86efac;">ollama pull qwen2.5:7b<br>ollama pull nomic-embed-text</div>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
<button class="btn btn-primary" style="width:100%;margin-top:16px;" onclick="checkOllama()">✅ 已安装完成,重新检测</button>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
<!-- Cloud tab -->
|
|
573
|
+
<div class="tab-panel" id="tab-cloud">
|
|
574
|
+
<p style="font-size:13px;color:var(--text-muted);margin-bottom:20px;">填入 API Key 即可使用云端大模型,Key 仅保存在本地配置文件中</p>
|
|
575
|
+
<div id="cloud-providers"></div>
|
|
576
|
+
<button class="btn btn-primary" style="width:100%;" onclick="saveCloudConfig()">💾 保存云端配置</button>
|
|
577
|
+
<div id="cloud-save-msg" style="text-align:center;font-size:13px;color:var(--green);margin-top:8px;"></div>
|
|
578
|
+
</div>
|
|
579
|
+
<!-- Model assignment -->
|
|
580
|
+
<div class="card" style="margin-top:24px;">
|
|
581
|
+
<div style="font-size:15px;font-weight:600;margin-bottom:4px;">模型分配</div>
|
|
582
|
+
<p style="font-size:12px;color:var(--text-muted);margin-bottom:16px;">将不同模型分配给不同任务,获得最佳效果</p>
|
|
583
|
+
<div class="form-group">
|
|
584
|
+
<label class="label">对话模型 <span style="color:var(--red);">*</span></label>
|
|
585
|
+
<p class="help-text" style="margin-bottom:6px;">Agent 用来理解和回复消息的主模型</p>
|
|
586
|
+
<select class="input" id="chat-model-select"></select>
|
|
587
|
+
</div>
|
|
588
|
+
<div class="form-group">
|
|
589
|
+
<label class="label">嵌入模型(记忆专用)</label>
|
|
590
|
+
<p class="help-text" style="margin-bottom:6px;">将记忆转为向量存储,推荐 nomic-embed-text</p>
|
|
591
|
+
<select class="input" id="embed-model-select"></select>
|
|
592
|
+
</div>
|
|
593
|
+
<div style="display:flex;align-items:center;gap:12px;">
|
|
594
|
+
<button class="btn btn-primary" onclick="saveModelAssignment()">💾 保存</button>
|
|
595
|
+
<span id="model-save-msg" style="font-size:13px;color:var(--green);"></span>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
|
|
600
|
+
<!-- SECTION: Channels -->
|
|
601
|
+
<div class="settings-section" id="section-channels">
|
|
602
|
+
<h2 style="font-size:18px;font-weight:700;margin-bottom:4px;">📡 渠道配置</h2>
|
|
603
|
+
<p style="color:var(--text-muted);font-size:13px;margin-bottom:20px;">设置 Agent 与用户沟通的渠道,点击卡片进行配置</p>
|
|
604
|
+
<div class="channel-grid" id="channel-cards"></div>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<!-- SECTION: Memory -->
|
|
608
|
+
<div class="settings-section" id="section-memory">
|
|
609
|
+
<h2 style="font-size:18px;font-weight:700;margin-bottom:4px;">🧠 记忆管理</h2>
|
|
610
|
+
<p style="color:var(--text-muted);font-size:13px;margin-bottom:16px;">管理 Agent 的长期记忆(由 DeepBrain 驱动)</p>
|
|
611
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;" id="deepbrain-status">
|
|
612
|
+
<span class="status-dot yellow"></span>
|
|
613
|
+
<span style="font-size:13px;color:var(--text-muted);">正在检测 DeepBrain...</span>
|
|
614
|
+
</div>
|
|
615
|
+
<div id="deepbrain-online" style="display:none;">
|
|
616
|
+
<div class="iframe-wrap"><iframe id="deepbrain-frame" src="http://localhost:4001" title="DeepBrain"></iframe></div>
|
|
617
|
+
</div>
|
|
618
|
+
<div id="deepbrain-offline" style="display:none;">
|
|
619
|
+
<div class="card empty-state">
|
|
620
|
+
<div class="empty-icon">🧠</div>
|
|
621
|
+
<div class="empty-title">DeepBrain 未启动</div>
|
|
622
|
+
<div class="empty-desc">DeepBrain 是独立的记忆管理模块,需要单独运行在端口 4001</div>
|
|
623
|
+
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap;">
|
|
624
|
+
<a href="http://localhost:4001" target="_blank" class="btn btn-primary">在新标签页打开 ↗</a>
|
|
625
|
+
<button class="btn btn-secondary" onclick="checkDeepBrain()">🔄 重新检测</button>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<!-- SECTION: Role -->
|
|
632
|
+
<div class="settings-section" id="section-role">
|
|
633
|
+
<h2 style="font-size:18px;font-weight:700;margin-bottom:4px;">👤 角色编辑</h2>
|
|
634
|
+
<p style="color:var(--text-muted);font-size:13px;margin-bottom:16px;">设计 Agent 的人格和行为方式(由 Workstation 驱动)</p>
|
|
635
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;" id="workstation-status">
|
|
636
|
+
<span class="status-dot yellow"></span>
|
|
637
|
+
<span style="font-size:13px;color:var(--text-muted);">正在检测 Workstation...</span>
|
|
638
|
+
</div>
|
|
639
|
+
<div id="workstation-online" style="display:none;">
|
|
640
|
+
<div class="iframe-wrap"><iframe id="workstation-frame" src="http://localhost:4003" title="Workstation"></iframe></div>
|
|
641
|
+
</div>
|
|
642
|
+
<div id="workstation-offline" style="display:none;">
|
|
643
|
+
<div class="card empty-state">
|
|
644
|
+
<div class="empty-icon">👤</div>
|
|
645
|
+
<div class="empty-title">Workstation 未启动</div>
|
|
646
|
+
<div class="empty-desc">Workstation 是独立的角色编辑模块,需要单独运行在端口 4003</div>
|
|
647
|
+
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap;">
|
|
648
|
+
<a href="http://localhost:4003" target="_blank" class="btn btn-primary">在新标签页打开 ↗</a>
|
|
649
|
+
<button class="btn btn-secondary" onclick="checkWorkstation()">🔄 重新检测</button>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
|
|
655
|
+
<!-- SECTION: Status -->
|
|
656
|
+
<div class="settings-section" id="section-status">
|
|
657
|
+
<h2 style="font-size:18px;font-weight:700;margin-bottom:4px;">📊 运行状态</h2>
|
|
658
|
+
<p style="color:var(--text-muted);font-size:13px;margin-bottom:20px;">查看 Studio 和 Agent 的运行情况</p>
|
|
659
|
+
<div class="card-grid" style="grid-template-columns:repeat(auto-fill,minmax(180px,1fr));margin-bottom:20px;">
|
|
660
|
+
<div class="card">
|
|
661
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">Studio 状态</div>
|
|
662
|
+
<div style="display:flex;align-items:center;gap:6px;font-size:15px;font-weight:600;">
|
|
663
|
+
<span class="status-dot green"></span> 运行中
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
<div class="card">
|
|
667
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">运行时长</div>
|
|
668
|
+
<div style="font-size:15px;font-weight:600;" id="status-uptime">--</div>
|
|
669
|
+
</div>
|
|
670
|
+
<div class="card">
|
|
671
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">内存占用</div>
|
|
672
|
+
<div style="font-size:15px;font-weight:600;" id="status-memory">-- MB</div>
|
|
673
|
+
</div>
|
|
674
|
+
<div class="card">
|
|
675
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">启动 Agent</div>
|
|
676
|
+
<code style="background:var(--bg-hover);padding:4px 8px;border-radius:4px;font-size:12px;color:var(--text-muted);">opc run</code>
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
<div class="card">
|
|
680
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
|
681
|
+
<div style="font-size:14px;font-weight:600;">📋 最近日志(最后 50 行)</div>
|
|
682
|
+
<button class="btn btn-secondary btn-sm" onclick="refreshStatus()">🔄 刷新</button>
|
|
683
|
+
</div>
|
|
684
|
+
<div class="log-viewer" id="status-logs">暂无日志记录</div>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
|
|
688
|
+
<!-- SECTION: Usage -->
|
|
689
|
+
<div class="settings-section" id="section-usage">
|
|
690
|
+
<h2 style="font-size:18px;font-weight:700;margin-bottom:4px;">💰 用量统计</h2>
|
|
691
|
+
<p style="color:var(--text-muted);font-size:13px;margin-bottom:20px;">Token 消耗和费用概览(近 7 天)</p>
|
|
692
|
+
<div class="card-grid" style="grid-template-columns:repeat(auto-fill,minmax(180px,1fr));margin-bottom:20px;">
|
|
693
|
+
<div class="card">
|
|
694
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:4px;">总 Token 用量</div>
|
|
695
|
+
<div style="font-size:26px;font-weight:700;" id="usage-total-tokens">0</div>
|
|
696
|
+
<div style="font-size:11px;color:var(--text-dim);margin-top:2px;">近 7 天</div>
|
|
697
|
+
</div>
|
|
698
|
+
<div class="card">
|
|
699
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:4px;">预估费用</div>
|
|
700
|
+
<div style="font-size:26px;font-weight:700;" id="usage-total-cost">$0.00</div>
|
|
701
|
+
<div style="font-size:11px;color:var(--text-dim);margin-top:2px;">近 7 天</div>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
<div class="card" style="margin-bottom:20px;">
|
|
705
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:16px;">📈 每日 Token 用量</div>
|
|
706
|
+
<div class="bar-chart-wrap" id="usage-chart"></div>
|
|
707
|
+
</div>
|
|
708
|
+
<div class="card">
|
|
709
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">按模型分类</div>
|
|
710
|
+
<div id="usage-by-model"><div style="font-size:13px;color:var(--text-muted);">暂无使用记录。开始与 Agent 对话后将自动统计。</div></div>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<!-- SECTION: Image Generation -->
|
|
715
|
+
<div class="settings-section" id="section-imagegen">
|
|
716
|
+
<h2 style="font-size:18px;font-weight:700;margin-bottom:4px;">🎨 图片生成</h2>
|
|
717
|
+
<p style="color:var(--text-muted);font-size:13px;margin-bottom:20px;">Configure image generation providers for your agents</p>
|
|
718
|
+
<div class="card" style="margin-bottom:16px;">
|
|
719
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">DALL·E (OpenAI)</div>
|
|
720
|
+
<div class="form-group">
|
|
721
|
+
<label class="label">OpenAI API Key</label>
|
|
722
|
+
<input class="input" id="ig-openai-key" type="password" placeholder="sk-...">
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
<div class="card" style="margin-bottom:16px;">
|
|
726
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">Stable Diffusion (Local)</div>
|
|
727
|
+
<div class="form-group">
|
|
728
|
+
<label class="label">API URL</label>
|
|
729
|
+
<input class="input" id="ig-sd-url" placeholder="http://localhost:7860">
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
<div class="card" style="margin-bottom:16px;">
|
|
733
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:12px;">Replicate</div>
|
|
734
|
+
<div class="form-group">
|
|
735
|
+
<label class="label">API Token</label>
|
|
736
|
+
<input class="input" id="ig-replicate-key" type="password" placeholder="r8_...">
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
<button class="btn btn-primary" onclick="saveImageGenConfig()">💾 Save Configuration</button>
|
|
740
|
+
<div id="ig-status" style="margin-top:12px;font-size:13px;color:var(--text-muted);"></div>
|
|
741
|
+
</div>
|
|
742
|
+
|
|
743
|
+
</div><!-- end settings-content -->
|
|
744
|
+
</div><!-- end settings-layout -->
|
|
745
|
+
</div><!-- end page-settings -->
|
|
746
|
+
|
|
747
|
+
<!-- Memory Page -->
|
|
748
|
+
<div class="page" id="page-memory">
|
|
749
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:24px;">
|
|
750
|
+
<button class="btn btn-secondary btn-sm" onclick="navigateToChat()">← Back to Chat</button>
|
|
751
|
+
<div>
|
|
752
|
+
<h1 class="page-title" style="margin-bottom:0;">🧠 Agent Memory</h1>
|
|
753
|
+
<p class="page-subtitle" style="margin-bottom:0;">Knowledge your agent has learned over time</p>
|
|
754
|
+
</div>
|
|
324
755
|
</div>
|
|
325
|
-
<div
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
<div class="page-header">
|
|
331
|
-
<div class="page-title">Plugins</div>
|
|
332
|
-
<div class="page-subtitle">Middleware and extensions</div>
|
|
756
|
+
<div id="memory-timeline"></div>
|
|
757
|
+
<div id="memory-empty" class="empty-state">
|
|
758
|
+
<div class="empty-icon">🧠</div>
|
|
759
|
+
<div class="empty-title">No memories yet</div>
|
|
760
|
+
<div class="empty-desc">Your agent will learn and remember things as you chat with it.</div>
|
|
333
761
|
</div>
|
|
334
|
-
<div class="card" id="plugins-list">Loading...</div>
|
|
335
762
|
</div>
|
|
336
763
|
|
|
337
|
-
<!--
|
|
338
|
-
<div class="page" id="page-
|
|
339
|
-
<div
|
|
340
|
-
<div
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
<button class="btn btn-primary" onclick="
|
|
764
|
+
<!-- Schedules Page -->
|
|
765
|
+
<div class="page" id="page-schedules">
|
|
766
|
+
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;margin-bottom:24px;">
|
|
767
|
+
<div>
|
|
768
|
+
<h1 class="page-title">⏰ Scheduled Tasks</h1>
|
|
769
|
+
<p class="page-subtitle">Automate recurring agent tasks with cron schedules</p>
|
|
770
|
+
</div>
|
|
771
|
+
<button class="btn btn-primary" onclick="showScheduleForm()">+ New Task</button>
|
|
345
772
|
</div>
|
|
346
|
-
</div>
|
|
347
773
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
<div class="
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<div
|
|
356
|
-
<label>
|
|
357
|
-
<select id="
|
|
358
|
-
<option value="
|
|
359
|
-
<option value="
|
|
360
|
-
<option value="
|
|
774
|
+
<!-- New/Edit Form (hidden by default) -->
|
|
775
|
+
<div id="schedule-form" class="card" style="display:none;margin-bottom:24px;">
|
|
776
|
+
<h3 style="font-size:16px;font-weight:600;margin-bottom:16px;" id="schedule-form-title">New Scheduled Task</h3>
|
|
777
|
+
<div class="form-group">
|
|
778
|
+
<label class="label">Task Name *</label>
|
|
779
|
+
<input class="input" id="sched-name" placeholder="e.g. Daily News Summary">
|
|
780
|
+
</div>
|
|
781
|
+
<div class="form-group">
|
|
782
|
+
<label class="label">Frequency</label>
|
|
783
|
+
<select class="input" id="sched-frequency" onchange="onSchedFreqChange()">
|
|
784
|
+
<option value="daily">Every Day</option>
|
|
785
|
+
<option value="weekly">Every Week (Monday)</option>
|
|
786
|
+
<option value="monthly">Every Month (1st)</option>
|
|
787
|
+
<option value="custom">Custom Cron</option>
|
|
361
788
|
</select>
|
|
362
|
-
<button class="btn btn-primary" onclick="runEval()" style="margin-left:8px">Run Suite</button>
|
|
363
789
|
</div>
|
|
364
|
-
<div id="
|
|
790
|
+
<div class="form-group" id="sched-time-group">
|
|
791
|
+
<label class="label">Time</label>
|
|
792
|
+
<input class="input" type="time" id="sched-time" value="09:00">
|
|
793
|
+
</div>
|
|
794
|
+
<div class="form-group" id="sched-cron-group" style="display:none;">
|
|
795
|
+
<label class="label">Cron Expression</label>
|
|
796
|
+
<input class="input" id="sched-cron" placeholder="*/5 * * * *">
|
|
797
|
+
<div class="help-text">Format: minute hour dayOfMonth month dayOfWeek</div>
|
|
798
|
+
</div>
|
|
799
|
+
<div class="form-group">
|
|
800
|
+
<label class="label">Description</label>
|
|
801
|
+
<textarea class="input" id="sched-desc" rows="2" placeholder="e.g. Send a news summary every morning at 8am"></textarea>
|
|
802
|
+
</div>
|
|
803
|
+
<div class="form-group">
|
|
804
|
+
<label class="label">Output Channel</label>
|
|
805
|
+
<select class="input" id="sched-channel">
|
|
806
|
+
<option value="web">Web</option>
|
|
807
|
+
<option value="telegram">Telegram</option>
|
|
808
|
+
<option value="email">Email</option>
|
|
809
|
+
</select>
|
|
810
|
+
</div>
|
|
811
|
+
<div style="display:flex;gap:8px;">
|
|
812
|
+
<button class="btn btn-primary" onclick="saveSchedule()">💾 Save</button>
|
|
813
|
+
<button class="btn btn-secondary" onclick="hideScheduleForm()">Cancel</button>
|
|
814
|
+
</div>
|
|
365
815
|
</div>
|
|
366
|
-
</div>
|
|
367
816
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
<div class="
|
|
371
|
-
<div class="
|
|
372
|
-
<div class="
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<div class="card" id="tel-total-spans"><div style="font-size:11px;color:var(--text-muted)">Total Spans</div><div style="font-size:24px;font-weight:700" id="tel-stat-spans">—</div></div>
|
|
376
|
-
<div class="card" id="tel-total-traces"><div style="font-size:11px;color:var(--text-muted)">Total Traces</div><div style="font-size:24px;font-weight:700" id="tel-stat-traces">—</div></div>
|
|
377
|
-
<div class="card" id="tel-error-rate"><div style="font-size:11px;color:var(--text-muted)">Error Rate</div><div style="font-size:24px;font-weight:700" id="tel-stat-errors">—</div></div>
|
|
378
|
-
<div class="card" id="tel-p95"><div style="font-size:11px;color:var(--text-muted)">P95 Latency</div><div style="font-size:24px;font-weight:700" id="tel-stat-p95">—</div></div>
|
|
817
|
+
<!-- Task List -->
|
|
818
|
+
<div id="schedules-list"></div>
|
|
819
|
+
<div id="schedules-empty" class="empty-state" style="display:none;">
|
|
820
|
+
<div class="empty-icon">⏰</div>
|
|
821
|
+
<div class="empty-title">No scheduled tasks</div>
|
|
822
|
+
<div class="empty-desc">Create your first automated task to get started.</div>
|
|
823
|
+
<button class="btn btn-primary" onclick="showScheduleForm()">+ Create Task</button>
|
|
379
824
|
</div>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
|
|
829
|
+
<!-- Channel Config Dialog -->
|
|
830
|
+
<div class="dialog-overlay" id="channel-dialog">
|
|
831
|
+
<div class="dialog" style="max-width:480px;width:92%;">
|
|
832
|
+
<div class="dialog-title" id="channel-dialog-title">配置渠道</div>
|
|
833
|
+
<div id="channel-dialog-body" style="margin-bottom:4px;"></div>
|
|
834
|
+
<div class="dialog-actions">
|
|
835
|
+
<button class="btn btn-secondary btn-sm" onclick="closeChannelDialog()">取消</button>
|
|
836
|
+
<button class="btn btn-primary btn-sm" onclick="saveCurrentChannel()">💾 保存</button>
|
|
837
|
+
</div>
|
|
838
|
+
<!-- Settings Page -->
|
|
839
|
+
<div class="page" id="page-settings">
|
|
840
|
+
<div class="settings-layout">
|
|
841
|
+
<div class="settings-nav">
|
|
842
|
+
<div class="settings-nav-item active" data-settings="models" onclick="showSettings('models')">🤖 模型配置</div>
|
|
843
|
+
<div class="settings-nav-item" data-settings="channels" onclick="showSettings('channels')">📡 渠道配置</div>
|
|
844
|
+
<div class="settings-nav-item" data-settings="memory" onclick="showSettings('memory')">🧠 记忆管理</div>
|
|
845
|
+
<div class="settings-nav-item" data-settings="role" onclick="showSettings('role')">👤 角色编辑</div>
|
|
846
|
+
<div class="settings-nav-item" data-settings="status" onclick="showSettings('status')">📊 运行状态</div>
|
|
847
|
+
<div class="settings-nav-item" data-settings="usage" onclick="showSettings('usage')">💰 用量统计</div>
|
|
848
|
+
<div class="settings-nav-item" data-settings="search" onclick="showSettings('search')">🔍 搜索配置</div>
|
|
849
|
+
</div>
|
|
850
|
+
<div class="settings-content">
|
|
851
|
+
|
|
852
|
+
<!-- Models Panel -->
|
|
853
|
+
<div class="settings-panel active" id="sp-models">
|
|
854
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">🤖 模型配置</h2>
|
|
855
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">选择 AI 模型,默认使用本地模型,完全免费</p>
|
|
856
|
+
|
|
857
|
+
<div class="tabs">
|
|
858
|
+
<div class="tab active" onclick="switchModelTab('local')">🏠 本地模型</div>
|
|
859
|
+
<div class="tab" onclick="switchModelTab('cloud')">☁️ 云端 API</div>
|
|
860
|
+
</div>
|
|
861
|
+
|
|
862
|
+
<!-- Local Models Tab -->
|
|
863
|
+
<div class="tab-panel active" id="mt-local">
|
|
864
|
+
<div id="ollama-status" style="margin-bottom:20px;"></div>
|
|
865
|
+
<div id="ollama-models" style="margin-bottom:20px;"></div>
|
|
866
|
+
<div id="ollama-tutorial" style="display:none;">
|
|
867
|
+
<div class="card" style="margin-bottom:20px;">
|
|
868
|
+
<h3 style="font-size:16px;margin-bottom:16px;">📖 3 步安装本地模型</h3>
|
|
869
|
+
<div class="tutorial-steps">
|
|
870
|
+
<div class="tutorial-step"><div class="tutorial-step-content"><h4>下载 Ollama</h4><p>访问 <a href="https://ollama.com" target="_blank">ollama.com</a> 下载安装包,支持 Windows / Mac / Linux</p></div></div>
|
|
871
|
+
<div class="tutorial-step"><div class="tutorial-step-content"><h4>安装并启动</h4><p>安装完成后,Ollama 会自动在后台运行</p></div></div>
|
|
872
|
+
<div class="tutorial-step"><div class="tutorial-step-content"><h4>拉取推荐模型</h4><p>打开终端,运行:<br><code>ollama pull qwen2.5:7b</code><br><code>ollama pull nomic-embed-text</code></p></div></div>
|
|
873
|
+
</div>
|
|
874
|
+
<button class="btn btn-primary btn-sm" onclick="detectOllama()" style="margin-top:8px;">🔄 重新检测</button>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
|
|
879
|
+
<!-- Cloud API Tab -->
|
|
880
|
+
<div class="tab-panel" id="mt-cloud">
|
|
881
|
+
<p style="color:var(--text-muted);font-size:13px;margin-bottom:16px;">填入 API Key 即可使用云端模型,按用量付费</p>
|
|
882
|
+
<div class="card-grid" id="cloud-providers">
|
|
883
|
+
<div class="card provider-card" onclick="configureProvider('openai')">
|
|
884
|
+
<div class="pv-header"><span style="font-size:20px;">🟢</span><span class="pv-name">OpenAI</span></div>
|
|
885
|
+
<p style="font-size:13px;color:var(--text-muted);">GPT-4o / GPT-4o-mini</p>
|
|
886
|
+
<div class="pv-status" id="pv-openai">未配置</div>
|
|
887
|
+
</div>
|
|
888
|
+
<div class="card provider-card" onclick="configureProvider('deepseek')">
|
|
889
|
+
<div class="pv-header"><span style="font-size:20px;">🔵</span><span class="pv-name">DeepSeek</span></div>
|
|
890
|
+
<p style="font-size:13px;color:var(--text-muted);">DeepSeek V3 / R1</p>
|
|
891
|
+
<div class="pv-status" id="pv-deepseek">未配置</div>
|
|
892
|
+
</div>
|
|
893
|
+
<div class="card provider-card" onclick="configureProvider('qwen')">
|
|
894
|
+
<div class="pv-header"><span style="font-size:20px;">🟣</span><span class="pv-name">通义千问</span></div>
|
|
895
|
+
<p style="font-size:13px;color:var(--text-muted);">Qwen-Max / Qwen-Plus</p>
|
|
896
|
+
<div class="pv-status" id="pv-qwen">未配置</div>
|
|
897
|
+
</div>
|
|
898
|
+
<div class="card provider-card" onclick="configureProvider('anthropic')">
|
|
899
|
+
<div class="pv-header"><span style="font-size:20px;">🟠</span><span class="pv-name">Anthropic</span></div>
|
|
900
|
+
<p style="font-size:13px;color:var(--text-muted);">Claude Sonnet / Haiku</p>
|
|
901
|
+
<div class="pv-status" id="pv-anthropic">未配置</div>
|
|
902
|
+
</div>
|
|
903
|
+
<div class="card provider-card" onclick="configureProvider('openrouter')">
|
|
904
|
+
<div class="pv-header"><span style="font-size:20px;">🌐</span><span class="pv-name">OpenRouter</span></div>
|
|
905
|
+
<p style="font-size:13px;color:var(--text-muted);">100+ 模型聚合</p>
|
|
906
|
+
<div class="pv-status" id="pv-openrouter">未配置</div>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
</div>
|
|
910
|
+
|
|
911
|
+
<!-- Model Assignment -->
|
|
912
|
+
<div class="card" style="margin-top:24px;">
|
|
913
|
+
<h3 style="font-size:16px;margin-bottom:16px;">🎯 模型用途分配</h3>
|
|
914
|
+
<div class="form-group">
|
|
915
|
+
<label class="label">聊天模型(必选)</label>
|
|
916
|
+
<select class="input" id="cfg-chat-model" onchange="saveModelAssignment()">
|
|
917
|
+
<option value="qwen2.5:7b">qwen2.5:7b (本地推荐) ⭐</option>
|
|
918
|
+
</select>
|
|
919
|
+
<p style="font-size:12px;color:var(--text-dim);margin-top:4px;">用于对话、回答问题、执行任务</p>
|
|
920
|
+
</div>
|
|
921
|
+
<div class="form-group" style="margin-bottom:0;">
|
|
922
|
+
<label class="label">Embedding 模型(记忆用)</label>
|
|
923
|
+
<select class="input" id="cfg-embed-model" onchange="saveModelAssignment()">
|
|
924
|
+
<option value="nomic-embed-text">nomic-embed-text (本地推荐) ⭐</option>
|
|
925
|
+
</select>
|
|
926
|
+
<p style="font-size:12px;color:var(--text-dim);margin-top:4px;">用于记忆存储和语义搜索,一般不需要更改</p>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
|
|
931
|
+
<!-- Channels Panel -->
|
|
932
|
+
<div class="settings-panel" id="sp-channels">
|
|
933
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">📡 渠道配置</h2>
|
|
934
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">连接你的聊天平台,让 Agent 在各个渠道工作</p>
|
|
935
|
+
<div class="card-grid" id="channels-grid"></div>
|
|
936
|
+
</div>
|
|
937
|
+
|
|
938
|
+
<!-- Memory Panel (DeepBrain) -->
|
|
939
|
+
<div class="settings-panel" id="sp-memory">
|
|
940
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">🧠 记忆管理</h2>
|
|
941
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">管理 Agent 的知识库和记忆,由 DeepBrain 提供</p>
|
|
942
|
+
<div id="memory-module-frame"></div>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
<!-- Role Panel (Workstation) -->
|
|
946
|
+
<div class="settings-panel" id="sp-role">
|
|
947
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">👤 角色编辑</h2>
|
|
948
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">编辑 Agent 的角色设定和技能,由 Workstation 提供</p>
|
|
949
|
+
<div id="role-module-frame"></div>
|
|
950
|
+
</div>
|
|
951
|
+
|
|
952
|
+
<!-- Status Panel -->
|
|
953
|
+
<div class="settings-panel" id="sp-status">
|
|
954
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">📊 运行状态</h2>
|
|
955
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">查看 Agent 和各模块的运行情况</p>
|
|
956
|
+
<div id="status-overview" style="margin-bottom:24px;"></div>
|
|
957
|
+
<div class="card" style="margin-bottom:16px;">
|
|
958
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
959
|
+
<h3 style="font-size:16px;">📋 最近日志</h3>
|
|
960
|
+
<button class="btn btn-secondary btn-sm" onclick="refreshStatus()">🔄 刷新</button>
|
|
961
|
+
</div>
|
|
962
|
+
<div class="log-viewer" id="status-logs">暂无日志</div>
|
|
963
|
+
</div>
|
|
964
|
+
</div>
|
|
965
|
+
|
|
966
|
+
<!-- Usage Panel -->
|
|
967
|
+
<div class="settings-panel" id="sp-usage">
|
|
968
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">💰 用量统计</h2>
|
|
969
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">查看 Token 消耗和费用估算</p>
|
|
970
|
+
<div id="usage-stats"></div>
|
|
971
|
+
</div>
|
|
972
|
+
|
|
973
|
+
<!-- Web Search Panel -->
|
|
974
|
+
<div class="settings-panel" id="sp-search">
|
|
975
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">🔍 搜索配置</h2>
|
|
976
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">Agent 可以自动搜索互联网回答问题,默认使用 DuckDuckGo(免费,无需配置)</p>
|
|
977
|
+
|
|
978
|
+
<div class="form-group">
|
|
979
|
+
<label class="label">搜索开关</label>
|
|
980
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
|
|
981
|
+
<input type="checkbox" id="search-enabled" checked onchange="updateSearchConfig()">
|
|
982
|
+
<span style="font-size:14px;">启用 Web 搜索</span>
|
|
983
|
+
</label>
|
|
984
|
+
</div>
|
|
985
|
+
|
|
986
|
+
<div class="form-group">
|
|
987
|
+
<label class="label">默认搜索引擎</label>
|
|
988
|
+
<select class="input" id="search-engine" onchange="updateSearchConfig()" style="padding:8px 12px;">
|
|
989
|
+
<option value="duckduckgo">🦆 DuckDuckGo(免费,默认)</option>
|
|
990
|
+
<option value="brave">🦁 Brave Search(需 API Key)</option>
|
|
991
|
+
<option value="searxng">🔧 SearXNG(自托管)</option>
|
|
992
|
+
<option value="google">🔍 Google Custom Search(需 API Key)</option>
|
|
993
|
+
</select>
|
|
994
|
+
</div>
|
|
995
|
+
|
|
996
|
+
<div class="form-group" id="search-apikey-group" style="display:none;">
|
|
997
|
+
<label class="label" id="search-apikey-label">API Key</label>
|
|
998
|
+
<input class="input" id="search-apikey" type="password" placeholder="输入 API Key" onchange="updateSearchConfig()">
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
<div class="form-group" id="search-baseurl-group" style="display:none;">
|
|
1002
|
+
<label class="label">SearXNG URL</label>
|
|
1003
|
+
<input class="input" id="search-baseurl" placeholder="https://searx.example.com" onchange="updateSearchConfig()">
|
|
1004
|
+
</div>
|
|
1005
|
+
|
|
1006
|
+
<div style="margin-top:16px;">
|
|
1007
|
+
<button class="btn btn-secondary btn-sm" onclick="testSearch()">🧪 测试搜索</button>
|
|
1008
|
+
</div>
|
|
1009
|
+
<div id="search-test-result" style="margin-top:12px;font-size:13px;"></div>
|
|
1010
|
+
|
|
1011
|
+
<div style="margin-top:24px;padding:16px;background:var(--bg-light);border-radius:8px;font-size:13px;color:var(--text-muted);">
|
|
1012
|
+
<strong>💡 提示</strong><br>
|
|
1013
|
+
Agent 会自动判断何时需要搜索,也可以通过 <code>web_search</code> 和 <code>web_read</code> 工具主动调用。<br>
|
|
1014
|
+
DuckDuckGo 完全免费、无需注册,适合大多数场景。
|
|
1015
|
+
</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
|
|
1018
|
+
</div>
|
|
387
1019
|
</div>
|
|
388
1020
|
</div>
|
|
389
1021
|
|
|
390
|
-
<!--
|
|
391
|
-
<div class="
|
|
392
|
-
<div class="
|
|
393
|
-
<div class="
|
|
394
|
-
<div class="
|
|
1022
|
+
<!-- Provider Config Dialog -->
|
|
1023
|
+
<div class="dialog-overlay" id="provider-dialog">
|
|
1024
|
+
<div class="dialog" style="max-width:480px;">
|
|
1025
|
+
<div class="dialog-title" id="pd-title">配置 Provider</div>
|
|
1026
|
+
<div class="dialog-desc" id="pd-desc">填入 API Key 即可开始使用</div>
|
|
1027
|
+
<div class="form-group">
|
|
1028
|
+
<label class="label">API Key</label>
|
|
1029
|
+
<input class="input" id="pd-apikey" type="password" placeholder="sk-...">
|
|
1030
|
+
</div>
|
|
1031
|
+
<div class="form-group" id="pd-baseurl-group" style="display:none;">
|
|
1032
|
+
<label class="label">自定义 Base URL(可选)</label>
|
|
1033
|
+
<input class="input" id="pd-baseurl" placeholder="https://api.example.com">
|
|
1034
|
+
</div>
|
|
1035
|
+
<div id="pd-test-result" style="margin-bottom:16px;font-size:13px;"></div>
|
|
1036
|
+
<div class="dialog-actions">
|
|
1037
|
+
<button class="btn btn-secondary btn-sm" onclick="closeProviderDialog()">取消</button>
|
|
1038
|
+
<button class="btn btn-secondary btn-sm" onclick="testProvider()">🔍 测试连接</button>
|
|
1039
|
+
<button class="btn btn-primary btn-sm" onclick="saveProvider()">💾 保存</button>
|
|
1040
|
+
</div>
|
|
395
1041
|
</div>
|
|
396
|
-
<div class="card"><pre id="logs-content" style="font-family:var(--mono);font-size:12px;max-height:600px;overflow:auto;color:var(--text-muted)">Loading...</pre></div>
|
|
397
1042
|
</div>
|
|
398
1043
|
|
|
399
|
-
<!--
|
|
400
|
-
<div class="
|
|
401
|
-
<div class="
|
|
402
|
-
<div class="
|
|
403
|
-
<div class="
|
|
1044
|
+
<!-- Channel Config Dialog -->
|
|
1045
|
+
<div class="dialog-overlay" id="channel-dialog">
|
|
1046
|
+
<div class="dialog" style="max-width:480px;">
|
|
1047
|
+
<div class="dialog-title" id="cd-title">配置渠道</div>
|
|
1048
|
+
<div class="dialog-desc" id="cd-desc"></div>
|
|
1049
|
+
<div id="cd-fields"></div>
|
|
1050
|
+
<div class="dialog-actions">
|
|
1051
|
+
<button class="btn btn-secondary btn-sm" onclick="closeChannelDialog()">取消</button>
|
|
1052
|
+
<button class="btn btn-primary btn-sm" onclick="saveChannel()">💾 保存</button>
|
|
1053
|
+
</div>
|
|
404
1054
|
</div>
|
|
405
|
-
<div id="modules-grid" class="card-grid">Loading...</div>
|
|
406
1055
|
</div>
|
|
407
1056
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1057
|
+
</div>
|
|
1058
|
+
</div>
|
|
1059
|
+
|
|
1060
|
+
<!-- Delete Confirm Dialog -->
|
|
1061
|
+
<div class="dialog-overlay" id="delete-dialog">
|
|
1062
|
+
<div class="dialog">
|
|
1063
|
+
<div class="dialog-title">Delete Agent?</div>
|
|
1064
|
+
<div class="dialog-desc">This action cannot be undone. The agent and all its data will be permanently removed.</div>
|
|
1065
|
+
<div class="dialog-actions">
|
|
1066
|
+
<button class="btn btn-secondary btn-sm" onclick="closeDeleteDialog()">Cancel</button>
|
|
1067
|
+
<button class="btn btn-danger btn-sm" onclick="confirmDelete()">Delete</button>
|
|
411
1068
|
</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
</div>
|
|
412
1071
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
1072
|
+
<!-- First Run Wizard Modal -->
|
|
1073
|
+
<div id="first-run-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:600;align-items:center;justify-content:center;">
|
|
1074
|
+
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:40px;max-width:580px;width:94%;max-height:90vh;overflow-y:auto;">
|
|
1075
|
+
<!-- Step indicators -->
|
|
1076
|
+
<div style="display:flex;justify-content:center;gap:6px;margin-bottom:32px;" id="fr-steps">
|
|
1077
|
+
<div class="wizard-step active" id="fr-step-1"><div class="step-num">1</div><span>Welcome</span></div>
|
|
1078
|
+
<div class="wizard-step" id="fr-step-2"><div class="step-line"></div><div class="step-num">2</div><span>Models</span></div>
|
|
1079
|
+
<div class="wizard-step" id="fr-step-3"><div class="step-line"></div><div class="step-num">3</div><span>Template</span></div>
|
|
1080
|
+
<div class="wizard-step" id="fr-step-4"><div class="step-line"></div><div class="step-num">4</div><span>Done</span></div>
|
|
416
1081
|
</div>
|
|
417
1082
|
|
|
418
|
-
<!--
|
|
419
|
-
<div class="
|
|
420
|
-
<
|
|
1083
|
+
<!-- Step 1: Welcome -->
|
|
1084
|
+
<div class="wizard-panel active" id="fr-panel-1">
|
|
1085
|
+
<div style="text-align:center;margin-bottom:32px;">
|
|
1086
|
+
<div style="font-size:64px;margin-bottom:16px;">⚡</div>
|
|
1087
|
+
<h2 style="font-size:24px;font-weight:700;margin-bottom:8px;">Welcome to OPC Studio</h2>
|
|
1088
|
+
<p style="color:var(--text-muted);font-size:15px;line-height:1.6;">Create AI agents in minutes — no coding required.<br>Let's get you set up in 3 quick steps.</p>
|
|
1089
|
+
</div>
|
|
1090
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:32px;">
|
|
1091
|
+
<div class="card" style="text-align:center;padding:16px 8px;"><div style="font-size:28px;margin-bottom:8px;">🤖</div><div style="font-size:13px;font-weight:500;">100+ Templates</div></div>
|
|
1092
|
+
<div class="card" style="text-align:center;padding:16px 8px;"><div style="font-size:28px;margin-bottom:8px;">🧠</div><div style="font-size:13px;font-weight:500;">Local AI Models</div></div>
|
|
1093
|
+
<div class="card" style="text-align:center;padding:16px 8px;"><div style="font-size:28px;margin-bottom:8px;">💬</div><div style="font-size:13px;font-weight:500;">Multi-channel</div></div>
|
|
1094
|
+
</div>
|
|
1095
|
+
<button class="btn btn-primary btn-lg" style="width:100%;" onclick="frNext()">Get Started →</button>
|
|
421
1096
|
</div>
|
|
422
1097
|
|
|
423
|
-
<!--
|
|
424
|
-
<div class="
|
|
425
|
-
<
|
|
426
|
-
|
|
427
|
-
|
|
1098
|
+
<!-- Step 2: Model Detection -->
|
|
1099
|
+
<div class="wizard-panel" id="fr-panel-2">
|
|
1100
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">🤖 Choose Your AI Model</h2>
|
|
1101
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">We recommend using free local models. Cloud APIs also supported.</p>
|
|
1102
|
+
<div id="fr-ollama-status" style="padding:16px;border-radius:var(--radius);margin-bottom:16px;background:var(--bg-hover);">
|
|
1103
|
+
<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot yellow"></span> Detecting Ollama...</div>
|
|
428
1104
|
</div>
|
|
429
|
-
<div style="display:
|
|
430
|
-
<div
|
|
431
|
-
<label
|
|
432
|
-
<select
|
|
433
|
-
<option>
|
|
1105
|
+
<div id="fr-model-choice" style="display:none;">
|
|
1106
|
+
<div class="form-group">
|
|
1107
|
+
<label class="label">Select a model to use</label>
|
|
1108
|
+
<select class="input" id="fr-model-select">
|
|
1109
|
+
<option value="qwen2.5:7b">qwen2.5:7b — Local, free ⭐ Recommended</option>
|
|
1110
|
+
<option value="gpt-4o-mini">GPT-4o Mini — Cloud, fast</option>
|
|
1111
|
+
<option value="gpt-4o">GPT-4o — Cloud, most capable</option>
|
|
1112
|
+
<option value="claude-sonnet-4">Claude Sonnet — Cloud, balanced</option>
|
|
1113
|
+
<option value="deepseek-v3">DeepSeek V3 — Cloud, affordable</option>
|
|
434
1114
|
</select>
|
|
435
1115
|
</div>
|
|
436
|
-
<div style="flex:0 0 160px;">
|
|
437
|
-
<label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px;">Temperature: <span id="pg-temp-val">0.7</span></label>
|
|
438
|
-
<input type="range" id="pg-temp" min="0" max="2" step="0.1" value="0.7" style="width:100%;" oninput="document.getElementById('pg-temp-val').textContent=this.value">
|
|
439
|
-
</div>
|
|
440
1116
|
</div>
|
|
441
|
-
<div style="margin-
|
|
442
|
-
<
|
|
443
|
-
<
|
|
1117
|
+
<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:24px;">
|
|
1118
|
+
<button class="btn btn-secondary" onclick="frBack()">← Back</button>
|
|
1119
|
+
<button class="btn btn-primary" onclick="frNext()">Next →</button>
|
|
444
1120
|
</div>
|
|
445
|
-
|
|
446
|
-
|
|
1121
|
+
</div>
|
|
1122
|
+
|
|
1123
|
+
<!-- Step 3: Choose Template -->
|
|
1124
|
+
<div class="wizard-panel" id="fr-panel-3">
|
|
1125
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">👤 Pick a Starting Template</h2>
|
|
1126
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">Choose what your first agent will do. You can always change this later.</p>
|
|
1127
|
+
<div style="display:flex;flex-direction:column;gap:8px;" id="fr-template-list">
|
|
1128
|
+
<div class="card" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;" onclick="frSelectTemplate('customer-service')" id="fr-tpl-customer-service">
|
|
1129
|
+
<span style="font-size:24px;">🎧</span><div><div style="font-weight:500;">Customer Service</div><div style="font-size:12px;color:var(--text-muted);">Answer questions, resolve issues</div></div>
|
|
1130
|
+
</div>
|
|
1131
|
+
<div class="card" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;" onclick="frSelectTemplate('executive-assistant')" id="fr-tpl-executive-assistant">
|
|
1132
|
+
<span style="font-size:24px;">💼</span><div><div style="font-weight:500;">Personal Assistant</div><div style="font-size:12px;color:var(--text-muted);">Scheduling, email drafting, planning</div></div>
|
|
1133
|
+
</div>
|
|
1134
|
+
<div class="card" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;" onclick="frSelectTemplate('content-writer')" id="fr-tpl-content-writer">
|
|
1135
|
+
<span style="font-size:24px;">✍️</span><div><div style="font-weight:500;">Content Writer</div><div style="font-size:12px;color:var(--text-muted);">Blog posts, social media, SEO</div></div>
|
|
1136
|
+
</div>
|
|
1137
|
+
<div class="card" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;" onclick="frSelectTemplate('data-analyst')" id="fr-tpl-data-analyst">
|
|
1138
|
+
<span style="font-size:24px;">📊</span><div><div style="font-weight:500;">Data Analyst</div><div style="font-size:12px;color:var(--text-muted);">Data querying, insights, reports</div></div>
|
|
1139
|
+
</div>
|
|
1140
|
+
<div class="card" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;" onclick="frSelectTemplate('teacher')" id="fr-tpl-teacher">
|
|
1141
|
+
<span style="font-size:24px;">📚</span><div><div style="font-weight:500;">Translator / Teacher</div><div style="font-size:12px;color:var(--text-muted);">Language learning, translation, explanation</div></div>
|
|
1142
|
+
</div>
|
|
447
1143
|
</div>
|
|
448
|
-
<div style="display:flex;gap:
|
|
449
|
-
<
|
|
450
|
-
<button onclick="
|
|
451
|
-
<button onclick="pgClear()" style="padding:10px 16px;border:1px solid var(--border);border-radius:8px;background:transparent;color:var(--text-muted);cursor:pointer;font-size:13px;">Clear</button>
|
|
1144
|
+
<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:20px;">
|
|
1145
|
+
<button class="btn btn-secondary" onclick="frBack()">← Back</button>
|
|
1146
|
+
<button class="btn btn-primary" onclick="frNext()" id="fr-next-3">Next →</button>
|
|
452
1147
|
</div>
|
|
453
1148
|
</div>
|
|
454
1149
|
|
|
455
|
-
|
|
1150
|
+
<!-- Step 4: Creating / Done -->
|
|
1151
|
+
<div class="wizard-panel" id="fr-panel-4">
|
|
1152
|
+
<div style="text-align:center;padding:20px 0;">
|
|
1153
|
+
<div id="fr-creating" style="">
|
|
1154
|
+
<div style="font-size:48px;margin-bottom:16px;">⏳</div>
|
|
1155
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Creating your agent...</h2>
|
|
1156
|
+
<p style="color:var(--text-muted);">Just a moment</p>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div id="fr-done" style="display:none;">
|
|
1159
|
+
<div style="font-size:64px;margin-bottom:16px;">🎉</div>
|
|
1160
|
+
<h2 style="font-size:22px;font-weight:700;margin-bottom:8px;">You're all set!</h2>
|
|
1161
|
+
<p style="color:var(--text-muted);margin-bottom:24px;">Your agent is ready. Start chatting now!</p>
|
|
1162
|
+
<button class="btn btn-primary btn-lg" onclick="frFinish()">Start Chatting →</button>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
456
1167
|
</div>
|
|
457
1168
|
|
|
458
1169
|
<script>
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1170
|
+
// === State ===
|
|
1171
|
+
let templates = [];
|
|
1172
|
+
let industries = [];
|
|
1173
|
+
let agents = [];
|
|
1174
|
+
let selectedTemplate = null;
|
|
1175
|
+
let currentAgent = null;
|
|
1176
|
+
let chatMessages = [];
|
|
1177
|
+
let wizardStep = 1;
|
|
1178
|
+
let selectedIndustry = '';
|
|
1179
|
+
let deleteTargetId = null;
|
|
1180
|
+
|
|
1181
|
+
const API = '';
|
|
1182
|
+
|
|
1183
|
+
// === Init ===
|
|
1184
|
+
async function init() {
|
|
1185
|
+
await Promise.all([loadTemplates(), loadAgents()]);
|
|
1186
|
+
handleRoute();
|
|
1187
|
+
window.addEventListener('popstate', handleRoute);
|
|
1188
|
+
checkFirstRun();
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function handleRoute() {
|
|
1192
|
+
const path = location.hash.slice(1) || '/dashboard';
|
|
1193
|
+
const parts = path.split('/').filter(Boolean);
|
|
1194
|
+
if (parts[0] === 'chat' && parts[1]) {
|
|
1195
|
+
openChat(parts[1]);
|
|
1196
|
+
} else if (parts[0] === 'settings') {
|
|
1197
|
+
if (parts[1]) currentSettingsTab = parts[1];
|
|
1198
|
+
navigate('settings');
|
|
1199
|
+
} else if (parts[0] === 'memory' && parts[1]) {
|
|
1200
|
+
openMemoryPage(parts[1]);
|
|
1201
|
+
} else if (parts[0] === 'create') {
|
|
1202
|
+
if (parts[1]) {
|
|
1203
|
+
// pre-select template
|
|
1204
|
+
selectedTemplate = templates.find(t => t.id === parts[1]) || null;
|
|
1205
|
+
if (selectedTemplate) {
|
|
1206
|
+
wizardStep = 2;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
showPage('create');
|
|
1210
|
+
renderWizard();
|
|
1211
|
+
} else {
|
|
1212
|
+
navigate(parts[0] || 'dashboard');
|
|
490
1213
|
}
|
|
491
1214
|
}
|
|
492
1215
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1216
|
+
// === API ===
|
|
1217
|
+
async function loadTemplates() {
|
|
1218
|
+
try {
|
|
1219
|
+
const res = await fetch(`${API}/api/templates`);
|
|
1220
|
+
const data = await res.json();
|
|
1221
|
+
templates = data.templates || [];
|
|
1222
|
+
industries = data.industries || [];
|
|
1223
|
+
renderIndustryChips();
|
|
1224
|
+
renderTemplates();
|
|
1225
|
+
} catch(e) { console.error('Failed to load templates:', e); }
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
async function loadAgents() {
|
|
1229
|
+
try {
|
|
1230
|
+
const res = await fetch(`${API}/api/agents`);
|
|
1231
|
+
const data = await res.json();
|
|
1232
|
+
agents = data.agents || [];
|
|
1233
|
+
renderAgents();
|
|
1234
|
+
} catch(e) { console.error('Failed to load agents:', e); }
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// === Navigation ===
|
|
1238
|
+
function navigate(page) {
|
|
1239
|
+
document.querySelectorAll('.page, .chat-container').forEach(p => { p.classList.remove('active'); p.style.display = ''; });
|
|
1240
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
1241
|
+
const navItem = document.querySelector(`.nav-item[data-page="${page}"]`);
|
|
1242
|
+
if (navItem) navItem.classList.add('active');
|
|
1243
|
+
|
|
1244
|
+
if (page === 'dashboard') { loadAgents(); loadHealthDashboard(); }
|
|
1245
|
+
if (page === 'create') { renderWizard(); renderWizardTemplates(); }
|
|
1246
|
+
if (page === 'settings') { showSettings(currentSettingsTab || 'models'); }
|
|
1247
|
+
if (page === 'schedules') { loadSchedules(); }
|
|
1248
|
+
if (page === 'skills') { loadSkillsMarketplace(); }
|
|
1249
|
+
|
|
1250
|
+
showPage(page);
|
|
1251
|
+
location.hash = `/${page}`;
|
|
1252
|
+
toggleSidebar(false);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function showPage(page) {
|
|
1256
|
+
document.querySelectorAll('.page, .chat-container').forEach(p => { p.classList.remove('active'); });
|
|
1257
|
+
const el = document.getElementById(`page-${page}`);
|
|
1258
|
+
if (el) el.classList.add('active');
|
|
496
1259
|
}
|
|
497
1260
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1261
|
+
function toggleSidebar(open) {
|
|
1262
|
+
document.querySelector('.sidebar').classList.toggle('open', open);
|
|
1263
|
+
document.querySelector('.sidebar-overlay').classList.toggle('show', open);
|
|
501
1264
|
}
|
|
502
1265
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1266
|
+
// === Templates Rendering ===
|
|
1267
|
+
function renderIndustryChips() {
|
|
1268
|
+
const html = `<span class="chip active" onclick="filterByIndustry('')">All</span>` +
|
|
1269
|
+
industries.map(i => `<span class="chip" onclick="filterByIndustry('${i.id}')">${i.nameZh} ${i.name}</span>`).join('');
|
|
1270
|
+
document.getElementById('industry-chips').innerHTML = html;
|
|
1271
|
+
document.getElementById('wizard-industry-chips').innerHTML = html.replace(/filterByIndustry/g, 'filterWizardByIndustry');
|
|
506
1272
|
}
|
|
507
1273
|
|
|
508
|
-
|
|
509
|
-
|
|
1274
|
+
function filterByIndustry(id) {
|
|
1275
|
+
selectedIndustry = id;
|
|
1276
|
+
document.querySelectorAll('#industry-chips .chip').forEach(c => c.classList.remove('active'));
|
|
1277
|
+
event.target.classList.add('active');
|
|
1278
|
+
renderTemplates();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function filterWizardByIndustry(id) {
|
|
1282
|
+
selectedIndustry = id;
|
|
1283
|
+
document.querySelectorAll('#wizard-industry-chips .chip').forEach(c => c.classList.remove('active'));
|
|
1284
|
+
event.target.classList.add('active');
|
|
1285
|
+
renderWizardTemplates();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function filterTemplates() {
|
|
1289
|
+
renderTemplates();
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// === Skills Marketplace ===
|
|
1293
|
+
let allSkills = [];
|
|
1294
|
+
let selectedSkillCategory = '';
|
|
1295
|
+
const SKILL_CATEGORIES = [
|
|
1296
|
+
{ id: '', label: 'All', labelZh: '全部' },
|
|
1297
|
+
{ id: 'productivity', label: 'Productivity', labelZh: '效率' },
|
|
1298
|
+
{ id: 'knowledge', label: 'Knowledge', labelZh: '知识' },
|
|
1299
|
+
{ id: 'creative', label: 'Creative', labelZh: '创作' },
|
|
1300
|
+
{ id: 'developer', label: 'Developer', labelZh: '开发' },
|
|
1301
|
+
{ id: 'lifestyle', label: 'Lifestyle', labelZh: '生活' },
|
|
1302
|
+
{ id: 'business', label: 'Business', labelZh: '业务' },
|
|
1303
|
+
];
|
|
1304
|
+
|
|
1305
|
+
async function loadSkillsMarketplace() {
|
|
510
1306
|
try {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1307
|
+
const res = await fetch('/api/skills/marketplace');
|
|
1308
|
+
allSkills = await res.json();
|
|
1309
|
+
} catch(e) { console.error('Failed to load skills:', e); allSkills = []; }
|
|
1310
|
+
renderSkillCategoryChips();
|
|
1311
|
+
renderSkills();
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function renderSkillCategoryChips() {
|
|
1315
|
+
const el = document.getElementById('skill-category-chips');
|
|
1316
|
+
if (!el) return;
|
|
1317
|
+
el.innerHTML = SKILL_CATEGORIES.map(c =>
|
|
1318
|
+
`<span class="chip ${selectedSkillCategory === c.id ? 'active' : ''}" onclick="selectSkillCategory('${c.id}')">${c.labelZh}</span>`
|
|
1319
|
+
).join('');
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function selectSkillCategory(cat) {
|
|
1323
|
+
selectedSkillCategory = cat;
|
|
1324
|
+
renderSkillCategoryChips();
|
|
1325
|
+
renderSkills();
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function filterSkills() {
|
|
1329
|
+
renderSkills();
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function renderSkills() {
|
|
1333
|
+
const q = (document.getElementById('skills-search')?.value || '').toLowerCase();
|
|
1334
|
+
let filtered = allSkills;
|
|
1335
|
+
if (selectedSkillCategory) {
|
|
1336
|
+
filtered = filtered.filter(s => s.category === selectedSkillCategory);
|
|
1337
|
+
}
|
|
1338
|
+
if (q) {
|
|
1339
|
+
filtered = filtered.filter(s =>
|
|
1340
|
+
s.name.toLowerCase().includes(q) || s.nameZh.includes(q) ||
|
|
1341
|
+
s.description.toLowerCase().includes(q) || s.descriptionZh.includes(q)
|
|
1342
|
+
);
|
|
525
1343
|
}
|
|
1344
|
+
const grid = document.getElementById('skills-grid');
|
|
1345
|
+
if (!grid) return;
|
|
1346
|
+
grid.innerHTML = filtered.map(s => `
|
|
1347
|
+
<div class="card" style="cursor:default;position:relative;">
|
|
1348
|
+
<div style="font-size:36px;margin-bottom:8px;">${s.icon}</div>
|
|
1349
|
+
<div style="font-weight:600;font-size:15px;">${s.nameZh}</div>
|
|
1350
|
+
<div style="font-size:12px;color:var(--text-dim);margin-bottom:4px;">${s.name}</div>
|
|
1351
|
+
<div style="font-size:13px;color:var(--text-muted);margin-bottom:12px;line-height:1.4;">${s.descriptionZh}</div>
|
|
1352
|
+
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;">
|
|
1353
|
+
${s.tools.slice(0,3).map(t => `<span style="font-size:11px;padding:2px 6px;background:var(--bg-hover);border-radius:4px;color:var(--text-dim);">${t}</span>`).join('')}
|
|
1354
|
+
${s.tools.length > 3 ? `<span style="font-size:11px;color:var(--text-dim);">+${s.tools.length-3}</span>` : ''}
|
|
1355
|
+
</div>
|
|
1356
|
+
${s.installed
|
|
1357
|
+
? `<button class="btn" style="width:100%;background:var(--bg-hover);color:var(--text-muted);cursor:pointer;" onclick="uninstallSkill('${s.id}',this)">✓ Installed</button>`
|
|
1358
|
+
: `<button class="btn btn-primary" style="width:100%;" onclick="installSkill('${s.id}',this)">Install</button>`
|
|
1359
|
+
}
|
|
1360
|
+
</div>
|
|
1361
|
+
`).join('');
|
|
526
1362
|
}
|
|
527
1363
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const input = document.getElementById('chat-input');
|
|
531
|
-
const msg = input.value.trim();
|
|
532
|
-
if (!msg) return;
|
|
533
|
-
input.value = '';
|
|
534
|
-
|
|
535
|
-
const messages = document.getElementById('chat-messages');
|
|
536
|
-
messages.innerHTML += `<div class="message message-user"><div class="message-label">You</div><div class="bubble">${escapeHtml(msg)}</div></div>`;
|
|
537
|
-
messages.innerHTML += `<div class="message message-agent" id="pending"><div class="message-label">Agent</div><div class="bubble"><span class="spinner"></span> Thinking...</div></div>`;
|
|
538
|
-
messages.scrollTop = messages.scrollHeight;
|
|
539
|
-
|
|
1364
|
+
async function installSkill(id, btn) {
|
|
1365
|
+
btn.disabled = true; btn.textContent = 'Installing...';
|
|
540
1366
|
try {
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1367
|
+
const res = await fetch(\`/api/skills/marketplace/\${id}/install\`, { method: 'POST' });
|
|
1368
|
+
const data = await res.json();
|
|
1369
|
+
if (data.success) {
|
|
1370
|
+
const skill = allSkills.find(s => s.id === id);
|
|
1371
|
+
if (skill) skill.installed = true;
|
|
1372
|
+
renderSkills();
|
|
1373
|
+
}
|
|
1374
|
+
} catch(e) { console.error(e); btn.disabled = false; btn.textContent = 'Install'; }
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
async function uninstallSkill(id, btn) {
|
|
1378
|
+
btn.disabled = true; btn.textContent = 'Removing...';
|
|
1379
|
+
try {
|
|
1380
|
+
const res = await fetch(\`/api/skills/marketplace/\${id}/uninstall\`, { method: 'DELETE' });
|
|
1381
|
+
const data = await res.json();
|
|
1382
|
+
if (data.success) {
|
|
1383
|
+
const skill = allSkills.find(s => s.id === id);
|
|
1384
|
+
if (skill) skill.installed = false;
|
|
1385
|
+
renderSkills();
|
|
1386
|
+
}
|
|
1387
|
+
} catch(e) { console.error(e); btn.disabled = false; btn.textContent = '✓ Installed'; }
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function filterWizardTemplates() {
|
|
1391
|
+
renderWizardTemplates();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function getFilteredTemplates(searchId) {
|
|
1395
|
+
const q = (document.getElementById(searchId)?.value || '').toLowerCase();
|
|
1396
|
+
return templates.filter(t => {
|
|
1397
|
+
if (selectedIndustry && t.industry !== selectedIndustry) return false;
|
|
1398
|
+
if (q && !t.name.toLowerCase().includes(q) && !t.nameZh.includes(q) && !t.description.toLowerCase().includes(q)) return false;
|
|
1399
|
+
return true;
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function renderTemplates() {
|
|
1404
|
+
const filtered = getFilteredTemplates('tpl-search');
|
|
1405
|
+
document.getElementById('templates-grid').innerHTML = filtered.map(t => `
|
|
1406
|
+
<div class="card tpl-card" onclick="selectTemplateAndCreate('${t.id}')">
|
|
1407
|
+
<div class="tpl-icon">${t.icon}</div>
|
|
1408
|
+
<div class="tpl-name">${t.name}</div>
|
|
1409
|
+
<div style="font-size:13px;color:var(--text-dim);margin-bottom:6px;">${t.nameZh}</div>
|
|
1410
|
+
<div class="tpl-desc">${t.description}</div>
|
|
1411
|
+
<div class="tpl-tags">
|
|
1412
|
+
<span class="tpl-tag">${t.industryZh}</span>
|
|
1413
|
+
${t.tags.map(tag => `<span class="tpl-tag">${tag}</span>`).join('')}
|
|
1414
|
+
</div>
|
|
572
1415
|
</div>
|
|
573
1416
|
`).join('');
|
|
574
1417
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
<div
|
|
584
|
-
<div class="memory-preview">${escapeHtml((r.compiled_truth || r.content || '').slice(0, 100))}</div>
|
|
1418
|
+
|
|
1419
|
+
function renderWizardTemplates() {
|
|
1420
|
+
const filtered = getFilteredTemplates('wizard-tpl-search');
|
|
1421
|
+
document.getElementById('wizard-tpl-grid').innerHTML = filtered.map(t => `
|
|
1422
|
+
<div class="card tpl-card ${selectedTemplate?.id === t.id ? 'selected' : ''}" onclick="selectWizardTemplate('${t.id}')"
|
|
1423
|
+
style="${selectedTemplate?.id === t.id ? 'border-color:var(--accent);background:var(--accent-light);' : ''}">
|
|
1424
|
+
<div class="tpl-icon">${t.icon}</div>
|
|
1425
|
+
<div class="tpl-name">${t.name}</div>
|
|
1426
|
+
<div style="font-size:12px;color:var(--text-dim);">${t.nameZh} · ${t.industryZh}</div>
|
|
585
1427
|
</div>
|
|
586
1428
|
`).join('');
|
|
587
1429
|
}
|
|
588
1430
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1431
|
+
function selectTemplateAndCreate(id) {
|
|
1432
|
+
selectedTemplate = templates.find(t => t.id === id);
|
|
1433
|
+
wizardStep = 2;
|
|
1434
|
+
navigate('create');
|
|
593
1435
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1436
|
+
|
|
1437
|
+
function selectWizardTemplate(id) {
|
|
1438
|
+
selectedTemplate = templates.find(t => t.id === id);
|
|
1439
|
+
renderWizardTemplates();
|
|
1440
|
+
// Auto-advance after selection
|
|
1441
|
+
setTimeout(() => wizardNext(), 300);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// === Wizard ===
|
|
1445
|
+
function renderWizard() {
|
|
1446
|
+
for (let i = 1; i <= 3; i++) {
|
|
1447
|
+
const ws = document.getElementById(`ws-${i}`);
|
|
1448
|
+
const wp = document.getElementById(`wp-${i}`);
|
|
1449
|
+
ws.className = 'wizard-step' + (i < wizardStep ? ' done' : i === wizardStep ? ' active' : '');
|
|
1450
|
+
wp.className = 'wizard-panel' + (i === wizardStep ? ' active' : '');
|
|
1451
|
+
}
|
|
1452
|
+
if (wizardStep === 2 && selectedTemplate) {
|
|
1453
|
+
document.getElementById('agent-name').placeholder = selectedTemplate.name;
|
|
1454
|
+
document.getElementById('agent-model').value = selectedTemplate.suggestedModel;
|
|
1455
|
+
}
|
|
1456
|
+
if (wizardStep === 3) {
|
|
1457
|
+
renderConfirmCard();
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function wizardNext() {
|
|
1462
|
+
if (wizardStep === 1 && !selectedTemplate) { alert('Please select a template first'); return; }
|
|
1463
|
+
if (wizardStep < 3) { wizardStep++; renderWizard(); }
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function wizardBack() {
|
|
1467
|
+
if (wizardStep > 1) { wizardStep--; renderWizard(); }
|
|
597
1468
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1469
|
+
|
|
1470
|
+
function renderConfirmCard() {
|
|
1471
|
+
const name = document.getElementById('agent-name').value || selectedTemplate?.name || 'My Agent';
|
|
1472
|
+
const model = document.getElementById('agent-model').value;
|
|
1473
|
+
const lang = document.getElementById('agent-lang').selectedOptions[0]?.text || 'English';
|
|
1474
|
+
document.getElementById('confirm-card').innerHTML = `
|
|
1475
|
+
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px;">
|
|
1476
|
+
<span style="font-size:48px;">${selectedTemplate?.icon || '🤖'}</span>
|
|
1477
|
+
<div>
|
|
1478
|
+
<div style="font-size:20px;font-weight:700;">${name}</div>
|
|
1479
|
+
<div style="color:var(--text-muted);font-size:14px;">Based on: ${selectedTemplate?.name || 'Custom'} (${selectedTemplate?.nameZh || ''})</div>
|
|
1480
|
+
</div>
|
|
1481
|
+
</div>
|
|
1482
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:14px;">
|
|
1483
|
+
<div><span style="color:var(--text-dim);">Model:</span> ${model}</div>
|
|
1484
|
+
<div><span style="color:var(--text-dim);">Language:</span> ${lang}</div>
|
|
1485
|
+
<div style="grid-column:span 2;"><span style="color:var(--text-dim);">Industry:</span> ${selectedTemplate?.industryZh || ''} (${selectedTemplate?.industry || ''})</div>
|
|
1486
|
+
</div>
|
|
1487
|
+
`;
|
|
601
1488
|
}
|
|
602
|
-
|
|
603
|
-
|
|
1489
|
+
|
|
1490
|
+
async function createAgent() {
|
|
1491
|
+
const btn = document.getElementById('create-btn');
|
|
1492
|
+
btn.textContent = '⏳ Creating...';
|
|
1493
|
+
btn.disabled = true;
|
|
604
1494
|
try {
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const data = await api('protocols');
|
|
627
|
-
const list = data.protocols || [];
|
|
628
|
-
const content = document.getElementById('content');
|
|
629
|
-
content.innerHTML = `<h2>📡 Protocols</h2><div class="grid">${list.map(p =>
|
|
630
|
-
`<div class="card stat"><div class="stat-value">${p.enabled ? '🟢' : '⚫'} ${p.name}</div><div class="stat-label">${p.description}</div></div>`
|
|
631
|
-
).join('')}</div>`;
|
|
632
|
-
}
|
|
633
|
-
async function loadLogs() {
|
|
634
|
-
const data = await api('logs/recent');
|
|
635
|
-
document.getElementById('logs-content').textContent = (data.lines || []).join('\n') || 'No logs';
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Modules
|
|
639
|
-
async function loadModules() {
|
|
640
|
-
const data = await api('modules');
|
|
641
|
-
const grid = document.getElementById('modules-grid');
|
|
642
|
-
if (data.modules) {
|
|
643
|
-
grid.innerHTML = data.modules.map(m => `
|
|
644
|
-
<div class="card stat" style="cursor:pointer" onclick="document.querySelector('[data-page=${m.path.replace(/\//g,'')}-module]')?.click()">
|
|
645
|
-
<div class="stat-value">${m.icon} ${m.name}</div>
|
|
646
|
-
<div class="stat-label">${m.running ? '🟢 Running on port ' + m.port : '⚫ Not running'}</div>
|
|
647
|
-
</div>`).join('');
|
|
648
|
-
} else {
|
|
649
|
-
grid.innerHTML = '<div class="card">Failed to load module status</div>';
|
|
1495
|
+
const res = await fetch(`${API}/api/agents`, {
|
|
1496
|
+
method: 'POST',
|
|
1497
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1498
|
+
body: JSON.stringify({
|
|
1499
|
+
name: document.getElementById('agent-name').value || selectedTemplate?.name,
|
|
1500
|
+
templateId: selectedTemplate?.id,
|
|
1501
|
+
description: document.getElementById('agent-desc').value,
|
|
1502
|
+
model: document.getElementById('agent-model').value,
|
|
1503
|
+
language: document.getElementById('agent-lang').value,
|
|
1504
|
+
}),
|
|
1505
|
+
});
|
|
1506
|
+
const agent = await res.json();
|
|
1507
|
+
// Reset wizard
|
|
1508
|
+
wizardStep = 1;
|
|
1509
|
+
selectedTemplate = null;
|
|
1510
|
+
document.getElementById('agent-name').value = '';
|
|
1511
|
+
document.getElementById('agent-desc').value = '';
|
|
1512
|
+
// Navigate to chat
|
|
1513
|
+
openChat(agent.id);
|
|
1514
|
+
} catch(e) {
|
|
1515
|
+
alert('Failed to create agent: ' + e.message);
|
|
650
1516
|
}
|
|
1517
|
+
btn.textContent = '🚀 Create Agent';
|
|
1518
|
+
btn.disabled = false;
|
|
651
1519
|
}
|
|
652
1520
|
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
document.getElementById('
|
|
658
|
-
|
|
659
|
-
document.getElementById('tel-stat-errors').textContent = (stats.errorRate * 100).toFixed(1) + '%';
|
|
660
|
-
document.getElementById('tel-stat-p95').textContent = stats.p95Latency.toFixed(0) + 'ms';
|
|
1521
|
+
// === Dashboard ===
|
|
1522
|
+
function renderAgents() {
|
|
1523
|
+
if (agents.length === 0) {
|
|
1524
|
+
document.getElementById('agents-list').style.display = 'none';
|
|
1525
|
+
document.getElementById('agents-empty').style.display = 'block';
|
|
1526
|
+
return;
|
|
661
1527
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1528
|
+
document.getElementById('agents-list').style.display = '';
|
|
1529
|
+
document.getElementById('agents-empty').style.display = 'none';
|
|
1530
|
+
document.getElementById('agents-list').innerHTML = agents.map(a => {
|
|
1531
|
+
const timeAgo = getTimeAgo(a.lastActive || a.created);
|
|
1532
|
+
return `
|
|
1533
|
+
<div class="card agent-card" onclick="openChat('${a.id}')">
|
|
1534
|
+
<div class="agent-actions">
|
|
1535
|
+
<button onclick="event.stopPropagation();openDeleteDialog('${a.id}')">🗑️</button>
|
|
1536
|
+
</div>
|
|
1537
|
+
<div class="agent-icon">${a.templateIcon || '🤖'}</div>
|
|
1538
|
+
<div class="agent-name">${a.name}</div>
|
|
1539
|
+
<div class="agent-template">${a.templateName || 'Custom'}</div>
|
|
1540
|
+
<div class="agent-stats">
|
|
1541
|
+
<span>💬 ${a.messageCount || 0} messages</span>
|
|
1542
|
+
<span>⏰ ${timeAgo}</span>
|
|
1543
|
+
</div>
|
|
1544
|
+
</div>
|
|
1545
|
+
`;
|
|
1546
|
+
}).join('');
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function getTimeAgo(dateStr) {
|
|
1550
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
1551
|
+
const mins = Math.floor(diff / 60000);
|
|
1552
|
+
if (mins < 1) return 'just now';
|
|
1553
|
+
if (mins < 60) return `${mins}m ago`;
|
|
1554
|
+
const hours = Math.floor(mins / 60);
|
|
1555
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1556
|
+
const days = Math.floor(hours / 24);
|
|
1557
|
+
return `${days}d ago`;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// === Delete ===
|
|
1561
|
+
function openDeleteDialog(id) { deleteTargetId = id; document.getElementById('delete-dialog').classList.add('show'); }
|
|
1562
|
+
function closeDeleteDialog() { deleteTargetId = null; document.getElementById('delete-dialog').classList.remove('show'); }
|
|
1563
|
+
async function confirmDelete() {
|
|
1564
|
+
if (!deleteTargetId) return;
|
|
1565
|
+
await fetch(`${API}/api/agents/${deleteTargetId}`, { method: 'DELETE' });
|
|
1566
|
+
closeDeleteDialog();
|
|
1567
|
+
loadAgents();
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// === Chat ===
|
|
1571
|
+
async function openLastChat() {
|
|
1572
|
+
if (currentAgent) { openChat(currentAgent.id); return; }
|
|
1573
|
+
const agentsRes = await fetch(`${API}/api/agents`).catch(() => null);
|
|
1574
|
+
if (agentsRes) {
|
|
1575
|
+
const data = await agentsRes.json().catch(() => ({}));
|
|
1576
|
+
const list = data.agents || [];
|
|
1577
|
+
if (list.length > 0) { openChat(list[0].id); return; }
|
|
673
1578
|
}
|
|
1579
|
+
navigate('dashboard');
|
|
674
1580
|
}
|
|
675
1581
|
|
|
676
|
-
async function
|
|
677
|
-
|
|
678
|
-
const el = document.getElementById('tel-waterfall');
|
|
679
|
-
if (!data.spans || data.spans.length === 0) { el.innerHTML = 'No spans found'; return; }
|
|
680
|
-
const spans = data.spans;
|
|
681
|
-
const minTime = Math.min(...spans.map(s => s.startTime));
|
|
682
|
-
const maxTime = Math.max(...spans.map(s => s.endTime || s.startTime));
|
|
683
|
-
const totalDur = maxTime - minTime || 1;
|
|
684
|
-
const depthMap = {};
|
|
685
|
-
spans.forEach(s => { depthMap[s.spanId] = s.parentSpanId && depthMap[s.parentSpanId] !== undefined ? depthMap[s.parentSpanId] + 1 : 0; });
|
|
686
|
-
el.innerHTML = spans.map(s => {
|
|
687
|
-
const left = ((s.startTime - minTime) / totalDur * 70).toFixed(1);
|
|
688
|
-
const width = Math.max(1, ((s.endTime || s.startTime) - s.startTime) / totalDur * 70).toFixed(1);
|
|
689
|
-
const color = s.status === 'ok' ? '#4ade80' : s.status === 'error' ? '#f87171' : '#60a5fa';
|
|
690
|
-
const depth = depthMap[s.spanId] || 0;
|
|
691
|
-
const dur = s.endTime ? (s.endTime - s.startTime) + 'ms' : '—';
|
|
692
|
-
return `<div style="display:flex;align-items:center;margin:2px 0;padding-left:${depth*20}px"><span style="width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.name}</span><div style="flex:1;position:relative;height:18px;background:var(--bg-hover);border-radius:3px"><div style="position:absolute;left:${left}%;width:${width}%;height:100%;background:${color};border-radius:3px;opacity:0.8" title="${dur}"></div></div><span style="width:60px;text-align:right;flex-shrink:0;margin-left:8px">${dur}</span></div>`;
|
|
693
|
-
}).join('');
|
|
1582
|
+
async function switchChatAgent(agentId) {
|
|
1583
|
+
if (agentId && agentId !== currentAgent?.id) openChat(agentId);
|
|
694
1584
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1585
|
+
|
|
1586
|
+
async function openChat(agentId) {
|
|
1587
|
+
try {
|
|
1588
|
+
const res = await fetch(`${API}/api/agents/${agentId}`);
|
|
1589
|
+
currentAgent = await res.json();
|
|
1590
|
+
if (currentAgent.error) { navigate('dashboard'); return; }
|
|
1591
|
+
} catch { navigate('dashboard'); return; }
|
|
1592
|
+
|
|
1593
|
+
// Load history from localStorage, fallback to empty
|
|
1594
|
+
const stored = localStorage.getItem(`opc-chat-${agentId}`);
|
|
1595
|
+
chatMessages = stored ? JSON.parse(stored) : [];
|
|
1596
|
+
|
|
1597
|
+
document.getElementById('chat-agent-icon').textContent = currentAgent.templateIcon || '🤖';
|
|
1598
|
+
document.getElementById('chat-agent-name').textContent = currentAgent.name;
|
|
1599
|
+
document.getElementById('chat-agent-status').textContent = `${currentAgent.templateName || 'Custom'} · ${currentAgent.model}`;
|
|
1600
|
+
|
|
1601
|
+
// Populate agent selector
|
|
1602
|
+
const sel = document.getElementById('chat-agent-select');
|
|
1603
|
+
sel.innerHTML = agents.map(a => `<option value="${a.id}" ${a.id === agentId ? 'selected' : ''}>${a.templateIcon || '🤖'} ${a.name}</option>`).join('');
|
|
1604
|
+
|
|
1605
|
+
// Render messages
|
|
1606
|
+
const msgEl = document.getElementById('chat-messages');
|
|
1607
|
+
if (chatMessages.length > 0) {
|
|
1608
|
+
msgEl.innerHTML = chatMessages.map(m => `
|
|
1609
|
+
<div class="msg ${m.role}">
|
|
1610
|
+
<div class="msg-bubble">${m.content.replace(/</g,'<')}</div>
|
|
709
1611
|
</div>
|
|
710
|
-
`).join('')
|
|
1612
|
+
`).join('');
|
|
711
1613
|
} else {
|
|
712
|
-
|
|
1614
|
+
msgEl.innerHTML = `
|
|
1615
|
+
<div class="msg assistant">
|
|
1616
|
+
<div class="msg-bubble">Hello! I'm ${currentAgent.name}. ${currentAgent.description ? 'I specialize in: ' + currentAgent.description : 'How can I help you today?'}</div>
|
|
1617
|
+
</div>
|
|
1618
|
+
`;
|
|
713
1619
|
}
|
|
1620
|
+
document.getElementById('chat-input').value = '';
|
|
1621
|
+
|
|
1622
|
+
showPage('chat');
|
|
1623
|
+
location.hash = `/chat/${agentId}`;
|
|
1624
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
1625
|
+
const chatNav = document.querySelector('.nav-item[data-page="chat"]');
|
|
1626
|
+
if (chatNav) chatNav.classList.add('active');
|
|
1627
|
+
msgEl.scrollTop = msgEl.scrollHeight;
|
|
1628
|
+
document.getElementById('chat-input').focus();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function clearChat() {
|
|
1632
|
+
if (!currentAgent) return;
|
|
1633
|
+
chatMessages = [];
|
|
1634
|
+
localStorage.removeItem(`opc-chat-${currentAgent.id}`);
|
|
1635
|
+
document.getElementById('chat-messages').innerHTML = `
|
|
1636
|
+
<div class="msg assistant">
|
|
1637
|
+
<div class="msg-bubble">Hello! I'm ${currentAgent.name}. How can I help you today?</div>
|
|
1638
|
+
</div>
|
|
1639
|
+
`;
|
|
714
1640
|
}
|
|
715
1641
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1642
|
+
async function handleDocUpload(input) {
|
|
1643
|
+
const file = input.files[0];
|
|
1644
|
+
if (!file || !currentAgent) return;
|
|
1645
|
+
input.value = '';
|
|
1646
|
+
|
|
1647
|
+
// Show uploading status in chat
|
|
1648
|
+
appendMessage('user', `📎 Uploading: ${file.name}`);
|
|
1649
|
+
const statusEl = appendMessage('assistant', '⏳ Processing document...');
|
|
1650
|
+
|
|
721
1651
|
try {
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
html += `<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px">${r.caseId}</td><td style="color:${color};text-align:center">${r.passed ? 'PASS' : 'FAIL'}</td><td style="text-align:center">${r.scores?.latency_ms || 0}ms</td></tr>`;
|
|
730
|
-
}
|
|
731
|
-
html += '</table>';
|
|
732
|
-
el.innerHTML = html;
|
|
733
|
-
} catch (e) { el.innerHTML = `<div style="color:#f87171">Error: ${e.message}</div>`; }
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function renderList(id, items, renderFn) {
|
|
737
|
-
const el = document.getElementById(id);
|
|
738
|
-
if (!items.length) { el.innerHTML = '<div class="loading">None configured</div>'; return; }
|
|
739
|
-
el.innerHTML = '<table class="table"><tbody>' +
|
|
740
|
-
items.map(i => `<tr><td>${renderFn(i)}</td></tr>`).join('') +
|
|
741
|
-
'</tbody></table>';
|
|
742
|
-
}
|
|
743
|
-
function escapeHtml(s) {
|
|
744
|
-
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// ═══════════════════════════════════════════════
|
|
748
|
-
// DAG Visual Editor
|
|
749
|
-
// ═══════════════════════════════════════════════
|
|
750
|
-
const dagEditor = (() => {
|
|
751
|
-
let canvas, ctx;
|
|
752
|
-
let nodes = [], edges = [];
|
|
753
|
-
let selected = null, dragging = null, dragOffset = { x: 0, y: 0 };
|
|
754
|
-
let connecting = null; // { nodeId, port:'out', mx, my }
|
|
755
|
-
let pan = { x: 0, y: 0 }, zoom = 1;
|
|
756
|
-
let isPanning = false, panStart = { x: 0, y: 0 };
|
|
757
|
-
let undoStack = [], redoStack = [];
|
|
758
|
-
let workflowId = null, workflowName = 'Untitled';
|
|
759
|
-
let initialized = false;
|
|
760
|
-
const GRID = 20, NODE_W = 160, NODE_H = 60, PORT_R = 7;
|
|
761
|
-
const NODE_COLORS = {
|
|
762
|
-
input: '#22c55e', output: '#ef4444', agent: '#3b82f6',
|
|
763
|
-
tool: '#eab308', condition: '#a855f7', loop: '#f97316', parallel: '#06b6d4'
|
|
764
|
-
};
|
|
765
|
-
const NODE_ICONS = {
|
|
766
|
-
input: '📥', output: '📤', agent: '🤖', tool: '🔧',
|
|
767
|
-
condition: '❓', loop: '🔁', parallel: '⚡'
|
|
768
|
-
};
|
|
1652
|
+
const formData = new FormData();
|
|
1653
|
+
formData.append('file', file);
|
|
1654
|
+
|
|
1655
|
+
const res = await fetch(`${API}/api/agents/${currentAgent.id}/upload`, {
|
|
1656
|
+
method: 'POST',
|
|
1657
|
+
body: formData,
|
|
1658
|
+
});
|
|
769
1659
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const rect = canvas.parentElement.getBoundingClientRect();
|
|
776
|
-
canvas.width = rect.width * (window.devicePixelRatio || 1);
|
|
777
|
-
canvas.height = 600 * (window.devicePixelRatio || 1);
|
|
778
|
-
canvas.style.width = rect.width + 'px';
|
|
779
|
-
canvas.style.height = '600px';
|
|
780
|
-
ctx = canvas.getContext('2d');
|
|
781
|
-
ctx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
|
|
782
|
-
|
|
783
|
-
if (!initialized) {
|
|
784
|
-
canvas.addEventListener('mousedown', onMouseDown);
|
|
785
|
-
canvas.addEventListener('mousemove', onMouseMove);
|
|
786
|
-
canvas.addEventListener('mouseup', onMouseUp);
|
|
787
|
-
canvas.addEventListener('dblclick', onDblClick);
|
|
788
|
-
canvas.addEventListener('wheel', onWheel);
|
|
789
|
-
canvas.addEventListener('dragover', e => e.preventDefault());
|
|
790
|
-
canvas.addEventListener('drop', onDrop);
|
|
791
|
-
canvas.addEventListener('contextmenu', e => e.preventDefault());
|
|
792
|
-
document.addEventListener('keydown', onKey);
|
|
793
|
-
initialized = true;
|
|
1660
|
+
const data = await res.json();
|
|
1661
|
+
if (data.error) {
|
|
1662
|
+
statusEl.textContent = `❌ ${data.error}`;
|
|
1663
|
+
} else {
|
|
1664
|
+
statusEl.textContent = `✅ Learned ${data.learnedCount} knowledge chunks from "${file.name}"`;
|
|
794
1665
|
}
|
|
795
|
-
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
statusEl.textContent = `❌ Upload failed: ${e.message}`;
|
|
796
1668
|
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
async function sendMessage() {
|
|
1672
|
+
const input = document.getElementById('chat-input');
|
|
1673
|
+
const text = input.value.trim();
|
|
1674
|
+
if (!text || !currentAgent) return;
|
|
1675
|
+
|
|
1676
|
+
input.value = '';
|
|
1677
|
+
chatMessages.push({ role: 'user', content: text });
|
|
1678
|
+
|
|
1679
|
+
// Render user message
|
|
1680
|
+
appendMessage('user', text);
|
|
1681
|
+
|
|
1682
|
+
// Show typing + streaming indicator
|
|
1683
|
+
document.getElementById('typing-indicator').classList.add('show');
|
|
1684
|
+
document.getElementById('streaming-indicator').style.display = 'inline';
|
|
1685
|
+
const msgContainer = document.getElementById('chat-messages');
|
|
1686
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1687
|
+
|
|
1688
|
+
try {
|
|
1689
|
+
const res = await fetch(`${API}/api/agents/${currentAgent.id}/chat`, {
|
|
1690
|
+
method: 'POST',
|
|
1691
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1692
|
+
body: JSON.stringify({ messages: chatMessages }),
|
|
1693
|
+
});
|
|
797
1694
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1695
|
+
document.getElementById('typing-indicator').classList.remove('show');
|
|
1696
|
+
|
|
1697
|
+
if (res.headers.get('content-type')?.includes('text/event-stream')) {
|
|
1698
|
+
// SSE streaming
|
|
1699
|
+
const reader = res.body.getReader();
|
|
1700
|
+
const decoder = new TextDecoder();
|
|
1701
|
+
let assistantText = '';
|
|
1702
|
+
const bubbleEl = appendMessage('assistant', '');
|
|
1703
|
+
|
|
1704
|
+
while (true) {
|
|
1705
|
+
const { done, value } = await reader.read();
|
|
1706
|
+
if (done) break;
|
|
1707
|
+
const chunk = decoder.decode(value);
|
|
1708
|
+
const lines = chunk.split('\n');
|
|
1709
|
+
for (const line of lines) {
|
|
1710
|
+
if (line.startsWith('data: ')) {
|
|
1711
|
+
const data = line.slice(6);
|
|
1712
|
+
if (data === '[DONE]') break;
|
|
1713
|
+
try {
|
|
1714
|
+
const parsed = JSON.parse(data);
|
|
1715
|
+
const content = parsed.choices?.[0]?.delta?.content || parsed.content || '';
|
|
1716
|
+
assistantText += content;
|
|
1717
|
+
bubbleEl.textContent = assistantText;
|
|
1718
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1719
|
+
} catch {}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
chatMessages.push({ role: 'assistant', content: assistantText });
|
|
1724
|
+
} else {
|
|
1725
|
+
const data = await res.json();
|
|
1726
|
+
const reply = data.response || data.error || 'No response';
|
|
1727
|
+
appendMessage('assistant', reply);
|
|
1728
|
+
chatMessages.push({ role: 'assistant', content: reply });
|
|
1729
|
+
}
|
|
1730
|
+
// Persist to localStorage
|
|
1731
|
+
if (currentAgent) {
|
|
1732
|
+
try { localStorage.setItem(`opc-chat-${currentAgent.id}`, JSON.stringify(chatMessages.slice(-100))); } catch {}
|
|
1733
|
+
}
|
|
1734
|
+
} catch(e) {
|
|
1735
|
+
document.getElementById('typing-indicator').classList.remove('show');
|
|
1736
|
+
appendMessage('assistant', `Error: ${e.message}`);
|
|
1737
|
+
} finally {
|
|
1738
|
+
document.getElementById('streaming-indicator').style.display = 'none';
|
|
801
1739
|
}
|
|
1740
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function appendMessage(role, text) {
|
|
1744
|
+
const msgContainer = document.getElementById('chat-messages');
|
|
1745
|
+
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1746
|
+
const div = document.createElement('div');
|
|
1747
|
+
div.className = `msg ${role}`;
|
|
1748
|
+
const bubble = document.createElement('div');
|
|
1749
|
+
bubble.className = 'msg-bubble';
|
|
1750
|
+
bubble.textContent = text;
|
|
1751
|
+
div.appendChild(bubble);
|
|
1752
|
+
const timeEl = document.createElement('div');
|
|
1753
|
+
timeEl.className = 'msg-time';
|
|
1754
|
+
timeEl.textContent = time;
|
|
1755
|
+
div.appendChild(timeEl);
|
|
1756
|
+
msgContainer.appendChild(div);
|
|
1757
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1758
|
+
return bubble;
|
|
1759
|
+
}
|
|
802
1760
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1761
|
+
// === Memory ===
|
|
1762
|
+
function openMemory() {
|
|
1763
|
+
if (currentAgent) openMemoryPage(currentAgent.id);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
async function openMemoryPage(agentId) {
|
|
1767
|
+
showPage('memory');
|
|
1768
|
+
location.hash = `/memory/${agentId}`;
|
|
1769
|
+
try {
|
|
1770
|
+
const res = await fetch(`${API}/api/agents/${agentId}/memory`);
|
|
1771
|
+
const data = await res.json();
|
|
1772
|
+
if (data.entries && data.entries.length > 0) {
|
|
1773
|
+
document.getElementById('memory-empty').style.display = 'none';
|
|
1774
|
+
document.getElementById('memory-timeline').innerHTML = `
|
|
1775
|
+
<div class="timeline">
|
|
1776
|
+
${data.entries.map(e => `
|
|
1777
|
+
<div class="timeline-item">
|
|
1778
|
+
<div class="timeline-date">${new Date(e.timestamp).toLocaleDateString()} ${new Date(e.timestamp).toLocaleTimeString()}</div>
|
|
1779
|
+
<div class="timeline-content">${e.summary || e.content || 'Learned something new'}</div>
|
|
1780
|
+
</div>
|
|
1781
|
+
`).join('')}
|
|
1782
|
+
</div>
|
|
1783
|
+
`;
|
|
1784
|
+
} else {
|
|
1785
|
+
document.getElementById('memory-empty').style.display = 'block';
|
|
1786
|
+
document.getElementById('memory-timeline').innerHTML = '';
|
|
807
1787
|
}
|
|
808
|
-
|
|
1788
|
+
} catch {
|
|
1789
|
+
document.getElementById('memory-empty').style.display = 'block';
|
|
809
1790
|
}
|
|
1791
|
+
}
|
|
810
1792
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1793
|
+
function navigateToChat() {
|
|
1794
|
+
if (currentAgent) openChat(currentAgent.id);
|
|
1795
|
+
else navigate('dashboard');
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// === Settings ===
|
|
1799
|
+
let currentSettingsTab = 'models';
|
|
1800
|
+
let currentProvider = null;
|
|
1801
|
+
let currentChannel = null;
|
|
1802
|
+
let modelConfig = {};
|
|
1803
|
+
let statusRefreshTimer = null;
|
|
1804
|
+
|
|
1805
|
+
function showSettings(tab) {
|
|
1806
|
+
currentSettingsTab = tab;
|
|
1807
|
+
document.querySelectorAll('.settings-nav-item').forEach(n => n.classList.remove('active'));
|
|
1808
|
+
document.querySelector(`.settings-nav-item[data-settings="${tab}"]`)?.classList.add('active');
|
|
1809
|
+
document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
|
|
1810
|
+
document.getElementById(`sp-${tab}`)?.classList.add('active');
|
|
1811
|
+
|
|
1812
|
+
if (tab === 'models') initModelsPanel();
|
|
1813
|
+
if (tab === 'channels') initChannelsPanel();
|
|
1814
|
+
if (tab === 'memory') initMemoryPanel();
|
|
1815
|
+
if (tab === 'role') initRolePanel();
|
|
1816
|
+
if (tab === 'status') refreshStatus();
|
|
1817
|
+
if (tab === 'usage') refreshUsage();
|
|
1818
|
+
if (tab === 'search') initSearchPanel();
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function switchModelTab(tab) {
|
|
1822
|
+
document.querySelectorAll('#sp-models .tab').forEach(t => t.classList.remove('active'));
|
|
1823
|
+
document.querySelectorAll('#sp-models .tab-panel').forEach(p => p.classList.remove('active'));
|
|
1824
|
+
if (tab === 'local') {
|
|
1825
|
+
document.querySelector('#sp-models .tab:first-child').classList.add('active');
|
|
1826
|
+
document.getElementById('mt-local').classList.add('active');
|
|
1827
|
+
} else {
|
|
1828
|
+
document.querySelector('#sp-models .tab:last-child').classList.add('active');
|
|
1829
|
+
document.getElementById('mt-cloud').classList.add('active');
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// --- Models Panel ---
|
|
1834
|
+
async function initModelsPanel() {
|
|
1835
|
+
try {
|
|
1836
|
+
const res = await fetch(`${API}/api/settings/models`);
|
|
1837
|
+
modelConfig = await res.json();
|
|
1838
|
+
} catch { modelConfig = {}; }
|
|
1839
|
+
detectOllama();
|
|
1840
|
+
updateProviderStatuses();
|
|
1841
|
+
updateModelDropdowns();
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
async function detectOllama() {
|
|
1845
|
+
const statusEl = document.getElementById('ollama-status');
|
|
1846
|
+
const modelsEl = document.getElementById('ollama-models');
|
|
1847
|
+
const tutorialEl = document.getElementById('ollama-tutorial');
|
|
1848
|
+
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot yellow"></span> 正在检测本地 Ollama...</div>';
|
|
1849
|
+
try {
|
|
1850
|
+
const res = await fetch(`${API}/api/settings/models/local`);
|
|
1851
|
+
const data = await res.json();
|
|
1852
|
+
if (data.running) {
|
|
1853
|
+
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot green"></span> <b>Ollama 运行中</b> — 本地模型可用,完全免费</div>';
|
|
1854
|
+
tutorialEl.style.display = 'none';
|
|
1855
|
+
if (data.models && data.models.length > 0) {
|
|
1856
|
+
modelsEl.innerHTML = '<div class="card"><h3 style="font-size:15px;margin-bottom:12px;">已安装的模型</h3>' +
|
|
1857
|
+
data.models.map(m => {
|
|
1858
|
+
const size = m.size ? `${(m.size / 1e9).toFixed(1)}GB` : '';
|
|
1859
|
+
const isChat = modelConfig.chatModel === m.name;
|
|
1860
|
+
const isEmbed = modelConfig.embeddingModel === m.name;
|
|
1861
|
+
const badge = isChat ? ' <span style="color:var(--accent);font-size:11px;">● 聊天</span>' : isEmbed ? ' <span style="color:var(--green);font-size:11px;">● 记忆</span>' : '';
|
|
1862
|
+
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border);">
|
|
1863
|
+
<div><span style="font-weight:500;">${m.name}</span>${badge}</div>
|
|
1864
|
+
<span style="font-size:12px;color:var(--text-dim);">${size}</span>
|
|
1865
|
+
</div>`;
|
|
1866
|
+
}).join('') + '</div>';
|
|
1867
|
+
// Update dropdowns with local models
|
|
1868
|
+
updateModelDropdowns(data.models);
|
|
1869
|
+
} else {
|
|
1870
|
+
modelsEl.innerHTML = '<div class="card"><p style="color:var(--text-muted);font-size:14px;">Ollama 已运行但没有安装任何模型。请在终端运行:<br><code style="background:var(--bg-hover);padding:2px 8px;border-radius:4px;font-family:var(--mono);">ollama pull qwen2.5:7b</code></p></div>';
|
|
822
1871
|
}
|
|
1872
|
+
} else {
|
|
1873
|
+
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot red"></span> <b>Ollama 未运行</b> — 按照下面的教程安装</div>';
|
|
1874
|
+
modelsEl.innerHTML = '';
|
|
1875
|
+
tutorialEl.style.display = 'block';
|
|
823
1876
|
}
|
|
824
|
-
|
|
1877
|
+
} catch {
|
|
1878
|
+
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot red"></span> 无法检测 Ollama</div>';
|
|
1879
|
+
tutorialEl.style.display = 'block';
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function updateModelDropdowns(localModels) {
|
|
1884
|
+
const chatSel = document.getElementById('cfg-chat-model');
|
|
1885
|
+
const embedSel = document.getElementById('cfg-embed-model');
|
|
1886
|
+
if (localModels && localModels.length > 0) {
|
|
1887
|
+
const chatOpts = localModels.map(m => `<option value="${m.name}" ${modelConfig.chatModel === m.name ? 'selected' : ''}>${m.name}${m.name === 'qwen2.5:7b' ? ' ⭐ 推荐' : ''}</option>`).join('');
|
|
1888
|
+
const embedOpts = localModels.map(m => `<option value="${m.name}" ${modelConfig.embeddingModel === m.name ? 'selected' : ''}>${m.name}${m.name === 'nomic-embed-text' ? ' ⭐ 推荐' : ''}</option>`).join('');
|
|
1889
|
+
chatSel.innerHTML = chatOpts;
|
|
1890
|
+
embedSel.innerHTML = embedOpts;
|
|
825
1891
|
}
|
|
1892
|
+
// Add cloud models if configured
|
|
1893
|
+
const providers = modelConfig.providers || {};
|
|
1894
|
+
const cloudModels = [];
|
|
1895
|
+
if (providers.openai?.apiKey) cloudModels.push({name:'gpt-4o',label:'GPT-4o (OpenAI)'},{name:'gpt-4o-mini',label:'GPT-4o Mini (OpenAI)'});
|
|
1896
|
+
if (providers.deepseek?.apiKey) cloudModels.push({name:'deepseek-chat',label:'DeepSeek V3'},{name:'deepseek-reasoner',label:'DeepSeek R1'});
|
|
1897
|
+
if (providers.anthropic?.apiKey) cloudModels.push({name:'claude-sonnet-4-20250514',label:'Claude Sonnet (Anthropic)'});
|
|
1898
|
+
if (providers.openrouter?.apiKey) cloudModels.push({name:'openrouter/auto',label:'OpenRouter Auto'});
|
|
1899
|
+
cloudModels.forEach(m => {
|
|
1900
|
+
chatSel.innerHTML += `<option value="${m.name}" ${modelConfig.chatModel === m.name ? 'selected' : ''}>${m.label}</option>`;
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function updateProviderStatuses() {
|
|
1905
|
+
const providers = modelConfig.providers || {};
|
|
1906
|
+
['openai','deepseek','qwen','anthropic','openrouter'].forEach(p => {
|
|
1907
|
+
const el = document.getElementById(`pv-${p}`);
|
|
1908
|
+
if (!el) return;
|
|
1909
|
+
if (providers[p]?.apiKey) {
|
|
1910
|
+
el.innerHTML = '<span style="color:var(--green);">✅ 已配置</span>';
|
|
1911
|
+
el.closest('.provider-card')?.classList.add('configured');
|
|
1912
|
+
} else {
|
|
1913
|
+
el.innerHTML = '未配置';
|
|
1914
|
+
el.closest('.provider-card')?.classList.remove('configured');
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
826
1918
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1919
|
+
async function saveModelAssignment() {
|
|
1920
|
+
const chatModel = document.getElementById('cfg-chat-model').value;
|
|
1921
|
+
const embeddingModel = document.getElementById('cfg-embed-model').value;
|
|
1922
|
+
try {
|
|
1923
|
+
await fetch(`${API}/api/settings/models`, {
|
|
1924
|
+
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
1925
|
+
body: JSON.stringify({ chatModel, embeddingModel })
|
|
1926
|
+
});
|
|
1927
|
+
modelConfig.chatModel = chatModel;
|
|
1928
|
+
modelConfig.embeddingModel = embeddingModel;
|
|
1929
|
+
} catch {}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// --- Provider Dialog ---
|
|
1933
|
+
const PROVIDER_INFO = {
|
|
1934
|
+
openai: { name: 'OpenAI', desc: '需要 OpenAI 账号。获取 Key: platform.openai.com/api-keys', placeholder: 'sk-...' },
|
|
1935
|
+
deepseek: { name: 'DeepSeek', desc: '国产大模型,性价比极高。获取 Key: platform.deepseek.com', placeholder: 'sk-...' },
|
|
1936
|
+
qwen: { name: '通义千问', desc: '阿里云大模型。获取 Key: dashscope.console.aliyun.com', placeholder: 'sk-...' },
|
|
1937
|
+
anthropic: { name: 'Anthropic', desc: 'Claude 系列模型。获取 Key: console.anthropic.com', placeholder: 'sk-ant-...' },
|
|
1938
|
+
openrouter: { name: 'OpenRouter', desc: '100+ 模型聚合平台。获取 Key: openrouter.ai/keys', placeholder: 'sk-or-...' },
|
|
1939
|
+
};
|
|
1940
|
+
|
|
1941
|
+
function configureProvider(provider) {
|
|
1942
|
+
currentProvider = provider;
|
|
1943
|
+
const info = PROVIDER_INFO[provider] || {};
|
|
1944
|
+
document.getElementById('pd-title').textContent = `配置 ${info.name || provider}`;
|
|
1945
|
+
document.getElementById('pd-desc').textContent = info.desc || '';
|
|
1946
|
+
document.getElementById('pd-apikey').placeholder = info.placeholder || 'API Key';
|
|
1947
|
+
document.getElementById('pd-apikey').value = modelConfig.providers?.[provider]?.apiKey || '';
|
|
1948
|
+
document.getElementById('pd-baseurl').value = modelConfig.providers?.[provider]?.baseUrl || '';
|
|
1949
|
+
document.getElementById('pd-test-result').innerHTML = '';
|
|
1950
|
+
document.getElementById('pd-baseurl-group').style.display = (provider === 'qwen' || provider === 'openrouter') ? 'block' : 'none';
|
|
1951
|
+
document.getElementById('provider-dialog').classList.add('show');
|
|
1952
|
+
}
|
|
1953
|
+
function closeProviderDialog() { document.getElementById('provider-dialog').classList.remove('show'); currentProvider = null; }
|
|
1954
|
+
|
|
1955
|
+
async function testProvider() {
|
|
1956
|
+
const apiKey = document.getElementById('pd-apikey').value.trim();
|
|
1957
|
+
const baseUrl = document.getElementById('pd-baseurl').value.trim();
|
|
1958
|
+
const resultEl = document.getElementById('pd-test-result');
|
|
1959
|
+
if (!apiKey) { resultEl.innerHTML = '<span style="color:var(--yellow);">请先填入 API Key</span>'; return; }
|
|
1960
|
+
resultEl.innerHTML = '<span style="color:var(--text-muted);">⏳ 测试中...</span>';
|
|
1961
|
+
try {
|
|
1962
|
+
const res = await fetch(`${API}/api/settings/models/test`, {
|
|
1963
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
1964
|
+
body: JSON.stringify({ provider: currentProvider, apiKey, baseUrl: baseUrl || undefined })
|
|
1965
|
+
});
|
|
1966
|
+
const data = await res.json();
|
|
1967
|
+
resultEl.innerHTML = data.success ? '<span style="color:var(--green);">✅ 连接成功!</span>' : `<span style="color:var(--red);">❌ 连接失败 (${data.error || data.statusCode})</span>`;
|
|
1968
|
+
} catch(e) {
|
|
1969
|
+
resultEl.innerHTML = `<span style="color:var(--red);">❌ 网络错误</span>`;
|
|
831
1970
|
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
async function saveProvider() {
|
|
1974
|
+
const apiKey = document.getElementById('pd-apikey').value.trim();
|
|
1975
|
+
const baseUrl = document.getElementById('pd-baseurl').value.trim();
|
|
1976
|
+
if (!modelConfig.providers) modelConfig.providers = {};
|
|
1977
|
+
modelConfig.providers[currentProvider] = { apiKey, baseUrl: baseUrl || undefined };
|
|
1978
|
+
try {
|
|
1979
|
+
await fetch(`${API}/api/settings/models`, {
|
|
1980
|
+
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
1981
|
+
body: JSON.stringify({ providers: modelConfig.providers })
|
|
1982
|
+
});
|
|
1983
|
+
} catch {}
|
|
1984
|
+
updateProviderStatuses();
|
|
1985
|
+
updateModelDropdowns();
|
|
1986
|
+
closeProviderDialog();
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// --- Channels Panel ---
|
|
1990
|
+
const CHANNELS = [
|
|
1991
|
+
{ id: 'telegram', name: 'Telegram', icon: '✈️', fields: [{key:'botToken',label:'Bot Token',placeholder:'123456:ABC-DEF...',help:'从 @BotFather 获取。<a href="https://t.me/botfather" target="_blank">打开 BotFather →</a>'}] },
|
|
1992
|
+
{ id: 'wechat', name: '微信', icon: '💬', fields: [], comingSoon: true },
|
|
1993
|
+
{ id: 'feishu', name: '飞书', icon: '🐦', fields: [{key:'appId',label:'App ID',placeholder:'cli_...'},{key:'appSecret',label:'App Secret',placeholder:'',type:'password'}] },
|
|
1994
|
+
{ id: 'discord', name: 'Discord', icon: '🎮', fields: [{key:'botToken',label:'Bot Token',placeholder:'',type:'password'}] },
|
|
1995
|
+
{ id: 'slack', name: 'Slack', icon: '💼', fields: [{key:'botToken',label:'Bot Token',placeholder:'xoxb-...',type:'password'}] },
|
|
1996
|
+
{ id: 'email', name: 'Email', icon: '📧', fields: [{key:'imapHost',label:'IMAP Host',placeholder:'imap.gmail.com'},{key:'smtpHost',label:'SMTP Host',placeholder:'smtp.gmail.com'},{key:'email',label:'Email',placeholder:'agent@example.com'},{key:'password',label:'Password',placeholder:'',type:'password'}] },
|
|
1997
|
+
{ id: 'web', name: 'Web', icon: '🌐', fields: [], alwaysOn: true },
|
|
1998
|
+
{ id: 'whatsapp', name: 'WhatsApp', icon: '📱', fields: [{key:'phoneId',label:'Phone Number ID',placeholder:''},{key:'accessToken',label:'Access Token',placeholder:'',type:'password'}] },
|
|
1999
|
+
];
|
|
2000
|
+
|
|
2001
|
+
let channelConfigs = {};
|
|
2002
|
+
|
|
2003
|
+
async function initChannelsPanel() {
|
|
2004
|
+
try {
|
|
2005
|
+
const res = await fetch(`${API}/api/settings/channels`);
|
|
2006
|
+
channelConfigs = await res.json();
|
|
2007
|
+
} catch { channelConfigs = {}; }
|
|
2008
|
+
renderChannels();
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function renderChannels() {
|
|
2012
|
+
document.getElementById('channels-grid').innerHTML = CHANNELS.map(ch => {
|
|
2013
|
+
const cfg = channelConfigs[ch.id] || {};
|
|
2014
|
+
const connected = ch.alwaysOn || (cfg && Object.keys(cfg).some(k => k !== 'updated' && cfg[k]));
|
|
2015
|
+
const statusDot = ch.comingSoon ? 'yellow' : connected ? 'green' : 'red';
|
|
2016
|
+
const statusText = ch.comingSoon ? '即将支持' : connected ? '已连接' : '未配置';
|
|
2017
|
+
return `<div class="card channel-card" onclick="${ch.comingSoon ? '' : `configureChannel('${ch.id}')`}" style="${ch.comingSoon ? 'opacity:0.6;cursor:default;' : ''}">
|
|
2018
|
+
<div class="ch-icon">${ch.icon}</div>
|
|
2019
|
+
<div class="ch-info">
|
|
2020
|
+
<div class="ch-name">${ch.name}</div>
|
|
2021
|
+
<div class="ch-status"><span class="status-dot ${statusDot}"></span> ${statusText}</div>
|
|
2022
|
+
</div>
|
|
2023
|
+
${!ch.comingSoon && !ch.alwaysOn ? '<span style="color:var(--text-dim);font-size:18px;">›</span>' : ''}
|
|
2024
|
+
${ch.alwaysOn ? '<span style="font-size:12px;color:var(--green);">默认开启</span>' : ''}
|
|
2025
|
+
</div>`;
|
|
2026
|
+
}).join('');
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function configureChannel(chId) {
|
|
2030
|
+
const ch = CHANNELS.find(c => c.id === chId);
|
|
2031
|
+
if (!ch || ch.comingSoon) return;
|
|
2032
|
+
if (ch.alwaysOn) return;
|
|
2033
|
+
currentChannel = chId;
|
|
2034
|
+
const cfg = channelConfigs[chId] || {};
|
|
2035
|
+
document.getElementById('cd-title').textContent = `配置 ${ch.name}`;
|
|
2036
|
+
document.getElementById('cd-desc').textContent = '';
|
|
2037
|
+
document.getElementById('cd-fields').innerHTML = ch.fields.map(f =>
|
|
2038
|
+
`<div class="form-group">
|
|
2039
|
+
<label class="label">${f.label}</label>
|
|
2040
|
+
<input class="input" id="cf-${f.key}" type="${f.type || 'text'}" placeholder="${f.placeholder || ''}" value="${cfg[f.key] || ''}">
|
|
2041
|
+
${f.help ? `<p style="font-size:12px;color:var(--text-dim);margin-top:4px;">${f.help}</p>` : ''}
|
|
2042
|
+
</div>`
|
|
2043
|
+
).join('');
|
|
2044
|
+
document.getElementById('channel-dialog').classList.add('show');
|
|
2045
|
+
}
|
|
2046
|
+
function closeChannelDialog() { document.getElementById('channel-dialog').classList.remove('show'); currentChannel = null; }
|
|
2047
|
+
|
|
2048
|
+
async function saveChannel() {
|
|
2049
|
+
const ch = CHANNELS.find(c => c.id === currentChannel);
|
|
2050
|
+
if (!ch) return;
|
|
2051
|
+
const cfg = {};
|
|
2052
|
+
ch.fields.forEach(f => { cfg[f.key] = document.getElementById(`cf-${f.key}`)?.value?.trim() || ''; });
|
|
2053
|
+
try {
|
|
2054
|
+
await fetch(`${API}/api/settings/channels/${currentChannel}`, {
|
|
2055
|
+
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
2056
|
+
body: JSON.stringify(cfg)
|
|
2057
|
+
});
|
|
2058
|
+
channelConfigs[currentChannel] = cfg;
|
|
2059
|
+
} catch {}
|
|
2060
|
+
renderChannels();
|
|
2061
|
+
closeChannelDialog();
|
|
2062
|
+
}
|
|
832
2063
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
2064
|
+
// --- Memory Panel (DeepBrain iframe) ---
|
|
2065
|
+
async function initMemoryPanel() {
|
|
2066
|
+
const container = document.getElementById('memory-module-frame');
|
|
2067
|
+
const running = await checkModulePort(4001);
|
|
2068
|
+
if (running) {
|
|
2069
|
+
container.innerHTML = `<div class="module-frame-container"><iframe src="http://localhost:4001" title="DeepBrain 记忆管理"></iframe></div>`;
|
|
2070
|
+
} else {
|
|
2071
|
+
container.innerHTML = `<div class="card module-frame-fallback">
|
|
2072
|
+
<div class="mf-icon">🧠</div>
|
|
2073
|
+
<h3 style="margin-bottom:8px;">DeepBrain 未运行</h3>
|
|
2074
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:16px;">记忆管理由 DeepBrain 模块提供(端口 4001)</p>
|
|
2075
|
+
<a href="http://localhost:4001" target="_blank" class="btn btn-primary">🔗 打开记忆管理</a>
|
|
2076
|
+
<p style="color:var(--text-dim);font-size:12px;margin-top:12px;">如果按钮无法打开,请先启动 DeepBrain 服务</p>
|
|
2077
|
+
</div>`;
|
|
846
2078
|
}
|
|
2079
|
+
}
|
|
847
2080
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
2081
|
+
// --- Role Panel (Workstation iframe) ---
|
|
2082
|
+
async function initRolePanel() {
|
|
2083
|
+
const container = document.getElementById('role-module-frame');
|
|
2084
|
+
const running = await checkModulePort(4003);
|
|
2085
|
+
if (running) {
|
|
2086
|
+
container.innerHTML = `<div class="module-frame-container"><iframe src="http://localhost:4003" title="Workstation 角色编辑"></iframe></div>`;
|
|
2087
|
+
} else {
|
|
2088
|
+
container.innerHTML = `<div class="card module-frame-fallback">
|
|
2089
|
+
<div class="mf-icon">👤</div>
|
|
2090
|
+
<h3 style="margin-bottom:8px;">Workstation 未运行</h3>
|
|
2091
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:16px;">角色编辑由 Workstation 模块提供(端口 4003)</p>
|
|
2092
|
+
<a href="http://localhost:4003" target="_blank" class="btn btn-primary">🔗 打开角色编辑</a>
|
|
2093
|
+
<p style="color:var(--text-dim);font-size:12px;margin-top:12px;">如果按钮无法打开,请先启动 Workstation 服务</p>
|
|
2094
|
+
</div>`;
|
|
854
2095
|
}
|
|
2096
|
+
}
|
|
855
2097
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
ctx.scale(zoom, zoom);
|
|
865
|
-
|
|
866
|
-
// Grid
|
|
867
|
-
ctx.strokeStyle = '#1a1a1a';
|
|
868
|
-
ctx.lineWidth = 0.5;
|
|
869
|
-
const gs = GRID;
|
|
870
|
-
const startX = Math.floor(-pan.x / zoom / gs) * gs - gs;
|
|
871
|
-
const startY = Math.floor(-pan.y / zoom / gs) * gs - gs;
|
|
872
|
-
const endX = startX + w / zoom + gs * 2;
|
|
873
|
-
const endY = startY + h / zoom + gs * 2;
|
|
874
|
-
for (let x = startX; x < endX; x += gs) { ctx.beginPath(); ctx.moveTo(x, startY); ctx.lineTo(x, endY); ctx.stroke(); }
|
|
875
|
-
for (let y = startY; y < endY; y += gs) { ctx.beginPath(); ctx.moveTo(startX, y); ctx.lineTo(endX, y); ctx.stroke(); }
|
|
876
|
-
|
|
877
|
-
// Edges
|
|
878
|
-
for (const e of edges) {
|
|
879
|
-
const from = nodes.find(n => n.id === e.from);
|
|
880
|
-
const to = nodes.find(n => n.id === e.to);
|
|
881
|
-
if (!from || !to) continue;
|
|
882
|
-
const x1 = from.x + NODE_W, y1 = from.y + NODE_H / 2;
|
|
883
|
-
const x2 = to.x, y2 = to.y + NODE_H / 2;
|
|
884
|
-
const cp = Math.abs(x2 - x1) * 0.5 + 40;
|
|
885
|
-
ctx.beginPath();
|
|
886
|
-
ctx.moveTo(x1, y1);
|
|
887
|
-
ctx.bezierCurveTo(x1 + cp, y1, x2 - cp, y2, x2, y2);
|
|
888
|
-
ctx.strokeStyle = '#555';
|
|
889
|
-
ctx.lineWidth = 2;
|
|
890
|
-
ctx.stroke();
|
|
891
|
-
// Arrow
|
|
892
|
-
const angle = Math.atan2(y2 - (y2 - 0.1), x2 - (x2 - cp * 0.1));
|
|
893
|
-
ctx.fillStyle = '#555';
|
|
894
|
-
ctx.beginPath();
|
|
895
|
-
ctx.moveTo(x2, y2);
|
|
896
|
-
ctx.lineTo(x2 - 8, y2 - 4);
|
|
897
|
-
ctx.lineTo(x2 - 8, y2 + 4);
|
|
898
|
-
ctx.fill();
|
|
899
|
-
}
|
|
2098
|
+
async function checkModulePort(port) {
|
|
2099
|
+
try {
|
|
2100
|
+
const res = await fetch(`${API}/api/modules`);
|
|
2101
|
+
const data = await res.json();
|
|
2102
|
+
const mod = (data.modules || []).find(m => m.port === port);
|
|
2103
|
+
return mod?.running || false;
|
|
2104
|
+
} catch { return false; }
|
|
2105
|
+
}
|
|
900
2106
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
2107
|
+
// --- Status Panel ---
|
|
2108
|
+
async function refreshStatus() {
|
|
2109
|
+
try {
|
|
2110
|
+
const res = await fetch(`${API}/api/settings/status`);
|
|
2111
|
+
const data = await res.json();
|
|
2112
|
+
|
|
2113
|
+
// Overview cards
|
|
2114
|
+
const upHrs = Math.floor(data.uptime / 3600);
|
|
2115
|
+
const upMins = Math.floor((data.uptime % 3600) / 60);
|
|
2116
|
+
const memMB = Math.round((data.memory?.rss || 0) / 1048576);
|
|
2117
|
+
|
|
2118
|
+
document.getElementById('status-overview').innerHTML = `
|
|
2119
|
+
<div class="card-grid" style="margin-bottom:16px;">
|
|
2120
|
+
<div class="card stat-card">
|
|
2121
|
+
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px;">
|
|
2122
|
+
<span class="status-dot green"></span><span style="font-size:14px;font-weight:600;">运行中</span>
|
|
2123
|
+
</div>
|
|
2124
|
+
<div class="stat-value">${upHrs}h ${upMins}m</div>
|
|
2125
|
+
<div class="stat-label">运行时间</div>
|
|
2126
|
+
</div>
|
|
2127
|
+
<div class="card stat-card">
|
|
2128
|
+
<div class="stat-value">${memMB} MB</div>
|
|
2129
|
+
<div class="stat-label">内存占用</div>
|
|
2130
|
+
</div>
|
|
2131
|
+
<div class="card stat-card">
|
|
2132
|
+
<div class="stat-value">${(data.modules || []).filter(m => m.running).length}/${(data.modules || []).length}</div>
|
|
2133
|
+
<div class="stat-label">模块在线</div>
|
|
2134
|
+
</div>
|
|
2135
|
+
</div>
|
|
2136
|
+
<div class="card" style="margin-bottom:16px;">
|
|
2137
|
+
<h3 style="font-size:15px;margin-bottom:12px;">模块状态</h3>
|
|
2138
|
+
${(data.modules || []).map(m => `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;">
|
|
2139
|
+
<span class="status-dot ${m.running ? 'green' : 'red'}"></span>
|
|
2140
|
+
<span>${m.icon} ${m.name}</span>
|
|
2141
|
+
<span style="color:var(--text-dim);font-size:12px;margin-left:auto;">:${m.port}</span>
|
|
2142
|
+
</div>`).join('')}
|
|
2143
|
+
</div>
|
|
2144
|
+
`;
|
|
917
2145
|
|
|
918
|
-
//
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
ctx.roundRect(n.x + 2, n.y + 2, NODE_W, NODE_H, 10);
|
|
926
|
-
ctx.fill();
|
|
927
|
-
// Body
|
|
928
|
-
ctx.fillStyle = isSelected ? '#252525' : '#1e1e1e';
|
|
929
|
-
ctx.strokeStyle = isSelected ? color : '#333';
|
|
930
|
-
ctx.lineWidth = isSelected ? 2.5 : 1;
|
|
931
|
-
ctx.beginPath();
|
|
932
|
-
ctx.roundRect(n.x, n.y, NODE_W, NODE_H, 10);
|
|
933
|
-
ctx.fill();
|
|
934
|
-
ctx.stroke();
|
|
935
|
-
// Color bar top
|
|
936
|
-
ctx.fillStyle = color;
|
|
937
|
-
ctx.beginPath();
|
|
938
|
-
ctx.roundRect(n.x, n.y, NODE_W, 4, [10, 10, 0, 0]);
|
|
939
|
-
ctx.fill();
|
|
940
|
-
// Icon + name
|
|
941
|
-
ctx.fillStyle = '#e5e5e5';
|
|
942
|
-
ctx.font = '13px -apple-system, sans-serif';
|
|
943
|
-
ctx.textBaseline = 'middle';
|
|
944
|
-
const icon = NODE_ICONS[n.type] || '⬜';
|
|
945
|
-
ctx.fillText(icon + ' ' + n.name, n.x + 12, n.y + 28);
|
|
946
|
-
// Type label
|
|
947
|
-
ctx.fillStyle = '#737373';
|
|
948
|
-
ctx.font = '10px -apple-system, sans-serif';
|
|
949
|
-
ctx.fillText(n.type, n.x + 12, n.y + 46);
|
|
950
|
-
// Input port
|
|
951
|
-
if (n.type !== 'input') {
|
|
952
|
-
ctx.beginPath();
|
|
953
|
-
ctx.arc(n.x, n.y + NODE_H / 2, PORT_R, 0, Math.PI * 2);
|
|
954
|
-
ctx.fillStyle = '#333';
|
|
955
|
-
ctx.fill();
|
|
956
|
-
ctx.strokeStyle = color;
|
|
957
|
-
ctx.lineWidth = 1.5;
|
|
958
|
-
ctx.stroke();
|
|
959
|
-
}
|
|
960
|
-
// Output port
|
|
961
|
-
if (n.type !== 'output') {
|
|
962
|
-
ctx.beginPath();
|
|
963
|
-
ctx.arc(n.x + NODE_W, n.y + NODE_H / 2, PORT_R, 0, Math.PI * 2);
|
|
964
|
-
ctx.fillStyle = '#333';
|
|
965
|
-
ctx.fill();
|
|
966
|
-
ctx.strokeStyle = color;
|
|
967
|
-
ctx.lineWidth = 1.5;
|
|
968
|
-
ctx.stroke();
|
|
969
|
-
}
|
|
2146
|
+
// Logs
|
|
2147
|
+
const logsEl = document.getElementById('status-logs');
|
|
2148
|
+
if (data.logs && data.logs.length > 0) {
|
|
2149
|
+
logsEl.textContent = data.logs.join('\n');
|
|
2150
|
+
logsEl.scrollTop = logsEl.scrollHeight;
|
|
2151
|
+
} else {
|
|
2152
|
+
logsEl.textContent = '暂无日志。Agent 运行后日志会显示在这里。';
|
|
970
2153
|
}
|
|
971
|
-
|
|
2154
|
+
} catch {
|
|
2155
|
+
document.getElementById('status-overview').innerHTML = '<div class="card"><p style="color:var(--text-muted);">无法获取状态信息</p></div>';
|
|
972
2156
|
}
|
|
2157
|
+
}
|
|
973
2158
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
2159
|
+
// --- Usage Panel ---
|
|
2160
|
+
async function refreshUsage() {
|
|
2161
|
+
try {
|
|
2162
|
+
const res = await fetch(`${API}/api/settings/usage`);
|
|
2163
|
+
const data = await res.json();
|
|
2164
|
+
const totalTokens = data.totalTokens || 0;
|
|
2165
|
+
const totalCost = data.totalCost || 0;
|
|
2166
|
+
const byModel = data.byModel || {};
|
|
2167
|
+
const daily = data.daily || [];
|
|
2168
|
+
|
|
2169
|
+
document.getElementById('usage-stats').innerHTML = `
|
|
2170
|
+
<div class="card-grid" style="margin-bottom:24px;">
|
|
2171
|
+
<div class="card stat-card">
|
|
2172
|
+
<div class="stat-value">${totalTokens > 1000 ? (totalTokens/1000).toFixed(1) + 'K' : totalTokens}</div>
|
|
2173
|
+
<div class="stat-label">总 Token 消耗</div>
|
|
2174
|
+
</div>
|
|
2175
|
+
<div class="card stat-card">
|
|
2176
|
+
<div class="stat-value">$${totalCost.toFixed(4)}</div>
|
|
2177
|
+
<div class="stat-label">估算费用</div>
|
|
2178
|
+
</div>
|
|
2179
|
+
<div class="card stat-card">
|
|
2180
|
+
<div class="stat-value">${Object.keys(byModel).length || 0}</div>
|
|
2181
|
+
<div class="stat-label">使用模型数</div>
|
|
2182
|
+
</div>
|
|
2183
|
+
</div>
|
|
2184
|
+
${Object.keys(byModel).length > 0 ? `
|
|
2185
|
+
<div class="card" style="margin-bottom:16px;">
|
|
2186
|
+
<h3 style="font-size:15px;margin-bottom:12px;">按模型分布</h3>
|
|
2187
|
+
${Object.entries(byModel).map(([m, v]) => `<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border);">
|
|
2188
|
+
<span style="font-size:14px;">${m}</span>
|
|
2189
|
+
<span style="font-size:13px;color:var(--text-muted);">${v.tokens || 0} tokens · $${(v.cost || 0).toFixed(4)}</span>
|
|
2190
|
+
</div>`).join('')}
|
|
2191
|
+
</div>
|
|
2192
|
+
` : ''}
|
|
2193
|
+
${totalTokens === 0 ? `
|
|
2194
|
+
<div class="card" style="text-align:center;padding:40px;">
|
|
2195
|
+
<div style="font-size:36px;margin-bottom:12px;">📊</div>
|
|
2196
|
+
<p style="color:var(--text-muted);">还没有使用记录。开始和 Agent 聊天后,用量数据会自动记录在这里。</p>
|
|
2197
|
+
${modelConfig.mode === 'local' || !modelConfig.mode ? '<p style="color:var(--green);font-size:13px;margin-top:8px;">💡 使用本地模型完全免费,不产生费用</p>' : ''}
|
|
2198
|
+
</div>
|
|
2199
|
+
` : ''}
|
|
2200
|
+
`;
|
|
2201
|
+
} catch {
|
|
2202
|
+
document.getElementById('usage-stats').innerHTML = '<div class="card"><p style="color:var(--text-muted);">无法获取用量数据</p></div>';
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// === Web Search Settings ===
|
|
2207
|
+
async function initSearchPanel() {
|
|
2208
|
+
try {
|
|
2209
|
+
const res = await fetch(`${API}/api/settings/search`);
|
|
2210
|
+
const cfg = await res.json();
|
|
2211
|
+
document.getElementById('search-enabled').checked = cfg.enabled !== false;
|
|
2212
|
+
document.getElementById('search-engine').value = cfg.defaultEngine || 'duckduckgo';
|
|
2213
|
+
updateSearchEngineUI(cfg.defaultEngine || 'duckduckgo');
|
|
2214
|
+
if (cfg.engines) {
|
|
2215
|
+
const eng = cfg.engines[cfg.defaultEngine];
|
|
2216
|
+
if (eng?.apiKey) document.getElementById('search-apikey').value = eng.apiKey;
|
|
2217
|
+
if (eng?.baseUrl) document.getElementById('search-baseurl').value = eng.baseUrl;
|
|
982
2218
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
2219
|
+
} catch { /* defaults are fine */ }
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
function updateSearchEngineUI(engine) {
|
|
2223
|
+
const needsKey = ['brave', 'google'].includes(engine);
|
|
2224
|
+
const needsUrl = engine === 'searxng';
|
|
2225
|
+
document.getElementById('search-apikey-group').style.display = needsKey ? '' : 'none';
|
|
2226
|
+
document.getElementById('search-baseurl-group').style.display = needsUrl ? '' : 'none';
|
|
2227
|
+
if (engine === 'brave') document.getElementById('search-apikey-label').textContent = 'Brave Search API Key';
|
|
2228
|
+
if (engine === 'google') document.getElementById('search-apikey-label').textContent = 'Google API Key:CX';
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
async function updateSearchConfig() {
|
|
2232
|
+
const engine = document.getElementById('search-engine').value;
|
|
2233
|
+
updateSearchEngineUI(engine);
|
|
2234
|
+
const cfg = {
|
|
2235
|
+
enabled: document.getElementById('search-enabled').checked,
|
|
2236
|
+
defaultEngine: engine,
|
|
2237
|
+
engines: {}
|
|
2238
|
+
};
|
|
2239
|
+
cfg.engines[engine] = { enabled: true };
|
|
2240
|
+
const apiKey = document.getElementById('search-apikey').value;
|
|
2241
|
+
const baseUrl = document.getElementById('search-baseurl').value;
|
|
2242
|
+
if (apiKey) cfg.engines[engine].apiKey = apiKey;
|
|
2243
|
+
if (baseUrl) cfg.engines[engine].baseUrl = baseUrl;
|
|
2244
|
+
cfg.engines.duckduckgo = { enabled: true };
|
|
2245
|
+
try {
|
|
2246
|
+
await fetch(`${API}/api/settings/search`, {
|
|
2247
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
2248
|
+
body: JSON.stringify(cfg)
|
|
2249
|
+
});
|
|
2250
|
+
} catch { /* silent */ }
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
async function testSearch() {
|
|
2254
|
+
const el = document.getElementById('search-test-result');
|
|
2255
|
+
el.innerHTML = '<span style="color:var(--yellow);">🔍 正在搜索...</span>';
|
|
2256
|
+
try {
|
|
2257
|
+
const res = await fetch(`${API}/api/settings/search/test`, {
|
|
2258
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2259
|
+
body: JSON.stringify({ query: 'hello world test' })
|
|
2260
|
+
});
|
|
2261
|
+
const data = await res.json();
|
|
2262
|
+
if (data.success && data.results?.length) {
|
|
2263
|
+
el.innerHTML = `<span style="color:var(--green);">✅ 搜索成功!找到 ${data.results.length} 条结果</span><br>` +
|
|
2264
|
+
data.results.map(r => `<div style="margin-top:8px;font-size:12px;"><a href="${r.url}" target="_blank">${r.title}</a><br><span style="color:var(--text-muted);">${r.snippet?.slice(0,100)}</span></div>`).join('');
|
|
991
2265
|
} else {
|
|
992
|
-
|
|
993
|
-
hideProps();
|
|
994
|
-
isPanning = true;
|
|
995
|
-
panStart = { x: e.clientX - pan.x, y: e.clientY - pan.y };
|
|
996
|
-
render();
|
|
2266
|
+
el.innerHTML = `<span style="color:var(--red);">❌ ${data.error || '未找到结果'}</span>`;
|
|
997
2267
|
}
|
|
2268
|
+
} catch (e) {
|
|
2269
|
+
el.innerHTML = `<span style="color:var(--red);">❌ 测试失败: ${e.message}</span>`;
|
|
998
2270
|
}
|
|
2271
|
+
}
|
|
999
2272
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
2273
|
+
// === Health Dashboard ===
|
|
2274
|
+
async function loadHealthDashboard() {
|
|
2275
|
+
const el = document.getElementById('health-section');
|
|
2276
|
+
if (!el) return;
|
|
2277
|
+
try {
|
|
2278
|
+
const [modRes, ollamaRes] = await Promise.all([
|
|
2279
|
+
fetch(`${API}/api/modules`),
|
|
2280
|
+
fetch(`${API}/api/settings/models/local`),
|
|
2281
|
+
]);
|
|
2282
|
+
const modData = await modRes.json();
|
|
2283
|
+
const ollamaData = await ollamaRes.json();
|
|
2284
|
+
const modules = modData.modules || [];
|
|
2285
|
+
const runningCount = modules.filter(m => m.running).length;
|
|
2286
|
+
el.innerHTML = `
|
|
2287
|
+
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
|
2288
|
+
${modules.map(m => `
|
|
2289
|
+
<div class="card" style="flex:1;min-width:140px;display:flex;align-items:center;gap:10px;padding:12px 14px;">
|
|
2290
|
+
<span class="status-dot ${m.running ? 'green' : 'red'}"></span>
|
|
2291
|
+
<span style="font-size:13px;">${m.icon} ${m.name}</span>
|
|
2292
|
+
<span style="font-size:11px;color:var(--text-dim);margin-left:auto;">:${m.port}</span>
|
|
2293
|
+
</div>
|
|
2294
|
+
`).join('')}
|
|
2295
|
+
<div class="card" style="flex:1;min-width:140px;display:flex;align-items:center;gap:10px;padding:12px 14px;">
|
|
2296
|
+
<span class="status-dot ${ollamaData.running ? 'green' : 'red'}"></span>
|
|
2297
|
+
<span style="font-size:13px;">🦙 Ollama</span>
|
|
2298
|
+
<span style="font-size:11px;color:var(--text-dim);margin-left:auto;">${ollamaData.running ? (ollamaData.models?.length || 0) + ' models' : 'offline'}</span>
|
|
2299
|
+
</div>
|
|
2300
|
+
</div>
|
|
2301
|
+
`;
|
|
2302
|
+
} catch {
|
|
2303
|
+
el.innerHTML = '';
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// === First Run Wizard ===
|
|
2308
|
+
let frStep = 1;
|
|
2309
|
+
let frSelectedTemplate = null;
|
|
2310
|
+
let frCreatedAgentId = null;
|
|
2311
|
+
|
|
2312
|
+
async function checkFirstRun() {
|
|
2313
|
+
try {
|
|
2314
|
+
const res = await fetch(`${API}/api/first-run/status`);
|
|
2315
|
+
const data = await res.json();
|
|
2316
|
+
if (data.firstRun) {
|
|
2317
|
+
showFirstRunWizard(data);
|
|
1013
2318
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
2319
|
+
} catch {}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
function showFirstRunWizard(data) {
|
|
2323
|
+
frStep = 1;
|
|
2324
|
+
const overlay = document.getElementById('first-run-overlay');
|
|
2325
|
+
overlay.style.display = 'flex';
|
|
2326
|
+
frRenderStep();
|
|
2327
|
+
if (data?.ollamaDetected) {
|
|
2328
|
+
const statusEl = document.getElementById('fr-ollama-status');
|
|
2329
|
+
if (statusEl) {
|
|
2330
|
+
statusEl.innerHTML = `<div style="display:flex;align-items:center;gap:8px;color:var(--green);"><span class="status-dot green"></span> <b>Ollama detected!</b> ${data.ollamaModels?.length ? data.ollamaModels.length + ' models available.' : ''} Local AI is free.</div>`;
|
|
2331
|
+
const choiceEl = document.getElementById('fr-model-choice');
|
|
2332
|
+
if (choiceEl) choiceEl.style.display = 'block';
|
|
2333
|
+
const sel = document.getElementById('fr-model-select');
|
|
2334
|
+
if (sel && data.ollamaModels?.length) {
|
|
2335
|
+
sel.innerHTML = data.ollamaModels.map(m => `<option value="${m.name}">${m.name} (local)</option>`).join('') + '<option value="gpt-4o-mini">GPT-4o Mini (cloud)</option>';
|
|
2336
|
+
}
|
|
1018
2337
|
}
|
|
2338
|
+
} else {
|
|
2339
|
+
detectFrOllama();
|
|
1019
2340
|
}
|
|
2341
|
+
}
|
|
1020
2342
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
2343
|
+
async function detectFrOllama() {
|
|
2344
|
+
try {
|
|
2345
|
+
const res = await fetch(`${API}/api/settings/models/local`);
|
|
2346
|
+
const data = await res.json();
|
|
2347
|
+
const statusEl = document.getElementById('fr-ollama-status');
|
|
2348
|
+
const choiceEl = document.getElementById('fr-model-choice');
|
|
2349
|
+
if (!statusEl) return;
|
|
2350
|
+
if (data.running) {
|
|
2351
|
+
statusEl.innerHTML = `<div style="display:flex;align-items:center;gap:8px;color:var(--green);"><span class="status-dot green"></span> <b>Ollama running</b> — free local models available!</div>`;
|
|
2352
|
+
if (choiceEl) choiceEl.style.display = 'block';
|
|
2353
|
+
const sel = document.getElementById('fr-model-select');
|
|
2354
|
+
if (sel && data.models?.length) {
|
|
2355
|
+
sel.innerHTML = data.models.map(m => `<option value="${m.name}">${m.name} (local)</option>`).join('') + '<option value="gpt-4o-mini">GPT-4o Mini (cloud)</option>';
|
|
1027
2356
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
2357
|
+
} else {
|
|
2358
|
+
statusEl.innerHTML = `<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot red"></span> Ollama not detected — you can use cloud models or <a href="https://ollama.com" target="_blank">install Ollama</a> for free local AI.</div>`;
|
|
2359
|
+
if (choiceEl) choiceEl.style.display = 'block';
|
|
1030
2360
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
2361
|
+
} catch {}
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
function frRenderStep() {
|
|
2365
|
+
for (let i = 1; i <= 4; i++) {
|
|
2366
|
+
const stepEl = document.getElementById(`fr-step-${i}`);
|
|
2367
|
+
const panelEl = document.getElementById(`fr-panel-${i}`);
|
|
2368
|
+
if (stepEl) stepEl.className = 'wizard-step' + (i < frStep ? ' done' : i === frStep ? ' active' : '');
|
|
2369
|
+
if (panelEl) panelEl.className = 'wizard-panel' + (i === frStep ? ' active' : '');
|
|
1033
2370
|
}
|
|
2371
|
+
}
|
|
1034
2372
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (node) {
|
|
1039
|
-
const newName = prompt('Node name:', node.name);
|
|
1040
|
-
if (newName !== null) { pushUndo(); node.name = newName; render(); showProps(node); }
|
|
1041
|
-
}
|
|
2373
|
+
function frNext() {
|
|
2374
|
+
if (frStep === 3 && !frSelectedTemplate) {
|
|
2375
|
+
frSelectedTemplate = 'customer-service';
|
|
1042
2376
|
}
|
|
2377
|
+
if (frStep === 3) {
|
|
2378
|
+
frStep = 4;
|
|
2379
|
+
frRenderStep();
|
|
2380
|
+
frCreateAgent();
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
if (frStep < 4) { frStep++; frRenderStep(); }
|
|
2384
|
+
}
|
|
1043
2385
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
2386
|
+
function frBack() {
|
|
2387
|
+
if (frStep > 1) { frStep--; frRenderStep(); }
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
function frSelectTemplate(id) {
|
|
2391
|
+
frSelectedTemplate = id;
|
|
2392
|
+
document.querySelectorAll('#fr-template-list .card').forEach(c => {
|
|
2393
|
+
c.style.borderColor = '';
|
|
2394
|
+
c.style.background = '';
|
|
2395
|
+
});
|
|
2396
|
+
const el = document.getElementById(`fr-tpl-${id}`);
|
|
2397
|
+
if (el) { el.style.borderColor = 'var(--accent)'; el.style.background = 'var(--accent-light)'; }
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
async function frCreateAgent() {
|
|
2401
|
+
const model = document.getElementById('fr-model-select')?.value || 'qwen2.5:7b';
|
|
2402
|
+
try {
|
|
2403
|
+
// Save first-run complete
|
|
2404
|
+
await fetch(`${API}/api/first-run/complete`, {
|
|
2405
|
+
method: 'POST',
|
|
2406
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2407
|
+
body: JSON.stringify({ templateId: frSelectedTemplate, model }),
|
|
2408
|
+
});
|
|
2409
|
+
// Create the agent
|
|
2410
|
+
const res = await fetch(`${API}/api/agents`, {
|
|
2411
|
+
method: 'POST',
|
|
2412
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2413
|
+
body: JSON.stringify({ name: '', templateId: frSelectedTemplate || 'customer-service', model }),
|
|
2414
|
+
});
|
|
2415
|
+
const agent = await res.json();
|
|
2416
|
+
frCreatedAgentId = agent.id;
|
|
2417
|
+
document.getElementById('fr-creating').style.display = 'none';
|
|
2418
|
+
document.getElementById('fr-done').style.display = 'block';
|
|
2419
|
+
await loadAgents();
|
|
2420
|
+
} catch(e) {
|
|
2421
|
+
document.getElementById('fr-creating').innerHTML = `<div style="color:var(--red);">Error: ${e.message}</div>`;
|
|
1054
2422
|
}
|
|
2423
|
+
}
|
|
1055
2424
|
|
|
1056
|
-
|
|
2425
|
+
function frFinish() {
|
|
2426
|
+
document.getElementById('first-run-overlay').style.display = 'none';
|
|
2427
|
+
if (frCreatedAgentId) openChat(frCreatedAgentId);
|
|
2428
|
+
else navigate('dashboard');
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// === Drag & drop document upload ===
|
|
2432
|
+
const chatArea = document.getElementById('chat-messages');
|
|
2433
|
+
if (chatArea) {
|
|
2434
|
+
chatArea.addEventListener('dragover', (e) => { e.preventDefault(); chatArea.style.outline = '2px dashed var(--primary)'; });
|
|
2435
|
+
chatArea.addEventListener('dragleave', () => { chatArea.style.outline = ''; });
|
|
2436
|
+
chatArea.addEventListener('drop', (e) => {
|
|
1057
2437
|
e.preventDefault();
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
2438
|
+
chatArea.style.outline = '';
|
|
2439
|
+
const file = e.dataTransfer?.files?.[0];
|
|
2440
|
+
if (file) {
|
|
2441
|
+
const dt = new DataTransfer();
|
|
2442
|
+
dt.items.add(file);
|
|
2443
|
+
const inp = document.getElementById('doc-upload-input');
|
|
2444
|
+
inp.files = dt.files;
|
|
2445
|
+
handleDocUpload(inp);
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// === Start ===
|
|
2451
|
+
init();
|
|
2452
|
+
|
|
2453
|
+
// =============================================
|
|
2454
|
+
// === Schedules Management ===
|
|
2455
|
+
// =============================================
|
|
2456
|
+
let editingScheduleId = null;
|
|
2457
|
+
|
|
2458
|
+
async function loadSchedules() {
|
|
2459
|
+
try {
|
|
2460
|
+
const res = await fetch('/api/schedules');
|
|
2461
|
+
const tasks = await res.json();
|
|
2462
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
2463
|
+
renderSchedules(list);
|
|
2464
|
+
} catch(e) {
|
|
2465
|
+
console.error('Failed to load schedules:', e);
|
|
2466
|
+
renderSchedules([]);
|
|
1062
2467
|
}
|
|
2468
|
+
}
|
|
1063
2469
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
2470
|
+
function renderSchedules(tasks) {
|
|
2471
|
+
const listEl = document.getElementById('schedules-list');
|
|
2472
|
+
const emptyEl = document.getElementById('schedules-empty');
|
|
2473
|
+
if (!tasks.length) {
|
|
2474
|
+
listEl.innerHTML = '';
|
|
2475
|
+
emptyEl.style.display = '';
|
|
2476
|
+
return;
|
|
1069
2477
|
}
|
|
2478
|
+
emptyEl.style.display = 'none';
|
|
2479
|
+
listEl.innerHTML = tasks.map(t => `
|
|
2480
|
+
<div class="card" style="margin-bottom:12px;display:flex;align-items:center;gap:16px;">
|
|
2481
|
+
<div style="font-size:28px;">⏰</div>
|
|
2482
|
+
<div style="flex:1;">
|
|
2483
|
+
<div style="font-size:15px;font-weight:600;">${esc(t.name)}</div>
|
|
2484
|
+
<div style="font-size:12px;color:var(--text-muted);">${esc(t.description || '')}</div>
|
|
2485
|
+
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;">
|
|
2486
|
+
${esc(t.schedule)} · ${t.outputChannel || 'web'} · Next: ${t.nextRun ? new Date(t.nextRun).toLocaleString() : 'N/A'}
|
|
2487
|
+
</div>
|
|
2488
|
+
</div>
|
|
2489
|
+
<div style="display:flex;gap:8px;align-items:center;">
|
|
2490
|
+
<label style="position:relative;display:inline-block;width:40px;height:22px;cursor:pointer;">
|
|
2491
|
+
<input type="checkbox" ${t.enabled ? 'checked' : ''} onchange="toggleSchedule('${t.id}', this.checked)" style="opacity:0;width:0;height:0;">
|
|
2492
|
+
<span style="position:absolute;inset:0;border-radius:11px;background:${t.enabled ? 'var(--green)' : 'var(--border)'};transition:0.3s;"></span>
|
|
2493
|
+
<span style="position:absolute;top:2px;left:${t.enabled ? '20px' : '2px'};width:18px;height:18px;border-radius:50%;background:white;transition:0.3s;"></span>
|
|
2494
|
+
</label>
|
|
2495
|
+
<button class="btn btn-sm btn-secondary" onclick="runScheduleNow('${t.id}')" title="Run now">▶️</button>
|
|
2496
|
+
<button class="btn btn-sm btn-secondary" onclick="editSchedule('${t.id}')" title="Edit">✏️</button>
|
|
2497
|
+
<button class="btn btn-sm btn-danger" onclick="deleteSchedule('${t.id}')" title="Delete">🗑</button>
|
|
2498
|
+
</div>
|
|
2499
|
+
</div>
|
|
2500
|
+
`).join('');
|
|
2501
|
+
}
|
|
1070
2502
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
2503
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
2504
|
+
|
|
2505
|
+
function showScheduleForm(task) {
|
|
2506
|
+
editingScheduleId = task ? task.id : null;
|
|
2507
|
+
document.getElementById('schedule-form').style.display = '';
|
|
2508
|
+
document.getElementById('schedule-form-title').textContent = task ? 'Edit Task' : 'New Scheduled Task';
|
|
2509
|
+
document.getElementById('sched-name').value = task ? task.name : '';
|
|
2510
|
+
document.getElementById('sched-frequency').value = task ? task.frequency : 'daily';
|
|
2511
|
+
document.getElementById('sched-time').value = task ? (task.time || '09:00') : '09:00';
|
|
2512
|
+
document.getElementById('sched-cron').value = task ? task.schedule : '';
|
|
2513
|
+
document.getElementById('sched-desc').value = task ? task.description : '';
|
|
2514
|
+
document.getElementById('sched-channel').value = task ? task.outputChannel : 'web';
|
|
2515
|
+
onSchedFreqChange();
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
function hideScheduleForm() {
|
|
2519
|
+
document.getElementById('schedule-form').style.display = 'none';
|
|
2520
|
+
editingScheduleId = null;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
function onSchedFreqChange() {
|
|
2524
|
+
const freq = document.getElementById('sched-frequency').value;
|
|
2525
|
+
document.getElementById('sched-time-group').style.display = freq === 'custom' ? 'none' : '';
|
|
2526
|
+
document.getElementById('sched-cron-group').style.display = freq === 'custom' ? '' : 'none';
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
async function saveSchedule() {
|
|
2530
|
+
const data = {
|
|
2531
|
+
name: document.getElementById('sched-name').value.trim(),
|
|
2532
|
+
frequency: document.getElementById('sched-frequency').value,
|
|
2533
|
+
time: document.getElementById('sched-time').value,
|
|
2534
|
+
schedule: document.getElementById('sched-frequency').value === 'custom' ? document.getElementById('sched-cron').value.trim() : '',
|
|
2535
|
+
description: document.getElementById('sched-desc').value.trim(),
|
|
2536
|
+
outputChannel: document.getElementById('sched-channel').value,
|
|
2537
|
+
enabled: true,
|
|
2538
|
+
};
|
|
2539
|
+
if (!data.name) { alert('Task name is required'); return; }
|
|
2540
|
+
try {
|
|
2541
|
+
if (editingScheduleId) {
|
|
2542
|
+
await fetch(`/api/schedules/${editingScheduleId}`, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
2543
|
+
} else {
|
|
2544
|
+
await fetch('/api/schedules', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
1084
2545
|
}
|
|
1085
|
-
|
|
2546
|
+
hideScheduleForm();
|
|
2547
|
+
loadSchedules();
|
|
2548
|
+
} catch(e) { alert('Failed to save: ' + e.message); }
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
async function toggleSchedule(id, enabled) {
|
|
2552
|
+
await fetch(`/api/schedules/${id}`, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ enabled }) });
|
|
2553
|
+
loadSchedules();
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
async function deleteSchedule(id) {
|
|
2557
|
+
if (!confirm('Delete this task?')) return;
|
|
2558
|
+
await fetch(`/api/schedules/${id}`, { method: 'DELETE' });
|
|
2559
|
+
loadSchedules();
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
async function runScheduleNow(id) {
|
|
2563
|
+
await fetch(`/api/schedules/${id}/run`, { method: 'POST' });
|
|
2564
|
+
alert('Task executed!');
|
|
2565
|
+
loadSchedules();
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
async function editSchedule(id) {
|
|
2569
|
+
const res = await fetch('/api/schedules');
|
|
2570
|
+
const tasks = await res.json();
|
|
2571
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
2572
|
+
const task = list.find(t => t.id === id);
|
|
2573
|
+
if (task) showScheduleForm(task);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// =============================================
|
|
2577
|
+
// === Voice Interaction ===
|
|
2578
|
+
// =============================================
|
|
2579
|
+
let voiceRecognition = null;
|
|
2580
|
+
let isRecording = false;
|
|
2581
|
+
|
|
2582
|
+
function toggleVoiceInput() {
|
|
2583
|
+
if (isRecording) {
|
|
2584
|
+
stopVoiceInput();
|
|
2585
|
+
} else {
|
|
2586
|
+
startVoiceInput();
|
|
1086
2587
|
}
|
|
2588
|
+
}
|
|
1087
2589
|
|
|
1088
|
-
|
|
1089
|
-
|
|
2590
|
+
function startVoiceInput() {
|
|
2591
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
2592
|
+
if (!SpeechRecognition) {
|
|
2593
|
+
alert('Speech recognition is not supported in this browser. Try Chrome.');
|
|
2594
|
+
return;
|
|
1090
2595
|
}
|
|
2596
|
+
voiceRecognition = new SpeechRecognition();
|
|
2597
|
+
voiceRecognition.continuous = false;
|
|
2598
|
+
voiceRecognition.interimResults = true;
|
|
2599
|
+
voiceRecognition.lang = navigator.language || 'en-US';
|
|
2600
|
+
|
|
2601
|
+
voiceRecognition.onstart = () => {
|
|
2602
|
+
isRecording = true;
|
|
2603
|
+
const btn = document.getElementById('voice-btn');
|
|
2604
|
+
btn.style.background = 'var(--red)';
|
|
2605
|
+
btn.style.color = 'white';
|
|
2606
|
+
btn.style.borderColor = 'var(--red)';
|
|
2607
|
+
btn.textContent = '⏹';
|
|
2608
|
+
};
|
|
1091
2609
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
pushUndo();
|
|
1099
|
-
edges = edges.filter(e => e.from !== selected.id && e.to !== selected.id);
|
|
1100
|
-
nodes = nodes.filter(n => n.id !== selected.id);
|
|
1101
|
-
selected = null;
|
|
1102
|
-
hideProps();
|
|
1103
|
-
render();
|
|
1104
|
-
},
|
|
1105
|
-
undo() {
|
|
1106
|
-
if (!undoStack.length) return;
|
|
1107
|
-
redoStack.push(JSON.stringify({ nodes, edges }));
|
|
1108
|
-
const state = JSON.parse(undoStack.pop());
|
|
1109
|
-
nodes = state.nodes; edges = state.edges; selected = null; hideProps(); render();
|
|
1110
|
-
},
|
|
1111
|
-
redo() {
|
|
1112
|
-
if (!redoStack.length) return;
|
|
1113
|
-
undoStack.push(JSON.stringify({ nodes, edges }));
|
|
1114
|
-
const state = JSON.parse(redoStack.pop());
|
|
1115
|
-
nodes = state.nodes; edges = state.edges; selected = null; hideProps(); render();
|
|
1116
|
-
},
|
|
1117
|
-
zoomIn() { zoom = Math.min(3, zoom * 1.2); render(); },
|
|
1118
|
-
zoomOut() { zoom = Math.max(0.2, zoom * 0.8); render(); },
|
|
1119
|
-
fitView() {
|
|
1120
|
-
if (!nodes.length) { pan = { x: 50, y: 50 }; zoom = 1; render(); return; }
|
|
1121
|
-
const minX = Math.min(...nodes.map(n => n.x));
|
|
1122
|
-
const minY = Math.min(...nodes.map(n => n.y));
|
|
1123
|
-
const maxX = Math.max(...nodes.map(n => n.x + NODE_W));
|
|
1124
|
-
const maxY = Math.max(...nodes.map(n => n.y + NODE_H));
|
|
1125
|
-
const cw = canvas.width / (window.devicePixelRatio || 1);
|
|
1126
|
-
const ch = canvas.height / (window.devicePixelRatio || 1);
|
|
1127
|
-
zoom = Math.min(cw / (maxX - minX + 100), ch / (maxY - minY + 100), 2);
|
|
1128
|
-
pan.x = (cw - (maxX + minX) * zoom) / 2;
|
|
1129
|
-
pan.y = (ch - (maxY + minY) * zoom) / 2;
|
|
1130
|
-
render();
|
|
1131
|
-
},
|
|
1132
|
-
updateProp(nodeId, key, value) {
|
|
1133
|
-
const n = nodes.find(n => n.id === nodeId);
|
|
1134
|
-
if (n) { pushUndo(); n[key] = value; render(); }
|
|
1135
|
-
},
|
|
1136
|
-
updateConfig(nodeId, key, value) {
|
|
1137
|
-
const n = nodes.find(n => n.id === nodeId);
|
|
1138
|
-
if (n) { pushUndo(); n.config[key] = value; }
|
|
1139
|
-
},
|
|
1140
|
-
async save() {
|
|
1141
|
-
const name = prompt('Workflow name:', workflowName);
|
|
1142
|
-
if (!name) return;
|
|
1143
|
-
workflowName = name;
|
|
1144
|
-
const wf = { id: workflowId || undefined, name, nodes, edges };
|
|
1145
|
-
try {
|
|
1146
|
-
const result = await apiPost('workflows', wf);
|
|
1147
|
-
workflowId = result.id;
|
|
1148
|
-
alert('Saved: ' + result.id);
|
|
1149
|
-
loadWorkflows();
|
|
1150
|
-
} catch (e) { alert('Save failed: ' + e.message); }
|
|
1151
|
-
},
|
|
1152
|
-
async loadSelected() {
|
|
1153
|
-
const id = document.getElementById('wf-list-select').value;
|
|
1154
|
-
if (!id) { nodes = []; edges = []; workflowId = null; workflowName = 'Untitled'; selected = null; hideProps(); render(); return; }
|
|
1155
|
-
try {
|
|
1156
|
-
const wf = await api('workflows/' + id);
|
|
1157
|
-
if (wf.error) { alert(wf.error); return; }
|
|
1158
|
-
nodes = wf.nodes || []; edges = wf.edges || [];
|
|
1159
|
-
workflowId = wf.id; workflowName = wf.name;
|
|
1160
|
-
selected = null; hideProps(); undoStack = []; redoStack = [];
|
|
1161
|
-
render();
|
|
1162
|
-
} catch (e) { alert('Load failed: ' + e.message); }
|
|
1163
|
-
},
|
|
1164
|
-
exportJSON() {
|
|
1165
|
-
const json = JSON.stringify({ name: workflowName, nodes, edges }, null, 2);
|
|
1166
|
-
const blob = new Blob([json], { type: 'application/json' });
|
|
1167
|
-
const a = document.createElement('a');
|
|
1168
|
-
a.href = URL.createObjectURL(blob);
|
|
1169
|
-
a.download = (workflowName || 'workflow') + '.json';
|
|
1170
|
-
a.click();
|
|
1171
|
-
},
|
|
1172
|
-
importJSON() {
|
|
1173
|
-
const input = document.createElement('input');
|
|
1174
|
-
input.type = 'file';
|
|
1175
|
-
input.accept = '.json';
|
|
1176
|
-
input.onchange = async (e) => {
|
|
1177
|
-
const file = e.target.files[0];
|
|
1178
|
-
if (!file) return;
|
|
1179
|
-
const text = await file.text();
|
|
1180
|
-
try {
|
|
1181
|
-
const wf = JSON.parse(text);
|
|
1182
|
-
pushUndo();
|
|
1183
|
-
nodes = wf.nodes || []; edges = wf.edges || [];
|
|
1184
|
-
workflowName = wf.name || 'Imported';
|
|
1185
|
-
workflowId = null; selected = null; hideProps();
|
|
1186
|
-
render();
|
|
1187
|
-
} catch { alert('Invalid JSON'); }
|
|
1188
|
-
};
|
|
1189
|
-
input.click();
|
|
1190
|
-
},
|
|
1191
|
-
async run() {
|
|
1192
|
-
if (!workflowId) { alert('Save workflow first'); return; }
|
|
1193
|
-
const outEl = document.getElementById('dag-run-output');
|
|
1194
|
-
const resEl = document.getElementById('dag-run-results');
|
|
1195
|
-
outEl.style.display = 'block';
|
|
1196
|
-
resEl.textContent = 'Running...';
|
|
1197
|
-
try {
|
|
1198
|
-
const result = await apiPost('workflows/' + workflowId + '/run', {});
|
|
1199
|
-
resEl.textContent = JSON.stringify(result, null, 2);
|
|
1200
|
-
} catch (e) { resEl.textContent = 'Error: ' + e.message; }
|
|
1201
|
-
},
|
|
1202
|
-
// For serialization tests
|
|
1203
|
-
serialize() { return { name: workflowName, nodes, edges }; },
|
|
1204
|
-
deserialize(wf) { nodes = wf.nodes || []; edges = wf.edges || []; workflowName = wf.name || ''; render(); },
|
|
1205
|
-
getNodes() { return nodes; },
|
|
1206
|
-
getEdges() { return edges; },
|
|
2610
|
+
voiceRecognition.onresult = (event) => {
|
|
2611
|
+
let transcript = '';
|
|
2612
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
2613
|
+
transcript += event.results[i][0].transcript;
|
|
2614
|
+
}
|
|
2615
|
+
document.getElementById('chat-input').value = transcript;
|
|
1207
2616
|
};
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
2617
|
+
|
|
2618
|
+
voiceRecognition.onend = () => {
|
|
2619
|
+
isRecording = false;
|
|
2620
|
+
const btn = document.getElementById('voice-btn');
|
|
2621
|
+
btn.style.background = 'transparent';
|
|
2622
|
+
btn.style.color = '';
|
|
2623
|
+
btn.style.borderColor = 'var(--border)';
|
|
2624
|
+
btn.textContent = '🎤';
|
|
2625
|
+
// Auto-send if we got text
|
|
2626
|
+
const input = document.getElementById('chat-input');
|
|
2627
|
+
if (input.value.trim()) {
|
|
2628
|
+
sendMessage();
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
|
|
2632
|
+
voiceRecognition.onerror = (event) => {
|
|
2633
|
+
console.error('Speech recognition error:', event.error);
|
|
2634
|
+
isRecording = false;
|
|
2635
|
+
const btn = document.getElementById('voice-btn');
|
|
2636
|
+
btn.style.background = 'transparent';
|
|
2637
|
+
btn.style.color = '';
|
|
2638
|
+
btn.style.borderColor = 'var(--border)';
|
|
2639
|
+
btn.textContent = '🎤';
|
|
2640
|
+
};
|
|
2641
|
+
|
|
2642
|
+
voiceRecognition.start();
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
function stopVoiceInput() {
|
|
2646
|
+
if (voiceRecognition) {
|
|
2647
|
+
voiceRecognition.stop();
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
function speakText(text) {
|
|
2652
|
+
if (!window.speechSynthesis) return;
|
|
2653
|
+
window.speechSynthesis.cancel();
|
|
2654
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
2655
|
+
utterance.lang = navigator.language || 'en-US';
|
|
2656
|
+
utterance.rate = 1.0;
|
|
2657
|
+
window.speechSynthesis.speak(utterance);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// Patch message rendering to add TTS button to assistant messages
|
|
2661
|
+
const _origAppendMsg = typeof appendMessage === 'function' ? appendMessage : null;
|
|
2662
|
+
if (typeof window._patchedMsgRender === 'undefined') {
|
|
2663
|
+
window._patchedMsgRender = true;
|
|
2664
|
+
const observer = new MutationObserver((mutations) => {
|
|
2665
|
+
for (const m of mutations) {
|
|
2666
|
+
for (const node of m.addedNodes) {
|
|
2667
|
+
if (node.nodeType === 1 && node.classList?.contains('msg') && node.classList?.contains('assistant')) {
|
|
2668
|
+
const bubble = node.querySelector('.msg-bubble');
|
|
2669
|
+
if (bubble && !node.querySelector('.tts-btn')) {
|
|
2670
|
+
const btn = document.createElement('button');
|
|
2671
|
+
btn.className = 'tts-btn';
|
|
2672
|
+
btn.textContent = '🔊';
|
|
2673
|
+
btn.title = 'Read aloud';
|
|
2674
|
+
btn.style.cssText = 'background:none;border:1px solid var(--border);border-radius:50%;padding:4px 6px;cursor:pointer;font-size:14px;margin-top:4px;color:var(--text-muted);';
|
|
2675
|
+
btn.onclick = () => speakText(bubble.textContent);
|
|
2676
|
+
node.appendChild(btn);
|
|
2677
|
+
}
|
|
1263
2678
|
}
|
|
1264
2679
|
}
|
|
1265
|
-
document.getElementById('pg-messages').scrollTop = document.getElementById('pg-messages').scrollHeight;
|
|
1266
2680
|
}
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
input.focus();
|
|
2681
|
+
});
|
|
2682
|
+
const chatMsgs = document.getElementById('chat-messages');
|
|
2683
|
+
if (chatMsgs) observer.observe(chatMsgs, { childList: true });
|
|
1271
2684
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
2685
|
+
|
|
2686
|
+
// =============================================
|
|
2687
|
+
// === Image Generation Config ===
|
|
2688
|
+
// =============================================
|
|
2689
|
+
async function saveImageGenConfig() {
|
|
2690
|
+
const data = {
|
|
2691
|
+
openaiApiKey: document.getElementById('ig-openai-key').value.trim(),
|
|
2692
|
+
sdApiUrl: document.getElementById('ig-sd-url').value.trim(),
|
|
2693
|
+
replicateApiKey: document.getElementById('ig-replicate-key').value.trim(),
|
|
2694
|
+
};
|
|
2695
|
+
try {
|
|
2696
|
+
await fetch('/api/image-gen/config', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
2697
|
+
document.getElementById('ig-status').textContent = '✅ Configuration saved!';
|
|
2698
|
+
document.getElementById('ig-status').style.color = 'var(--green)';
|
|
2699
|
+
} catch(e) {
|
|
2700
|
+
document.getElementById('ig-status').textContent = '❌ Failed: ' + e.message;
|
|
2701
|
+
document.getElementById('ig-status').style.color = 'var(--red)';
|
|
2702
|
+
}
|
|
1275
2703
|
}
|
|
2704
|
+
|
|
1276
2705
|
</script>
|
|
1277
2706
|
</body>
|
|
1278
2707
|
</html>
|