opc-agent 4.1.24 → 4.2.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/CHANGELOG.md +59 -119
- package/STUDIO-REWRITE-TASK.md +76 -0
- package/dist/core/a2a-http.d.ts +75 -0
- package/dist/core/a2a-http.d.ts.map +1 -0
- package/dist/core/a2a-http.js +217 -0
- package/dist/core/a2a-http.js.map +1 -0
- package/dist/core/a2a.d.ts +2 -0
- package/dist/core/a2a.d.ts.map +1 -1
- package/dist/core/a2a.js +6 -1
- package/dist/core/a2a.js.map +1 -1
- package/dist/core/gateway-registry.d.ts +116 -0
- package/dist/core/gateway-registry.d.ts.map +1 -0
- package/dist/core/gateway-registry.js +280 -0
- package/dist/core/gateway-registry.js.map +1 -0
- package/dist/core/priority-queue.d.ts +100 -0
- package/dist/core/priority-queue.d.ts.map +1 -0
- package/dist/core/priority-queue.js +181 -0
- package/dist/core/priority-queue.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/studio/server.d.ts.map +1 -1
- package/dist/studio/server.js +6 -5
- package/dist/studio/server.js.map +1 -1
- package/dist/studio-ui/index.html +860 -2942
- package/package.json +66 -66
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="zh">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -7,3169 +7,1087 @@
|
|
|
7
7
|
<style>
|
|
8
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
9
|
:root {
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--font:
|
|
20
|
-
--mono: '
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; font-size: 18px; line-height: 1.6; background-image: radial-gradient(ellipse at 20% 0%, rgba(139,92,246,0.12) 0%, transparent 50%), radial-gradient(ellipse at 80% 100%, rgba(6,182,212,0.08) 0%, transparent 50%), url("data:image/svg+xml,%3Csvg width='60' height='60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 30h60M30 0v60' stroke='rgba(139,92,246,0.04)' stroke-width='0.5'/%3E%3C/svg%3E"); }
|
|
24
|
-
a { color: var(--accent); text-decoration: none; }
|
|
10
|
+
--blue: #1a73e8; --blue-light: #e8f0fe; --blue-hover: #1557b0;
|
|
11
|
+
--green: #188038; --green-light: #e6f4ea;
|
|
12
|
+
--red: #d93025; --red-light: #fce8e6;
|
|
13
|
+
--yellow: #e37400; --yellow-light: #fef7e0;
|
|
14
|
+
--bg: #f8f9fa; --white: #fff; --border: #dadce0;
|
|
15
|
+
--text: #202124; --text-muted: #5f6368; --text-dim: #9aa0a6;
|
|
16
|
+
--radius: 12px; --radius-sm: 8px;
|
|
17
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
18
|
+
--shadow-hover: 0 4px 12px rgba(0,0,0,0.12);
|
|
19
|
+
--font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
20
|
+
--mono: 'SF Mono', 'Fira Code', monospace;
|
|
21
|
+
}
|
|
22
|
+
body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; font-size: 14px; line-height: 1.6; }
|
|
25
23
|
button { font-family: var(--font); cursor: pointer; border: none; }
|
|
26
|
-
input, select, textarea { font-family: var(--font); }
|
|
24
|
+
input, select, textarea { font-family: var(--font); font-size: 14px; }
|
|
27
25
|
|
|
28
26
|
/* Layout */
|
|
29
27
|
.app { display: flex; min-height: 100vh; }
|
|
30
|
-
.sidebar {
|
|
31
|
-
width: 270px; background: rgba(5,5,30,0.9); backdrop-filter: blur(30px); border-right: 1px solid var(--border);
|
|
32
|
-
padding: 24px 16px; display: flex; flex-direction: column; position: fixed; height: 100vh; z-index: 100;
|
|
33
|
-
background-image: linear-gradient(180deg, rgba(139,92,246,0.05) 0%, transparent 30%);
|
|
34
|
-
}
|
|
35
|
-
.sidebar-logo { font-size: 24px; font-weight: 700; padding: 12px 12px; margin-bottom: 28px; display: flex; align-items: center; gap: 10px; background: var(--gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.5px; }
|
|
36
|
-
.sidebar-logo span { background: var(--gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
37
|
-
.nav-item {
|
|
38
|
-
display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-radius: 12px;
|
|
39
|
-
cursor: pointer; color: var(--text-muted); transition: all 0.2s ease; font-size: 18px; margin-bottom: 4px; position: relative;
|
|
40
|
-
}
|
|
41
|
-
.nav-item:hover { background: var(--bg-hover); color: var(--text); transform: translateX(4px); }
|
|
42
|
-
.nav-item.active { background: var(--accent-light); color: #fff; font-weight: 600; box-shadow: var(--glow-sm); border: 1px solid var(--border); }
|
|
43
|
-
.nav-item .icon { width: 28px; text-align: center; font-size: 22px; }
|
|
44
|
-
.main { flex: 1; margin-left: 270px; min-height: 100vh; }
|
|
45
|
-
|
|
46
|
-
/* Mobile */
|
|
47
|
-
.mobile-header { display: none; background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 12px 16px; position: sticky; top: 0; z-index: 50; }
|
|
48
|
-
.mobile-header button { background: none; border: none; color: var(--text); font-size: 20px; }
|
|
49
|
-
@media (max-width: 768px) {
|
|
50
|
-
.sidebar { transform: translateX(-100%); transition: transform 0.2s; width: 260px; }
|
|
51
|
-
.sidebar.open { transform: translateX(0); }
|
|
52
|
-
.mobile-header { display: flex; align-items: center; justify-content: space-between; }
|
|
53
|
-
.main { margin-left: 0; }
|
|
54
|
-
.sidebar-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 99; }
|
|
55
|
-
.sidebar-overlay.show { display: block; }
|
|
56
|
-
}
|
|
57
28
|
|
|
58
|
-
/*
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
.
|
|
29
|
+
/* Sidebar */
|
|
30
|
+
.sidebar { width: 240px; background: var(--white); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto; }
|
|
31
|
+
.sidebar-logo { padding: 20px 16px; font-size: 18px; font-weight: 700; color: var(--blue); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; }
|
|
32
|
+
.sidebar-section { padding: 8px; }
|
|
33
|
+
.sidebar-label { font-size: 11px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 12px 4px; }
|
|
34
|
+
.sidebar-item { padding: 8px 12px; border-radius: var(--radius-sm); margin: 1px 0; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 8px; color: var(--text); transition: background 0.15s; }
|
|
35
|
+
.sidebar-item:hover { background: var(--bg); }
|
|
36
|
+
.sidebar-item.active { background: var(--blue-light); color: var(--blue); font-weight: 500; }
|
|
37
|
+
.sidebar-item.action { color: var(--blue); font-weight: 500; }
|
|
38
|
+
.sidebar-item .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); margin-left: auto; flex-shrink: 0; }
|
|
39
|
+
.sidebar-divider { height: 1px; background: var(--border); margin: 4px 12px; }
|
|
40
|
+
|
|
41
|
+
/* Main */
|
|
42
|
+
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
43
|
+
|
|
44
|
+
/* Header */
|
|
45
|
+
.page-header { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; background: var(--white); font-weight: 600; flex-shrink: 0; }
|
|
46
|
+
.page-header .status { width: 8px; height: 8px; border-radius: 50%; background: var(--green); }
|
|
47
|
+
.page-header .status-text { font-size: 12px; color: var(--green); font-weight: 400; }
|
|
48
|
+
.page-header .settings-btn { margin-left: auto; font-size: 18px; cursor: pointer; padding: 4px; border-radius: 6px; background: none; color: var(--text-muted); }
|
|
49
|
+
.page-header .settings-btn:hover { background: var(--bg); }
|
|
50
|
+
|
|
51
|
+
/* Chat */
|
|
52
|
+
.chat-container { display: flex; flex: 1; min-height: 0; }
|
|
53
|
+
.chat-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
54
|
+
.chat-messages { flex: 1; overflow-y: auto; padding: 20px; }
|
|
55
|
+
.chat-msg { margin: 8px 0; display: flex; }
|
|
56
|
+
.chat-msg.user { justify-content: flex-end; }
|
|
57
|
+
.chat-msg .bubble { max-width: 75%; padding: 10px 16px; border-radius: 16px; line-height: 1.6; font-size: 14px; word-break: break-word; }
|
|
58
|
+
.chat-msg.agent .bubble { background: #f1f3f4; border-bottom-left-radius: 4px; }
|
|
59
|
+
.chat-msg.user .bubble { background: var(--blue); color: white; border-bottom-right-radius: 4px; }
|
|
60
|
+
.chat-msg .bubble pre { background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 8px; overflow-x: auto; margin: 8px 0; font-family: var(--mono); font-size: 13px; }
|
|
61
|
+
.chat-msg .bubble code { background: rgba(0,0,0,0.06); padding: 2px 6px; border-radius: 4px; font-family: var(--mono); font-size: 13px; }
|
|
62
|
+
.chat-msg .bubble pre code { background: none; padding: 0; }
|
|
63
|
+
.chat-msg .bubble p { margin: 4px 0; }
|
|
64
|
+
.chat-msg .bubble ul, .chat-msg .bubble ol { margin: 4px 0 4px 20px; }
|
|
65
|
+
.chat-input-bar { padding: 12px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; align-items: center; background: var(--white); }
|
|
66
|
+
.chat-input-bar input { flex: 1; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 14px; font-size: 14px; outline: none; background: var(--bg); }
|
|
67
|
+
.chat-input-bar input:focus { border-color: var(--blue); background: var(--white); }
|
|
68
|
+
.chat-input-bar .send-btn { background: var(--blue); color: white; padding: 10px 20px; border-radius: var(--radius-sm); font-size: 14px; font-weight: 500; }
|
|
69
|
+
.chat-input-bar .send-btn:hover { background: var(--blue-hover); }
|
|
70
|
+
.chat-input-bar .send-btn:disabled { background: var(--border); cursor: not-allowed; }
|
|
71
|
+
.chat-input-bar .icon-btn { font-size: 18px; background: none; color: var(--text-muted); padding: 4px; cursor: pointer; }
|
|
72
|
+
|
|
73
|
+
/* Settings Panel */
|
|
74
|
+
.settings-panel { width: 380px; border-left: 1px solid var(--border); background: var(--bg); overflow-y: auto; display: none; flex-shrink: 0; }
|
|
75
|
+
.settings-panel.open { display: block; }
|
|
76
|
+
.settings-tabs { display: flex; gap: 6px; padding: 12px 16px; flex-wrap: wrap; }
|
|
77
|
+
.settings-tab { padding: 4px 12px; border-radius: 16px; font-size: 12px; cursor: pointer; border: 1px solid var(--border); background: var(--white); color: var(--text-muted); }
|
|
78
|
+
.settings-tab.active { background: var(--blue); color: white; border-color: var(--blue); }
|
|
79
|
+
.settings-content { padding: 16px; }
|
|
80
|
+
.form-group { margin-bottom: 14px; }
|
|
81
|
+
.form-label { font-weight: 600; font-size: 13px; margin-bottom: 4px; display: block; }
|
|
82
|
+
.form-input { width: 100%; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 12px; font-size: 14px; background: var(--white); outline: none; }
|
|
83
|
+
.form-input:focus { border-color: var(--blue); }
|
|
84
|
+
.form-textarea { min-height: 80px; resize: vertical; }
|
|
85
|
+
.btn-primary { background: var(--blue); color: white; padding: 8px 20px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; }
|
|
86
|
+
.btn-primary:hover { background: var(--blue-hover); }
|
|
87
|
+
.btn-secondary { background: var(--white); color: var(--text); padding: 8px 20px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; border: 1px solid var(--border); }
|
|
88
|
+
|
|
89
|
+
/* Pages */
|
|
90
|
+
.page { display: none; flex: 1; flex-direction: column; overflow: hidden; }
|
|
91
|
+
.page.active { display: flex; }
|
|
92
|
+
.page-body { flex: 1; overflow-y: auto; padding: 24px; }
|
|
64
93
|
|
|
65
94
|
/* Cards */
|
|
66
|
-
.card { background: var(--
|
|
67
|
-
.card
|
|
68
|
-
.card
|
|
69
|
-
.card
|
|
70
|
-
.card-
|
|
71
|
-
|
|
72
|
-
/*
|
|
73
|
-
.
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
.
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
.
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.
|
|
135
|
-
.
|
|
136
|
-
.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.chat-header .chat-back { background: none; border: none; color: var(--text-muted); font-size: 18px; cursor: pointer; margin-right: 4px; }
|
|
141
|
-
.chat-messages { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 16px; }
|
|
142
|
-
.msg { max-width: 75%; display: flex; flex-direction: column; gap: 4px; }
|
|
143
|
-
.msg.user { align-self: flex-end; }
|
|
144
|
-
.msg.assistant { align-self: flex-start; }
|
|
145
|
-
.msg-bubble { padding: 12px 16px; border-radius: 6px; font-size: 14px; line-height: 1.6; white-space: pre-wrap; }
|
|
146
|
-
.msg.user .msg-bubble { background: #238636; color: white; border-bottom-right-radius: 4px; }
|
|
147
|
-
.msg.assistant .msg-bubble { background: var(--bg-card); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
|
|
148
|
-
.msg-time { font-size: 11px; color: var(--text-dim); }
|
|
149
|
-
.msg.user .msg-time { text-align: right; }
|
|
150
|
-
.typing-indicator { display: none; align-self: flex-start; }
|
|
151
|
-
.typing-indicator.show { display: flex; }
|
|
152
|
-
.typing-indicator .dots { display: flex; gap: 4px; padding: 12px 16px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 16px; }
|
|
153
|
-
.typing-indicator .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim); animation: bounce 1.4s infinite ease-in-out; }
|
|
154
|
-
.typing-indicator .dot:nth-child(2) { animation-delay: 0.2s; }
|
|
155
|
-
.typing-indicator .dot:nth-child(3) { animation-delay: 0.4s; }
|
|
156
|
-
@keyframes bounce { 0%,80%,100% { transform: scale(0.6); } 40% { transform: scale(1); } }
|
|
157
|
-
.chat-input-bar { padding: 16px 24px; border-top: 1px solid var(--border); background: var(--bg-card); display: flex; gap: 12px; align-items: center; }
|
|
158
|
-
.chat-input-bar .input { flex: 1; border-radius: 6px; padding: 5px 12px; height: 32px; }
|
|
159
|
-
.chat-input-bar .btn { border-radius: 6px; padding: 6px 16px; }
|
|
160
|
-
|
|
161
|
-
/* Memory timeline */
|
|
162
|
-
.timeline { position: relative; padding-left: 24px; }
|
|
163
|
-
.timeline::before { content: ''; position: absolute; left: 8px; top: 0; bottom: 0; width: 2px; background: var(--border); }
|
|
164
|
-
.timeline-item { position: relative; margin-bottom: 24px; }
|
|
165
|
-
.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); }
|
|
166
|
-
.timeline-date { font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }
|
|
167
|
-
.timeline-content { font-size: 14px; color: var(--text-muted); line-height: 1.5; }
|
|
168
|
-
|
|
169
|
-
/* Empty state */
|
|
170
|
-
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
|
|
171
|
-
.empty-state .empty-icon { font-size: 48px; margin-bottom: 16px; }
|
|
172
|
-
.empty-state .empty-title { font-size: 18px; font-weight: 600; color: var(--text); margin-bottom: 8px; }
|
|
173
|
-
.empty-state .empty-desc { font-size: 14px; margin-bottom: 24px; }
|
|
174
|
-
|
|
175
|
-
/* Confirm dialog */
|
|
176
|
-
.dialog-overlay { display: none; position: fixed; inset: 0; background: rgba(5,5,26,0.85); backdrop-filter: blur(12px); z-index: 200; align-items: center; justify-content: center; }
|
|
177
|
-
.dialog-overlay.show { display: flex; }
|
|
178
|
-
.dialog { background: rgba(15,15,45,0.95); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 32px; max-width: 420px; width: 90%; backdrop-filter: blur(30px); box-shadow: 0 25px 80px rgba(0,0,0,0.6), var(--glow); }
|
|
179
|
-
.dialog-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
|
180
|
-
.dialog-desc { font-size: 14px; color: var(--text-muted); margin-bottom: 20px; }
|
|
181
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
182
|
-
|
|
183
|
-
/* Settings layout */
|
|
184
|
-
.settings-layout { display: flex; gap: 0; min-height: calc(100vh - 64px); }
|
|
185
|
-
.settings-nav { width: 200px; background: var(--bg-card); border-right: 1px solid var(--border); padding: 16px 8px; flex-shrink: 0; }
|
|
186
|
-
.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; }
|
|
187
|
-
.settings-nav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
188
|
-
.settings-nav-item.active { background: var(--accent-light); color: var(--accent); font-weight: 500; }
|
|
189
|
-
.settings-content { flex: 1; padding: 32px; max-width: 900px; }
|
|
190
|
-
.settings-panel { display: none; }
|
|
191
|
-
.settings-panel.active { display: block; }
|
|
192
|
-
@media (max-width: 768px) {
|
|
193
|
-
.settings-layout { flex-direction: column; }
|
|
194
|
-
.settings-nav { width: 100%; display: flex; overflow-x: auto; padding: 8px; gap: 4px; border-right: none; border-bottom: 1px solid var(--border); }
|
|
195
|
-
.settings-nav-item { white-space: nowrap; font-size: 13px; padding: 8px 12px; }
|
|
196
|
-
.settings-content { padding: 16px; }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/* Tabs */
|
|
200
|
-
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 1px solid var(--border); }
|
|
201
|
-
.tab { padding: 10px 20px; cursor: pointer; color: var(--text-muted); font-size: 14px; border-bottom: 2px solid transparent; transition: all 0.15s; }
|
|
202
|
-
.tab:hover { color: var(--text); }
|
|
203
|
-
.tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 500; }
|
|
204
|
-
.tab-panel { display: none; }
|
|
205
|
-
.tab-panel.active { display: block; }
|
|
206
|
-
|
|
207
|
-
/* Status dot */
|
|
208
|
-
.status-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; animation: pulse 2s ease-in-out infinite; }
|
|
209
|
-
.status-dot.green { background: var(--green); box-shadow: 0 0 12px var(--green), 0 0 4px var(--green); }
|
|
210
|
-
.status-dot.red { background: var(--red); box-shadow: 0 0 12px var(--red), 0 0 4px var(--red); }
|
|
211
|
-
.status-dot.yellow { background: var(--yellow); box-shadow: 0 0 12px var(--yellow), 0 0 4px var(--yellow); }
|
|
212
|
-
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(0.85); } }
|
|
213
|
-
|
|
214
|
-
/* Channel card */
|
|
215
|
-
.channel-card { display: flex; align-items: center; gap: 16px; cursor: pointer; }
|
|
216
|
-
.channel-card .ch-icon { font-size: 28px; }
|
|
217
|
-
.channel-card .ch-info { flex: 1; }
|
|
218
|
-
.channel-card .ch-name { font-size: 15px; font-weight: 600; }
|
|
219
|
-
.channel-card .ch-status { font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
|
|
220
|
-
|
|
221
|
-
/* Stat card */
|
|
222
|
-
.stat-card { text-align: center; }
|
|
223
|
-
.stat-value { font-size: 28px; font-weight: 700; color: var(--accent); }
|
|
224
|
-
.stat-label { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
|
|
225
|
-
|
|
226
|
-
/* Log viewer */
|
|
227
|
-
.log-viewer { background: #0d1117; 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; }
|
|
228
|
-
|
|
229
|
-
/* Module iframe */
|
|
230
|
-
.module-frame-container { position: relative; border-radius: var(--radius-lg); overflow: hidden; border: 1px solid var(--border); }
|
|
231
|
-
.module-frame-container iframe { width: 100%; height: 600px; border: none; background: var(--bg); }
|
|
232
|
-
.module-frame-fallback { text-align: center; padding: 48px 24px; }
|
|
233
|
-
.module-frame-fallback .mf-icon { font-size: 48px; margin-bottom: 16px; }
|
|
234
|
-
|
|
235
|
-
/* Ollama tutorial */
|
|
236
|
-
.tutorial-steps { counter-reset: step; }
|
|
237
|
-
.tutorial-step { display: flex; gap: 16px; margin-bottom: 20px; align-items: flex-start; }
|
|
238
|
-
.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; }
|
|
239
|
-
.tutorial-step-content { flex: 1; }
|
|
240
|
-
.tutorial-step-content h4 { font-size: 15px; margin-bottom: 4px; }
|
|
241
|
-
.tutorial-step-content p { font-size: 13px; color: var(--text-muted); line-height: 1.5; }
|
|
242
|
-
.tutorial-step-content code { background: var(--bg-hover); padding: 2px 8px; border-radius: 4px; font-family: var(--mono); font-size: 13px; }
|
|
243
|
-
|
|
244
|
-
/* Provider card */
|
|
245
|
-
.provider-card { cursor: pointer; transition: all 0.15s; }
|
|
246
|
-
.provider-card:hover { border-color: var(--accent); }
|
|
247
|
-
.provider-card.configured { border-color: var(--green); }
|
|
248
|
-
.provider-card .pv-header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
|
249
|
-
.provider-card .pv-name { font-size: 15px; font-weight: 600; }
|
|
250
|
-
.provider-card .pv-status { font-size: 12px; }
|
|
251
|
-
|
|
252
|
-
/* Bar chart */
|
|
253
|
-
.bar-chart { display: flex; align-items: flex-end; gap: 4px; height: 120px; padding-top: 8px; }
|
|
254
|
-
.bar-chart .bar { flex: 1; background: var(--accent); border-radius: 3px 3px 0 0; min-height: 4px; transition: height 0.3s; position: relative; }
|
|
255
|
-
.bar-chart .bar:hover { opacity: 0.8; }
|
|
256
|
-
.bar-chart .bar-label { position: absolute; bottom: -20px; left: 50%; transform: translateX(-50%); font-size: 10px; color: var(--text-dim); white-space: nowrap; }
|
|
257
|
-
|
|
258
|
-
/* Scrollbar */
|
|
259
|
-
::-webkit-scrollbar { width: 6px; }
|
|
260
|
-
::-webkit-scrollbar-track { background: transparent; }
|
|
261
|
-
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
|
|
262
|
-
|
|
263
|
-
/* Settings layout */
|
|
264
|
-
.settings-layout { display: flex; gap: 24px; align-items: flex-start; }
|
|
265
|
-
.settings-subnav { width: 190px; flex-shrink: 0; }
|
|
266
|
-
.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; }
|
|
267
|
-
.snav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
268
|
-
.snav-item.active { background: var(--accent-light); color: var(--accent); font-weight: 500; }
|
|
269
|
-
.settings-content { flex: 1; min-width: 0; }
|
|
270
|
-
.settings-section { display: none; }
|
|
271
|
-
.settings-section.active { display: block; }
|
|
272
|
-
.tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
|
|
273
|
-
.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; }
|
|
274
|
-
.tab:hover { color: var(--text); }
|
|
275
|
-
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
276
|
-
.tab-panel { display: none; }
|
|
277
|
-
.tab-panel.active { display: block; }
|
|
278
|
-
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; vertical-align: middle; flex-shrink: 0; }
|
|
279
|
-
.status-dot.green { background: var(--green); }
|
|
280
|
-
.status-dot.red { background: var(--red); }
|
|
281
|
-
.status-dot.yellow { background: var(--yellow); animation: sdpulse 1.5s ease-in-out infinite; }
|
|
282
|
-
@keyframes sdpulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
283
|
-
.channel-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
|
|
284
|
-
.channel-card { cursor: pointer; text-align: center; padding: 20px 12px; }
|
|
285
|
-
.channel-card:hover { border-color: var(--accent); }
|
|
286
|
-
.channel-card.connected { border-color: var(--green); }
|
|
287
|
-
.log-viewer { background: #0d1117; 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; }
|
|
288
|
-
.bar-chart-wrap { display: flex; align-items: flex-end; gap: 6px; height: 80px; }
|
|
289
|
-
.bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; height: 100%; justify-content: flex-end; }
|
|
290
|
-
.bar-fill { width: 100%; background: var(--accent); border-radius: 3px 3px 0 0; min-height: 2px; transition: height 0.4s ease; }
|
|
291
|
-
.bar-lbl { font-size: 10px; color: var(--text-dim); }
|
|
292
|
-
.iframe-wrap { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
|
293
|
-
.iframe-wrap iframe { width: 100%; height: 580px; border: none; display: block; background: var(--bg); }
|
|
294
|
-
.help-text { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
|
|
95
|
+
.card { background: var(--white); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin: 12px 0; box-shadow: var(--shadow); }
|
|
96
|
+
.card:hover { box-shadow: var(--shadow-hover); }
|
|
97
|
+
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; margin: 16px 0; }
|
|
98
|
+
.card-title { font-weight: 600; margin-bottom: 4px; }
|
|
99
|
+
.card-desc { font-size: 13px; color: var(--text-muted); }
|
|
100
|
+
|
|
101
|
+
/* Tags */
|
|
102
|
+
.tag { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 12px; margin: 2px; }
|
|
103
|
+
.tag-blue { background: var(--blue-light); color: var(--blue); }
|
|
104
|
+
.tag-green { background: var(--green-light); color: var(--green); }
|
|
105
|
+
.tag-yellow { background: var(--yellow-light); color: var(--yellow); }
|
|
106
|
+
.tag-red { background: var(--red-light); color: var(--red); }
|
|
107
|
+
|
|
108
|
+
/* Stat Cards */
|
|
109
|
+
.stat-row { display: flex; gap: 12px; margin-bottom: 20px; }
|
|
110
|
+
.stat-card { flex: 1; border-radius: var(--radius-sm); padding: 16px; text-align: center; }
|
|
111
|
+
.stat-card .num { font-size: 28px; font-weight: 700; }
|
|
112
|
+
.stat-card .label { font-size: 12px; color: var(--text-muted); }
|
|
113
|
+
|
|
114
|
+
/* Provider Card */
|
|
115
|
+
.provider-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px; }
|
|
116
|
+
.provider-card.verified { border-color: var(--green); background: #f6fef8; }
|
|
117
|
+
.provider-card .provider-name { font-weight: 600; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
|
|
118
|
+
.provider-card .provider-status { font-size: 12px; margin-bottom: 6px; }
|
|
119
|
+
.provider-card .provider-models { font-size: 11px; color: var(--text-muted); }
|
|
120
|
+
.provider-card .key-row { margin-top: 8px; display: flex; gap: 4px; align-items: center; }
|
|
121
|
+
.provider-card .key-input { flex: 1; border: 1px solid var(--border); border-radius: 4px; padding: 4px 8px; font-size: 12px; background: var(--white); }
|
|
122
|
+
.provider-card .verify-btn { background: var(--blue); color: white; border-radius: 4px; padding: 3px 10px; font-size: 12px; cursor: pointer; }
|
|
123
|
+
|
|
124
|
+
/* Breadcrumb */
|
|
125
|
+
.breadcrumb { font-size: 13px; color: var(--text-muted); margin-bottom: 16px; }
|
|
126
|
+
.breadcrumb a { color: var(--blue); cursor: pointer; font-weight: 500; text-decoration: none; }
|
|
127
|
+
.breadcrumb .sep { margin: 0 6px; }
|
|
128
|
+
|
|
129
|
+
/* Skill Tags */
|
|
130
|
+
.skill-box { border-radius: var(--radius-sm); padding: 10px; margin: 8px 0; font-size: 13px; }
|
|
131
|
+
.skill-box.industry { background: #f8faff; border: 1px solid var(--blue-light); }
|
|
132
|
+
.skill-box.job { background: #f6fef8; border: 1px solid #c8e6c9; }
|
|
133
|
+
.skill-box.station { background: #fff8f0; border: 1px solid #ffe0b2; }
|
|
134
|
+
.skill-tag { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; margin: 2px; }
|
|
135
|
+
|
|
136
|
+
/* Upload Zone */
|
|
137
|
+
.upload-zone { border: 2px dashed var(--blue); border-radius: var(--radius); padding: 32px; text-align: center; background: #f8faff; margin-bottom: 20px; cursor: pointer; transition: background 0.15s; }
|
|
138
|
+
.upload-zone:hover { background: var(--blue-light); }
|
|
139
|
+
.upload-zone .icon { font-size: 36px; margin-bottom: 8px; }
|
|
140
|
+
.upload-zone .title { font-weight: 600; font-size: 16px; color: var(--blue); }
|
|
141
|
+
.upload-zone .desc { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
|
|
142
|
+
|
|
143
|
+
/* Search */
|
|
144
|
+
.search-input { width: 100%; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 14px; font-size: 14px; background: var(--bg); outline: none; margin-bottom: 16px; }
|
|
145
|
+
.search-input:focus { border-color: var(--blue); background: var(--white); }
|
|
146
|
+
|
|
147
|
+
/* Filter Tags */
|
|
148
|
+
.filter-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
|
149
|
+
.filter-tag { padding: 6px 14px; border-radius: 20px; font-size: 12px; cursor: pointer; border: 1px solid var(--border); background: var(--white); color: var(--text-muted); }
|
|
150
|
+
.filter-tag.active { background: var(--blue); color: white; border-color: var(--blue); }
|
|
151
|
+
|
|
152
|
+
/* Knowledge Layer Cards */
|
|
153
|
+
.knowledge-card { border-left: 4px solid; border-radius: var(--radius); padding: 16px; margin: 8px 0; background: var(--white); box-shadow: var(--shadow); }
|
|
154
|
+
.knowledge-card.industry { border-left-color: var(--blue); }
|
|
155
|
+
.knowledge-card.job { border-left-color: var(--green); }
|
|
156
|
+
.knowledge-card.station { border-left-color: var(--yellow); }
|
|
157
|
+
|
|
158
|
+
/* Toast */
|
|
159
|
+
.toast { position: fixed; bottom: 20px; right: 20px; background: var(--text); color: white; padding: 12px 20px; border-radius: var(--radius-sm); font-size: 14px; z-index: 999; display: none; box-shadow: var(--shadow-hover); }
|
|
160
|
+
|
|
161
|
+
/* Typing indicator */
|
|
162
|
+
.typing { display: inline-flex; gap: 4px; padding: 10px 16px; background: #f1f3f4; border-radius: 16px; border-bottom-left-radius: 4px; }
|
|
163
|
+
.typing span { width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim); animation: typing 1.2s infinite; }
|
|
164
|
+
.typing span:nth-child(2) { animation-delay: 0.2s; }
|
|
165
|
+
.typing span:nth-child(3) { animation-delay: 0.4s; }
|
|
166
|
+
@keyframes typing { 0%,60%,100% { opacity: 0.3; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-4px); } }
|
|
167
|
+
|
|
168
|
+
/* Responsive */
|
|
295
169
|
@media (max-width: 768px) {
|
|
296
|
-
.
|
|
297
|
-
.
|
|
298
|
-
.
|
|
170
|
+
.sidebar { width: 60px; }
|
|
171
|
+
.sidebar-label, .sidebar-item span:not(.status-dot) { display: none; }
|
|
172
|
+
.settings-panel { width: 100%; position: absolute; right: 0; top: 0; bottom: 0; z-index: 10; }
|
|
299
173
|
}
|
|
300
|
-
|
|
301
|
-
/* Sidebar restructure */
|
|
302
|
-
.sidebar-section-title { font-size: 18px; letter-spacing: 0.3px; color: var(--text-dim); margin: 24px 16px 10px; font-weight: 600; }
|
|
303
|
-
.sidebar-divider { height: 1px; background: var(--border); margin: 8px 12px; }
|
|
304
|
-
.pattern-card.active { border-color: var(--accent); box-shadow: var(--glow-sm); }
|
|
305
|
-
.agent-list-container { overflow-y: auto; flex: 1; min-height: 0; }
|
|
306
|
-
.agent-list-item {
|
|
307
|
-
display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-radius: 12px;
|
|
308
|
-
cursor: pointer; color: var(--text-muted); transition: all 0.2s ease; font-size: 18px; margin-bottom: 2px; position: relative;
|
|
309
|
-
}
|
|
310
|
-
.agent-list-item:hover { background: var(--bg-hover); color: var(--text); transform: translateX(4px); }
|
|
311
|
-
.agent-list-item.active { background: var(--accent-light); color: #fff; font-weight: 600; box-shadow: var(--glow-sm); border: 1px solid var(--border); }
|
|
312
|
-
.agent-list-item .agent-icon { width: 24px; text-align: center; font-size: 16px; }
|
|
313
|
-
.agent-list-item .agent-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
314
|
-
.agent-list-item .status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
315
|
-
.agent-list-item .status-dot.online { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
316
|
-
.agent-list-item .status-dot.offline { background: var(--text-dim); }
|
|
317
|
-
.agent-list-item .status-dot.error { background: var(--red); box-shadow: 0 0 6px var(--red); }
|
|
318
|
-
.sidebar-bottom { margin-top: auto; flex-shrink: 0; }
|
|
319
|
-
.sidebar-nav { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
|
|
320
|
-
|
|
321
|
-
/* Agent Detail Page */
|
|
322
|
-
.agent-detail-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 28px; border-bottom: 1px solid var(--border); }
|
|
323
|
-
.agent-detail-info { display: flex; align-items: center; gap: 12px; }
|
|
324
|
-
.agent-detail-icon { font-size: 28px; }
|
|
325
|
-
.agent-detail-name { font-size: 20px; font-weight: 700; margin: 0; }
|
|
326
|
-
.agent-detail-toggle { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; width: 40px; height: 40px; font-size: 18px; cursor: pointer; color: var(--text-muted); transition: all 0.2s; display: flex; align-items: center; justify-content: center; }
|
|
327
|
-
.agent-detail-toggle:hover { background: var(--bg-hover); color: var(--text); border-color: var(--accent); }
|
|
328
|
-
.agent-detail-toggle.active { background: var(--accent-light); color: var(--accent); border-color: var(--accent); }
|
|
329
|
-
#page-agent-detail { display: none; flex-direction: column; height: 100vh; }
|
|
330
|
-
#page-agent-detail.active { display: flex; }
|
|
331
|
-
.agent-chat-view { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
332
|
-
.agent-chat-messages { flex: 1; overflow-y: auto; padding: 24px 28px; display: flex; flex-direction: column; gap: 16px; }
|
|
333
|
-
.agent-chat-welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; color: var(--text-dim); }
|
|
334
|
-
.agent-chat-input-bar { display: flex; gap: 12px; padding: 16px 28px; border-top: 1px solid var(--border); background: rgba(5,5,30,0.5); backdrop-filter: blur(10px); }
|
|
335
|
-
.agent-chat-input { flex: 1; background: var(--bg-input); border: 1px solid var(--border); border-radius: 12px; padding: 12px 16px; color: var(--text); font-size: 14px; resize: none; outline: none; font-family: var(--font); max-height: 120px; }
|
|
336
|
-
.agent-chat-input:focus { border-color: var(--accent); }
|
|
337
|
-
.agent-chat-send { padding: 12px 20px; border-radius: 12px; font-weight: 600; flex-shrink: 0; }
|
|
338
|
-
.agent-chat-msg { max-width: 75%; padding: 12px 16px; border-radius: 16px; font-size: 14px; line-height: 1.6; word-break: break-word; }
|
|
339
|
-
.agent-chat-msg.user { align-self: flex-end; background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
|
|
340
|
-
.agent-chat-msg.assistant { align-self: flex-start; background: var(--bg-card); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
|
|
341
|
-
.agent-settings-view { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
|
342
|
-
.agent-settings-tabs { display: flex; gap: 4px; padding: 16px 28px 0; border-bottom: 1px solid var(--border); overflow-x: auto; flex-shrink: 0; }
|
|
343
|
-
.agent-tab { padding: 10px 16px; border-radius: 10px 10px 0 0; cursor: pointer; color: var(--text-muted); font-size: 13px; white-space: nowrap; transition: all 0.15s; border-bottom: 2px solid transparent; }
|
|
344
|
-
.agent-tab:hover { color: var(--text); background: var(--bg-hover); }
|
|
345
|
-
.agent-tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
|
346
|
-
.agent-settings-content { flex: 1; overflow-y: auto; padding: 24px 28px; }
|
|
347
|
-
.agent-tab-panel { display: none; }
|
|
348
|
-
.agent-tab-panel.active { display: block; }
|
|
349
|
-
.agent-tab-panel h3 { margin-bottom: 12px; font-size: 18px; }
|
|
350
174
|
</style>
|
|
351
175
|
</head>
|
|
352
176
|
<body>
|
|
353
177
|
<div class="app">
|
|
354
178
|
<!-- Sidebar -->
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
<div class="sidebar-logo" onclick="navigate('dashboard')" style="cursor:pointer;">⚡ <span>OPC Studio</span></div>
|
|
358
|
-
<div class="sidebar-nav">
|
|
359
|
-
<!-- Section 1: My Agents -->
|
|
360
|
-
<div class="sidebar-section-title">🤖 我的 Agent</div>
|
|
361
|
-
<div class="agent-list-container" id="sidebar-agent-list">
|
|
362
|
-
<div style="padding: 12px 16px; color: var(--text-dim); font-size: 13px;">加载中...</div>
|
|
363
|
-
</div>
|
|
179
|
+
<nav class="sidebar" id="sidebar">
|
|
180
|
+
<div class="sidebar-logo">ΓÜí OPC Studio</div>
|
|
364
181
|
|
|
365
|
-
|
|
366
|
-
<div class="sidebar-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
<!-- dynamically loaded -->
|
|
370
|
-
</div>
|
|
371
|
-
<div class="nav-item" data-page="create-group" onclick="navigate('create-group')">
|
|
372
|
-
<span class="icon">➕</span> 新建群组
|
|
182
|
+
<div class="sidebar-section">
|
|
183
|
+
<div class="sidebar-item active" onclick="navigate('assistant')" id="nav-assistant">
|
|
184
|
+
<span>🧑‍💻</span><span>OPC 助手</span>
|
|
185
|
+
<span class="status-dot"></span>
|
|
373
186
|
</div>
|
|
187
|
+
</div>
|
|
374
188
|
|
|
375
|
-
|
|
376
|
-
<div class="sidebar-divider"></div>
|
|
377
|
-
<div class="nav-item" data-page="create" onclick="navigate('create')">
|
|
378
|
-
<span class="icon">➕</span> 新建 Agent
|
|
379
|
-
</div>
|
|
189
|
+
<div class="sidebar-divider"></div>
|
|
380
190
|
|
|
381
|
-
|
|
382
|
-
<div class="sidebar-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
</div>
|
|
388
|
-
<div class="nav-item" data-page="global-models" onclick="navigate('global-models')">
|
|
389
|
-
<span class="icon">🧠</span> Models
|
|
390
|
-
</div>
|
|
391
|
-
<div class="nav-item" data-page="global-channels" onclick="navigate('global-channels')">
|
|
392
|
-
<span class="icon">📡</span> Channels
|
|
393
|
-
</div>
|
|
394
|
-
<div class="nav-item" data-page="global-memory" onclick="navigate('global-memory')">
|
|
395
|
-
<span class="icon">💾</span> Memory
|
|
396
|
-
</div>
|
|
397
|
-
<div class="nav-item" data-page="global-templates" onclick="navigate('global-templates')">
|
|
398
|
-
<span class="icon">📋</span> Templates
|
|
399
|
-
</div>
|
|
400
|
-
</div>
|
|
191
|
+
<div class="sidebar-section">
|
|
192
|
+
<div class="sidebar-label">🤖 OPC Agent</div>
|
|
193
|
+
<div id="agent-list"></div>
|
|
194
|
+
<div class="sidebar-item action" onclick="navigate('new-agent')"><span>Γ₧ò</span><span>µû░σ╗║ Agent</span></div>
|
|
195
|
+
<div class="sidebar-item action" onclick="showToast('群组功能即将推出')"><span>👥</span><span>新建群组</span></div>
|
|
196
|
+
<div class="sidebar-item" onclick="navigate('channels')"><span>📡</span><span>渠道配置</span></div>
|
|
401
197
|
</div>
|
|
402
|
-
|
|
403
|
-
|
|
198
|
+
|
|
199
|
+
<div class="sidebar-divider"></div>
|
|
200
|
+
|
|
201
|
+
<div class="sidebar-section">
|
|
202
|
+
<div class="sidebar-label">🧩 AgentKits</div>
|
|
203
|
+
<div class="sidebar-item" onclick="navigate('models')" id="nav-models"><span>🤖</span><span>模型配置</span></div>
|
|
404
204
|
</div>
|
|
405
|
-
</nav>
|
|
406
205
|
|
|
407
|
-
|
|
408
|
-
<div class="mobile-header">
|
|
409
|
-
<button onclick="toggleSidebar(true)">☰</button>
|
|
410
|
-
<span style="font-weight:600;">⚡ OPC Studio</span>
|
|
411
|
-
<span></span>
|
|
412
|
-
</div>
|
|
206
|
+
<div class="sidebar-divider"></div>
|
|
413
207
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
<div class="page active" id="page-dashboard">
|
|
418
|
-
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;margin-bottom:24px;">
|
|
419
|
-
<div>
|
|
420
|
-
<h1 class="page-title">My Agents</h1>
|
|
421
|
-
<p class="page-subtitle">Manage and chat with your AI agents</p>
|
|
422
|
-
</div>
|
|
423
|
-
<button class="btn btn-primary" onclick="navigate('create')">✨ Create New Agent</button>
|
|
424
|
-
</div>
|
|
425
|
-
<!-- Dashboard Stats -->
|
|
426
|
-
<div class="card-grid" style="margin-bottom:24px;" id="dashboard-stats"></div>
|
|
427
|
-
<!-- Quick Actions -->
|
|
428
|
-
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:24px;" id="dashboard-actions">
|
|
429
|
-
<button class="btn btn-secondary" onclick="navigate('create')">✨ Create Agent</button>
|
|
430
|
-
<button class="btn btn-secondary" onclick="navigate('global-models')">🧠 Configure Model</button>
|
|
431
|
-
<button class="btn btn-secondary" onclick="navigate('templates')">📋 Browse Templates</button>
|
|
432
|
-
</div>
|
|
433
|
-
<!-- Health Status Section -->
|
|
434
|
-
<div id="health-section" style="margin-bottom:24px;"></div>
|
|
435
|
-
<div id="agents-list" class="card-grid"></div>
|
|
436
|
-
<div id="agents-empty" class="empty-state" style="display:none;">
|
|
437
|
-
<div class="empty-icon">🤖</div>
|
|
438
|
-
<div class="empty-title">No agents yet</div>
|
|
439
|
-
<div class="empty-desc">Create your first AI agent in just 3 steps — no coding required!</div>
|
|
440
|
-
<button class="btn btn-primary btn-lg" onclick="navigate('create')">✨ Create My First Agent</button>
|
|
441
|
-
</div>
|
|
208
|
+
<div class="sidebar-section">
|
|
209
|
+
<div class="sidebar-label">🧠 DeepBrain</div>
|
|
210
|
+
<div class="sidebar-item" onclick="navigate('knowledge')" id="nav-knowledge"><span>📖</span><span>知识库浏览</span></div>
|
|
442
211
|
</div>
|
|
443
212
|
|
|
444
|
-
|
|
445
|
-
<div class="page" id="page-agent-detail">
|
|
446
|
-
<div class="agent-detail-header">
|
|
447
|
-
<div class="agent-detail-info">
|
|
448
|
-
<span class="agent-detail-icon" id="agent-detail-icon">🤖</span>
|
|
449
|
-
<h1 class="agent-detail-name" id="agent-detail-name">Agent</h1>
|
|
450
|
-
<span class="status-dot online" id="agent-detail-status"></span>
|
|
451
|
-
</div>
|
|
452
|
-
<button class="agent-detail-toggle" id="agent-detail-toggle" onclick="toggleAgentSettings()">⚙️</button>
|
|
453
|
-
</div>
|
|
213
|
+
<div class="sidebar-divider"></div>
|
|
454
214
|
|
|
455
|
-
|
|
456
|
-
<div class="
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
<div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;">开始对话</div>
|
|
461
|
-
<div style="color: var(--text-muted); font-size: 14px;">向你的 Agent 发送第一条消息</div>
|
|
462
|
-
</div>
|
|
463
|
-
</div>
|
|
464
|
-
<div class="agent-chat-input-bar">
|
|
465
|
-
<textarea class="agent-chat-input" id="agent-chat-input" placeholder="输入消息..." rows="1" onkeydown="handleAgentChatKey(event)"></textarea>
|
|
466
|
-
<button class="btn btn-primary agent-chat-send" onclick="sendAgentChat()">发送</button>
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
215
|
+
<div class="sidebar-section">
|
|
216
|
+
<div class="sidebar-label">🖥️ Workstation</div>
|
|
217
|
+
<div class="sidebar-item" onclick="navigate('workstation')" id="nav-workstation"><span>📋</span><span>岗位模板库</span></div>
|
|
218
|
+
</div>
|
|
219
|
+
</nav>
|
|
469
220
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
<div class="
|
|
482
|
-
<div class="
|
|
483
|
-
<
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:18px;"><input type="checkbox" id="atab-model-override"> 覆盖全局模型设置</label>
|
|
494
|
-
</div>
|
|
495
|
-
<div id="atab-model-fields" style="display:none;">
|
|
496
|
-
<div class="form-group"><label class="label">Provider</label><select class="input" id="atab-model-provider"><option value="ollama">Ollama (Local)</option><option value="openai">OpenAI</option><option value="anthropic">Anthropic</option><option value="deepseek">DeepSeek</option></select></div>
|
|
497
|
-
<div class="form-group"><label class="label">Model</label><input class="input" id="atab-model-name" placeholder="e.g. gpt-4o-mini"></div>
|
|
498
|
-
<div class="form-group"><label class="label">Temperature</label><input class="input" id="atab-model-temp" type="number" min="0" max="2" step="0.1" value="0.7"></div>
|
|
221
|
+
<!-- Main Content -->
|
|
222
|
+
<div class="main">
|
|
223
|
+
|
|
224
|
+
<!-- Page: OPC Assistant Chat -->
|
|
225
|
+
<div class="page active" id="page-assistant">
|
|
226
|
+
<div class="page-header">
|
|
227
|
+
<span>🧑‍💻</span> OPC 助手
|
|
228
|
+
<span class="status"></span>
|
|
229
|
+
<span class="status-text">在线</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="chat-container">
|
|
232
|
+
<div class="chat-main">
|
|
233
|
+
<div class="chat-messages" id="assistant-messages">
|
|
234
|
+
<div class="chat-msg agent">
|
|
235
|
+
<div class="bubble">
|
|
236
|
+
👋 你好!我是 OPC 助手。<br><br>
|
|
237
|
+
我可以帮你:<br>
|
|
238
|
+
• 直接聊天<br>
|
|
239
|
+
• 创建 AI Agent<br>
|
|
240
|
+
• 配置渠道(Telegram / 微信 / 飞书)<br>
|
|
241
|
+
• 回答关于 OPC 的任何问题<br><br>
|
|
242
|
+
试试跟我说点什么吧!
|
|
243
|
+
</div>
|
|
499
244
|
</div>
|
|
500
|
-
<button class="btn btn-primary" onclick="saveAgentModel()">💾 保存</button>
|
|
501
|
-
<span id="atab-model-status" style="margin-left:12px;font-size:18px;"></span>
|
|
502
|
-
</div>
|
|
503
|
-
<div class="agent-tab-panel" id="atab-channels">
|
|
504
|
-
<h3>渠道配置</h3>
|
|
505
|
-
<div id="atab-channels-list" style="font-size:18px;color:var(--text-muted);">加载中...</div>
|
|
506
|
-
<button class="btn btn-primary" style="margin-top:16px;" onclick="saveAgentChannels()">💾 保存</button>
|
|
507
|
-
<span id="atab-channels-status" style="margin-left:12px;font-size:18px;"></span>
|
|
508
|
-
</div>
|
|
509
|
-
<div class="agent-tab-panel" id="atab-memory">
|
|
510
|
-
<h3>记忆管理</h3>
|
|
511
|
-
<div id="atab-memory-list" style="font-size:18px;color:var(--text-muted);">加载中...</div>
|
|
512
|
-
</div>
|
|
513
|
-
<div class="agent-tab-panel" id="atab-skills">
|
|
514
|
-
<h3>技能配置</h3>
|
|
515
|
-
<div id="atab-skills-list" style="font-size:18px;color:var(--text-muted);">加载中...</div>
|
|
516
245
|
</div>
|
|
517
|
-
<div class="
|
|
518
|
-
<
|
|
519
|
-
<
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
<h3>用量统计</h3>
|
|
523
|
-
<div id="atab-usage-content" style="font-size:18px;color:var(--text-muted);">加载中...</div>
|
|
246
|
+
<div class="chat-input-bar">
|
|
247
|
+
<button class="icon-btn">📎</button>
|
|
248
|
+
<button class="icon-btn">🎤</button>
|
|
249
|
+
<input type="text" id="assistant-input" placeholder="输入消息..." onkeydown="if(event.key==='Enter')sendAssistantMsg()">
|
|
250
|
+
<button class="send-btn" onclick="sendAssistantMsg()">发送</button>
|
|
524
251
|
</div>
|
|
525
252
|
</div>
|
|
526
253
|
</div>
|
|
527
254
|
</div>
|
|
528
255
|
|
|
529
|
-
<!--
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
<
|
|
536
|
-
<
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
<
|
|
543
|
-
<
|
|
544
|
-
<
|
|
545
|
-
|
|
546
|
-
<div class="card pattern-card" onclick="selectPattern('voting')" id="pat-voting" style="padding:20px;cursor:pointer;">
|
|
547
|
-
<div style="font-size:28px;margin-bottom:8px;">🗳️</div>
|
|
548
|
-
<div style="font-size:15px;font-weight:700;margin-bottom:6px;">Voting 投票模式</div>
|
|
549
|
-
<div style="font-size:13px;color:var(--text-muted);line-height:1.5;margin-bottom:10px;">每个 Agent 独立给出判断和投票,汇总统计后输出多数意见和少数意见。</div>
|
|
550
|
-
<div style="font-size:12px;color:var(--accent);">💡 适合:质量评审、内容审核、多人打分</div>
|
|
551
|
-
<div style="font-size:12px;color:var(--green);margin-top:4px;">📤 产出:投票结果 + 各方理由汇总</div>
|
|
552
|
-
</div>
|
|
553
|
-
<div class="card pattern-card" onclick="selectPattern('pipeline')" id="pat-pipeline" style="padding:20px;cursor:pointer;">
|
|
554
|
-
<div style="font-size:28px;margin-bottom:8px;">🔗</div>
|
|
555
|
-
<div style="font-size:15px;font-weight:700;margin-bottom:6px;">Pipeline 流水线模式</div>
|
|
556
|
-
<div style="font-size:13px;color:var(--text-muted);line-height:1.5;margin-bottom:10px;">Agent A 的输出自动传给 Agent B 作为输入,逐步处理和加工,像工厂流水线。</div>
|
|
557
|
-
<div style="font-size:12px;color:var(--accent);">💡 适合:内容创作、数据处理、翻译校对</div>
|
|
558
|
-
<div style="font-size:12px;color:var(--green);margin-top:4px;">📤 产出:经过多轮加工的最终成果</div>
|
|
256
|
+
<!-- Page: Agent Chat -->
|
|
257
|
+
<div class="page" id="page-agent-chat">
|
|
258
|
+
<div class="page-header">
|
|
259
|
+
<span id="agent-chat-icon">🤖</span>
|
|
260
|
+
<span id="agent-chat-name">Agent</span>
|
|
261
|
+
<span class="status"></span>
|
|
262
|
+
<span class="status-text">在线</span>
|
|
263
|
+
<button class="settings-btn" onclick="toggleSettings()">⚙️</button>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="chat-container">
|
|
266
|
+
<div class="chat-main">
|
|
267
|
+
<div class="chat-messages" id="agent-messages"></div>
|
|
268
|
+
<div class="chat-input-bar">
|
|
269
|
+
<button class="icon-btn">📎</button>
|
|
270
|
+
<button class="icon-btn">🎤</button>
|
|
271
|
+
<input type="text" id="agent-input" placeholder="输入消息..." onkeydown="if(event.key==='Enter')sendAgentMsg()">
|
|
272
|
+
<button class="send-btn" onclick="sendAgentMsg()">发送</button>
|
|
559
273
|
</div>
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
<div
|
|
564
|
-
<div
|
|
565
|
-
<div
|
|
274
|
+
</div>
|
|
275
|
+
<div class="settings-panel" id="agent-settings">
|
|
276
|
+
<div class="settings-tabs" id="settings-tabs">
|
|
277
|
+
<div class="settings-tab active" onclick="switchSettingsTab('role')">角色</div>
|
|
278
|
+
<div class="settings-tab" onclick="switchSettingsTab('model')">模型</div>
|
|
279
|
+
<div class="settings-tab" onclick="switchSettingsTab('channel')">渠道</div>
|
|
280
|
+
<div class="settings-tab" onclick="switchSettingsTab('memory')">记忆</div>
|
|
281
|
+
<div class="settings-tab" onclick="switchSettingsTab('skills')">技能</div>
|
|
566
282
|
</div>
|
|
567
|
-
<div class="
|
|
568
|
-
|
|
569
|
-
<div style="font-size:15px;font-weight:700;margin-bottom:6px;">Shared Memory 共享记忆模式</div>
|
|
570
|
-
<div style="font-size:13px;color:var(--text-muted);line-height:1.5;margin-bottom:10px;">所有 Agent 共享一个知识空间,各自贡献信息,互相读取和补充,持续积累。</div>
|
|
571
|
-
<div style="font-size:12px;color:var(--accent);">💡 适合:知识构建、团队学习、长期项目</div>
|
|
572
|
-
<div style="font-size:12px;color:var(--green);margin-top:4px;">📤 产出:持续进化的共享知识库</div>
|
|
283
|
+
<div class="settings-content" id="settings-content">
|
|
284
|
+
<!-- Filled by JS -->
|
|
573
285
|
</div>
|
|
574
286
|
</div>
|
|
575
|
-
<div class="label">选择 Agent 成员</div>
|
|
576
|
-
<div id="group-agent-select" style="margin-bottom:16px;">
|
|
577
|
-
<p style="color:var(--text-muted);font-size:13px;">请先创建 Agent,再拉入群组</p>
|
|
578
|
-
</div>
|
|
579
|
-
<button class="btn btn-primary" onclick="createGroup()">✨ 创建群组</button>
|
|
580
|
-
</div>
|
|
581
|
-
</div>
|
|
582
|
-
|
|
583
|
-
<div class="page" id="page-templates">
|
|
584
|
-
<h1 class="page-title">Template Market</h1>
|
|
585
|
-
<p class="page-subtitle">Browse 100+ ready-to-use agent templates across 19 industries</p>
|
|
586
|
-
<div class="search-bar">
|
|
587
|
-
<input class="input" id="tpl-search" placeholder="Search templates..." oninput="filterTemplates()">
|
|
588
287
|
</div>
|
|
589
|
-
<div class="chip-group" id="industry-chips"></div>
|
|
590
|
-
<div class="card-grid" id="templates-grid"></div>
|
|
591
288
|
</div>
|
|
592
289
|
|
|
593
|
-
<!--
|
|
594
|
-
<div class="page" id="page-
|
|
595
|
-
<
|
|
596
|
-
<
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
290
|
+
<!-- Page: Models Config (AgentKits) -->
|
|
291
|
+
<div class="page" id="page-models">
|
|
292
|
+
<div class="page-header"><span>🧩</span> AgentKits — 模型配置</div>
|
|
293
|
+
<div class="page-body" id="models-body">
|
|
294
|
+
<div style="background:var(--yellow-light);border:1px solid var(--yellow);border-radius:var(--radius-sm);padding:12px 16px;margin-bottom:16px;font-size:14px;">
|
|
295
|
+
<strong style="color:var(--yellow);">⚠️ 规则:</strong>没有配通 API Key 的 Provider,其模型不会出现在 Agent 的模型选择列表中。
|
|
296
|
+
</div>
|
|
297
|
+
<div id="models-content">加载中...</div>
|
|
600
298
|
</div>
|
|
601
|
-
<div class="card-grid" id="skills-grid" style="margin-top:16px;"></div>
|
|
602
299
|
</div>
|
|
603
300
|
|
|
604
|
-
<!--
|
|
605
|
-
<div class="page" id="page-
|
|
606
|
-
<div class="
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
<div class="
|
|
610
|
-
<div class="
|
|
301
|
+
<!-- Page: Knowledge (DeepBrain) -->
|
|
302
|
+
<div class="page" id="page-knowledge">
|
|
303
|
+
<div class="page-header"><span>🧠</span> DeepBrain — 知识库</div>
|
|
304
|
+
<div class="page-body">
|
|
305
|
+
<div class="upload-zone" onclick="document.getElementById('file-upload').click()">
|
|
306
|
+
<div class="icon">📂</div>
|
|
307
|
+
<div class="title">拖入文档,自动分类</div>
|
|
308
|
+
<div class="desc">支持 PDF · Word · TXT · Markdown · 网页链接</div>
|
|
309
|
+
<div style="font-size:12px;color:var(--text-dim);margin-top:8px;">DeepBrain 自动分析内容,归入行业 / 岗位 / 工位层</div>
|
|
310
|
+
<input type="file" id="file-upload" style="display:none" multiple accept=".pdf,.doc,.docx,.txt,.md">
|
|
611
311
|
</div>
|
|
612
312
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
<
|
|
616
|
-
<
|
|
617
|
-
<div class="search-bar">
|
|
618
|
-
<input class="input" id="wizard-tpl-search" placeholder="Search templates..." oninput="filterWizardTemplates()">
|
|
619
|
-
</div>
|
|
620
|
-
<div class="chip-group" id="wizard-industry-chips"></div>
|
|
621
|
-
<div class="card-grid" id="wizard-tpl-grid" style="max-height:400px;overflow-y:auto;"></div>
|
|
313
|
+
<div class="stat-row">
|
|
314
|
+
<div class="stat-card" style="background:var(--blue-light);"><div class="num" style="color:var(--blue);" id="kb-total">0</div><div class="label">知识条目</div></div>
|
|
315
|
+
<div class="stat-card" style="background:var(--green-light);"><div class="num" style="color:var(--green);" id="kb-evolve">0</div><div class="label">进化次数</div></div>
|
|
316
|
+
<div class="stat-card" style="background:var(--yellow-light);"><div class="num" style="color:var(--yellow);" id="kb-docs">0</div><div class="label">文档已导入</div></div>
|
|
622
317
|
</div>
|
|
623
318
|
|
|
624
|
-
|
|
625
|
-
<div class="wizard-panel" id="wp-2">
|
|
626
|
-
<h2 style="font-size:20px;margin-bottom:8px;">Configure Your Agent</h2>
|
|
627
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:24px;">Give your agent a name and tell it about your business.</p>
|
|
628
|
-
<div class="form-group">
|
|
629
|
-
<label class="label">Agent Name *</label>
|
|
630
|
-
<input class="input" id="agent-name" placeholder="e.g. My Sales Coach">
|
|
631
|
-
</div>
|
|
632
|
-
<div class="form-group">
|
|
633
|
-
<label class="label">Company / Business Description</label>
|
|
634
|
-
<textarea class="input" id="agent-desc" rows="3" placeholder="Brief description of your business so the agent can better help you..."></textarea>
|
|
635
|
-
</div>
|
|
636
|
-
|
|
637
|
-
<!-- AI Prompt Generator -->
|
|
638
|
-
<div class="form-group">
|
|
639
|
-
<label class="label">What should this agent do? <span style="color:var(--text-dim);font-size:13px;">(Describe in plain words, AI generates the prompt)</span></label>
|
|
640
|
-
<textarea class="input" id="agent-task-desc" rows="2" placeholder="e.g. Help me answer customer questions about our products, be friendly and professional..."></textarea>
|
|
641
|
-
<button class="btn btn-sm" style="margin-top:8px;background:var(--accent-light);color:var(--accent);border:1px solid var(--border);" onclick="generatePrompt()">✨ AI Generate Prompt</button>
|
|
642
|
-
<textarea class="input" id="agent-prompt" rows="4" placeholder="System prompt will appear here (or write your own)..." style="margin-top:8px;display:none;"></textarea>
|
|
643
|
-
</div>
|
|
644
|
-
|
|
645
|
-
<div class="form-group">
|
|
646
|
-
<label class="label">AI Model</label>
|
|
647
|
-
<select class="input" id="agent-model">
|
|
648
|
-
<option value="auto">🤖 Auto-detect (Ollama local first)</option>
|
|
649
|
-
<option value="ollama/qwen2.5:7b">🏠 Ollama: Qwen 2.5 7B (Free, Local)</option>
|
|
650
|
-
<option value="ollama/llama3.1:8b">🏠 Ollama: Llama 3.1 8B (Free, Local)</option>
|
|
651
|
-
<option value="ollama/qwen2.5:0.5b">🏠 Ollama: Qwen 2.5 0.5B (Ultra-fast)</option>
|
|
652
|
-
<option value="gpt-4o-mini">☁️ GPT-4o Mini (Fast & Affordable)</option>
|
|
653
|
-
<option value="gpt-4o">☁️ GPT-4o (Most Capable)</option>
|
|
654
|
-
<option value="claude-sonnet-4">☁️ Claude Sonnet (Balanced)</option>
|
|
655
|
-
<option value="deepseek-v3">☁️ DeepSeek V3 (Affordable)</option>
|
|
656
|
-
<option value="gemini-2.0-flash">☁️ Gemini Flash (Google)</option>
|
|
657
|
-
</select>
|
|
658
|
-
<p style="color:var(--text-dim);font-size:12px;margin-top:4px;">🏠 = runs locally, free forever. ☁️ = needs API key.</p>
|
|
659
|
-
</div>
|
|
660
|
-
|
|
661
|
-
<!-- Skill Toggles -->
|
|
662
|
-
<div class="form-group">
|
|
663
|
-
<label class="label">Enable Skills <span style="color:var(--text-dim);font-size:13px;">(What can your agent do?)</span></label>
|
|
664
|
-
<div id="skill-toggles" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px;max-height:200px;overflow-y:auto;padding:4px;"></div>
|
|
665
|
-
</div>
|
|
319
|
+
<input type="text" class="search-input" placeholder="🔍 搜索知识..." oninput="searchKnowledge(this.value)">
|
|
666
320
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
<
|
|
670
|
-
|
|
671
|
-
<option value="en">English</option>
|
|
672
|
-
<option value="auto">Auto-detect</option>
|
|
673
|
-
</select>
|
|
321
|
+
<div id="knowledge-layers">
|
|
322
|
+
<div class="knowledge-card industry">
|
|
323
|
+
<div style="font-weight:600;margin-bottom:4px;">🏢 行业知识</div>
|
|
324
|
+
<div style="font-size:13px;color:var(--text-muted);">加载中...</div>
|
|
674
325
|
</div>
|
|
675
|
-
<div
|
|
676
|
-
<
|
|
677
|
-
<
|
|
326
|
+
<div class="knowledge-card job">
|
|
327
|
+
<div style="font-weight:600;margin-bottom:4px;">👔 岗位知识</div>
|
|
328
|
+
<div style="font-size:13px;color:var(--text-muted);">加载中...</div>
|
|
678
329
|
</div>
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
<div class="wizard-panel" id="wp-3">
|
|
683
|
-
<h2 style="font-size:20px;margin-bottom:8px;">Review & Create</h2>
|
|
684
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:24px;">Everything looks good? Let's bring your agent to life!</p>
|
|
685
|
-
<div class="card" id="confirm-card" style="margin-bottom:24px;"></div>
|
|
686
|
-
<div style="display:flex;gap:12px;justify-content:flex-end;">
|
|
687
|
-
<button class="btn btn-secondary" onclick="wizardBack()">← Back</button>
|
|
688
|
-
<button class="btn btn-primary btn-lg" onclick="createAgent()" id="create-btn">🚀 Create Agent</button>
|
|
330
|
+
<div class="knowledge-card station">
|
|
331
|
+
<div style="font-weight:600;margin-bottom:4px;">🖥️ 工位知识</div>
|
|
332
|
+
<div style="font-size:13px;color:var(--text-muted);">加载中...</div>
|
|
689
333
|
</div>
|
|
690
334
|
</div>
|
|
691
|
-
</div>
|
|
692
|
-
</div>
|
|
693
335
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
<div class="chat-header">
|
|
697
|
-
<button class="chat-back" onclick="navigate('dashboard')">←</button>
|
|
698
|
-
<span class="chat-icon" id="chat-agent-icon">🤖</span>
|
|
699
|
-
<div>
|
|
700
|
-
<div class="chat-name" id="chat-agent-name">Agent</div>
|
|
701
|
-
<div class="chat-status" id="chat-agent-status">Online</div>
|
|
702
|
-
</div>
|
|
703
|
-
<div style="margin-left:auto;display:flex;gap:8px;align-items:center;">
|
|
704
|
-
<select id="chat-agent-select" class="input" style="width:auto;padding:6px 10px;font-size:13px;border-radius:8px;" onchange="switchChatAgent(this.value)"></select>
|
|
705
|
-
<span id="streaming-indicator" style="display:none;font-size:16px;" title="Thinking...">⏳</span>
|
|
706
|
-
<button class="btn btn-sm btn-secondary" onclick="clearChat()">🗑 Clear</button>
|
|
707
|
-
<button class="btn btn-sm btn-secondary" onclick="openMemory()">🧠 Memory</button>
|
|
336
|
+
<div style="font-size:12px;color:var(--text-dim);margin-top:16px;text-align:center;">
|
|
337
|
+
💡 所有 Agent 自动从知识库 recall 相关知识,无需手动分配
|
|
708
338
|
</div>
|
|
709
339
|
</div>
|
|
710
|
-
<div class="chat-messages" id="chat-messages">
|
|
711
|
-
<div class="msg assistant">
|
|
712
|
-
<div class="msg-bubble" id="chat-welcome">Hello! How can I help you today?</div>
|
|
713
|
-
</div>
|
|
714
|
-
</div>
|
|
715
|
-
<div class="typing-indicator" id="typing-indicator">
|
|
716
|
-
<div class="dots"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
|
|
717
|
-
</div>
|
|
718
|
-
<div class="chat-input-bar">
|
|
719
|
-
<input type="file" id="doc-upload-input" style="display:none" accept=".pdf,.txt,.md,.docx,.csv,.json" onchange="handleDocUpload(this)">
|
|
720
|
-
<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>
|
|
721
|
-
<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>
|
|
722
|
-
<input class="input" id="chat-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendMessage()">
|
|
723
|
-
<button class="btn btn-primary" onclick="sendMessage()">Send</button>
|
|
724
|
-
</div>
|
|
725
340
|
</div>
|
|
726
341
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
<div
|
|
731
|
-
<
|
|
732
|
-
<div>
|
|
733
|
-
<
|
|
734
|
-
<p class="page-subtitle" style="margin-bottom:0;">Knowledge your agent has learned over time</p>
|
|
342
|
+
<!-- Page: Workstation -->
|
|
343
|
+
<div class="page" id="page-workstation">
|
|
344
|
+
<div class="page-header"><span>🖥️</span> Workstation — 岗位模板库</div>
|
|
345
|
+
<div class="page-body">
|
|
346
|
+
<input type="text" class="search-input" placeholder="🔍 搜索模板..." oninput="searchTemplates(this.value)">
|
|
347
|
+
<div class="breadcrumb" id="ws-breadcrumb">
|
|
348
|
+
<a onclick="wsNavigate('root')">全部行业</a>
|
|
735
349
|
</div>
|
|
736
|
-
|
|
737
|
-
<div id="memory-timeline"></div>
|
|
738
|
-
<div id="memory-empty" class="empty-state">
|
|
739
|
-
<div class="empty-icon">🧠</div>
|
|
740
|
-
<div class="empty-title">No memories yet</div>
|
|
741
|
-
<div class="empty-desc">Your agent will learn and remember things as you chat with it.</div>
|
|
350
|
+
<div id="ws-content">加载中...</div>
|
|
742
351
|
</div>
|
|
743
352
|
</div>
|
|
744
353
|
|
|
745
|
-
<!--
|
|
746
|
-
<div class="page" id="page-
|
|
747
|
-
<div
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
<p class="page-subtitle">Automate recurring agent tasks with cron schedules</p>
|
|
751
|
-
</div>
|
|
752
|
-
<button class="btn btn-primary" onclick="showScheduleForm()">+ New Task</button>
|
|
753
|
-
</div>
|
|
754
|
-
|
|
755
|
-
<!-- New/Edit Form (hidden by default) -->
|
|
756
|
-
<div id="schedule-form" class="card" style="display:none;margin-bottom:24px;">
|
|
757
|
-
<h3 style="font-size:16px;font-weight:600;margin-bottom:16px;" id="schedule-form-title">New Scheduled Task</h3>
|
|
758
|
-
<div class="form-group">
|
|
759
|
-
<label class="label">Task Name *</label>
|
|
760
|
-
<input class="input" id="sched-name" placeholder="e.g. Daily News Summary">
|
|
761
|
-
</div>
|
|
762
|
-
<div class="form-group">
|
|
763
|
-
<label class="label">Frequency</label>
|
|
764
|
-
<select class="input" id="sched-frequency" onchange="onSchedFreqChange()">
|
|
765
|
-
<option value="daily">Every Day</option>
|
|
766
|
-
<option value="weekly">Every Week (Monday)</option>
|
|
767
|
-
<option value="monthly">Every Month (1st)</option>
|
|
768
|
-
<option value="custom">Custom Cron</option>
|
|
769
|
-
</select>
|
|
770
|
-
</div>
|
|
771
|
-
<div class="form-group" id="sched-time-group">
|
|
772
|
-
<label class="label">Time</label>
|
|
773
|
-
<input class="input" type="time" id="sched-time" value="09:00">
|
|
774
|
-
</div>
|
|
775
|
-
<div class="form-group" id="sched-cron-group" style="display:none;">
|
|
776
|
-
<label class="label">Cron Expression</label>
|
|
777
|
-
<input class="input" id="sched-cron" placeholder="*/5 * * * *">
|
|
778
|
-
<div class="help-text">Format: minute hour dayOfMonth month dayOfWeek</div>
|
|
779
|
-
</div>
|
|
780
|
-
<div class="form-group">
|
|
781
|
-
<label class="label">Description</label>
|
|
782
|
-
<textarea class="input" id="sched-desc" rows="2" placeholder="e.g. Send a news summary every morning at 8am"></textarea>
|
|
783
|
-
</div>
|
|
784
|
-
<div class="form-group">
|
|
785
|
-
<label class="label">Agent</label>
|
|
786
|
-
<select class="input" id="sched-agent"></select>
|
|
787
|
-
</div>
|
|
788
|
-
<div class="form-group">
|
|
789
|
-
<label class="label">Output Channel</label>
|
|
790
|
-
<select class="input" id="sched-channel">
|
|
791
|
-
<option value="web">Web</option>
|
|
792
|
-
<option value="telegram">Telegram</option>
|
|
793
|
-
<option value="email">Email</option>
|
|
794
|
-
</select>
|
|
795
|
-
</div>
|
|
796
|
-
<div style="display:flex;gap:8px;">
|
|
797
|
-
<button class="btn btn-primary" onclick="saveSchedule()">💾 Save</button>
|
|
798
|
-
<button class="btn btn-secondary" onclick="hideScheduleForm()">Cancel</button>
|
|
799
|
-
</div>
|
|
800
|
-
</div>
|
|
801
|
-
|
|
802
|
-
<!-- Task List -->
|
|
803
|
-
<div id="schedules-list"></div>
|
|
804
|
-
<div id="schedules-empty" class="empty-state" style="display:none;">
|
|
805
|
-
<div class="empty-icon">⏰</div>
|
|
806
|
-
<div class="empty-title">No scheduled tasks</div>
|
|
807
|
-
<div class="empty-desc">Create your first automated task to get started.</div>
|
|
808
|
-
<button class="btn btn-primary" onclick="showScheduleForm()">+ Create Task</button>
|
|
354
|
+
<!-- Page: Channels -->
|
|
355
|
+
<div class="page" id="page-channels">
|
|
356
|
+
<div class="page-header"><span>📡</span> 渠道配置</div>
|
|
357
|
+
<div class="page-body" id="channels-body">
|
|
358
|
+
<div id="channels-content">加载中...</div>
|
|
809
359
|
</div>
|
|
810
360
|
</div>
|
|
811
|
-
</div>
|
|
812
|
-
</div>
|
|
813
|
-
|
|
814
|
-
<!-- Settings Page -->
|
|
815
|
-
<div class="page" id="page-settings">
|
|
816
|
-
<div class="settings-layout">
|
|
817
|
-
<div class="settings-nav">
|
|
818
|
-
<div class="settings-nav-item active" data-settings="models" onclick="showSettings('models')">🤖 模型配置</div>
|
|
819
|
-
<div class="settings-nav-item" data-settings="channels" onclick="showSettings('channels')">📡 渠道配置</div>
|
|
820
|
-
<div class="settings-nav-item" data-settings="memory" onclick="showSettings('memory')">🧠 记忆管理</div>
|
|
821
|
-
<div class="settings-nav-item" data-settings="role" onclick="showSettings('role')">👤 角色编辑</div>
|
|
822
|
-
<div class="settings-nav-item" data-settings="status" onclick="showSettings('status')">📊 运行状态</div>
|
|
823
|
-
<div class="settings-nav-item" data-settings="usage" onclick="showSettings('usage')">💰 用量统计</div>
|
|
824
|
-
<div class="settings-nav-item" data-settings="search" onclick="showSettings('search')">🔍 搜索配置</div>
|
|
825
|
-
</div>
|
|
826
|
-
<div class="settings-content">
|
|
827
|
-
|
|
828
|
-
<!-- Models Panel -->
|
|
829
|
-
<div class="settings-panel active" id="sp-models">
|
|
830
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">🤖 模型配置</h2>
|
|
831
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">选择 AI 模型,默认使用本地模型,完全免费</p>
|
|
832
|
-
|
|
833
|
-
<div class="tabs">
|
|
834
|
-
<div class="tab active" onclick="switchModelTab('local')">🏠 本地模型</div>
|
|
835
|
-
<div class="tab" onclick="switchModelTab('cloud')">☁️ 云端 API</div>
|
|
836
|
-
</div>
|
|
837
|
-
|
|
838
|
-
<!-- Local Models Tab -->
|
|
839
|
-
<div class="tab-panel active" id="mt-local">
|
|
840
|
-
<div id="ollama-status" style="margin-bottom:20px;"></div>
|
|
841
|
-
<div id="ollama-models" style="margin-bottom:20px;"></div>
|
|
842
|
-
<div id="ollama-tutorial" style="display:none;">
|
|
843
|
-
<div class="card" style="margin-bottom:20px;">
|
|
844
|
-
<h3 style="font-size:16px;margin-bottom:16px;">📖 3 步安装本地模型</h3>
|
|
845
|
-
<div class="tutorial-steps">
|
|
846
|
-
<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>
|
|
847
|
-
<div class="tutorial-step"><div class="tutorial-step-content"><h4>安装并启动</h4><p>安装完成后,Ollama 会自动在后台运行</p></div></div>
|
|
848
|
-
<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>
|
|
849
|
-
</div>
|
|
850
|
-
<button class="btn btn-primary btn-sm" onclick="detectOllama()" style="margin-top:8px;">🔄 重新检测</button>
|
|
851
|
-
</div>
|
|
852
|
-
</div>
|
|
853
|
-
</div>
|
|
854
|
-
|
|
855
|
-
<!-- Cloud API Tab -->
|
|
856
|
-
<div class="tab-panel" id="mt-cloud">
|
|
857
|
-
<p style="color:var(--text-muted);font-size:13px;margin-bottom:16px;">填入 API Key 即可使用云端模型,按用量付费</p>
|
|
858
|
-
<div class="card-grid" id="cloud-providers">
|
|
859
|
-
<div class="card provider-card" onclick="configureProvider('openai')">
|
|
860
|
-
<div class="pv-header"><span style="font-size:20px;">🟢</span><span class="pv-name">OpenAI</span></div>
|
|
861
|
-
<p style="font-size:13px;color:var(--text-muted);">GPT-4o / GPT-4o-mini</p>
|
|
862
|
-
<div class="pv-status" id="pv-openai">未配置</div>
|
|
863
|
-
</div>
|
|
864
|
-
<div class="card provider-card" onclick="configureProvider('deepseek')">
|
|
865
|
-
<div class="pv-header"><span style="font-size:20px;">🔵</span><span class="pv-name">DeepSeek</span></div>
|
|
866
|
-
<p style="font-size:13px;color:var(--text-muted);">DeepSeek V3 / R1</p>
|
|
867
|
-
<div class="pv-status" id="pv-deepseek">未配置</div>
|
|
868
|
-
</div>
|
|
869
|
-
<div class="card provider-card" onclick="configureProvider('qwen')">
|
|
870
|
-
<div class="pv-header"><span style="font-size:20px;">🟣</span><span class="pv-name">通义千问</span></div>
|
|
871
|
-
<p style="font-size:13px;color:var(--text-muted);">Qwen-Max / Qwen-Plus</p>
|
|
872
|
-
<div class="pv-status" id="pv-qwen">未配置</div>
|
|
873
|
-
</div>
|
|
874
|
-
<div class="card provider-card" onclick="configureProvider('anthropic')">
|
|
875
|
-
<div class="pv-header"><span style="font-size:20px;">🟠</span><span class="pv-name">Anthropic</span></div>
|
|
876
|
-
<p style="font-size:13px;color:var(--text-muted);">Claude Sonnet / Haiku</p>
|
|
877
|
-
<div class="pv-status" id="pv-anthropic">未配置</div>
|
|
878
|
-
</div>
|
|
879
|
-
<div class="card provider-card" onclick="configureProvider('openrouter')">
|
|
880
|
-
<div class="pv-header"><span style="font-size:20px;">🌐</span><span class="pv-name">OpenRouter</span></div>
|
|
881
|
-
<p style="font-size:13px;color:var(--text-muted);">100+ 模型聚合</p>
|
|
882
|
-
<div class="pv-status" id="pv-openrouter">未配置</div>
|
|
883
|
-
</div>
|
|
884
|
-
</div>
|
|
885
|
-
</div>
|
|
886
|
-
|
|
887
|
-
<!-- Model Assignment -->
|
|
888
|
-
<div class="card" style="margin-top:24px;">
|
|
889
|
-
<h3 style="font-size:16px;margin-bottom:16px;">🎯 模型用途分配</h3>
|
|
890
|
-
<div class="form-group">
|
|
891
|
-
<label class="label">聊天模型(必选)</label>
|
|
892
|
-
<select class="input" id="cfg-chat-model" onchange="saveModelAssignment()">
|
|
893
|
-
<option value="qwen2.5:7b">qwen2.5:7b (本地推荐) ⭐</option>
|
|
894
|
-
</select>
|
|
895
|
-
<p style="font-size:12px;color:var(--text-dim);margin-top:4px;">用于对话、回答问题、执行任务</p>
|
|
896
|
-
</div>
|
|
897
|
-
<div class="form-group" style="margin-bottom:0;">
|
|
898
|
-
<label class="label">Embedding 模型(记忆用)</label>
|
|
899
|
-
<select class="input" id="cfg-embed-model" onchange="saveModelAssignment()">
|
|
900
|
-
<option value="nomic-embed-text">nomic-embed-text (本地推荐) ⭐</option>
|
|
901
|
-
</select>
|
|
902
|
-
<p style="font-size:12px;color:var(--text-dim);margin-top:4px;">用于记忆存储和语义搜索,一般不需要更改</p>
|
|
903
|
-
</div>
|
|
904
|
-
</div>
|
|
905
|
-
</div>
|
|
906
361
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
362
|
+
<!-- Page: New Agent -->
|
|
363
|
+
<div class="page" id="page-new-agent">
|
|
364
|
+
<div class="page-header"><span>Γ₧ò</span> µû░σ╗║ Agent</div>
|
|
365
|
+
<div class="page-body">
|
|
366
|
+
<div class="card" style="max-width:600px;">
|
|
367
|
+
<div class="form-group">
|
|
368
|
+
<label class="form-label">名称</label>
|
|
369
|
+
<input class="form-input" id="new-agent-name" placeholder="给你的 Agent 起个名字">
|
|
912
370
|
</div>
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">🧠 记忆管理</h2>
|
|
917
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">管理 Agent 的知识库和记忆,由 DeepBrain 提供</p>
|
|
918
|
-
<div id="memory-module-frame"></div>
|
|
371
|
+
<div class="form-group">
|
|
372
|
+
<label class="form-label">描述</label>
|
|
373
|
+
<input class="form-input" id="new-agent-desc" placeholder="这个 Agent 做什么?">
|
|
919
374
|
</div>
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">👤 角色编辑</h2>
|
|
924
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">编辑 Agent 的角色设定和技能,由 Workstation 提供</p>
|
|
925
|
-
<div id="role-module-frame"></div>
|
|
375
|
+
<div class="form-group">
|
|
376
|
+
<label class="form-label">图标</label>
|
|
377
|
+
<input class="form-input" id="new-agent-icon" placeholder="🤖" value="🤖" style="width:60px;">
|
|
926
378
|
</div>
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">📊 运行状态</h2>
|
|
931
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">查看 Agent 和各模块的运行情况</p>
|
|
932
|
-
<div id="status-overview" style="margin-bottom:24px;"></div>
|
|
933
|
-
<div class="card" style="margin-bottom:16px;">
|
|
934
|
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
935
|
-
<h3 style="font-size:16px;">📋 最近日志</h3>
|
|
936
|
-
<button class="btn btn-secondary btn-sm" onclick="refreshStatus()">🔄 刷新</button>
|
|
937
|
-
</div>
|
|
938
|
-
<div class="log-viewer" id="status-logs">暂无日志</div>
|
|
939
|
-
</div>
|
|
379
|
+
<div class="form-group">
|
|
380
|
+
<label class="form-label">System Prompt</label>
|
|
381
|
+
<textarea class="form-input form-textarea" id="new-agent-prompt" placeholder="描述 Agent 的角色和行为..."></textarea>
|
|
940
382
|
</div>
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
<div id="usage-stats"></div>
|
|
383
|
+
<div class="form-group">
|
|
384
|
+
<label class="form-label">模型</label>
|
|
385
|
+
<select class="form-input" id="new-agent-model">
|
|
386
|
+
<option value="">使用全局默认</option>
|
|
387
|
+
</select>
|
|
947
388
|
</div>
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">🔍 搜索配置</h2>
|
|
952
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">Agent 可以自动搜索互联网回答问题,默认使用 DuckDuckGo(免费,无需配置)</p>
|
|
953
|
-
|
|
954
|
-
<div class="form-group">
|
|
955
|
-
<label class="label">搜索开关</label>
|
|
956
|
-
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
|
|
957
|
-
<input type="checkbox" id="search-enabled" checked onchange="updateSearchConfig()">
|
|
958
|
-
<span style="font-size:14px;">启用 Web 搜索</span>
|
|
959
|
-
</label>
|
|
960
|
-
</div>
|
|
961
|
-
|
|
962
|
-
<div class="form-group">
|
|
963
|
-
<label class="label">默认搜索引擎</label>
|
|
964
|
-
<select class="input" id="search-engine" onchange="updateSearchConfig()" style="padding:8px 12px;">
|
|
965
|
-
<option value="duckduckgo">🦆 DuckDuckGo(免费,默认)</option>
|
|
966
|
-
<option value="brave">🦁 Brave Search(需 API Key)</option>
|
|
967
|
-
<option value="searxng">🔧 SearXNG(自托管)</option>
|
|
968
|
-
<option value="google">🔍 Google Custom Search(需 API Key)</option>
|
|
969
|
-
</select>
|
|
970
|
-
</div>
|
|
971
|
-
|
|
972
|
-
<div class="form-group" id="search-apikey-group" style="display:none;">
|
|
973
|
-
<label class="label" id="search-apikey-label">API Key</label>
|
|
974
|
-
<input class="input" id="search-apikey" type="password" placeholder="输入 API Key" onchange="updateSearchConfig()">
|
|
975
|
-
</div>
|
|
976
|
-
|
|
977
|
-
<div class="form-group" id="search-baseurl-group" style="display:none;">
|
|
978
|
-
<label class="label">SearXNG URL</label>
|
|
979
|
-
<input class="input" id="search-baseurl" placeholder="https://searx.example.com" onchange="updateSearchConfig()">
|
|
980
|
-
</div>
|
|
981
|
-
|
|
982
|
-
<div style="margin-top:16px;">
|
|
983
|
-
<button class="btn btn-secondary btn-sm" onclick="testSearch()">🧪 测试搜索</button>
|
|
984
|
-
</div>
|
|
985
|
-
<div id="search-test-result" style="margin-top:12px;font-size:13px;"></div>
|
|
986
|
-
|
|
987
|
-
<div style="margin-top:24px;padding:16px;background:var(--bg-light);border-radius:8px;font-size:13px;color:var(--text-muted);">
|
|
988
|
-
<strong>💡 提示</strong><br>
|
|
989
|
-
Agent 会自动判断何时需要搜索,也可以通过 <code>web_search</code> 和 <code>web_read</code> 工具主动调用。<br>
|
|
990
|
-
DuckDuckGo 完全免费、无需注册,适合大多数场景。
|
|
991
|
-
</div>
|
|
389
|
+
<div style="display:flex;gap:8px;margin-top:16px;">
|
|
390
|
+
<button class="btn-primary" onclick="createAgent()">创建 Agent</button>
|
|
391
|
+
<button class="btn-secondary" onclick="navigate('assistant')">取消</button>
|
|
992
392
|
</div>
|
|
993
|
-
|
|
994
|
-
</div>
|
|
995
|
-
</div>
|
|
996
|
-
</div>
|
|
997
|
-
|
|
998
|
-
<!-- Provider Config Dialog -->
|
|
999
|
-
<div class="dialog-overlay" id="provider-dialog">
|
|
1000
|
-
<div class="dialog" style="max-width:480px;">
|
|
1001
|
-
<div class="dialog-title" id="pd-title">配置 Provider</div>
|
|
1002
|
-
<div class="dialog-desc" id="pd-desc">填入 API Key 即可开始使用</div>
|
|
1003
|
-
<div class="form-group">
|
|
1004
|
-
<label class="label">API Key</label>
|
|
1005
|
-
<input class="input" id="pd-apikey" type="password" placeholder="sk-...">
|
|
1006
|
-
</div>
|
|
1007
|
-
<div class="form-group" id="pd-baseurl-group" style="display:none;">
|
|
1008
|
-
<label class="label">自定义 Base URL(可选)</label>
|
|
1009
|
-
<input class="input" id="pd-baseurl" placeholder="https://api.example.com">
|
|
1010
|
-
</div>
|
|
1011
|
-
<div id="pd-test-result" style="margin-bottom:16px;font-size:13px;"></div>
|
|
1012
|
-
<div class="dialog-actions">
|
|
1013
|
-
<button class="btn btn-secondary btn-sm" onclick="closeProviderDialog()">取消</button>
|
|
1014
|
-
<button class="btn btn-secondary btn-sm" onclick="testProvider()">🔍 测试连接</button>
|
|
1015
|
-
<button class="btn btn-primary btn-sm" onclick="saveProvider()">💾 保存</button>
|
|
1016
|
-
</div>
|
|
1017
|
-
</div>
|
|
1018
|
-
</div>
|
|
1019
|
-
|
|
1020
|
-
<!-- Channel Config Dialog -->
|
|
1021
|
-
<div class="dialog-overlay" id="channel-dialog">
|
|
1022
|
-
<div class="dialog" style="max-width:480px;">
|
|
1023
|
-
<div class="dialog-title" id="cd-title">配置渠道</div>
|
|
1024
|
-
<div class="dialog-desc" id="cd-desc"></div>
|
|
1025
|
-
<div id="cd-fields"></div>
|
|
1026
|
-
<div class="dialog-actions">
|
|
1027
|
-
<button class="btn btn-secondary btn-sm" onclick="closeChannelDialog()">取消</button>
|
|
1028
|
-
<button class="btn btn-primary btn-sm" onclick="saveChannel()">💾 保存</button>
|
|
1029
|
-
</div>
|
|
1030
|
-
</div>
|
|
1031
|
-
</div>
|
|
1032
|
-
|
|
1033
|
-
<!-- Delete Confirm Dialog -->
|
|
1034
|
-
<div class="dialog-overlay" id="delete-dialog">
|
|
1035
|
-
<div class="dialog">
|
|
1036
|
-
<div class="dialog-title">Delete Agent?</div>
|
|
1037
|
-
<div class="dialog-desc">This action cannot be undone. The agent and all its data will be permanently removed.</div>
|
|
1038
|
-
<div class="dialog-actions">
|
|
1039
|
-
<button class="btn btn-secondary btn-sm" onclick="closeDeleteDialog()">Cancel</button>
|
|
1040
|
-
<button class="btn btn-danger btn-sm" onclick="confirmDelete()">Delete</button>
|
|
1041
|
-
</div>
|
|
1042
|
-
</div>
|
|
1043
|
-
</div>
|
|
1044
|
-
|
|
1045
|
-
<!-- First Run Wizard Modal -->
|
|
1046
|
-
<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;">
|
|
1047
|
-
<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;">
|
|
1048
|
-
<!-- Step indicators -->
|
|
1049
|
-
<div style="display:flex;justify-content:center;gap:6px;margin-bottom:32px;" id="fr-steps">
|
|
1050
|
-
<div class="wizard-step active" id="fr-step-1"><div class="step-num">1</div><span>Welcome</span></div>
|
|
1051
|
-
<div class="wizard-step" id="fr-step-2"><div class="step-line"></div><div class="step-num">2</div><span>Models</span></div>
|
|
1052
|
-
<div class="wizard-step" id="fr-step-3"><div class="step-line"></div><div class="step-num">3</div><span>Template</span></div>
|
|
1053
|
-
<div class="wizard-step" id="fr-step-4"><div class="step-line"></div><div class="step-num">4</div><span>Done</span></div>
|
|
1054
|
-
</div>
|
|
1055
|
-
|
|
1056
|
-
<!-- Step 1: Welcome -->
|
|
1057
|
-
<div class="wizard-panel active" id="fr-panel-1">
|
|
1058
|
-
<div style="text-align:center;margin-bottom:32px;">
|
|
1059
|
-
<div style="font-size:64px;margin-bottom:16px;">⚡</div>
|
|
1060
|
-
<h2 style="font-size:24px;font-weight:700;margin-bottom:8px;">Welcome to OPC Studio</h2>
|
|
1061
|
-
<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>
|
|
1062
|
-
</div>
|
|
1063
|
-
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:32px;">
|
|
1064
|
-
<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>
|
|
1065
|
-
<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>
|
|
1066
|
-
<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>
|
|
1067
|
-
</div>
|
|
1068
|
-
<button class="btn btn-primary btn-lg" style="width:100%;" onclick="frNext()">Get Started →</button>
|
|
1069
|
-
</div>
|
|
1070
|
-
|
|
1071
|
-
<!-- Step 2: Model Detection -->
|
|
1072
|
-
<div class="wizard-panel" id="fr-panel-2">
|
|
1073
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">🤖 Choose Your AI Model</h2>
|
|
1074
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:20px;">We recommend using free local models. Cloud APIs also supported.</p>
|
|
1075
|
-
<div id="fr-ollama-status" style="padding:16px;border-radius:var(--radius);margin-bottom:16px;background:var(--bg-hover);">
|
|
1076
|
-
<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot yellow"></span> Detecting Ollama...</div>
|
|
1077
|
-
</div>
|
|
1078
|
-
<div id="fr-model-choice" style="display:none;">
|
|
1079
|
-
<div class="form-group">
|
|
1080
|
-
<label class="label">Select a model to use</label>
|
|
1081
|
-
<select class="input" id="fr-model-select">
|
|
1082
|
-
<option value="qwen2.5:7b">qwen2.5:7b — Local, free ⭐ Recommended</option>
|
|
1083
|
-
<option value="gpt-4o-mini">GPT-4o Mini — Cloud, fast</option>
|
|
1084
|
-
<option value="gpt-4o">GPT-4o — Cloud, most capable</option>
|
|
1085
|
-
<option value="claude-sonnet-4">Claude Sonnet — Cloud, balanced</option>
|
|
1086
|
-
<option value="deepseek-v3">DeepSeek V3 — Cloud, affordable</option>
|
|
1087
|
-
</select>
|
|
1088
393
|
</div>
|
|
1089
394
|
</div>
|
|
1090
|
-
<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:24px;">
|
|
1091
|
-
<button class="btn btn-secondary" onclick="frBack()">← Back</button>
|
|
1092
|
-
<button class="btn btn-primary" onclick="frNext()">Next →</button>
|
|
1093
|
-
</div>
|
|
1094
395
|
</div>
|
|
1095
396
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">👤 Pick a Starting Template</h2>
|
|
1099
|
-
<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>
|
|
1100
|
-
<div style="display:flex;flex-direction:column;gap:8px;" id="fr-template-list">
|
|
1101
|
-
<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">
|
|
1102
|
-
<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>
|
|
1103
|
-
</div>
|
|
1104
|
-
<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">
|
|
1105
|
-
<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>
|
|
1106
|
-
</div>
|
|
1107
|
-
<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">
|
|
1108
|
-
<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>
|
|
1109
|
-
</div>
|
|
1110
|
-
<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">
|
|
1111
|
-
<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>
|
|
1112
|
-
</div>
|
|
1113
|
-
<div class="card" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;" onclick="frSelectTemplate('teacher')" id="fr-tpl-teacher">
|
|
1114
|
-
<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>
|
|
1115
|
-
</div>
|
|
1116
|
-
</div>
|
|
1117
|
-
<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:20px;">
|
|
1118
|
-
<button class="btn btn-secondary" onclick="frBack()">← Back</button>
|
|
1119
|
-
<button class="btn btn-primary" onclick="frNext()" id="fr-next-3">Next →</button>
|
|
1120
|
-
</div>
|
|
1121
|
-
</div>
|
|
397
|
+
</div><!-- main -->
|
|
398
|
+
</div><!-- app -->
|
|
1122
399
|
|
|
1123
|
-
|
|
1124
|
-
<div class="wizard-panel" id="fr-panel-4">
|
|
1125
|
-
<div style="text-align:center;padding:20px 0;">
|
|
1126
|
-
<div id="fr-creating" style="">
|
|
1127
|
-
<div style="font-size:48px;margin-bottom:16px;">⏳</div>
|
|
1128
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Creating your agent...</h2>
|
|
1129
|
-
<p style="color:var(--text-muted);">Just a moment</p>
|
|
1130
|
-
</div>
|
|
1131
|
-
<div id="fr-done" style="display:none;">
|
|
1132
|
-
<div style="font-size:64px;margin-bottom:16px;">🎉</div>
|
|
1133
|
-
<h2 style="font-size:22px;font-weight:700;margin-bottom:8px;">You're all set!</h2>
|
|
1134
|
-
<p style="color:var(--text-muted);margin-bottom:24px;">Your agent is ready. Start chatting now!</p>
|
|
1135
|
-
<button class="btn btn-primary btn-lg" onclick="frFinish()">Start Chatting →</button>
|
|
1136
|
-
</div>
|
|
1137
|
-
</div>
|
|
1138
|
-
</div>
|
|
1139
|
-
</div>
|
|
1140
|
-
</div>
|
|
400
|
+
<div class="toast" id="toast"></div>
|
|
1141
401
|
|
|
1142
402
|
<script>
|
|
1143
|
-
// === Debug: catch all JS errors ===
|
|
1144
|
-
window.onerror = function(msg, url, line, col, err) {
|
|
1145
|
-
console.error('JS ERROR:', msg, 'at line', line, ':', col);
|
|
1146
|
-
const d = document.createElement('div');
|
|
1147
|
-
d.style.cssText = 'position:fixed;bottom:0;left:0;right:0;background:red;color:white;padding:8px;z-index:9999;font-size:12px;';
|
|
1148
|
-
d.textContent = 'JS Error: ' + msg + ' (line ' + line + ')';
|
|
1149
|
-
document.body.appendChild(d);
|
|
1150
|
-
};
|
|
1151
|
-
// === State ===
|
|
1152
|
-
let templates = [];
|
|
1153
|
-
let industries = [];
|
|
1154
|
-
let agents = [];
|
|
1155
|
-
let selectedTemplate = null;
|
|
1156
|
-
let currentAgent = null;
|
|
1157
|
-
let chatMessages = [];
|
|
1158
|
-
let wizardStep = 1;
|
|
1159
|
-
let selectedIndustry = '';
|
|
1160
|
-
let deleteTargetId = null;
|
|
1161
|
-
|
|
1162
403
|
const API = '';
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
404
|
+
let agents = [];
|
|
405
|
+
let currentAgentId = null;
|
|
406
|
+
let currentPage = 'assistant';
|
|
407
|
+
let settingsPanelOpen = false;
|
|
408
|
+
let currentSettingsTab = 'role';
|
|
409
|
+
let modelsConfig = null;
|
|
410
|
+
let wsState = { level: 'root', industry: null, job: null };
|
|
411
|
+
let chatHistories = {};
|
|
412
|
+
|
|
413
|
+
// ======================== Navigation ========================
|
|
414
|
+
function navigate(page, data) {
|
|
415
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
416
|
+
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
|
|
417
|
+
|
|
418
|
+
if (page === 'assistant') {
|
|
419
|
+
document.getElementById('page-assistant').classList.add('active');
|
|
420
|
+
document.getElementById('nav-assistant').classList.add('active');
|
|
421
|
+
} else if (page === 'agent' && data) {
|
|
422
|
+
currentAgentId = data.id;
|
|
423
|
+
document.getElementById('page-agent-chat').classList.add('active');
|
|
424
|
+
document.getElementById('agent-chat-icon').textContent = data.icon || '🤖';
|
|
425
|
+
document.getElementById('agent-chat-name').textContent = data.name || data.id;
|
|
426
|
+
const navItem = document.querySelector(`[data-agent-id="${data.id}"]`);
|
|
427
|
+
if (navItem) navItem.classList.add('active');
|
|
428
|
+
loadAgentChat(data.id);
|
|
429
|
+
loadAgentSettings(data.id);
|
|
430
|
+
} else if (page === 'models') {
|
|
431
|
+
document.getElementById('page-models').classList.add('active');
|
|
432
|
+
document.getElementById('nav-models').classList.add('active');
|
|
433
|
+
loadModelsConfig();
|
|
434
|
+
} else if (page === 'knowledge') {
|
|
435
|
+
document.getElementById('page-knowledge').classList.add('active');
|
|
436
|
+
document.getElementById('nav-knowledge').classList.add('active');
|
|
437
|
+
loadKnowledge();
|
|
438
|
+
} else if (page === 'workstation') {
|
|
439
|
+
document.getElementById('page-workstation').classList.add('active');
|
|
440
|
+
document.getElementById('nav-workstation').classList.add('active');
|
|
441
|
+
loadWorkstation();
|
|
442
|
+
} else if (page === 'channels') {
|
|
443
|
+
document.getElementById('page-channels').classList.add('active');
|
|
444
|
+
loadChannels();
|
|
445
|
+
} else if (page === 'new-agent') {
|
|
446
|
+
document.getElementById('page-new-agent').classList.add('active');
|
|
447
|
+
loadAvailableModels();
|
|
1198
448
|
}
|
|
449
|
+
currentPage = page;
|
|
1199
450
|
}
|
|
1200
451
|
|
|
1201
|
-
//
|
|
1202
|
-
async function loadTemplates() {
|
|
1203
|
-
try {
|
|
1204
|
-
const res = await fetch(`${API}/api/templates`);
|
|
1205
|
-
const data = await res.json();
|
|
1206
|
-
templates = data.templates || [];
|
|
1207
|
-
industries = data.industries || [];
|
|
1208
|
-
renderIndustryChips();
|
|
1209
|
-
renderTemplates();
|
|
1210
|
-
} catch(e) { console.error('Failed to load templates:', e); }
|
|
1211
|
-
}
|
|
1212
|
-
|
|
452
|
+
// ======================== Agents List ========================
|
|
1213
453
|
async function loadAgents() {
|
|
1214
454
|
try {
|
|
1215
455
|
const res = await fetch(`${API}/api/agents`);
|
|
1216
456
|
const data = await res.json();
|
|
1217
|
-
agents = data.agents || [];
|
|
1218
|
-
|
|
1219
|
-
} catch(e) { console.error('
|
|
457
|
+
agents = Array.isArray(data) ? data : (data.agents || []);
|
|
458
|
+
renderAgentList();
|
|
459
|
+
} catch (e) { console.error('loadAgents:', e); }
|
|
1220
460
|
}
|
|
1221
461
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
const res = await fetch('/api/agents');
|
|
1228
|
-
const data = await res.json();
|
|
1229
|
-
const agents = data.agents || data || [];
|
|
1230
|
-
window._sidebarAgents = agents;
|
|
1231
|
-
const container = document.getElementById('sidebar-agent-list');
|
|
1232
|
-
if (!agents.length) {
|
|
1233
|
-
container.innerHTML = '<div style="padding: 12px 16px; color: var(--text-dim); font-size: 13px;">暂无 Agent</div>';
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
container.innerHTML = agents.map(a => {
|
|
1237
|
-
const status = (a.status || 'offline').toLowerCase();
|
|
1238
|
-
const icon = a.emoji || a.icon || '🤖';
|
|
1239
|
-
const name = a.name || a.id;
|
|
1240
|
-
return `<div class="agent-list-item${selectedAgentId === a.id ? ' active' : ''}" data-agent-id="${a.id}" onclick="navigateToAgent('${a.id}')">
|
|
1241
|
-
<span class="agent-icon">${icon}</span>
|
|
1242
|
-
<span class="agent-name">${name}</span>
|
|
1243
|
-
<span class="status-dot ${status}"></span>
|
|
1244
|
-
</div>`;
|
|
1245
|
-
}).join('');
|
|
1246
|
-
} catch(e) {
|
|
1247
|
-
console.error('Failed to load sidebar agents:', e);
|
|
1248
|
-
const container = document.getElementById('sidebar-agent-list');
|
|
1249
|
-
if (container) container.innerHTML = '<div style="padding: 12px 16px; color: var(--text-dim); font-size: 13px;">加载失败</div>';
|
|
462
|
+
function renderAgentList() {
|
|
463
|
+
const el = document.getElementById('agent-list');
|
|
464
|
+
if (!agents.length) {
|
|
465
|
+
el.innerHTML = '<div style="padding:4px 12px;font-size:12px;color:var(--text-dim);font-style:italic;">还没有 Agent</div>';
|
|
466
|
+
return;
|
|
1250
467
|
}
|
|
468
|
+
el.innerHTML = agents.map(a => `
|
|
469
|
+
<div class="sidebar-item${currentAgentId === a.id ? ' active' : ''}" data-agent-id="${a.id}" onclick="navigate('agent', ${JSON.stringify(a).replace(/"/g, '"')})">
|
|
470
|
+
<span>${a.icon || '🤖'}</span><span>${a.name || a.id}</span>
|
|
471
|
+
</div>
|
|
472
|
+
`).join('');
|
|
1251
473
|
}
|
|
1252
474
|
|
|
1253
|
-
//
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const data = await res.json();
|
|
1265
|
-
const agents = data.agents || data || [];
|
|
1266
|
-
const container = document.getElementById('group-agent-select');
|
|
1267
|
-
if (!agents.length) {
|
|
1268
|
-
container.innerHTML = '<p style="color:var(--text-muted);font-size:13px;">请先创建 Agent,再拉入群组</p>';
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
container.innerHTML = agents.map(a => `<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;"><input type="checkbox" class="group-agent-cb" value="${a.id}"> <span>${a.templateIcon || a.icon || '🤖'}</span> <span style="font-size:14px;">${a.name}</span></label>`).join('');
|
|
1272
|
-
} catch(e) { console.error('loadGroupAgentSelect error', e); }
|
|
1273
|
-
}
|
|
1274
|
-
async function createGroup() {
|
|
1275
|
-
const name = document.getElementById('group-name').value.trim();
|
|
1276
|
-
if (!name) { alert('请输入群组名称'); return; }
|
|
1277
|
-
const members = [...document.querySelectorAll('.group-agent-cb:checked')].map(cb => cb.value);
|
|
1278
|
-
if (members.length < 2) { alert('至少选择 2 个 Agent'); return; }
|
|
1279
|
-
// TODO: POST to /api/groups
|
|
1280
|
-
alert('群组 "' + name + '" 创建成功!模式: ' + selectedPattern + ', 成员: ' + members.length + ' 个 Agent');
|
|
1281
|
-
navigate('dashboard');
|
|
1282
|
-
}
|
|
1283
|
-
async function loadSidebarGroups() {
|
|
1284
|
-
// TODO: fetch /api/groups and render
|
|
1285
|
-
const container = document.getElementById('groups-list');
|
|
1286
|
-
if (container) container.innerHTML = '<div style="padding:6px 16px;color:var(--text-dim);font-size:12px;">暂无群组</div>';
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
function navigateToAgent(agentId) {
|
|
1290
|
-
selectedAgentId = agentId;
|
|
1291
|
-
// Update sidebar active state
|
|
1292
|
-
document.querySelectorAll('.agent-list-item').forEach(el => el.classList.remove('active'));
|
|
1293
|
-
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
1294
|
-
const item = document.querySelector(`.agent-list-item[data-agent-id="${agentId}"]`);
|
|
1295
|
-
if (item) item.classList.add('active');
|
|
1296
|
-
|
|
1297
|
-
// Find agent data
|
|
1298
|
-
const agent = (window._sidebarAgents || []).find(a => a.id === agentId) || { id: agentId, name: agentId };
|
|
1299
|
-
document.getElementById('agent-detail-icon').textContent = agent.emoji || agent.icon || '🤖';
|
|
1300
|
-
document.getElementById('agent-detail-name').textContent = agent.name || agentId;
|
|
1301
|
-
const statusDot = document.getElementById('agent-detail-status');
|
|
1302
|
-
const status = (agent.status || 'offline').toLowerCase();
|
|
1303
|
-
statusDot.className = 'status-dot ' + status;
|
|
1304
|
-
|
|
1305
|
-
// Reset to chat view
|
|
1306
|
-
document.getElementById('agent-chat-view').style.display = '';
|
|
1307
|
-
document.getElementById('agent-settings-view').style.display = 'none';
|
|
1308
|
-
document.getElementById('agent-detail-toggle').classList.remove('active');
|
|
1309
|
-
const welcomeEl = document.querySelector('.agent-chat-welcome'); document.getElementById('agent-chat-messages').innerHTML = welcomeEl ? welcomeEl.outerHTML : '<div class="agent-chat-welcome"><div style="font-size:48px;margin-bottom:16px">💬</div><div style="font-size:18px;font-weight:600;margin-bottom:8px">开始对话</div><div style="color:var(--text-muted);font-size:14px">向你的 Agent 发送第一条消息</div></div>';
|
|
1310
|
-
document.getElementById('agent-chat-input').value = '';
|
|
1311
|
-
|
|
1312
|
-
// Show agent detail page
|
|
1313
|
-
document.querySelectorAll('.page, .chat-container').forEach(p => { p.classList.remove('active'); p.style.display = ''; });
|
|
1314
|
-
document.getElementById('page-agent-detail').classList.add('active');
|
|
1315
|
-
location.hash = `/agent/${agentId}`;
|
|
1316
|
-
toggleSidebar(false);
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
function toggleAgentSettings() {
|
|
1320
|
-
const chatView = document.getElementById('agent-chat-view');
|
|
1321
|
-
const settingsView = document.getElementById('agent-settings-view');
|
|
1322
|
-
const toggleBtn = document.getElementById('agent-detail-toggle');
|
|
1323
|
-
const showSettings = chatView.style.display !== 'none';
|
|
1324
|
-
chatView.style.display = showSettings ? 'none' : '';
|
|
1325
|
-
settingsView.style.display = showSettings ? '' : 'none';
|
|
1326
|
-
toggleBtn.classList.toggle('active', showSettings);
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
function switchAgentTab(tab) {
|
|
1330
|
-
document.querySelectorAll('.agent-tab').forEach(t => t.classList.remove('active'));
|
|
1331
|
-
document.querySelector(`.agent-tab[data-atab="${tab}"]`)?.classList.add('active');
|
|
1332
|
-
document.querySelectorAll('.agent-tab-panel').forEach(p => p.classList.remove('active'));
|
|
1333
|
-
document.getElementById(`atab-${tab}`)?.classList.add('active');
|
|
1334
|
-
if (typeof loadAgentTabData === 'function') loadAgentTabData(tab);
|
|
475
|
+
// ======================== Chat (SSE) ========================
|
|
476
|
+
function renderMarkdown(text) {
|
|
477
|
+
if (!text) return '';
|
|
478
|
+
return text
|
|
479
|
+
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
|
480
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
481
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
482
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
483
|
+
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
484
|
+
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
|
485
|
+
.replace(/\n/g, '<br>');
|
|
1335
486
|
}
|
|
1336
487
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
async function sendAgentChat() {
|
|
1340
|
-
const input = document.getElementById('agent-chat-input');
|
|
488
|
+
async function sendChat(inputId, messagesId, agentId) {
|
|
489
|
+
const input = document.getElementById(inputId);
|
|
1341
490
|
const msg = input.value.trim();
|
|
1342
|
-
if (!msg
|
|
491
|
+
if (!msg) return;
|
|
1343
492
|
input.value = '';
|
|
1344
|
-
input.style.height = 'auto';
|
|
1345
493
|
|
|
1346
|
-
const messagesEl = document.getElementById(
|
|
1347
|
-
// Remove welcome screen
|
|
1348
|
-
const welcome = messagesEl.querySelector('.agent-chat-welcome');
|
|
1349
|
-
if (welcome) welcome.remove();
|
|
494
|
+
const messagesEl = document.getElementById(messagesId);
|
|
1350
495
|
|
|
1351
496
|
// Add user message
|
|
1352
497
|
const userDiv = document.createElement('div');
|
|
1353
|
-
userDiv.className = '
|
|
1354
|
-
userDiv.
|
|
498
|
+
userDiv.className = 'chat-msg user';
|
|
499
|
+
userDiv.innerHTML = `<div class="bubble">${escapeHtml(msg)}</div>`;
|
|
1355
500
|
messagesEl.appendChild(userDiv);
|
|
1356
|
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1357
|
-
|
|
1358
|
-
agentChatHistory.push({ role: 'user', content: msg });
|
|
1359
501
|
|
|
1360
|
-
// Add
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
502
|
+
// Add typing indicator
|
|
503
|
+
const typingDiv = document.createElement('div');
|
|
504
|
+
typingDiv.className = 'chat-msg agent';
|
|
505
|
+
typingDiv.id = 'typing-indicator';
|
|
506
|
+
typingDiv.innerHTML = '<div class="typing"><span></span><span></span><span></span></div>';
|
|
507
|
+
messagesEl.appendChild(typingDiv);
|
|
508
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1365
509
|
|
|
1366
|
-
// Send to API and parse SSE stream
|
|
1367
510
|
try {
|
|
1368
|
-
const
|
|
511
|
+
const chatId = agentId || 'opc-assistant';
|
|
512
|
+
const res = await fetch(`${API}/api/agents/${chatId}/chat`, {
|
|
1369
513
|
method: 'POST',
|
|
1370
514
|
headers: { 'Content-Type': 'application/json' },
|
|
1371
|
-
body: JSON.stringify({ message: msg
|
|
515
|
+
body: JSON.stringify({ message: msg })
|
|
1372
516
|
});
|
|
1373
517
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
518
|
+
// Remove typing indicator
|
|
519
|
+
const ti = document.getElementById('typing-indicator');
|
|
520
|
+
if (ti) ti.remove();
|
|
521
|
+
|
|
522
|
+
// Create agent response bubble
|
|
523
|
+
const agentDiv = document.createElement('div');
|
|
524
|
+
agentDiv.className = 'chat-msg agent';
|
|
525
|
+
const bubble = document.createElement('div');
|
|
526
|
+
bubble.className = 'bubble';
|
|
527
|
+
agentDiv.appendChild(bubble);
|
|
528
|
+
messagesEl.appendChild(agentDiv);
|
|
529
|
+
|
|
530
|
+
if (res.headers.get('content-type')?.includes('text/event-stream')) {
|
|
531
|
+
// SSE streaming
|
|
532
|
+
const reader = res.body.getReader();
|
|
533
|
+
const decoder = new TextDecoder();
|
|
534
|
+
let fullText = '';
|
|
535
|
+
let buffer = '';
|
|
536
|
+
|
|
537
|
+
while (true) {
|
|
538
|
+
const { done, value } = await reader.read();
|
|
539
|
+
if (done) break;
|
|
540
|
+
buffer += decoder.decode(value, { stream: true });
|
|
541
|
+
const lines = buffer.split('\n');
|
|
542
|
+
buffer = lines.pop() || '';
|
|
1379
543
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
fullReply += content;
|
|
1401
|
-
assistantDiv.textContent = fullReply;
|
|
1402
|
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
544
|
+
for (const line of lines) {
|
|
545
|
+
if (line.startsWith('data: ')) {
|
|
546
|
+
const data = line.slice(6);
|
|
547
|
+
if (data === '[DONE]') continue;
|
|
548
|
+
try {
|
|
549
|
+
const parsed = JSON.parse(data);
|
|
550
|
+
const token = parsed.choices?.[0]?.delta?.content || parsed.token || parsed.content || '';
|
|
551
|
+
if (token) {
|
|
552
|
+
fullText += token;
|
|
553
|
+
bubble.innerHTML = renderMarkdown(fullText);
|
|
554
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
// plain text token
|
|
558
|
+
if (data && data !== '[DONE]') {
|
|
559
|
+
fullText += data;
|
|
560
|
+
bubble.innerHTML = renderMarkdown(fullText);
|
|
561
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
1403
564
|
}
|
|
1404
|
-
}
|
|
565
|
+
}
|
|
1405
566
|
}
|
|
567
|
+
if (!fullText) bubble.innerHTML = '<em style="color:var(--text-dim);">(无响应)</em>';
|
|
568
|
+
} else {
|
|
569
|
+
// JSON response
|
|
570
|
+
const data = await res.json();
|
|
571
|
+
const text = data.response || data.message || data.content || JSON.stringify(data);
|
|
572
|
+
bubble.innerHTML = renderMarkdown(text);
|
|
1406
573
|
}
|
|
574
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1407
575
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
} catch(e) {
|
|
1413
|
-
assistantDiv.style.borderColor = 'var(--red)';
|
|
1414
|
-
assistantDiv.textContent = `⚠️ 发送失败: ${e.message}`;
|
|
1415
|
-
}
|
|
1416
|
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
function handleAgentChatKey(e) {
|
|
1420
|
-
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAgentChat(); }
|
|
1421
|
-
// Auto-resize textarea
|
|
1422
|
-
e.target.style.height = 'auto';
|
|
1423
|
-
e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';
|
|
1424
|
-
}
|
|
576
|
+
// Save to history
|
|
577
|
+
if (!chatHistories[chatId]) chatHistories[chatId] = [];
|
|
578
|
+
chatHistories[chatId].push({ role: 'user', content: msg });
|
|
579
|
+
chatHistories[chatId].push({ role: 'agent', content: bubble.innerHTML });
|
|
1425
580
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
if (page === 'settings') { showSettings(currentSettingsTab || 'models'); }
|
|
1436
|
-
if (page === 'global-runtime') { currentSettingsTab='status'; showSettings('status'); showPage('settings'); return; }
|
|
1437
|
-
if (page === 'global-models') { currentSettingsTab='models'; showSettings('models'); showPage('settings'); return; }
|
|
1438
|
-
if (page === 'global-channels') { currentSettingsTab='channels'; showSettings('channels'); showPage('settings'); return; }
|
|
1439
|
-
if (page === 'global-memory') { currentSettingsTab='memory'; showSettings('memory'); showPage('settings'); return; }
|
|
1440
|
-
if (page === 'global-templates') { navigate('templates'); return; }
|
|
1441
|
-
if (page === 'create-group') { loadGroupAgentSelect(); }
|
|
1442
|
-
if (page === 'schedules') { loadSchedules(); }
|
|
1443
|
-
if (page === 'skills') { loadSkillsMarketplace(); }
|
|
1444
|
-
|
|
1445
|
-
showPage(page);
|
|
1446
|
-
location.hash = `/${page}`;
|
|
1447
|
-
toggleSidebar(false);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
const ti = document.getElementById('typing-indicator');
|
|
583
|
+
if (ti) ti.remove();
|
|
584
|
+
const errDiv = document.createElement('div');
|
|
585
|
+
errDiv.className = 'chat-msg agent';
|
|
586
|
+
errDiv.innerHTML = `<div class="bubble" style="color:var(--red);">⚠️ 发送失败: ${escapeHtml(e.message)}</div>`;
|
|
587
|
+
messagesEl.appendChild(errDiv);
|
|
588
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
589
|
+
}
|
|
1448
590
|
}
|
|
1449
591
|
|
|
1450
|
-
function
|
|
1451
|
-
|
|
1452
|
-
const el = document.getElementById(`page-${page}`);
|
|
1453
|
-
if (el) el.classList.add('active');
|
|
1454
|
-
}
|
|
592
|
+
function sendAssistantMsg() { sendChat('assistant-input', 'assistant-messages', null); }
|
|
593
|
+
function sendAgentMsg() { sendChat('agent-input', 'agent-messages', currentAgentId); }
|
|
1455
594
|
|
|
1456
|
-
function
|
|
1457
|
-
document.
|
|
1458
|
-
|
|
595
|
+
function loadAgentChat(agentId) {
|
|
596
|
+
const el = document.getElementById('agent-messages');
|
|
597
|
+
const history = chatHistories[agentId];
|
|
598
|
+
if (history && history.length) {
|
|
599
|
+
el.innerHTML = history.map(m =>
|
|
600
|
+
`<div class="chat-msg ${m.role === 'user' ? 'user' : 'agent'}"><div class="bubble">${m.role === 'user' ? escapeHtml(m.content) : m.content}</div></div>`
|
|
601
|
+
).join('');
|
|
602
|
+
} else {
|
|
603
|
+
el.innerHTML = `<div class="chat-msg agent"><div class="bubble">你好!有什么可以帮你的吗?</div></div>`;
|
|
604
|
+
}
|
|
605
|
+
el.scrollTop = el.scrollHeight;
|
|
1459
606
|
}
|
|
1460
607
|
|
|
1461
|
-
//
|
|
1462
|
-
function
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
document.getElementById('wizard-industry-chips').innerHTML = html.replace(/filterByIndustry/g, 'filterWizardByIndustry');
|
|
608
|
+
// ======================== Agent Settings ========================
|
|
609
|
+
function toggleSettings() {
|
|
610
|
+
const panel = document.getElementById('agent-settings');
|
|
611
|
+
settingsPanelOpen = !settingsPanelOpen;
|
|
612
|
+
panel.classList.toggle('open', settingsPanelOpen);
|
|
1467
613
|
}
|
|
1468
614
|
|
|
1469
|
-
function
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
615
|
+
async function loadAgentSettings(agentId) {
|
|
616
|
+
try {
|
|
617
|
+
const res = await fetch(`${API}/api/agents/${agentId}`);
|
|
618
|
+
const agent = await res.json();
|
|
619
|
+
renderSettingsTab('role', agent);
|
|
620
|
+
} catch (e) { console.error('loadAgentSettings:', e); }
|
|
1474
621
|
}
|
|
1475
622
|
|
|
1476
|
-
function
|
|
1477
|
-
|
|
1478
|
-
document.querySelectorAll('
|
|
1479
|
-
|
|
1480
|
-
|
|
623
|
+
function switchSettingsTab(tab) {
|
|
624
|
+
currentSettingsTab = tab;
|
|
625
|
+
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
|
|
626
|
+
document.querySelector(`.settings-tab[onclick*="${tab}"]`).classList.add('active');
|
|
627
|
+
if (currentAgentId) {
|
|
628
|
+
fetch(`${API}/api/agents/${currentAgentId}`).then(r => r.json()).then(a => renderSettingsTab(tab, a)).catch(console.error);
|
|
629
|
+
}
|
|
1481
630
|
}
|
|
1482
631
|
|
|
1483
|
-
function
|
|
1484
|
-
|
|
1485
|
-
|
|
632
|
+
function renderSettingsTab(tab, agent) {
|
|
633
|
+
const el = document.getElementById('settings-content');
|
|
634
|
+
const cfg = agent.config || agent;
|
|
1486
635
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
el.innerHTML = SKILL_CATEGORIES.map(c =>
|
|
1513
|
-
`<span class="chip ${selectedSkillCategory === c.id ? 'active' : ''}" onclick="selectSkillCategory('${c.id}')">${c.labelZh}</span>`
|
|
1514
|
-
).join('');
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function selectSkillCategory(cat) {
|
|
1518
|
-
selectedSkillCategory = cat;
|
|
1519
|
-
renderSkillCategoryChips();
|
|
1520
|
-
renderSkills();
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
function filterSkills() {
|
|
1524
|
-
renderSkills();
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
function renderSkills() {
|
|
1528
|
-
const q = (document.getElementById('skills-search')?.value || '').toLowerCase();
|
|
1529
|
-
let filtered = allSkills;
|
|
1530
|
-
if (selectedSkillCategory) {
|
|
1531
|
-
filtered = filtered.filter(s => s.category === selectedSkillCategory);
|
|
1532
|
-
}
|
|
1533
|
-
if (q) {
|
|
1534
|
-
filtered = filtered.filter(s =>
|
|
1535
|
-
s.name.toLowerCase().includes(q) || s.nameZh.includes(q) ||
|
|
1536
|
-
s.description.toLowerCase().includes(q) || s.descriptionZh.includes(q)
|
|
1537
|
-
);
|
|
636
|
+
if (tab === 'role') {
|
|
637
|
+
el.innerHTML = `
|
|
638
|
+
<div class="form-group"><label class="form-label">名称</label><input class="form-input" id="s-name" value="${escapeAttr(cfg.name || agent.name || '')}"></div>
|
|
639
|
+
<div class="form-group"><label class="form-label">描述</label><input class="form-input" id="s-desc" value="${escapeAttr(cfg.description || '')}"></div>
|
|
640
|
+
<div class="form-group"><label class="form-label">图标</label><input class="form-input" id="s-icon" value="${escapeAttr(cfg.icon || agent.icon || '🤖')}" style="width:60px"></div>
|
|
641
|
+
<div class="form-group"><label class="form-label">System Prompt</label><textarea class="form-input form-textarea" id="s-prompt">${escapeHtml(cfg.systemPrompt || '')}</textarea></div>
|
|
642
|
+
<button class="btn-primary" onclick="saveAgentSettings()">保存</button>
|
|
643
|
+
`;
|
|
644
|
+
} else if (tab === 'model') {
|
|
645
|
+
el.innerHTML = '<div style="color:var(--text-dim);">加载可用模型...</div>';
|
|
646
|
+
loadModelTab(agent);
|
|
647
|
+
} else if (tab === 'memory') {
|
|
648
|
+
el.innerHTML = '<div style="color:var(--text-dim);">加载记忆...</div>';
|
|
649
|
+
loadMemoryTab(agent.id || currentAgentId);
|
|
650
|
+
} else if (tab === 'skills') {
|
|
651
|
+
const skills = cfg.skills || [];
|
|
652
|
+
el.innerHTML = `
|
|
653
|
+
<div class="form-label">已安装技能</div>
|
|
654
|
+
${skills.length ? skills.map(s => `<div class="tag tag-blue">${s}</div>`).join(' ') : '<div style="color:var(--text-dim);font-size:13px;">暂无技能</div>'}
|
|
655
|
+
`;
|
|
656
|
+
} else if (tab === 'channel') {
|
|
657
|
+
el.innerHTML = `
|
|
658
|
+
<div class="form-label">Agent 渠道</div>
|
|
659
|
+
<div style="color:var(--text-dim);font-size:13px;">渠道在全局配置中管理 → <a style="color:var(--blue);cursor:pointer;" onclick="navigate('channels')">前往渠道配置</a></div>
|
|
660
|
+
`;
|
|
1538
661
|
}
|
|
1539
|
-
const grid = document.getElementById('skills-grid');
|
|
1540
|
-
if (!grid) return;
|
|
1541
|
-
grid.innerHTML = filtered.map(s => `
|
|
1542
|
-
<div class="card" style="cursor:default;position:relative;">
|
|
1543
|
-
<div style="font-size:36px;margin-bottom:8px;">${s.icon}</div>
|
|
1544
|
-
<div style="font-weight:600;font-size:15px;">${s.nameZh}</div>
|
|
1545
|
-
<div style="font-size:12px;color:var(--text-dim);margin-bottom:4px;">${s.name}</div>
|
|
1546
|
-
<div style="font-size:13px;color:var(--text-muted);margin-bottom:12px;line-height:1.4;">${s.descriptionZh}</div>
|
|
1547
|
-
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;">
|
|
1548
|
-
${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('')}
|
|
1549
|
-
${s.tools.length > 3 ? `<span style="font-size:11px;color:var(--text-dim);">+${s.tools.length-3}</span>` : ''}
|
|
1550
|
-
</div>
|
|
1551
|
-
${s.installed
|
|
1552
|
-
? `<button class="btn" style="width:100%;background:var(--bg-hover);color:var(--text-muted);cursor:pointer;" onclick="uninstallSkill('${s.id}',this)">✓ Installed</button>`
|
|
1553
|
-
: `<button class="btn btn-primary" style="width:100%;" onclick="installSkill('${s.id}',this)">Install</button>`
|
|
1554
|
-
}
|
|
1555
|
-
</div>
|
|
1556
|
-
`).join('');
|
|
1557
662
|
}
|
|
1558
663
|
|
|
1559
|
-
async function
|
|
1560
|
-
|
|
664
|
+
async function loadModelTab(agent) {
|
|
665
|
+
const el = document.getElementById('settings-content');
|
|
1561
666
|
try {
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
}
|
|
1569
|
-
} catch(e) { console.error(e); btn.disabled = false; btn.textContent = 'Install'; }
|
|
1570
|
-
}
|
|
667
|
+
const [modelsRes, ollamaRes] = await Promise.all([
|
|
668
|
+
fetch(`${API}/api/settings/models`),
|
|
669
|
+
fetch(`${API}/api/settings/models/local`).catch(() => null)
|
|
670
|
+
]);
|
|
671
|
+
const models = await modelsRes.json();
|
|
672
|
+
const ollama = ollamaRes ? await ollamaRes.json() : { available: false, models: [] };
|
|
1571
673
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
674
|
+
let options = '<option value="">使用全局默认</option>';
|
|
675
|
+
if (ollama.available && ollama.models) {
|
|
676
|
+
ollama.models.filter(m => !m.name?.includes('embed')).forEach(m => {
|
|
677
|
+
options += `<option value="ollama:${m.name}" ${agent.model === `ollama:${m.name}` ? 'selected' : ''}>${m.name} (Ollama)</option>`;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
// Add verified cloud models
|
|
681
|
+
const providers = models.providers || {};
|
|
682
|
+
for (const [name, cfg] of Object.entries(providers)) {
|
|
683
|
+
if (cfg.verified) {
|
|
684
|
+
const pModels = cfg.models || [name];
|
|
685
|
+
pModels.forEach(m => {
|
|
686
|
+
options += `<option value="${name}:${m}" ${agent.model === `${name}:${m}` ? 'selected' : ''}>${m} (${name})</option>`;
|
|
687
|
+
});
|
|
688
|
+
}
|
|
1581
689
|
}
|
|
1582
|
-
} catch(e) { console.error(e); btn.disabled = false; btn.textContent = '✓ Installed'; }
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function filterWizardTemplates() {
|
|
1586
|
-
renderWizardTemplates();
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
function getFilteredTemplates(searchId) {
|
|
1590
|
-
const q = (document.getElementById(searchId)?.value || '').toLowerCase();
|
|
1591
|
-
return templates.filter(t => {
|
|
1592
|
-
if (selectedIndustry && t.industry !== selectedIndustry) return false;
|
|
1593
|
-
if (q && !t.name.toLowerCase().includes(q) && !t.nameZh.includes(q) && !t.description.toLowerCase().includes(q)) return false;
|
|
1594
|
-
return true;
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
690
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
<div class="tpl-icon">${t.icon}</div>
|
|
1603
|
-
<div class="tpl-name">${t.name}</div>
|
|
1604
|
-
<div style="font-size:13px;color:var(--text-dim);margin-bottom:6px;">${t.nameZh}</div>
|
|
1605
|
-
<div class="tpl-desc">${t.description}</div>
|
|
1606
|
-
<div class="tpl-tags">
|
|
1607
|
-
<span class="tpl-tag">${t.industryZh}</span>
|
|
1608
|
-
${t.tags.map(tag => `<span class="tpl-tag">${tag}</span>`).join('')}
|
|
691
|
+
el.innerHTML = `
|
|
692
|
+
<div class="form-group">
|
|
693
|
+
<label class="form-label">聊天模型</label>
|
|
694
|
+
<select class="form-input" id="s-model">${options}</select>
|
|
1609
695
|
</div>
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
function renderWizardTemplates() {
|
|
1615
|
-
const filtered = getFilteredTemplates('wizard-tpl-search');
|
|
1616
|
-
document.getElementById('wizard-tpl-grid').innerHTML = filtered.map(t => `
|
|
1617
|
-
<div class="card tpl-card ${selectedTemplate?.id === t.id ? 'selected' : ''}" onclick="selectWizardTemplate('${t.id}')"
|
|
1618
|
-
style="${selectedTemplate?.id === t.id ? 'border-color:var(--accent);background:var(--accent-light);' : ''}">
|
|
1619
|
-
<div class="tpl-icon">${t.icon}</div>
|
|
1620
|
-
<div class="tpl-name">${t.name}</div>
|
|
1621
|
-
<div style="font-size:12px;color:var(--text-dim);">${t.nameZh} · ${t.industryZh}</div>
|
|
1622
|
-
</div>
|
|
1623
|
-
`).join('');
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
function selectTemplateAndCreate(id) {
|
|
1627
|
-
selectedTemplate = templates.find(t => t.id === id);
|
|
1628
|
-
wizardStep = 2;
|
|
1629
|
-
navigate('create');
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
function selectWizardTemplate(id) {
|
|
1633
|
-
selectedTemplate = templates.find(t => t.id === id);
|
|
1634
|
-
renderWizardTemplates();
|
|
1635
|
-
// Auto-advance after selection
|
|
1636
|
-
setTimeout(() => wizardNext(), 300);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// === Wizard ===
|
|
1640
|
-
function renderWizard() {
|
|
1641
|
-
for (let i = 1; i <= 3; i++) {
|
|
1642
|
-
const ws = document.getElementById(`ws-${i}`);
|
|
1643
|
-
const wp = document.getElementById(`wp-${i}`);
|
|
1644
|
-
ws.className = 'wizard-step' + (i < wizardStep ? ' done' : i === wizardStep ? ' active' : '');
|
|
1645
|
-
wp.className = 'wizard-panel' + (i === wizardStep ? ' active' : '');
|
|
1646
|
-
}
|
|
1647
|
-
if (wizardStep === 2 && selectedTemplate) {
|
|
1648
|
-
document.getElementById('agent-name').placeholder = selectedTemplate.name;
|
|
1649
|
-
document.getElementById('agent-model').value = selectedTemplate.suggestedModel || 'auto';
|
|
1650
|
-
renderSkillToggles();
|
|
1651
|
-
}
|
|
1652
|
-
if (wizardStep === 3) {
|
|
1653
|
-
renderConfirmCard();
|
|
696
|
+
<button class="btn-primary" onclick="saveAgentSettings()">保存</button>
|
|
697
|
+
`;
|
|
698
|
+
} catch (e) {
|
|
699
|
+
el.innerHTML = `<div style="color:var(--red);">加载失败: ${e.message}</div>`;
|
|
1654
700
|
}
|
|
1655
701
|
}
|
|
1656
702
|
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
productivity: { icon: '📋', label: 'Productivity' },
|
|
1660
|
-
knowledge: { icon: '📚', label: 'Knowledge' },
|
|
1661
|
-
creative: { icon: '✍️', label: 'Creative' },
|
|
1662
|
-
developer: { icon: '💻', label: 'Developer' },
|
|
1663
|
-
lifestyle: { icon: '🏠', label: 'Lifestyle' },
|
|
1664
|
-
business: { icon: '💼', label: 'Business' },
|
|
1665
|
-
education: { icon: '🎓', label: 'Education' },
|
|
1666
|
-
};
|
|
1667
|
-
const PRESET_SKILLS = [
|
|
1668
|
-
{ id: 'note-assistant', name: '📝 Notes', cat: 'productivity', default: true },
|
|
1669
|
-
{ id: 'todo-manager', name: '✅ Todo', cat: 'productivity', default: true },
|
|
1670
|
-
{ id: 'reminder', name: '🔔 Reminder', cat: 'productivity', default: true },
|
|
1671
|
-
{ id: 'reading-summary', name: '📚 Summarize', cat: 'knowledge', default: true },
|
|
1672
|
-
{ id: 'knowledge-search', name: '🔍 Search', cat: 'knowledge', default: true },
|
|
1673
|
-
{ id: 'dictionary-translator', name: '📖 Translate', cat: 'knowledge', default: false },
|
|
1674
|
-
{ id: 'calculator', name: '🧮 Calculate', cat: 'knowledge', default: false },
|
|
1675
|
-
{ id: 'writing-assistant', name: '✍️ Writing', cat: 'creative', default: true },
|
|
1676
|
-
{ id: 'social-media', name: '📱 Social', cat: 'creative', default: false },
|
|
1677
|
-
{ id: 'code-assistant', name: '💻 Code', cat: 'developer', default: false },
|
|
1678
|
-
{ id: 'data-analysis', name: '📊 Data', cat: 'knowledge', default: false },
|
|
1679
|
-
{ id: 'customer-followup', name: '🤝 CRM', cat: 'business', default: false },
|
|
1680
|
-
{ id: 'weather-query', name: '🌤 Weather', cat: 'lifestyle', default: false },
|
|
1681
|
-
{ id: 'language-tutor', name: '🗣 Language', cat: 'education', default: false },
|
|
1682
|
-
{ id: 'meeting-summarizer', name: '📋 Meeting', cat: 'productivity', default: false },
|
|
1683
|
-
{ id: 'brainstorm', name: '💡 Brainstorm', cat: 'creative', default: false },
|
|
1684
|
-
];
|
|
1685
|
-
let selectedSkills = new Set(PRESET_SKILLS.filter(s => s.default).map(s => s.id));
|
|
1686
|
-
|
|
1687
|
-
function renderSkillToggles() {
|
|
1688
|
-
const grid = document.getElementById('skill-toggles');
|
|
1689
|
-
if (!grid) return;
|
|
1690
|
-
grid.innerHTML = PRESET_SKILLS.map(s => `
|
|
1691
|
-
<label style="display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:10px;border:1px solid ${selectedSkills.has(s.id) ? 'var(--accent)' : 'var(--border)'};background:${selectedSkills.has(s.id) ? 'var(--accent-light)' : 'transparent'};cursor:pointer;font-size:14px;transition:all 0.2s;" onclick="toggleSkill('${s.id}',this)">
|
|
1692
|
-
<input type="checkbox" ${selectedSkills.has(s.id) ? 'checked' : ''} style="display:none;">
|
|
1693
|
-
<span>${s.name}</span>
|
|
1694
|
-
</label>
|
|
1695
|
-
`).join('');
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
function toggleSkill(id, el) {
|
|
1699
|
-
if (selectedSkills.has(id)) { selectedSkills.delete(id); } else { selectedSkills.add(id); }
|
|
1700
|
-
renderSkillToggles();
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
// --- AI Prompt Generator ---
|
|
1704
|
-
async function generatePrompt() {
|
|
1705
|
-
const taskDesc = document.getElementById('agent-task-desc').value.trim();
|
|
1706
|
-
const tplName = selectedTemplate?.name || '';
|
|
1707
|
-
const agentName = document.getElementById('agent-name').value.trim();
|
|
1708
|
-
const bizDesc = document.getElementById('agent-desc').value.trim();
|
|
1709
|
-
|
|
1710
|
-
if (!taskDesc && !tplName) { alert('Please describe what your agent should do'); return; }
|
|
1711
|
-
|
|
1712
|
-
const promptArea = document.getElementById('agent-prompt');
|
|
1713
|
-
promptArea.style.display = 'block';
|
|
1714
|
-
promptArea.value = '✨ Generating prompt...';
|
|
1715
|
-
|
|
1716
|
-
// Build the meta-prompt
|
|
1717
|
-
const context = [
|
|
1718
|
-
tplName ? `Role template: ${tplName}` : '',
|
|
1719
|
-
agentName ? `Agent name: ${agentName}` : '',
|
|
1720
|
-
bizDesc ? `Business: ${bizDesc}` : '',
|
|
1721
|
-
taskDesc ? `User request: ${taskDesc}` : '',
|
|
1722
|
-
`Enabled skills: ${[...selectedSkills].join(', ')}`,
|
|
1723
|
-
].filter(Boolean).join('\n');
|
|
1724
|
-
|
|
1725
|
-
const metaPrompt = `Generate a system prompt for an AI agent with these requirements:\n${context}\n\nRules:\n- Be specific and actionable\n- Include personality traits\n- Mention the skills it can use\n- Keep it under 300 words\n- Write in the same language as the user's request\n\nOutput ONLY the system prompt, no explanation.`;
|
|
1726
|
-
|
|
703
|
+
async function loadMemoryTab(agentId) {
|
|
704
|
+
const el = document.getElementById('settings-content');
|
|
1727
705
|
try {
|
|
1728
|
-
const res = await fetch(`${API}/api/agents/
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
}
|
|
1740
|
-
} catch {
|
|
1741
|
-
// Offline fallback
|
|
1742
|
-
promptArea.value = generateLocalPrompt(tplName, taskDesc, bizDesc);
|
|
706
|
+
const res = await fetch(`${API}/api/agents/${agentId}/memory`);
|
|
707
|
+
const data = await res.json();
|
|
708
|
+
const items = data.memories || data.items || [];
|
|
709
|
+
el.innerHTML = `
|
|
710
|
+
<div class="form-label">Agent 记忆 (${items.length} 条)</div>
|
|
711
|
+
${items.length ? items.slice(0, 20).map(m =>
|
|
712
|
+
`<div style="background:var(--white);border:1px solid var(--border);border-radius:6px;padding:8px;margin:4px 0;font-size:12px;">${escapeHtml(typeof m === 'string' ? m : m.content || JSON.stringify(m))}</div>`
|
|
713
|
+
).join('') : '<div style="color:var(--text-dim);font-size:13px;">暂无记忆</div>'}
|
|
714
|
+
`;
|
|
715
|
+
} catch (e) {
|
|
716
|
+
el.innerHTML = `<div style="color:var(--text-dim);font-size:13px;">暂无记忆数据</div>`;
|
|
1743
717
|
}
|
|
1744
718
|
}
|
|
1745
719
|
|
|
1746
|
-
function
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
if (
|
|
1757
|
-
if (
|
|
1758
|
-
|
|
720
|
+
async function saveAgentSettings() {
|
|
721
|
+
if (!currentAgentId) return;
|
|
722
|
+
const body = {};
|
|
723
|
+
const name = document.getElementById('s-name');
|
|
724
|
+
const desc = document.getElementById('s-desc');
|
|
725
|
+
const icon = document.getElementById('s-icon');
|
|
726
|
+
const prompt = document.getElementById('s-prompt');
|
|
727
|
+
const model = document.getElementById('s-model');
|
|
728
|
+
if (name) body.name = name.value;
|
|
729
|
+
if (desc) body.description = desc.value;
|
|
730
|
+
if (icon) body.icon = icon.value;
|
|
731
|
+
if (prompt) body.systemPrompt = prompt.value;
|
|
732
|
+
if (model) body.model = model.value;
|
|
1759
733
|
|
|
1760
|
-
function wizardBack() {
|
|
1761
|
-
if (wizardStep > 1) { wizardStep--; renderWizard(); }
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
function renderConfirmCard() {
|
|
1765
|
-
const name = document.getElementById('agent-name').value || selectedTemplate?.name || 'My Agent';
|
|
1766
|
-
const model = document.getElementById('agent-model').value;
|
|
1767
|
-
const lang = document.getElementById('agent-lang').selectedOptions[0]?.text || 'English';
|
|
1768
|
-
document.getElementById('confirm-card').innerHTML = `
|
|
1769
|
-
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px;">
|
|
1770
|
-
<span style="font-size:48px;">${selectedTemplate?.icon || '🤖'}</span>
|
|
1771
|
-
<div>
|
|
1772
|
-
<div style="font-size:20px;font-weight:700;">${name}</div>
|
|
1773
|
-
<div style="color:var(--text-muted);font-size:14px;">Based on: ${selectedTemplate?.name || 'Custom'} (${selectedTemplate?.nameZh || ''})</div>
|
|
1774
|
-
</div>
|
|
1775
|
-
</div>
|
|
1776
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:14px;">
|
|
1777
|
-
<div><span style="color:var(--text-dim);">Model:</span> ${model}</div>
|
|
1778
|
-
<div><span style="color:var(--text-dim);">Language:</span> ${lang}</div>
|
|
1779
|
-
<div style="grid-column:span 2;"><span style="color:var(--text-dim);">Industry:</span> ${selectedTemplate?.industryZh || ''} (${selectedTemplate?.industry || ''})</div>
|
|
1780
|
-
</div>
|
|
1781
|
-
`;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
async function createAgent() {
|
|
1785
|
-
const btn = document.getElementById('create-btn');
|
|
1786
|
-
btn.textContent = '⏳ Creating...';
|
|
1787
|
-
btn.disabled = true;
|
|
1788
734
|
try {
|
|
1789
|
-
|
|
1790
|
-
method: '
|
|
1791
|
-
|
|
1792
|
-
body: JSON.stringify({
|
|
1793
|
-
name: document.getElementById('agent-name').value || selectedTemplate?.name,
|
|
1794
|
-
templateId: selectedTemplate?.id,
|
|
1795
|
-
description: document.getElementById('agent-desc').value,
|
|
1796
|
-
model: document.getElementById('agent-model').value,
|
|
1797
|
-
language: document.getElementById('agent-lang').value,
|
|
1798
|
-
}),
|
|
735
|
+
await fetch(`${API}/api/agents/${currentAgentId}`, {
|
|
736
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
737
|
+
body: JSON.stringify(body)
|
|
1799
738
|
});
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
selectedTemplate = null;
|
|
1804
|
-
document.getElementById('agent-name').value = '';
|
|
1805
|
-
document.getElementById('agent-desc').value = '';
|
|
1806
|
-
// Navigate to chat
|
|
1807
|
-
openChat(agent.id);
|
|
1808
|
-
} catch(e) {
|
|
1809
|
-
alert('Failed to create agent: ' + e.message);
|
|
1810
|
-
}
|
|
1811
|
-
btn.textContent = '🚀 Create Agent';
|
|
1812
|
-
btn.disabled = false;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
// === Dashboard ===
|
|
1816
|
-
function renderAgents() {
|
|
1817
|
-
if (agents.length === 0) {
|
|
1818
|
-
document.getElementById('agents-list').style.display = 'none';
|
|
1819
|
-
document.getElementById('agents-empty').style.display = 'block';
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
document.getElementById('agents-list').style.display = '';
|
|
1823
|
-
document.getElementById('agents-empty').style.display = 'none';
|
|
1824
|
-
document.getElementById('agents-list').innerHTML = agents.map(a => {
|
|
1825
|
-
const timeAgo = getTimeAgo(a.lastActive || a.created);
|
|
1826
|
-
return `
|
|
1827
|
-
<div class="card agent-card" onclick="openChat('${a.id}')">
|
|
1828
|
-
<div class="agent-actions">
|
|
1829
|
-
<button onclick="event.stopPropagation();openDeleteDialog('${a.id}')">🗑️</button>
|
|
1830
|
-
</div>
|
|
1831
|
-
<div class="agent-icon">${a.templateIcon || '🤖'}</div>
|
|
1832
|
-
<div class="agent-name">${a.name}</div>
|
|
1833
|
-
<div class="agent-template">${a.templateName || 'Custom'}</div>
|
|
1834
|
-
<div class="agent-stats">
|
|
1835
|
-
<span>💬 ${a.messageCount || 0} messages</span>
|
|
1836
|
-
<span>⏰ ${timeAgo}</span>
|
|
1837
|
-
</div>
|
|
1838
|
-
</div>
|
|
1839
|
-
`;
|
|
1840
|
-
}).join('');
|
|
739
|
+
showToast('✅ 已保存');
|
|
740
|
+
loadAgents();
|
|
741
|
+
} catch (e) { showToast('❌ 保存失败: ' + e.message); }
|
|
1841
742
|
}
|
|
1842
743
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
// === Delete ===
|
|
1855
|
-
function openDeleteDialog(id) { deleteTargetId = id; document.getElementById('delete-dialog').classList.add('show'); }
|
|
1856
|
-
function closeDeleteDialog() { deleteTargetId = null; document.getElementById('delete-dialog').classList.remove('show'); }
|
|
1857
|
-
async function confirmDelete() {
|
|
1858
|
-
if (!deleteTargetId) return;
|
|
1859
|
-
await fetch(`${API}/api/agents/${deleteTargetId}`, { method: 'DELETE' });
|
|
1860
|
-
closeDeleteDialog();
|
|
1861
|
-
loadAgents();
|
|
1862
|
-
}
|
|
744
|
+
// ======================== Models Config ========================
|
|
745
|
+
async function loadModelsConfig() {
|
|
746
|
+
const el = document.getElementById('models-content');
|
|
747
|
+
try {
|
|
748
|
+
const [modelsRes, ollamaRes] = await Promise.all([
|
|
749
|
+
fetch(`${API}/api/settings/models`),
|
|
750
|
+
fetch(`${API}/api/settings/models/local`).catch(() => null)
|
|
751
|
+
]);
|
|
752
|
+
modelsConfig = await modelsRes.json();
|
|
753
|
+
const ollama = ollamaRes ? await ollamaRes.json() : { available: false, models: [] };
|
|
1863
754
|
|
|
1864
|
-
|
|
1865
|
-
async function openLastChat() {
|
|
1866
|
-
if (currentAgent) { openChat(currentAgent.id); return; }
|
|
1867
|
-
const agentsRes = await fetch(`${API}/api/agents`).catch(() => null);
|
|
1868
|
-
if (agentsRes) {
|
|
1869
|
-
const data = await agentsRes.json().catch(() => ({}));
|
|
1870
|
-
const list = data.agents || [];
|
|
1871
|
-
if (list.length > 0) { openChat(list[0].id); return; }
|
|
1872
|
-
}
|
|
1873
|
-
navigate('dashboard');
|
|
1874
|
-
}
|
|
755
|
+
let html = '';
|
|
1875
756
|
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
757
|
+
// Ollama
|
|
758
|
+
html += `<div class="card">
|
|
759
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
|
760
|
+
<span style="font-size:20px;">🦙</span><strong>本地模型(Ollama)</strong>
|
|
761
|
+
${ollama.available ? '<span class="status-dot" style="width:8px;height:8px;border-radius:50%;background:var(--green);display:inline-block;"></span><span style="font-size:12px;color:var(--green);">自动检测 · 无需 Key</span>' : '<span style="font-size:12px;color:var(--red);">未检测到</span>'}
|
|
762
|
+
</div>
|
|
763
|
+
${ollama.available && ollama.models?.length ? `<table style="width:100%;font-size:13px;border-collapse:collapse;">
|
|
764
|
+
<tr><th style="text-align:left;padding:6px;border-bottom:1px solid var(--border);">模型</th><th style="text-align:left;padding:6px;border-bottom:1px solid var(--border);">大小</th><th style="text-align:left;padding:6px;border-bottom:1px solid var(--border);">状态</th></tr>
|
|
765
|
+
${ollama.models.map(m => `<tr><td style="padding:6px;">${m.name}</td><td style="padding:6px;">${formatSize(m.size)}</td><td style="padding:6px;color:var(--green);">✅ 可用</td></tr>`).join('')}
|
|
766
|
+
</table>` : '<div style="font-size:13px;color:var(--text-muted);">请安装 Ollama 并拉取模型</div>'}
|
|
767
|
+
</div>`;
|
|
1879
768
|
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
document.getElementById('chat-agent-name').textContent = currentAgent.name;
|
|
1893
|
-
document.getElementById('chat-agent-status').textContent = `${currentAgent.templateName || 'Custom'} · ${currentAgent.model}`;
|
|
1894
|
-
|
|
1895
|
-
// Populate agent selector
|
|
1896
|
-
const sel = document.getElementById('chat-agent-select');
|
|
1897
|
-
sel.innerHTML = agents.map(a => `<option value="${a.id}" ${a.id === agentId ? 'selected' : ''}>${a.templateIcon || '🤖'} ${a.name}</option>`).join('');
|
|
1898
|
-
|
|
1899
|
-
// Render messages
|
|
1900
|
-
const msgEl = document.getElementById('chat-messages');
|
|
1901
|
-
if (chatMessages.length > 0) {
|
|
1902
|
-
msgEl.innerHTML = chatMessages.map(m => `
|
|
1903
|
-
<div class="msg ${m.role}">
|
|
1904
|
-
<div class="msg-bubble">${m.content.replace(/</g,'<')}</div>
|
|
769
|
+
// Cloud providers
|
|
770
|
+
const cloudProviders = [
|
|
771
|
+
{ id: 'openai', name: 'OpenAI', models: 'GPT-4o ┬╖ GPT-4o-mini ┬╖ o1 ┬╖ o3' },
|
|
772
|
+
{ id: 'deepseek', name: 'DeepSeek', models: 'DeepSeek-V3 ┬╖ DeepSeek-R1' },
|
|
773
|
+
{ id: 'anthropic', name: 'Anthropic', models: 'Claude Opus ┬╖ Sonnet ┬╖ Haiku' },
|
|
774
|
+
{ id: 'google', name: 'Google Gemini', models: 'Gemini Pro ┬╖ Flash ┬╖ Ultra' },
|
|
775
|
+
];
|
|
776
|
+
const providers = modelsConfig.providers || {};
|
|
777
|
+
|
|
778
|
+
html += `<div class="card">
|
|
779
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
|
780
|
+
<span style="font-size:20px;">☁️</span><strong>云端 API — 填 Key → 验证 → 解锁模型</strong>
|
|
1905
781
|
</div>
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
782
|
+
<div class="card-grid">
|
|
783
|
+
${cloudProviders.map(p => {
|
|
784
|
+
const cfg = providers[p.id] || {};
|
|
785
|
+
const verified = cfg.verified;
|
|
786
|
+
return `<div class="provider-card${verified ? ' verified' : ''}">
|
|
787
|
+
<div class="provider-name">${p.name} <span style="width:8px;height:8px;border-radius:50%;background:${verified ? 'var(--green)' : 'var(--red)'};display:inline-block;"></span></div>
|
|
788
|
+
<div class="provider-status" style="color:${verified ? 'var(--green)' : 'var(--red)'};">${verified ? '✅ 已验证' : '❌ 未配置'}</div>
|
|
789
|
+
<div class="provider-models">${verified ? '解锁:' : ''}${p.models}${verified ? '' : '(需配 Key)'}</div>
|
|
790
|
+
<div class="key-row">
|
|
791
|
+
<input class="key-input" id="key-${p.id}" type="password" placeholder="输入 API Key..." value="${cfg.apiKey ? '••••••••' : ''}">
|
|
792
|
+
<button class="verify-btn" onclick="verifyProvider('${p.id}')">验证</button>
|
|
793
|
+
</div>
|
|
794
|
+
</div>`;
|
|
795
|
+
}).join('')}
|
|
1911
796
|
</div>
|
|
1912
|
-
|
|
1913
|
-
}
|
|
1914
|
-
document.getElementById('chat-input').value = '';
|
|
1915
|
-
|
|
1916
|
-
showPage('chat');
|
|
1917
|
-
location.hash = `/chat/${agentId}`;
|
|
1918
|
-
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
1919
|
-
const chatNav = document.querySelector('.nav-item[data-page="chat"]');
|
|
1920
|
-
if (chatNav) chatNav.classList.add('active');
|
|
1921
|
-
msgEl.scrollTop = msgEl.scrollHeight;
|
|
1922
|
-
document.getElementById('chat-input').focus();
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
function clearChat() {
|
|
1926
|
-
if (!currentAgent) return;
|
|
1927
|
-
chatMessages = [];
|
|
1928
|
-
localStorage.removeItem(`opc-chat-${currentAgent.id}`);
|
|
1929
|
-
document.getElementById('chat-messages').innerHTML = `
|
|
1930
|
-
<div class="msg assistant">
|
|
1931
|
-
<div class="msg-bubble">Hello! I'm ${currentAgent.name}. How can I help you today?</div>
|
|
1932
|
-
</div>
|
|
1933
|
-
`;
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
async function handleDocUpload(input) {
|
|
1937
|
-
const file = input.files[0];
|
|
1938
|
-
if (!file || !currentAgent) return;
|
|
1939
|
-
input.value = '';
|
|
1940
|
-
|
|
1941
|
-
// Show uploading status in chat
|
|
1942
|
-
appendMessage('user', `📎 Uploading: ${file.name}`);
|
|
1943
|
-
const statusEl = appendMessage('assistant', '⏳ Processing document...');
|
|
1944
|
-
|
|
1945
|
-
try {
|
|
1946
|
-
const formData = new FormData();
|
|
1947
|
-
formData.append('file', file);
|
|
797
|
+
</div>`;
|
|
1948
798
|
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
799
|
+
// Agent model assignment
|
|
800
|
+
html += `<div class="card">
|
|
801
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
|
802
|
+
<span style="font-size:20px;">🤖</span><strong>Agent 模型分配</strong>
|
|
803
|
+
<span style="font-size:12px;color:var(--text-muted);">(只显示已配通的模型)</span>
|
|
804
|
+
</div>
|
|
805
|
+
${agents.length ? `<table style="width:100%;font-size:13px;border-collapse:collapse;">
|
|
806
|
+
<tr><th style="text-align:left;padding:6px;border-bottom:1px solid var(--border);">Agent</th><th style="text-align:left;padding:6px;border-bottom:1px solid var(--border);">模型</th><th style="text-align:left;padding:6px;border-bottom:1px solid var(--border);">来源</th></tr>
|
|
807
|
+
${agents.map(a => `<tr><td style="padding:6px;">${a.icon || '🤖'} ${a.name || a.id}</td><td style="padding:6px;">${a.model || modelsConfig.chatModel || 'auto'}</td><td style="padding:6px;"><span class="tag ${a.model ? 'tag-green' : 'tag-blue'}">${a.model ? 'Agent 覆盖' : '全局默认'}</span></td></tr>`).join('')}
|
|
808
|
+
</table>` : '<div style="font-size:13px;color:var(--text-dim);">暂无 Agent</div>'}
|
|
809
|
+
</div>`;
|
|
1953
810
|
|
|
1954
|
-
|
|
1955
|
-
if (data.error) {
|
|
1956
|
-
statusEl.textContent = `❌ ${data.error}`;
|
|
1957
|
-
} else {
|
|
1958
|
-
statusEl.textContent = `✅ Learned ${data.learnedCount} knowledge chunks from "${file.name}"`;
|
|
1959
|
-
}
|
|
811
|
+
el.innerHTML = html;
|
|
1960
812
|
} catch (e) {
|
|
1961
|
-
|
|
813
|
+
el.innerHTML = `<div style="color:var(--red);">加载失败: ${e.message}</div>`;
|
|
1962
814
|
}
|
|
1963
815
|
}
|
|
1964
816
|
|
|
1965
|
-
async function
|
|
1966
|
-
const
|
|
1967
|
-
const
|
|
1968
|
-
if (!
|
|
1969
|
-
|
|
1970
|
-
input.value = '';
|
|
1971
|
-
chatMessages.push({ role: 'user', content: text });
|
|
1972
|
-
|
|
1973
|
-
// Render user message
|
|
1974
|
-
appendMessage('user', text);
|
|
1975
|
-
|
|
1976
|
-
// Show typing + streaming indicator
|
|
1977
|
-
document.getElementById('typing-indicator').classList.add('show');
|
|
1978
|
-
document.getElementById('streaming-indicator').style.display = 'inline';
|
|
1979
|
-
const msgContainer = document.getElementById('chat-messages');
|
|
1980
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
817
|
+
async function verifyProvider(providerId) {
|
|
818
|
+
const keyInput = document.getElementById(`key-${providerId}`);
|
|
819
|
+
const apiKey = keyInput.value;
|
|
820
|
+
if (!apiKey || apiKey === '••••••••') { showToast('请输入 API Key'); return; }
|
|
1981
821
|
|
|
822
|
+
showToast('验证中...');
|
|
1982
823
|
try {
|
|
1983
|
-
const res = await fetch(`${API}/api/
|
|
1984
|
-
method: 'POST',
|
|
1985
|
-
|
|
1986
|
-
body: JSON.stringify({ messages: chatMessages }),
|
|
824
|
+
const res = await fetch(`${API}/api/settings/models/test`, {
|
|
825
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
826
|
+
body: JSON.stringify({ provider: providerId, apiKey })
|
|
1987
827
|
});
|
|
1988
|
-
|
|
1989
|
-
document.getElementById('typing-indicator').classList.remove('show');
|
|
1990
|
-
|
|
1991
|
-
if (true) { // Always try SSE first — server returns text/event-stream for chat
|
|
1992
|
-
// SSE streaming
|
|
1993
|
-
const reader = res.body.getReader();
|
|
1994
|
-
const decoder = new TextDecoder();
|
|
1995
|
-
let assistantText = '';
|
|
1996
|
-
const bubbleEl = appendMessage('assistant', '');
|
|
1997
|
-
|
|
1998
|
-
while (true) {
|
|
1999
|
-
const { done, value } = await reader.read();
|
|
2000
|
-
if (done) break;
|
|
2001
|
-
const chunk = decoder.decode(value);
|
|
2002
|
-
const lines = chunk.split('\n');
|
|
2003
|
-
for (const line of lines) {
|
|
2004
|
-
if (line.startsWith('data: ')) {
|
|
2005
|
-
const data = line.slice(6);
|
|
2006
|
-
if (data === '[DONE]') break;
|
|
2007
|
-
try {
|
|
2008
|
-
const parsed = JSON.parse(data);
|
|
2009
|
-
const content = parsed.choices?.[0]?.delta?.content || parsed.content || '';
|
|
2010
|
-
assistantText += content;
|
|
2011
|
-
bubbleEl.textContent = assistantText;
|
|
2012
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
2013
|
-
} catch {}
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
chatMessages.push({ role: 'assistant', content: assistantText });
|
|
2018
|
-
} else {
|
|
2019
|
-
const data = await res.json();
|
|
2020
|
-
const reply = data.response || data.error || 'No response';
|
|
2021
|
-
appendMessage('assistant', reply);
|
|
2022
|
-
chatMessages.push({ role: 'assistant', content: reply });
|
|
2023
|
-
}
|
|
2024
|
-
// Persist to localStorage
|
|
2025
|
-
if (currentAgent) {
|
|
2026
|
-
try { localStorage.setItem(`opc-chat-${currentAgent.id}`, JSON.stringify(chatMessages.slice(-100))); } catch {}
|
|
2027
|
-
}
|
|
2028
|
-
} catch(e) {
|
|
2029
|
-
document.getElementById('typing-indicator').classList.remove('show');
|
|
2030
|
-
appendMessage('assistant', `Error: ${e.message}`);
|
|
2031
|
-
} finally {
|
|
2032
|
-
document.getElementById('streaming-indicator').style.display = 'none';
|
|
2033
|
-
}
|
|
2034
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
function appendMessage(role, text) {
|
|
2038
|
-
const msgContainer = document.getElementById('chat-messages');
|
|
2039
|
-
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2040
|
-
const div = document.createElement('div');
|
|
2041
|
-
div.className = `msg ${role}`;
|
|
2042
|
-
const bubble = document.createElement('div');
|
|
2043
|
-
bubble.className = 'msg-bubble';
|
|
2044
|
-
bubble.textContent = text;
|
|
2045
|
-
div.appendChild(bubble);
|
|
2046
|
-
const timeEl = document.createElement('div');
|
|
2047
|
-
timeEl.className = 'msg-time';
|
|
2048
|
-
timeEl.textContent = time;
|
|
2049
|
-
div.appendChild(timeEl);
|
|
2050
|
-
msgContainer.appendChild(div);
|
|
2051
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
2052
|
-
return bubble;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
// === Memory ===
|
|
2056
|
-
function openMemory() {
|
|
2057
|
-
if (currentAgent) openMemoryPage(currentAgent.id);
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
async function openMemoryPage(agentId) {
|
|
2061
|
-
showPage('memory');
|
|
2062
|
-
location.hash = `/memory/${agentId}`;
|
|
2063
|
-
try {
|
|
2064
|
-
const res = await fetch(`${API}/api/agents/${agentId}/memory`);
|
|
2065
828
|
const data = await res.json();
|
|
2066
|
-
if (data.
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
</div>
|
|
2075
|
-
`).join('')}
|
|
2076
|
-
</div>
|
|
2077
|
-
`;
|
|
829
|
+
if (data.success) {
|
|
830
|
+
// Save
|
|
831
|
+
await fetch(`${API}/api/settings/models`, {
|
|
832
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
833
|
+
body: JSON.stringify({ providers: { ...modelsConfig.providers, [providerId]: { apiKey, verified: true, models: data.models || [] } } })
|
|
834
|
+
});
|
|
835
|
+
showToast(`✅ ${providerId} 验证成功`);
|
|
836
|
+
loadModelsConfig();
|
|
2078
837
|
} else {
|
|
2079
|
-
|
|
2080
|
-
document.getElementById('memory-timeline').innerHTML = '';
|
|
838
|
+
showToast(`❌ 验证失败: ${data.error || '无效的 API Key'}`);
|
|
2081
839
|
}
|
|
2082
|
-
} catch {
|
|
2083
|
-
document.getElementById('memory-empty').style.display = 'block';
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
function navigateToChat() {
|
|
2088
|
-
if (currentAgent) openChat(currentAgent.id);
|
|
2089
|
-
else navigate('dashboard');
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
// === Settings ===
|
|
2093
|
-
let currentSettingsTab = 'models';
|
|
2094
|
-
let currentProvider = null;
|
|
2095
|
-
let currentChannel = null;
|
|
2096
|
-
let modelConfig = {};
|
|
2097
|
-
let statusRefreshTimer = null;
|
|
2098
|
-
|
|
2099
|
-
function showSettings(tab) {
|
|
2100
|
-
currentSettingsTab = tab;
|
|
2101
|
-
document.querySelectorAll('.settings-nav-item').forEach(n => n.classList.remove('active'));
|
|
2102
|
-
document.querySelector(`.settings-nav-item[data-settings="${tab}"]`)?.classList.add('active');
|
|
2103
|
-
document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
|
|
2104
|
-
document.getElementById(`sp-${tab}`)?.classList.add('active');
|
|
2105
|
-
|
|
2106
|
-
if (tab === 'models') initModelsPanel();
|
|
2107
|
-
if (tab === 'channels') initChannelsPanel();
|
|
2108
|
-
if (tab === 'memory') initMemoryPanel();
|
|
2109
|
-
if (tab === 'role') initRolePanel();
|
|
2110
|
-
if (tab === 'status') refreshStatus();
|
|
2111
|
-
if (tab === 'usage') refreshUsage();
|
|
2112
|
-
if (tab === 'search') initSearchPanel();
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
function switchModelTab(tab) {
|
|
2116
|
-
document.querySelectorAll('#sp-models .tab').forEach(t => t.classList.remove('active'));
|
|
2117
|
-
document.querySelectorAll('#sp-models .tab-panel').forEach(p => p.classList.remove('active'));
|
|
2118
|
-
if (tab === 'local') {
|
|
2119
|
-
document.querySelector('#sp-models .tab:first-child').classList.add('active');
|
|
2120
|
-
document.getElementById('mt-local').classList.add('active');
|
|
2121
|
-
} else {
|
|
2122
|
-
document.querySelector('#sp-models .tab:last-child').classList.add('active');
|
|
2123
|
-
document.getElementById('mt-cloud').classList.add('active');
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// --- Models Panel ---
|
|
2128
|
-
async function initModelsPanel() {
|
|
2129
|
-
try {
|
|
2130
|
-
const res = await fetch(`${API}/api/settings/models`);
|
|
2131
|
-
modelConfig = await res.json();
|
|
2132
|
-
} catch { modelConfig = {}; }
|
|
2133
|
-
detectOllama();
|
|
2134
|
-
updateProviderStatuses();
|
|
2135
|
-
updateModelDropdowns();
|
|
840
|
+
} catch (e) { showToast(`❌ 验证失败: ${e.message}`); }
|
|
2136
841
|
}
|
|
2137
842
|
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
const modelsEl = document.getElementById('ollama-models');
|
|
2141
|
-
const tutorialEl = document.getElementById('ollama-tutorial');
|
|
2142
|
-
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot yellow"></span> 正在检测本地 Ollama...</div>';
|
|
843
|
+
// ======================== Knowledge ========================
|
|
844
|
+
async function loadKnowledge() {
|
|
2143
845
|
try {
|
|
2144
|
-
const res = await fetch(`${API}/api/
|
|
846
|
+
const res = await fetch(`${API}/api/memory`);
|
|
2145
847
|
const data = await res.json();
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
data.models.map(m => {
|
|
2152
|
-
const size = m.size ? `${(m.size / 1e9).toFixed(1)}GB` : '';
|
|
2153
|
-
const isChat = modelConfig.chatModel === m.name;
|
|
2154
|
-
const isEmbed = modelConfig.embeddingModel === m.name;
|
|
2155
|
-
const badge = isChat ? ' <span style="color:var(--accent);font-size:11px;">● 聊天</span>' : isEmbed ? ' <span style="color:var(--green);font-size:11px;">● 记忆</span>' : '';
|
|
2156
|
-
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border);">
|
|
2157
|
-
<div><span style="font-weight:500;">${m.name}</span>${badge}</div>
|
|
2158
|
-
<span style="font-size:12px;color:var(--text-dim);">${size}</span>
|
|
2159
|
-
</div>`;
|
|
2160
|
-
}).join('') + '</div>';
|
|
2161
|
-
// Update dropdowns with local models
|
|
2162
|
-
updateModelDropdowns(data.models);
|
|
2163
|
-
} else {
|
|
2164
|
-
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>';
|
|
2165
|
-
}
|
|
2166
|
-
} else {
|
|
2167
|
-
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot red"></span> <b>Ollama 未运行</b> — 按照下面的教程安装</div>';
|
|
2168
|
-
modelsEl.innerHTML = '';
|
|
2169
|
-
tutorialEl.style.display = 'block';
|
|
2170
|
-
}
|
|
2171
|
-
} catch {
|
|
2172
|
-
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot red"></span> 无法检测 Ollama</div>';
|
|
2173
|
-
tutorialEl.style.display = 'block';
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
function updateModelDropdowns(localModels) {
|
|
2178
|
-
const chatSel = document.getElementById('cfg-chat-model');
|
|
2179
|
-
const embedSel = document.getElementById('cfg-embed-model');
|
|
2180
|
-
if (localModels && localModels.length > 0) {
|
|
2181
|
-
const chatOpts = localModels.map(m => `<option value="${m.name}" ${modelConfig.chatModel === m.name ? 'selected' : ''}>${m.name}${m.name === 'qwen2.5:7b' ? ' ⭐ 推荐' : ''}</option>`).join('');
|
|
2182
|
-
const embedOpts = localModels.map(m => `<option value="${m.name}" ${modelConfig.embeddingModel === m.name ? 'selected' : ''}>${m.name}${m.name === 'nomic-embed-text' ? ' ⭐ 推荐' : ''}</option>`).join('');
|
|
2183
|
-
chatSel.innerHTML = chatOpts;
|
|
2184
|
-
embedSel.innerHTML = embedOpts;
|
|
2185
|
-
}
|
|
2186
|
-
// Add cloud models if configured
|
|
2187
|
-
const providers = modelConfig.providers || {};
|
|
2188
|
-
const cloudModels = [];
|
|
2189
|
-
if (providers.openai?.apiKey) cloudModels.push({name:'gpt-4o',label:'GPT-4o (OpenAI)'},{name:'gpt-4o-mini',label:'GPT-4o Mini (OpenAI)'});
|
|
2190
|
-
if (providers.deepseek?.apiKey) cloudModels.push({name:'deepseek-chat',label:'DeepSeek V3'},{name:'deepseek-reasoner',label:'DeepSeek R1'});
|
|
2191
|
-
if (providers.anthropic?.apiKey) cloudModels.push({name:'claude-sonnet-4-20250514',label:'Claude Sonnet (Anthropic)'});
|
|
2192
|
-
if (providers.openrouter?.apiKey) cloudModels.push({name:'openrouter/auto',label:'OpenRouter Auto'});
|
|
2193
|
-
cloudModels.forEach(m => {
|
|
2194
|
-
chatSel.innerHTML += `<option value="${m.name}" ${modelConfig.chatModel === m.name ? 'selected' : ''}>${m.label}</option>`;
|
|
2195
|
-
});
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
function updateProviderStatuses() {
|
|
2199
|
-
const providers = modelConfig.providers || {};
|
|
2200
|
-
['openai','deepseek','qwen','anthropic','openrouter'].forEach(p => {
|
|
2201
|
-
const el = document.getElementById(`pv-${p}`);
|
|
2202
|
-
if (!el) return;
|
|
2203
|
-
if (providers[p]?.apiKey) {
|
|
2204
|
-
el.innerHTML = '<span style="color:var(--green);">✅ 已配置</span>';
|
|
2205
|
-
el.closest('.provider-card')?.classList.add('configured');
|
|
2206
|
-
} else {
|
|
2207
|
-
el.innerHTML = '未配置';
|
|
2208
|
-
el.closest('.provider-card')?.classList.remove('configured');
|
|
2209
|
-
}
|
|
2210
|
-
});
|
|
848
|
+
const items = data.memories || data.items || [];
|
|
849
|
+
document.getElementById('kb-total').textContent = items.length;
|
|
850
|
+
document.getElementById('kb-evolve').textContent = data.evolveCount || 0;
|
|
851
|
+
document.getElementById('kb-docs').textContent = data.docCount || 0;
|
|
852
|
+
} catch (e) { console.error('loadKnowledge:', e); }
|
|
2211
853
|
}
|
|
2212
854
|
|
|
2213
|
-
|
|
2214
|
-
const chatModel = document.getElementById('cfg-chat-model').value;
|
|
2215
|
-
const embeddingModel = document.getElementById('cfg-embed-model').value;
|
|
2216
|
-
try {
|
|
2217
|
-
await fetch(`${API}/api/settings/models`, {
|
|
2218
|
-
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
2219
|
-
body: JSON.stringify({ chatModel, embeddingModel })
|
|
2220
|
-
});
|
|
2221
|
-
modelConfig.chatModel = chatModel;
|
|
2222
|
-
modelConfig.embeddingModel = embeddingModel;
|
|
2223
|
-
} catch {}
|
|
2224
|
-
}
|
|
855
|
+
function searchKnowledge(query) { /* TODO: filter knowledge */ }
|
|
2225
856
|
|
|
2226
|
-
//
|
|
2227
|
-
|
|
2228
|
-
openai: { name: 'OpenAI', desc: '需要 OpenAI 账号。获取 Key: platform.openai.com/api-keys', placeholder: 'sk-...' },
|
|
2229
|
-
deepseek: { name: 'DeepSeek', desc: '国产大模型,性价比极高。获取 Key: platform.deepseek.com', placeholder: 'sk-...' },
|
|
2230
|
-
qwen: { name: '通义千问', desc: '阿里云大模型。获取 Key: dashscope.console.aliyun.com', placeholder: 'sk-...' },
|
|
2231
|
-
anthropic: { name: 'Anthropic', desc: 'Claude 系列模型。获取 Key: console.anthropic.com', placeholder: 'sk-ant-...' },
|
|
2232
|
-
openrouter: { name: 'OpenRouter', desc: '100+ 模型聚合平台。获取 Key: openrouter.ai/keys', placeholder: 'sk-or-...' },
|
|
2233
|
-
};
|
|
2234
|
-
|
|
2235
|
-
function configureProvider(provider) {
|
|
2236
|
-
currentProvider = provider;
|
|
2237
|
-
const info = PROVIDER_INFO[provider] || {};
|
|
2238
|
-
document.getElementById('pd-title').textContent = `配置 ${info.name || provider}`;
|
|
2239
|
-
document.getElementById('pd-desc').textContent = info.desc || '';
|
|
2240
|
-
document.getElementById('pd-apikey').placeholder = info.placeholder || 'API Key';
|
|
2241
|
-
document.getElementById('pd-apikey').value = modelConfig.providers?.[provider]?.apiKey || '';
|
|
2242
|
-
document.getElementById('pd-baseurl').value = modelConfig.providers?.[provider]?.baseUrl || '';
|
|
2243
|
-
document.getElementById('pd-test-result').innerHTML = '';
|
|
2244
|
-
document.getElementById('pd-baseurl-group').style.display = (provider === 'qwen' || provider === 'openrouter') ? 'block' : 'none';
|
|
2245
|
-
document.getElementById('provider-dialog').classList.add('show');
|
|
2246
|
-
}
|
|
2247
|
-
function closeProviderDialog() { document.getElementById('provider-dialog').classList.remove('show'); currentProvider = null; }
|
|
2248
|
-
|
|
2249
|
-
async function testProvider() {
|
|
2250
|
-
const apiKey = document.getElementById('pd-apikey').value.trim();
|
|
2251
|
-
const baseUrl = document.getElementById('pd-baseurl').value.trim();
|
|
2252
|
-
const resultEl = document.getElementById('pd-test-result');
|
|
2253
|
-
if (!apiKey) { resultEl.innerHTML = '<span style="color:var(--yellow);">请先填入 API Key</span>'; return; }
|
|
2254
|
-
resultEl.innerHTML = '<span style="color:var(--text-muted);">⏳ 测试中...</span>';
|
|
857
|
+
// ======================== Workstation ========================
|
|
858
|
+
async function loadWorkstation() {
|
|
2255
859
|
try {
|
|
2256
|
-
const res = await fetch(`${API}/api/
|
|
2257
|
-
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
2258
|
-
body: JSON.stringify({ provider: currentProvider, apiKey, baseUrl: baseUrl || undefined })
|
|
2259
|
-
});
|
|
860
|
+
const res = await fetch(`${API}/api/templates?industry=${wsState.industry || ''}&q=`);
|
|
2260
861
|
const data = await res.json();
|
|
2261
|
-
|
|
2262
|
-
} catch(e) {
|
|
2263
|
-
|
|
862
|
+
renderWorkstation(data);
|
|
863
|
+
} catch (e) {
|
|
864
|
+
document.getElementById('ws-content').innerHTML = `<div style="color:var(--red);">加载失败: ${e.message}</div>`;
|
|
2264
865
|
}
|
|
2265
866
|
}
|
|
2266
867
|
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
if (
|
|
2271
|
-
|
|
2272
|
-
try {
|
|
2273
|
-
await fetch(`${API}/api/settings/models`, {
|
|
2274
|
-
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
2275
|
-
body: JSON.stringify({ providers: modelConfig.providers })
|
|
2276
|
-
});
|
|
2277
|
-
} catch {}
|
|
2278
|
-
updateProviderStatuses();
|
|
2279
|
-
updateModelDropdowns();
|
|
2280
|
-
closeProviderDialog();
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
// --- Channels Panel ---
|
|
2284
|
-
const CHANNELS = [
|
|
2285
|
-
{ 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>'}] },
|
|
2286
|
-
{ id: 'wechat', name: '微信', icon: '💬', fields: [], comingSoon: true },
|
|
2287
|
-
{ id: 'feishu', name: '飞书', icon: '🐦', fields: [{key:'appId',label:'App ID',placeholder:'cli_...'},{key:'appSecret',label:'App Secret',placeholder:'',type:'password'}] },
|
|
2288
|
-
{ id: 'discord', name: 'Discord', icon: '🎮', fields: [{key:'botToken',label:'Bot Token',placeholder:'',type:'password'}] },
|
|
2289
|
-
{ id: 'slack', name: 'Slack', icon: '💼', fields: [{key:'botToken',label:'Bot Token',placeholder:'xoxb-...',type:'password'}] },
|
|
2290
|
-
{ 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'}] },
|
|
2291
|
-
{ id: 'web', name: 'Web', icon: '🌐', fields: [], alwaysOn: true },
|
|
2292
|
-
{ id: 'whatsapp', name: 'WhatsApp', icon: '📱', fields: [{key:'phoneId',label:'Phone Number ID',placeholder:''},{key:'accessToken',label:'Access Token',placeholder:'',type:'password'}] },
|
|
2293
|
-
];
|
|
2294
|
-
|
|
2295
|
-
let channelConfigs = {};
|
|
2296
|
-
|
|
2297
|
-
async function initChannelsPanel() {
|
|
2298
|
-
try {
|
|
2299
|
-
const res = await fetch(`${API}/api/settings/channels`);
|
|
2300
|
-
channelConfigs = await res.json();
|
|
2301
|
-
} catch { channelConfigs = {}; }
|
|
2302
|
-
renderChannels();
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
function renderChannels() {
|
|
2306
|
-
document.getElementById('channels-grid').innerHTML = CHANNELS.map(ch => {
|
|
2307
|
-
const cfg = channelConfigs[ch.id] || {};
|
|
2308
|
-
const connected = ch.alwaysOn || (cfg && Object.keys(cfg).some(k => k !== 'updated' && cfg[k]));
|
|
2309
|
-
const statusDot = ch.comingSoon ? 'yellow' : connected ? 'green' : 'red';
|
|
2310
|
-
const statusText = ch.comingSoon ? '即将支持' : connected ? '已连接' : '未配置';
|
|
2311
|
-
return `<div class="card channel-card" onclick="${ch.comingSoon ? '' : `configureChannel('${ch.id}')`}" style="${ch.comingSoon ? 'opacity:0.6;cursor:default;' : ''}">
|
|
2312
|
-
<div class="ch-icon">${ch.icon}</div>
|
|
2313
|
-
<div class="ch-info">
|
|
2314
|
-
<div class="ch-name">${ch.name}</div>
|
|
2315
|
-
<div class="ch-status"><span class="status-dot ${statusDot}"></span> ${statusText}</div>
|
|
2316
|
-
</div>
|
|
2317
|
-
${!ch.comingSoon && !ch.alwaysOn ? '<span style="color:var(--text-dim);font-size:18px;">›</span>' : ''}
|
|
2318
|
-
${ch.alwaysOn ? '<span style="font-size:12px;color:var(--green);">默认开启</span>' : ''}
|
|
2319
|
-
</div>`;
|
|
2320
|
-
}).join('');
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
function configureChannel(chId) {
|
|
2324
|
-
const ch = CHANNELS.find(c => c.id === chId);
|
|
2325
|
-
if (!ch || ch.comingSoon) return;
|
|
2326
|
-
if (ch.alwaysOn) return;
|
|
2327
|
-
currentChannel = chId;
|
|
2328
|
-
const cfg = channelConfigs[chId] || {};
|
|
2329
|
-
document.getElementById('cd-title').textContent = `配置 ${ch.name}`;
|
|
2330
|
-
document.getElementById('cd-desc').textContent = '';
|
|
2331
|
-
document.getElementById('cd-fields').innerHTML = ch.fields.map(f =>
|
|
2332
|
-
`<div class="form-group">
|
|
2333
|
-
<label class="label">${f.label}</label>
|
|
2334
|
-
<input class="input" id="cf-${f.key}" type="${f.type || 'text'}" placeholder="${f.placeholder || ''}" value="${cfg[f.key] || ''}">
|
|
2335
|
-
${f.help ? `<p style="font-size:12px;color:var(--text-dim);margin-top:4px;">${f.help}</p>` : ''}
|
|
2336
|
-
</div>`
|
|
2337
|
-
).join('');
|
|
2338
|
-
document.getElementById('channel-dialog').classList.add('show');
|
|
868
|
+
function wsNavigate(level, value) {
|
|
869
|
+
if (level === 'root') { wsState = { level: 'root', industry: null, job: null }; }
|
|
870
|
+
else if (level === 'industry') { wsState = { level: 'industry', industry: value, job: null }; }
|
|
871
|
+
else if (level === 'job') { wsState.level = 'job'; wsState.job = value; }
|
|
872
|
+
loadWorkstation();
|
|
2339
873
|
}
|
|
2340
|
-
function closeChannelDialog() { document.getElementById('channel-dialog').classList.remove('show'); currentChannel = null; }
|
|
2341
874
|
|
|
2342
|
-
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
try {
|
|
2348
|
-
await fetch(`${API}/api/settings/channels/${currentChannel}`, {
|
|
2349
|
-
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
2350
|
-
body: JSON.stringify(cfg)
|
|
2351
|
-
});
|
|
2352
|
-
channelConfigs[currentChannel] = cfg;
|
|
2353
|
-
} catch {}
|
|
2354
|
-
renderChannels();
|
|
2355
|
-
closeChannelDialog();
|
|
2356
|
-
}
|
|
875
|
+
function renderWorkstation(data) {
|
|
876
|
+
const el = document.getElementById('ws-content');
|
|
877
|
+
const bc = document.getElementById('ws-breadcrumb');
|
|
878
|
+
const templates = data.templates || [];
|
|
879
|
+
const industries = data.industries || [];
|
|
2357
880
|
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
if (running) {
|
|
2363
|
-
container.innerHTML = `<div class="module-frame-container"><iframe src="http://localhost:4001" title="DeepBrain 记忆管理"></iframe></div>`;
|
|
2364
|
-
} else {
|
|
2365
|
-
container.innerHTML = `<div class="card module-frame-fallback">
|
|
2366
|
-
<div class="mf-icon">🧠</div>
|
|
2367
|
-
<h3 style="margin-bottom:8px;">DeepBrain 未运行</h3>
|
|
2368
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:16px;">记忆管理由 DeepBrain 模块提供(端口 4001)</p>
|
|
2369
|
-
<a href="http://localhost:4001" target="_blank" class="btn btn-primary">🔗 打开记忆管理</a>
|
|
2370
|
-
<p style="color:var(--text-dim);font-size:12px;margin-top:12px;">如果按钮无法打开,请先启动 DeepBrain 服务</p>
|
|
2371
|
-
</div>`;
|
|
881
|
+
// Breadcrumb
|
|
882
|
+
let bcHtml = '<a onclick="wsNavigate(\'root\')">全部行业</a>';
|
|
883
|
+
if (wsState.industry) {
|
|
884
|
+
bcHtml += `<span class="sep">›</span><a onclick="wsNavigate('industry','${wsState.industry}')">${wsState.industry}</a>`;
|
|
2372
885
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
// --- Role Panel (Workstation iframe) ---
|
|
2376
|
-
async function initRolePanel() {
|
|
2377
|
-
const container = document.getElementById('role-module-frame');
|
|
2378
|
-
const running = await checkModulePort(4003);
|
|
2379
|
-
if (running) {
|
|
2380
|
-
container.innerHTML = `<div class="module-frame-container"><iframe src="http://localhost:4003" title="Workstation 角色编辑"></iframe></div>`;
|
|
2381
|
-
} else {
|
|
2382
|
-
container.innerHTML = `<div class="card module-frame-fallback">
|
|
2383
|
-
<div class="mf-icon">👤</div>
|
|
2384
|
-
<h3 style="margin-bottom:8px;">Workstation 未运行</h3>
|
|
2385
|
-
<p style="color:var(--text-muted);font-size:14px;margin-bottom:16px;">角色编辑由 Workstation 模块提供(端口 4003)</p>
|
|
2386
|
-
<a href="http://localhost:4003" target="_blank" class="btn btn-primary">🔗 打开角色编辑</a>
|
|
2387
|
-
<p style="color:var(--text-dim);font-size:12px;margin-top:12px;">如果按钮无法打开,请先启动 Workstation 服务</p>
|
|
2388
|
-
</div>`;
|
|
886
|
+
if (wsState.job) {
|
|
887
|
+
bcHtml += `<span class="sep">›</span><span style="color:var(--text);font-weight:500;">${wsState.job}</span>`;
|
|
2389
888
|
}
|
|
2390
|
-
|
|
889
|
+
bc.innerHTML = bcHtml;
|
|
2391
890
|
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
const
|
|
2395
|
-
|
|
2396
|
-
const mod = (data.modules || []).find(m => m.port === port);
|
|
2397
|
-
return mod?.running || false;
|
|
2398
|
-
} catch { return false; }
|
|
2399
|
-
}
|
|
891
|
+
if (wsState.level === 'root') {
|
|
892
|
+
// Show industries
|
|
893
|
+
const uniqueIndustries = [...new Set(templates.map(t => t.industry || t.industryZh).filter(Boolean))];
|
|
894
|
+
if (industries.length) uniqueIndustries.push(...industries.map(i => i.nameZh || i.name || i.id).filter(n => !uniqueIndustries.includes(n)));
|
|
2400
895
|
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
// Overview cards
|
|
2408
|
-
const upHrs = Math.floor(data.uptime / 3600);
|
|
2409
|
-
const upMins = Math.floor((data.uptime % 3600) / 60);
|
|
2410
|
-
const memMB = Math.round((data.memory?.rss || 0) / 1048576);
|
|
2411
|
-
|
|
2412
|
-
document.getElementById('status-overview').innerHTML = `
|
|
2413
|
-
<div class="card-grid" style="margin-bottom:16px;">
|
|
2414
|
-
<div class="card stat-card">
|
|
2415
|
-
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px;">
|
|
2416
|
-
<span class="status-dot green"></span><span style="font-size:14px;font-weight:600;">运行中</span>
|
|
2417
|
-
</div>
|
|
2418
|
-
<div class="stat-value">${upHrs}h ${upMins}m</div>
|
|
2419
|
-
<div class="stat-label">运行时间</div>
|
|
2420
|
-
</div>
|
|
2421
|
-
<div class="card stat-card">
|
|
2422
|
-
<div class="stat-value">${memMB} MB</div>
|
|
2423
|
-
<div class="stat-label">内存占用</div>
|
|
2424
|
-
</div>
|
|
2425
|
-
<div class="card stat-card">
|
|
2426
|
-
<div class="stat-value">${(data.modules || []).filter(m => m.running).length}/${(data.modules || []).length}</div>
|
|
2427
|
-
<div class="stat-label">模块在线</div>
|
|
896
|
+
el.innerHTML = `
|
|
897
|
+
<div class="card" style="border-left:4px solid var(--blue);">
|
|
898
|
+
<div style="font-weight:600;margin-bottom:12px;">🏢 选择行业</div>
|
|
899
|
+
<div class="filter-row">
|
|
900
|
+
${uniqueIndustries.map(i => `<div class="filter-tag" onclick="wsNavigate('industry','${i}')">${i}</div>`).join('')}
|
|
901
|
+
${!uniqueIndustries.length ? '<div style="color:var(--text-dim);font-size:13px;">暂无模板</div>' : ''}
|
|
2428
902
|
</div>
|
|
2429
903
|
</div>
|
|
2430
|
-
<div class="card" style="margin-bottom:16px;">
|
|
2431
|
-
<h3 style="font-size:15px;margin-bottom:12px;">模块状态</h3>
|
|
2432
|
-
${(data.modules || []).map(m => `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;">
|
|
2433
|
-
<span class="status-dot ${m.running ? 'green' : 'red'}"></span>
|
|
2434
|
-
<span>${m.icon} ${m.name}</span>
|
|
2435
|
-
<span style="color:var(--text-dim);font-size:12px;margin-left:auto;">:${m.port}</span>
|
|
2436
|
-
</div>`).join('')}
|
|
2437
|
-
</div>
|
|
2438
904
|
`;
|
|
905
|
+
} else if (wsState.level === 'industry') {
|
|
906
|
+
// Show jobs for selected industry
|
|
907
|
+
const filtered = templates.filter(t => (t.industry || t.industryZh) === wsState.industry);
|
|
908
|
+
const jobs = [...new Set(filtered.map(t => t.tags?.[1] || t.function || 'ΘÇÜτö¿').filter(Boolean))];
|
|
2439
909
|
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
logsEl.textContent = '暂无日志。Agent 运行后日志会显示在这里。';
|
|
2447
|
-
}
|
|
2448
|
-
} catch {
|
|
2449
|
-
document.getElementById('status-overview').innerHTML = '<div class="card"><p style="color:var(--text-muted);">无法获取状态信息</p></div>';
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
// --- Usage Panel ---
|
|
2454
|
-
async function refreshUsage() {
|
|
2455
|
-
try {
|
|
2456
|
-
const res = await fetch(`${API}/api/settings/usage`);
|
|
2457
|
-
const data = await res.json();
|
|
2458
|
-
const totalTokens = data.totalTokens || 0;
|
|
2459
|
-
const totalCost = data.totalCost || 0;
|
|
2460
|
-
const byModel = data.byModel || {};
|
|
2461
|
-
const daily = data.daily || [];
|
|
2462
|
-
|
|
2463
|
-
document.getElementById('usage-stats').innerHTML = `
|
|
2464
|
-
<div class="card-grid" style="margin-bottom:24px;">
|
|
2465
|
-
<div class="card stat-card">
|
|
2466
|
-
<div class="stat-value">${totalTokens > 1000 ? (totalTokens/1000).toFixed(1) + 'K' : totalTokens}</div>
|
|
2467
|
-
<div class="stat-label">总 Token 消耗</div>
|
|
2468
|
-
</div>
|
|
2469
|
-
<div class="card stat-card">
|
|
2470
|
-
<div class="stat-value">$${totalCost.toFixed(4)}</div>
|
|
2471
|
-
<div class="stat-label">估算费用</div>
|
|
2472
|
-
</div>
|
|
2473
|
-
<div class="card stat-card">
|
|
2474
|
-
<div class="stat-value">${Object.keys(byModel).length || 0}</div>
|
|
2475
|
-
<div class="stat-label">使用模型数</div>
|
|
910
|
+
el.innerHTML = `
|
|
911
|
+
<div class="card" style="border-left:4px solid var(--green);">
|
|
912
|
+
<div style="font-weight:600;margin-bottom:12px;">👔 ${wsState.industry} — 选择岗位</div>
|
|
913
|
+
<div class="filter-row">
|
|
914
|
+
${jobs.map(j => `<div class="filter-tag" onclick="wsNavigate('job','${j}')">${j}</div>`).join('')}
|
|
915
|
+
${!jobs.length ? '<div style="color:var(--text-dim);font-size:13px;">暂无岗位</div>' : ''}
|
|
2476
916
|
</div>
|
|
2477
917
|
</div>
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
918
|
+
<div class="card-grid">
|
|
919
|
+
${filtered.slice(0, 12).map(t => renderTemplateCard(t)).join('')}
|
|
920
|
+
</div>
|
|
921
|
+
`;
|
|
922
|
+
} else {
|
|
923
|
+
// Show specific templates
|
|
924
|
+
const filtered = templates.filter(t =>
|
|
925
|
+
(t.industry || t.industryZh) === wsState.industry &&
|
|
926
|
+
(t.tags?.[1] || t.function || 'ΘÇÜτö¿') === wsState.job
|
|
927
|
+
);
|
|
928
|
+
el.innerHTML = `
|
|
929
|
+
<div class="card" style="border-left:4px solid var(--yellow);">
|
|
930
|
+
<div style="font-weight:600;margin-bottom:12px;">🖥️ ${wsState.industry} › ${wsState.job} — 工位模板</div>
|
|
931
|
+
<div class="card-grid" style="margin:0;">
|
|
932
|
+
${filtered.map(t => renderTemplateCard(t)).join('')}
|
|
933
|
+
${!filtered.length ? '<div style="color:var(--text-dim);font-size:13px;">暂无模板</div>' : ''}
|
|
2492
934
|
</div>
|
|
2493
|
-
|
|
935
|
+
</div>
|
|
936
|
+
<div style="background:var(--blue-light);border:1px solid var(--blue);border-radius:var(--radius-sm);padding:16px;margin-top:16px;font-size:13px;">
|
|
937
|
+
<strong style="color:var(--blue);">💡 Skill 自动叠加:</strong>选择工位模板后,Agent 自动获得行业 + 岗位 + 工位三层 Skill。
|
|
938
|
+
</div>
|
|
2494
939
|
`;
|
|
2495
|
-
} catch {
|
|
2496
|
-
document.getElementById('usage-stats').innerHTML = '<div class="card"><p style="color:var(--text-muted);">无法获取用量数据</p></div>';
|
|
2497
940
|
}
|
|
2498
941
|
}
|
|
2499
942
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const eng = cfg.engines[cfg.defaultEngine];
|
|
2510
|
-
if (eng?.apiKey) document.getElementById('search-apikey').value = eng.apiKey;
|
|
2511
|
-
if (eng?.baseUrl) document.getElementById('search-baseurl').value = eng.baseUrl;
|
|
2512
|
-
}
|
|
2513
|
-
} catch { /* defaults are fine */ }
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
function updateSearchEngineUI(engine) {
|
|
2517
|
-
const needsKey = ['brave', 'google'].includes(engine);
|
|
2518
|
-
const needsUrl = engine === 'searxng';
|
|
2519
|
-
document.getElementById('search-apikey-group').style.display = needsKey ? '' : 'none';
|
|
2520
|
-
document.getElementById('search-baseurl-group').style.display = needsUrl ? '' : 'none';
|
|
2521
|
-
if (engine === 'brave') document.getElementById('search-apikey-label').textContent = 'Brave Search API Key';
|
|
2522
|
-
if (engine === 'google') document.getElementById('search-apikey-label').textContent = 'Google API Key:CX';
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
async function updateSearchConfig() {
|
|
2526
|
-
const engine = document.getElementById('search-engine').value;
|
|
2527
|
-
updateSearchEngineUI(engine);
|
|
2528
|
-
const cfg = {
|
|
2529
|
-
enabled: document.getElementById('search-enabled').checked,
|
|
2530
|
-
defaultEngine: engine,
|
|
2531
|
-
engines: {}
|
|
2532
|
-
};
|
|
2533
|
-
cfg.engines[engine] = { enabled: true };
|
|
2534
|
-
const apiKey = document.getElementById('search-apikey').value;
|
|
2535
|
-
const baseUrl = document.getElementById('search-baseurl').value;
|
|
2536
|
-
if (apiKey) cfg.engines[engine].apiKey = apiKey;
|
|
2537
|
-
if (baseUrl) cfg.engines[engine].baseUrl = baseUrl;
|
|
2538
|
-
cfg.engines.duckduckgo = { enabled: true };
|
|
2539
|
-
try {
|
|
2540
|
-
await fetch(`${API}/api/settings/search`, {
|
|
2541
|
-
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
2542
|
-
body: JSON.stringify(cfg)
|
|
2543
|
-
});
|
|
2544
|
-
} catch { /* silent */ }
|
|
943
|
+
function renderTemplateCard(t) {
|
|
944
|
+
const skills = t.skills || [];
|
|
945
|
+
return `<div class="card" style="cursor:pointer;" onclick="useTemplate('${t.id}')">
|
|
946
|
+
<div style="font-size:24px;margin-bottom:8px;">${t.icon || '🤖'}</div>
|
|
947
|
+
<div style="font-weight:600;">${t.nameZh || t.name}</div>
|
|
948
|
+
<div style="font-size:12px;color:var(--text-muted);margin:4px 0;">${t.descriptionZh || t.description || ''}</div>
|
|
949
|
+
${skills.length ? `<div style="margin:4px 0;">${skills.slice(0, 3).map(s => `<span class="tag tag-yellow">${s}</span>`).join('')}</div>` : ''}
|
|
950
|
+
<button class="btn-primary" style="width:100%;margin-top:8px;font-size:12px;" onclick="event.stopPropagation();useTemplate('${t.id}')">使用模板</button>
|
|
951
|
+
</div>`;
|
|
2545
952
|
}
|
|
2546
953
|
|
|
2547
|
-
async function
|
|
2548
|
-
const el = document.getElementById('search-test-result');
|
|
2549
|
-
el.innerHTML = '<span style="color:var(--yellow);">🔍 正在搜索...</span>';
|
|
954
|
+
async function useTemplate(tplId) {
|
|
2550
955
|
try {
|
|
2551
|
-
const res = await fetch(`${API}/api/
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
async function loadHealthDashboard() {
|
|
2569
|
-
const el = document.getElementById('health-section');
|
|
2570
|
-
if (!el) return;
|
|
956
|
+
const res = await fetch(`${API}/api/templates/${tplId}`);
|
|
957
|
+
const tpl = await res.json();
|
|
958
|
+
// Pre-fill new agent form
|
|
959
|
+
document.getElementById('new-agent-name').value = tpl.nameZh || tpl.name || '';
|
|
960
|
+
document.getElementById('new-agent-desc').value = tpl.descriptionZh || tpl.description || '';
|
|
961
|
+
document.getElementById('new-agent-icon').value = tpl.icon || '🤖';
|
|
962
|
+
document.getElementById('new-agent-prompt').value = tpl.systemPrompt || '';
|
|
963
|
+
navigate('new-agent');
|
|
964
|
+
showToast(`已加载模板: ${tpl.nameZh || tpl.name}`);
|
|
965
|
+
} catch (e) { showToast('❌ 加载模板失败: ' + e.message); }
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function searchTemplates(query) { /* TODO: search filter */ }
|
|
969
|
+
|
|
970
|
+
// ======================== Channels ========================
|
|
971
|
+
async function loadChannels() {
|
|
972
|
+
const el = document.getElementById('channels-content');
|
|
2571
973
|
try {
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
974
|
+
const res = await fetch(`${API}/api/settings/channels`);
|
|
975
|
+
const channels = await res.json();
|
|
976
|
+
|
|
977
|
+
const channelDefs = [
|
|
978
|
+
{ id: 'telegram', name: 'Telegram', icon: '📱', fields: [{ key: 'token', label: 'Bot Token', placeholder: '123456:ABC-DEF...' }] },
|
|
979
|
+
{ id: 'wechat', name: '微信', icon: '💬', fields: [{ key: 'appId', label: 'App ID' }, { key: 'appSecret', label: 'App Secret' }] },
|
|
980
|
+
{ id: 'feishu', name: '飞书', icon: '🐦', fields: [{ key: 'appId', label: 'App ID' }, { key: 'appSecret', label: 'App Secret' }] },
|
|
981
|
+
{ id: 'slack', name: 'Slack', icon: '💼', fields: [{ key: 'token', label: 'Bot Token' }] },
|
|
982
|
+
{ id: 'discord', name: 'Discord', icon: '🎮', fields: [{ key: 'token', label: 'Bot Token' }] },
|
|
983
|
+
{ id: 'email', name: 'Email', icon: '📧', fields: [{ key: 'smtp', label: 'SMTP 地址' }, { key: 'password', label: '密码' }] },
|
|
984
|
+
];
|
|
985
|
+
|
|
986
|
+
el.innerHTML = `<div class="card-grid">${channelDefs.map(ch => {
|
|
987
|
+
const cfg = channels[ch.id] || {};
|
|
988
|
+
const configured = cfg.token || cfg.appId;
|
|
989
|
+
return `<div class="card">
|
|
990
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
|
991
|
+
<span style="font-size:24px;">${ch.icon}</span>
|
|
992
|
+
<strong>${ch.name}</strong>
|
|
993
|
+
<span style="width:8px;height:8px;border-radius:50%;background:${configured ? 'var(--green)' : 'var(--border)'};display:inline-block;margin-left:auto;"></span>
|
|
994
|
+
</div>
|
|
995
|
+
${ch.fields.map(f => `
|
|
996
|
+
<div class="form-group" style="margin-bottom:8px;">
|
|
997
|
+
<label class="form-label" style="font-size:12px;">${f.label}</label>
|
|
998
|
+
<input class="form-input" style="font-size:13px;" id="ch-${ch.id}-${f.key}" type="password" placeholder="${f.placeholder || ''}" value="${cfg[f.key] || ''}">
|
|
2587
999
|
</div>
|
|
2588
1000
|
`).join('')}
|
|
2589
|
-
<
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
</div>
|
|
2595
|
-
`;
|
|
2596
|
-
} catch {
|
|
2597
|
-
el.innerHTML = '';
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
// === First Run Wizard ===
|
|
2602
|
-
let frStep = 1;
|
|
2603
|
-
let frSelectedTemplate = null;
|
|
2604
|
-
let frCreatedAgentId = null;
|
|
2605
|
-
|
|
2606
|
-
async function checkFirstRun() {
|
|
2607
|
-
try {
|
|
2608
|
-
const res = await fetch(`${API}/api/first-run/status`);
|
|
2609
|
-
const data = await res.json();
|
|
2610
|
-
if (data.completed) return; // already configured
|
|
2611
|
-
// Check if models are configured — if so, skip
|
|
2612
|
-
const modelRes = await fetch(`${API}/api/settings/models`);
|
|
2613
|
-
const modelData = await modelRes.json();
|
|
2614
|
-
const hasCloudKey = modelData.providers && Object.values(modelData.providers).some(p => p && p.apiKey);
|
|
2615
|
-
if (hasCloudKey) return; // user already has API keys
|
|
2616
|
-
// Check if Ollama is running with models
|
|
2617
|
-
const ollamaRes = await fetch(`${API}/api/settings/models/local`);
|
|
2618
|
-
const ollamaData = await ollamaRes.json();
|
|
2619
|
-
if (ollamaData.running && ollamaData.models?.length > 0) {
|
|
2620
|
-
// Ollama ready — auto-set local mode and skip wizard
|
|
2621
|
-
await fetch(`${API}/api/settings/models`, {
|
|
2622
|
-
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
2623
|
-
body: JSON.stringify({ mode: 'local', provider: 'ollama', chatModel: ollamaData.models[0]?.name || 'qwen2.5:7b' })
|
|
2624
|
-
});
|
|
2625
|
-
return;
|
|
2626
|
-
}
|
|
2627
|
-
// Nothing configured — show wizard
|
|
2628
|
-
showFirstRunWizard({ ollamaDetected: ollamaData.running, ollamaModels: ollamaData.models });
|
|
2629
|
-
} catch {
|
|
2630
|
-
// API not ready, skip
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
function showFirstRunWizard(data) {
|
|
2635
|
-
frStep = 1;
|
|
2636
|
-
const overlay = document.getElementById('first-run-overlay');
|
|
2637
|
-
overlay.style.display = 'flex';
|
|
2638
|
-
frRenderStep();
|
|
2639
|
-
if (data?.ollamaDetected) {
|
|
2640
|
-
const statusEl = document.getElementById('fr-ollama-status');
|
|
2641
|
-
if (statusEl) {
|
|
2642
|
-
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>`;
|
|
2643
|
-
const choiceEl = document.getElementById('fr-model-choice');
|
|
2644
|
-
if (choiceEl) choiceEl.style.display = 'block';
|
|
2645
|
-
const sel = document.getElementById('fr-model-select');
|
|
2646
|
-
if (sel && data.ollamaModels?.length) {
|
|
2647
|
-
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>';
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
} else {
|
|
2651
|
-
detectFrOllama();
|
|
1001
|
+
<button class="btn-primary" style="font-size:12px;" onclick="saveChannel('${ch.id}', [${ch.fields.map(f => `'${f.key}'`).join(',')}])">保存</button>
|
|
1002
|
+
</div>`;
|
|
1003
|
+
}).join('')}</div>`;
|
|
1004
|
+
} catch (e) {
|
|
1005
|
+
el.innerHTML = `<div style="color:var(--red);">加载失败: ${e.message}</div>`;
|
|
2652
1006
|
}
|
|
2653
1007
|
}
|
|
2654
1008
|
|
|
2655
|
-
async function
|
|
1009
|
+
async function saveChannel(channelId, fields) {
|
|
1010
|
+
const body = {};
|
|
1011
|
+
fields.forEach(f => { body[f] = document.getElementById(`ch-${channelId}-${f}`).value; });
|
|
2656
1012
|
try {
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
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>`;
|
|
2664
|
-
if (choiceEl) choiceEl.style.display = 'block';
|
|
2665
|
-
const sel = document.getElementById('fr-model-select');
|
|
2666
|
-
if (sel && data.models?.length) {
|
|
2667
|
-
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>';
|
|
2668
|
-
}
|
|
2669
|
-
} else {
|
|
2670
|
-
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>`;
|
|
2671
|
-
if (choiceEl) choiceEl.style.display = 'block';
|
|
2672
|
-
}
|
|
2673
|
-
} catch {}
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
function frRenderStep() {
|
|
2677
|
-
for (let i = 1; i <= 4; i++) {
|
|
2678
|
-
const stepEl = document.getElementById(`fr-step-${i}`);
|
|
2679
|
-
const panelEl = document.getElementById(`fr-panel-${i}`);
|
|
2680
|
-
if (stepEl) stepEl.className = 'wizard-step' + (i < frStep ? ' done' : i === frStep ? ' active' : '');
|
|
2681
|
-
if (panelEl) panelEl.className = 'wizard-panel' + (i === frStep ? ' active' : '');
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
function frNext() {
|
|
2686
|
-
if (frStep === 3 && !frSelectedTemplate) {
|
|
2687
|
-
frSelectedTemplate = 'customer-service';
|
|
2688
|
-
}
|
|
2689
|
-
if (frStep === 3) {
|
|
2690
|
-
frStep = 4;
|
|
2691
|
-
frRenderStep();
|
|
2692
|
-
frCreateAgent();
|
|
2693
|
-
return;
|
|
2694
|
-
}
|
|
2695
|
-
if (frStep < 4) { frStep++; frRenderStep(); }
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
function frBack() {
|
|
2699
|
-
if (frStep > 1) { frStep--; frRenderStep(); }
|
|
1013
|
+
await fetch(`${API}/api/settings/channels/${channelId}`, {
|
|
1014
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
1015
|
+
body: JSON.stringify(body)
|
|
1016
|
+
});
|
|
1017
|
+
showToast(`✅ ${channelId} 已保存`);
|
|
1018
|
+
} catch (e) { showToast('❌ 保存失败: ' + e.message); }
|
|
2700
1019
|
}
|
|
2701
1020
|
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
document.
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
1021
|
+
// ======================== Create Agent ========================
|
|
1022
|
+
async function createAgent() {
|
|
1023
|
+
const name = document.getElementById('new-agent-name').value.trim();
|
|
1024
|
+
if (!name) { showToast('请输入名称'); return; }
|
|
1025
|
+
|
|
1026
|
+
const body = {
|
|
1027
|
+
name,
|
|
1028
|
+
description: document.getElementById('new-agent-desc').value,
|
|
1029
|
+
icon: document.getElementById('new-agent-icon').value || '🤖',
|
|
1030
|
+
systemPrompt: document.getElementById('new-agent-prompt').value,
|
|
1031
|
+
model: document.getElementById('new-agent-model').value,
|
|
1032
|
+
};
|
|
2711
1033
|
|
|
2712
|
-
async function frCreateAgent() {
|
|
2713
|
-
const model = document.getElementById('fr-model-select')?.value || 'qwen2.5:7b';
|
|
2714
1034
|
try {
|
|
2715
|
-
// Save first-run complete
|
|
2716
|
-
await fetch(`${API}/api/first-run/complete`, {
|
|
2717
|
-
method: 'POST',
|
|
2718
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2719
|
-
body: JSON.stringify({ templateId: frSelectedTemplate, model }),
|
|
2720
|
-
});
|
|
2721
|
-
// Create the agent
|
|
2722
1035
|
const res = await fetch(`${API}/api/agents`, {
|
|
2723
|
-
method: 'POST',
|
|
2724
|
-
|
|
2725
|
-
body: JSON.stringify({ name: '', templateId: frSelectedTemplate || 'customer-service', model }),
|
|
1036
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1037
|
+
body: JSON.stringify(body)
|
|
2726
1038
|
});
|
|
2727
|
-
const
|
|
2728
|
-
|
|
2729
|
-
document.getElementById('fr-creating').style.display = 'none';
|
|
2730
|
-
document.getElementById('fr-done').style.display = 'block';
|
|
1039
|
+
const data = await res.json();
|
|
1040
|
+
showToast(`✅ Agent "${name}" 已创建`);
|
|
2731
1041
|
await loadAgents();
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
}
|
|
1042
|
+
navigate('agent', { id: data.id || name.toLowerCase().replace(/\s+/g, '-'), name, icon: body.icon });
|
|
1043
|
+
} catch (e) { showToast('❌ 创建失败: ' + e.message); }
|
|
2735
1044
|
}
|
|
2736
1045
|
|
|
2737
|
-
function
|
|
2738
|
-
document.getElementById('
|
|
2739
|
-
|
|
2740
|
-
else navigate('dashboard');
|
|
2741
|
-
}
|
|
2742
|
-
|
|
2743
|
-
// === Drag & drop document upload ===
|
|
2744
|
-
const chatArea = document.getElementById('chat-messages');
|
|
2745
|
-
if (chatArea) {
|
|
2746
|
-
chatArea.addEventListener('dragover', (e) => { e.preventDefault(); chatArea.style.outline = '2px dashed var(--primary)'; });
|
|
2747
|
-
chatArea.addEventListener('dragleave', () => { chatArea.style.outline = ''; });
|
|
2748
|
-
chatArea.addEventListener('drop', (e) => {
|
|
2749
|
-
e.preventDefault();
|
|
2750
|
-
chatArea.style.outline = '';
|
|
2751
|
-
const file = e.dataTransfer?.files?.[0];
|
|
2752
|
-
if (file) {
|
|
2753
|
-
const dt = new DataTransfer();
|
|
2754
|
-
dt.items.add(file);
|
|
2755
|
-
const inp = document.getElementById('doc-upload-input');
|
|
2756
|
-
inp.files = dt.files;
|
|
2757
|
-
handleDocUpload(inp);
|
|
2758
|
-
}
|
|
2759
|
-
});
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
|
-
// === Dashboard Stats ===
|
|
2763
|
-
async function loadDashboardStats() {
|
|
2764
|
-
const el = document.getElementById('dashboard-stats');
|
|
2765
|
-
if (!el) return;
|
|
1046
|
+
async function loadAvailableModels() {
|
|
1047
|
+
const sel = document.getElementById('new-agent-model');
|
|
1048
|
+
sel.innerHTML = '<option value="">使用全局默认</option>';
|
|
2766
1049
|
try {
|
|
2767
|
-
const [
|
|
2768
|
-
fetch(
|
|
2769
|
-
fetch(
|
|
1050
|
+
const [modelsRes, ollamaRes] = await Promise.all([
|
|
1051
|
+
fetch(`${API}/api/settings/models`),
|
|
1052
|
+
fetch(`${API}/api/settings/models/local`).catch(() => null)
|
|
2770
1053
|
]);
|
|
2771
|
-
const
|
|
2772
|
-
const
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
async function loadAgentTabData(tab) {
|
|
2784
|
-
if (!selectedAgentId) return;
|
|
2785
|
-
const id = selectedAgentId;
|
|
2786
|
-
try {
|
|
2787
|
-
if (tab === 'role') {
|
|
2788
|
-
const res = await fetch(`/api/agents/${id}`);
|
|
2789
|
-
const a = await res.json();
|
|
2790
|
-
document.getElementById('atab-role-name').value = a.name || '';
|
|
2791
|
-
document.getElementById('atab-role-desc').value = a.description || '';
|
|
2792
|
-
document.getElementById('atab-role-prompt').value = a.systemPrompt || a.prompt || '';
|
|
2793
|
-
} else if (tab === 'models') {
|
|
2794
|
-
const res = await fetch(`/api/agents/${id}`);
|
|
2795
|
-
const a = await res.json();
|
|
2796
|
-
const ov = document.getElementById('atab-model-override');
|
|
2797
|
-
const fields = document.getElementById('atab-model-fields');
|
|
2798
|
-
ov.checked = !!a.modelOverride;
|
|
2799
|
-
fields.style.display = ov.checked ? '' : 'none';
|
|
2800
|
-
ov.onchange = () => { fields.style.display = ov.checked ? '' : 'none'; };
|
|
2801
|
-
if (a.modelOverride) {
|
|
2802
|
-
document.getElementById('atab-model-provider').value = a.provider || 'ollama';
|
|
2803
|
-
document.getElementById('atab-model-name').value = a.model || '';
|
|
2804
|
-
document.getElementById('atab-model-temp').value = a.temperature ?? 0.7;
|
|
1054
|
+
const models = await modelsRes.json();
|
|
1055
|
+
const ollama = ollamaRes ? await ollamaRes.json() : { models: [] };
|
|
1056
|
+
if (ollama.models) {
|
|
1057
|
+
ollama.models.filter(m => !m.name?.includes('embed')).forEach(m => {
|
|
1058
|
+
sel.innerHTML += `<option value="ollama:${m.name}">${m.name} (Ollama)</option>`;
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const providers = models.providers || {};
|
|
1062
|
+
for (const [name, cfg] of Object.entries(providers)) {
|
|
1063
|
+
if (cfg.verified && cfg.models) {
|
|
1064
|
+
cfg.models.forEach(m => { sel.innerHTML += `<option value="${name}:${m}">${m} (${name})</option>`; });
|
|
2805
1065
|
}
|
|
2806
|
-
} else if (tab === 'channels') {
|
|
2807
|
-
const chList = ['web','telegram','discord','slack','feishu','email','whatsapp'];
|
|
2808
|
-
const res = await fetch(`/api/agents/${id}`);
|
|
2809
|
-
const a = await res.json();
|
|
2810
|
-
const enabled = a.channels || ['web'];
|
|
2811
|
-
document.getElementById('atab-channels-list').innerHTML = chList.map(ch =>
|
|
2812
|
-
`<label style="display:flex;align-items:center;gap:10px;padding:8px 0;font-size:18px;cursor:pointer;">
|
|
2813
|
-
<input type="checkbox" class="atab-ch-cb" value="${ch}" ${enabled.includes(ch)?'checked':''}>
|
|
2814
|
-
${ch.charAt(0).toUpperCase()+ch.slice(1)}
|
|
2815
|
-
</label>`
|
|
2816
|
-
).join('');
|
|
2817
|
-
} else if (tab === 'memory') {
|
|
2818
|
-
const el = document.getElementById('atab-memory-list');
|
|
2819
|
-
el.innerHTML = '<span style="color:var(--text-muted);">⏳ Loading...</span>';
|
|
2820
|
-
try {
|
|
2821
|
-
const res = await fetch(`/api/agents/${id}/memory`);
|
|
2822
|
-
const data = await res.json();
|
|
2823
|
-
const entries = data.entries || [];
|
|
2824
|
-
if (!entries.length) { el.innerHTML = '<p style="color:var(--text-muted);">No memories yet.</p>'; return; }
|
|
2825
|
-
el.innerHTML = '<div class="timeline">' + entries.map(e =>
|
|
2826
|
-
`<div class="timeline-item"><div class="timeline-date">${new Date(e.timestamp).toLocaleString()}</div><div class="timeline-content">${esc(e.summary||e.content||'')}</div></div>`
|
|
2827
|
-
).join('') + '</div>';
|
|
2828
|
-
} catch { el.innerHTML = '<p style="color:var(--text-muted);">Failed to load memories.</p>'; }
|
|
2829
|
-
} else if (tab === 'skills') {
|
|
2830
|
-
const el = document.getElementById('atab-skills-list');
|
|
2831
|
-
el.innerHTML = '<span style="color:var(--text-muted);">⏳ Loading...</span>';
|
|
2832
|
-
try {
|
|
2833
|
-
const res = await fetch(`/api/agents/${id}`);
|
|
2834
|
-
const a = await res.json();
|
|
2835
|
-
const skills = a.skills || [];
|
|
2836
|
-
if (!skills.length) { el.innerHTML = '<p style="color:var(--text-muted);">No skills installed.</p>'; return; }
|
|
2837
|
-
el.innerHTML = skills.map(s => `<div class="card" style="margin-bottom:8px;padding:12px;"><span style="font-size:18px;font-weight:600;">${s.name||s}</span></div>`).join('');
|
|
2838
|
-
} catch { el.innerHTML = '<p style="color:var(--text-muted);">Failed to load skills.</p>'; }
|
|
2839
|
-
} else if (tab === 'schedules') {
|
|
2840
|
-
const el = document.getElementById('atab-schedules-list');
|
|
2841
|
-
el.innerHTML = '<span style="color:var(--text-muted);">⏳ Loading...</span>';
|
|
2842
|
-
try {
|
|
2843
|
-
const res = await fetch('/api/schedules');
|
|
2844
|
-
const all = await res.json();
|
|
2845
|
-
const tasks = (Array.isArray(all)?all:[]).filter(t => t.agentId === id);
|
|
2846
|
-
if (!tasks.length) { el.innerHTML = '<p style="color:var(--text-muted);">No schedules for this agent.</p>'; return; }
|
|
2847
|
-
el.innerHTML = tasks.map(t => `<div class="card" style="margin-bottom:8px;padding:12px;display:flex;align-items:center;gap:12px;">
|
|
2848
|
-
<span>⏰</span><div style="flex:1;"><div style="font-weight:600;font-size:18px;">${esc(t.name)}</div><div style="font-size:18px;color:var(--text-dim);">${esc(t.schedule||t.frequency||'')}</div></div>
|
|
2849
|
-
<span style="font-size:18px;color:${t.enabled?'var(--green)':'var(--text-dim)'};">${t.enabled?'Active':'Paused'}</span>
|
|
2850
|
-
</div>`).join('');
|
|
2851
|
-
} catch { el.innerHTML = '<p style="color:var(--text-muted);">Failed to load schedules.</p>'; }
|
|
2852
|
-
} else if (tab === 'usage') {
|
|
2853
|
-
const el = document.getElementById('atab-usage-content');
|
|
2854
|
-
el.innerHTML = '<span style="color:var(--text-muted);">⏳ Loading...</span>';
|
|
2855
|
-
try {
|
|
2856
|
-
const res = await fetch('/api/settings/usage');
|
|
2857
|
-
const data = await res.json();
|
|
2858
|
-
const tokens = data.totalTokens || 0;
|
|
2859
|
-
const cost = data.totalCost || 0;
|
|
2860
|
-
el.innerHTML = `<div class="card-grid"><div class="card stat-card"><div class="stat-value">${tokens>1000?(tokens/1000).toFixed(1)+'K':tokens}</div><div class="stat-label">Total Tokens</div></div><div class="card stat-card"><div class="stat-value">$${cost.toFixed(4)}</div><div class="stat-label">Est. Cost</div></div></div>`;
|
|
2861
|
-
} catch { el.innerHTML = '<p style="color:var(--text-muted);">Failed to load usage data.</p>'; }
|
|
2862
1066
|
}
|
|
2863
|
-
} catch(e) { console.error('
|
|
1067
|
+
} catch (e) { console.error('loadAvailableModels:', e); }
|
|
2864
1068
|
}
|
|
2865
1069
|
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
await fetch(`/api/agents/${selectedAgentId}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({
|
|
2871
|
-
name: document.getElementById('atab-role-name').value.trim(),
|
|
2872
|
-
description: document.getElementById('atab-role-desc').value.trim(),
|
|
2873
|
-
systemPrompt: document.getElementById('atab-role-prompt').value.trim(),
|
|
2874
|
-
})});
|
|
2875
|
-
st.textContent = '✅ Saved'; st.style.color = 'var(--green)';
|
|
2876
|
-
loadSidebarAgents();
|
|
2877
|
-
} catch { st.textContent = '❌ Failed'; st.style.color = 'var(--red)'; }
|
|
2878
|
-
}
|
|
1070
|
+
// ======================== Utils ========================
|
|
1071
|
+
function escapeHtml(s) { return s ? s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : ''; }
|
|
1072
|
+
function escapeAttr(s) { return s ? s.replace(/"/g, '"').replace(/'/g, ''') : ''; }
|
|
1073
|
+
function formatSize(bytes) { if (!bytes) return '?'; const gb = bytes / (1024*1024*1024); return gb >= 1 ? gb.toFixed(1) + ' GB' : (bytes / (1024*1024)).toFixed(0) + ' MB'; }
|
|
2879
1074
|
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
if (ov) { body.provider = document.getElementById('atab-model-provider').value; body.model = document.getElementById('atab-model-name').value; body.temperature = parseFloat(document.getElementById('atab-model-temp').value); }
|
|
2886
|
-
try {
|
|
2887
|
-
await fetch(`/api/agents/${selectedAgentId}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
|
|
2888
|
-
st.textContent = '✅ Saved'; st.style.color = 'var(--green)';
|
|
2889
|
-
} catch { st.textContent = '❌ Failed'; st.style.color = 'var(--red)'; }
|
|
1075
|
+
function showToast(msg) {
|
|
1076
|
+
const el = document.getElementById('toast');
|
|
1077
|
+
el.textContent = msg;
|
|
1078
|
+
el.style.display = 'block';
|
|
1079
|
+
setTimeout(() => el.style.display = 'none', 3000);
|
|
2890
1080
|
}
|
|
2891
1081
|
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
st.textContent = '✅ Saved'; st.style.color = 'var(--green)';
|
|
2899
|
-
} catch { st.textContent = '❌ Failed'; st.style.color = 'var(--red)'; }
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
// === Populate schedule form agent select ===
|
|
2903
|
-
async function populateSchedAgentSelect() {
|
|
2904
|
-
const sel = document.getElementById('sched-agent');
|
|
2905
|
-
if (!sel) return;
|
|
2906
|
-
try {
|
|
2907
|
-
const res = await fetch('/api/agents');
|
|
2908
|
-
const data = await res.json();
|
|
2909
|
-
const list = data.agents || [];
|
|
2910
|
-
sel.innerHTML = list.map(a => `<option value="${a.id}">${a.name||a.id}</option>`).join('');
|
|
2911
|
-
} catch { sel.innerHTML = '<option value="">No agents</option>'; }
|
|
1082
|
+
// ======================== Init ========================
|
|
1083
|
+
async function init() {
|
|
1084
|
+
await loadAgents();
|
|
1085
|
+
navigate('assistant');
|
|
1086
|
+
// Refresh agent list every 5s
|
|
1087
|
+
setInterval(loadAgents, 5000);
|
|
2912
1088
|
}
|
|
2913
1089
|
|
|
2914
|
-
// === Start ===
|
|
2915
1090
|
init();
|
|
2916
|
-
|
|
2917
|
-
// =============================================
|
|
2918
|
-
// === Schedules Management ===
|
|
2919
|
-
// =============================================
|
|
2920
|
-
let editingScheduleId = null;
|
|
2921
|
-
|
|
2922
|
-
async function loadSchedules() {
|
|
2923
|
-
try {
|
|
2924
|
-
const res = await fetch('/api/schedules');
|
|
2925
|
-
const tasks = await res.json();
|
|
2926
|
-
const list = Array.isArray(tasks) ? tasks : [];
|
|
2927
|
-
renderSchedules(list);
|
|
2928
|
-
} catch(e) {
|
|
2929
|
-
console.error('Failed to load schedules:', e);
|
|
2930
|
-
renderSchedules([]);
|
|
2931
|
-
}
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
function renderSchedules(tasks) {
|
|
2935
|
-
const listEl = document.getElementById('schedules-list');
|
|
2936
|
-
const emptyEl = document.getElementById('schedules-empty');
|
|
2937
|
-
if (!tasks.length) {
|
|
2938
|
-
listEl.innerHTML = '';
|
|
2939
|
-
emptyEl.style.display = '';
|
|
2940
|
-
return;
|
|
2941
|
-
}
|
|
2942
|
-
emptyEl.style.display = 'none';
|
|
2943
|
-
listEl.innerHTML = tasks.map(t => `
|
|
2944
|
-
<div class="card" style="margin-bottom:12px;display:flex;align-items:center;gap:16px;">
|
|
2945
|
-
<div style="font-size:28px;">⏰</div>
|
|
2946
|
-
<div style="flex:1;">
|
|
2947
|
-
<div style="font-size:15px;font-weight:600;">${esc(t.name)}</div>
|
|
2948
|
-
<div style="font-size:12px;color:var(--text-muted);">${esc(t.description || '')}</div>
|
|
2949
|
-
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;">
|
|
2950
|
-
${esc(t.schedule)} · ${t.outputChannel || 'web'} · Next: ${t.nextRun ? new Date(t.nextRun).toLocaleString() : 'N/A'}
|
|
2951
|
-
</div>
|
|
2952
|
-
</div>
|
|
2953
|
-
<div style="display:flex;gap:8px;align-items:center;">
|
|
2954
|
-
<label style="position:relative;display:inline-block;width:40px;height:22px;cursor:pointer;">
|
|
2955
|
-
<input type="checkbox" ${t.enabled ? 'checked' : ''} onchange="toggleSchedule('${t.id}', this.checked)" style="opacity:0;width:0;height:0;">
|
|
2956
|
-
<span style="position:absolute;inset:0;border-radius:11px;background:${t.enabled ? 'var(--green)' : 'var(--border)'};transition:0.3s;"></span>
|
|
2957
|
-
<span style="position:absolute;top:2px;left:${t.enabled ? '20px' : '2px'};width:18px;height:18px;border-radius:50%;background:white;transition:0.3s;"></span>
|
|
2958
|
-
</label>
|
|
2959
|
-
<button class="btn btn-sm btn-secondary" onclick="runScheduleNow('${t.id}')" title="Run now">▶️</button>
|
|
2960
|
-
<button class="btn btn-sm btn-secondary" onclick="editSchedule('${t.id}')" title="Edit">✏️</button>
|
|
2961
|
-
<button class="btn btn-sm btn-danger" onclick="deleteSchedule('${t.id}')" title="Delete">🗑</button>
|
|
2962
|
-
</div>
|
|
2963
|
-
</div>
|
|
2964
|
-
`).join('');
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
2968
|
-
|
|
2969
|
-
function showScheduleForm(task) {
|
|
2970
|
-
editingScheduleId = task ? task.id : null;
|
|
2971
|
-
document.getElementById('schedule-form').style.display = '';
|
|
2972
|
-
document.getElementById('schedule-form-title').textContent = task ? 'Edit Task' : 'New Scheduled Task';
|
|
2973
|
-
document.getElementById('sched-name').value = task ? task.name : '';
|
|
2974
|
-
document.getElementById('sched-frequency').value = task ? task.frequency : 'daily';
|
|
2975
|
-
document.getElementById('sched-time').value = task ? (task.time || '09:00') : '09:00';
|
|
2976
|
-
document.getElementById('sched-cron').value = task ? task.schedule : '';
|
|
2977
|
-
document.getElementById('sched-desc').value = task ? task.description : '';
|
|
2978
|
-
document.getElementById('sched-channel').value = task ? task.outputChannel : 'web';
|
|
2979
|
-
onSchedFreqChange();
|
|
2980
|
-
populateSchedAgentSelect().then(() => {
|
|
2981
|
-
if (task?.agentId) document.getElementById('sched-agent').value = task.agentId;
|
|
2982
|
-
});
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
function hideScheduleForm() {
|
|
2986
|
-
document.getElementById('schedule-form').style.display = 'none';
|
|
2987
|
-
editingScheduleId = null;
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
function onSchedFreqChange() {
|
|
2991
|
-
const freq = document.getElementById('sched-frequency').value;
|
|
2992
|
-
document.getElementById('sched-time-group').style.display = freq === 'custom' ? 'none' : '';
|
|
2993
|
-
document.getElementById('sched-cron-group').style.display = freq === 'custom' ? '' : 'none';
|
|
2994
|
-
}
|
|
2995
|
-
|
|
2996
|
-
async function saveSchedule() {
|
|
2997
|
-
const data = {
|
|
2998
|
-
name: document.getElementById('sched-name').value.trim(),
|
|
2999
|
-
frequency: document.getElementById('sched-frequency').value,
|
|
3000
|
-
time: document.getElementById('sched-time').value,
|
|
3001
|
-
schedule: document.getElementById('sched-frequency').value === 'custom' ? document.getElementById('sched-cron').value.trim() : '',
|
|
3002
|
-
description: document.getElementById('sched-desc').value.trim(),
|
|
3003
|
-
agentId: document.getElementById('sched-agent').value,
|
|
3004
|
-
outputChannel: document.getElementById('sched-channel').value,
|
|
3005
|
-
enabled: true,
|
|
3006
|
-
};
|
|
3007
|
-
if (!data.name) { alert('Task name is required'); return; }
|
|
3008
|
-
try {
|
|
3009
|
-
if (editingScheduleId) {
|
|
3010
|
-
await fetch(`/api/schedules/${editingScheduleId}`, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
3011
|
-
} else {
|
|
3012
|
-
await fetch('/api/schedules', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
3013
|
-
}
|
|
3014
|
-
hideScheduleForm();
|
|
3015
|
-
loadSchedules();
|
|
3016
|
-
} catch(e) { alert('Failed to save: ' + e.message); }
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
async function toggleSchedule(id, enabled) {
|
|
3020
|
-
await fetch(`/api/schedules/${id}`, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ enabled }) });
|
|
3021
|
-
loadSchedules();
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
async function deleteSchedule(id) {
|
|
3025
|
-
if (!confirm('Delete this task?')) return;
|
|
3026
|
-
await fetch(`/api/schedules/${id}`, { method: 'DELETE' });
|
|
3027
|
-
loadSchedules();
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
|
-
async function runScheduleNow(id) {
|
|
3031
|
-
await fetch(`/api/schedules/${id}/run`, { method: 'POST' });
|
|
3032
|
-
alert('Task executed!');
|
|
3033
|
-
loadSchedules();
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
async function editSchedule(id) {
|
|
3037
|
-
const res = await fetch('/api/schedules');
|
|
3038
|
-
const tasks = await res.json();
|
|
3039
|
-
const list = Array.isArray(tasks) ? tasks : [];
|
|
3040
|
-
const task = list.find(t => t.id === id);
|
|
3041
|
-
if (task) showScheduleForm(task);
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
// =============================================
|
|
3045
|
-
// === Voice Interaction ===
|
|
3046
|
-
// =============================================
|
|
3047
|
-
let voiceRecognition = null;
|
|
3048
|
-
let isRecording = false;
|
|
3049
|
-
|
|
3050
|
-
function toggleVoiceInput() {
|
|
3051
|
-
if (isRecording) {
|
|
3052
|
-
stopVoiceInput();
|
|
3053
|
-
} else {
|
|
3054
|
-
startVoiceInput();
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
|
|
3058
|
-
function startVoiceInput() {
|
|
3059
|
-
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
3060
|
-
if (!SpeechRecognition) {
|
|
3061
|
-
alert('Speech recognition is not supported in this browser. Try Chrome.');
|
|
3062
|
-
return;
|
|
3063
|
-
}
|
|
3064
|
-
voiceRecognition = new SpeechRecognition();
|
|
3065
|
-
voiceRecognition.continuous = false;
|
|
3066
|
-
voiceRecognition.interimResults = true;
|
|
3067
|
-
voiceRecognition.lang = navigator.language || 'en-US';
|
|
3068
|
-
|
|
3069
|
-
voiceRecognition.onstart = () => {
|
|
3070
|
-
isRecording = true;
|
|
3071
|
-
const btn = document.getElementById('voice-btn');
|
|
3072
|
-
btn.style.background = 'var(--red)';
|
|
3073
|
-
btn.style.color = 'white';
|
|
3074
|
-
btn.style.borderColor = 'var(--red)';
|
|
3075
|
-
btn.textContent = '⏹';
|
|
3076
|
-
};
|
|
3077
|
-
|
|
3078
|
-
voiceRecognition.onresult = (event) => {
|
|
3079
|
-
let transcript = '';
|
|
3080
|
-
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
3081
|
-
transcript += event.results[i][0].transcript;
|
|
3082
|
-
}
|
|
3083
|
-
document.getElementById('chat-input').value = transcript;
|
|
3084
|
-
};
|
|
3085
|
-
|
|
3086
|
-
voiceRecognition.onend = () => {
|
|
3087
|
-
isRecording = false;
|
|
3088
|
-
const btn = document.getElementById('voice-btn');
|
|
3089
|
-
btn.style.background = 'transparent';
|
|
3090
|
-
btn.style.color = '';
|
|
3091
|
-
btn.style.borderColor = 'var(--border)';
|
|
3092
|
-
btn.textContent = '🎤';
|
|
3093
|
-
// Auto-send if we got text
|
|
3094
|
-
const input = document.getElementById('chat-input');
|
|
3095
|
-
if (input.value.trim()) {
|
|
3096
|
-
sendMessage();
|
|
3097
|
-
}
|
|
3098
|
-
};
|
|
3099
|
-
|
|
3100
|
-
voiceRecognition.onerror = (event) => {
|
|
3101
|
-
console.error('Speech recognition error:', event.error);
|
|
3102
|
-
isRecording = false;
|
|
3103
|
-
const btn = document.getElementById('voice-btn');
|
|
3104
|
-
btn.style.background = 'transparent';
|
|
3105
|
-
btn.style.color = '';
|
|
3106
|
-
btn.style.borderColor = 'var(--border)';
|
|
3107
|
-
btn.textContent = '🎤';
|
|
3108
|
-
};
|
|
3109
|
-
|
|
3110
|
-
voiceRecognition.start();
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
function stopVoiceInput() {
|
|
3114
|
-
if (voiceRecognition) {
|
|
3115
|
-
voiceRecognition.stop();
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
function speakText(text) {
|
|
3120
|
-
if (!window.speechSynthesis) return;
|
|
3121
|
-
window.speechSynthesis.cancel();
|
|
3122
|
-
const utterance = new SpeechSynthesisUtterance(text);
|
|
3123
|
-
utterance.lang = navigator.language || 'en-US';
|
|
3124
|
-
utterance.rate = 1.0;
|
|
3125
|
-
window.speechSynthesis.speak(utterance);
|
|
3126
|
-
}
|
|
3127
|
-
|
|
3128
|
-
// Patch message rendering to add TTS button to assistant messages
|
|
3129
|
-
const _origAppendMsg = typeof appendMessage === 'function' ? appendMessage : null;
|
|
3130
|
-
if (typeof window._patchedMsgRender === 'undefined') {
|
|
3131
|
-
window._patchedMsgRender = true;
|
|
3132
|
-
const observer = new MutationObserver((mutations) => {
|
|
3133
|
-
for (const m of mutations) {
|
|
3134
|
-
for (const node of m.addedNodes) {
|
|
3135
|
-
if (node.nodeType === 1 && node.classList?.contains('msg') && node.classList?.contains('assistant')) {
|
|
3136
|
-
const bubble = node.querySelector('.msg-bubble');
|
|
3137
|
-
if (bubble && !node.querySelector('.tts-btn')) {
|
|
3138
|
-
const btn = document.createElement('button');
|
|
3139
|
-
btn.className = 'tts-btn';
|
|
3140
|
-
btn.textContent = '🔊';
|
|
3141
|
-
btn.title = 'Read aloud';
|
|
3142
|
-
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);';
|
|
3143
|
-
btn.onclick = () => speakText(bubble.textContent);
|
|
3144
|
-
node.appendChild(btn);
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
});
|
|
3150
|
-
const chatMsgs = document.getElementById('chat-messages');
|
|
3151
|
-
if (chatMsgs) observer.observe(chatMsgs, { childList: true });
|
|
3152
|
-
}
|
|
3153
|
-
|
|
3154
|
-
// =============================================
|
|
3155
|
-
// === Image Generation Config ===
|
|
3156
|
-
// =============================================
|
|
3157
|
-
async function saveImageGenConfig() {
|
|
3158
|
-
const data = {
|
|
3159
|
-
openaiApiKey: document.getElementById('ig-openai-key')?.value?.trim() || '',
|
|
3160
|
-
sdApiUrl: document.getElementById('ig-sd-url')?.value?.trim() || '',
|
|
3161
|
-
replicateApiKey: document.getElementById('ig-replicate-key')?.value?.trim() || '',
|
|
3162
|
-
};
|
|
3163
|
-
try {
|
|
3164
|
-
await fetch('/api/image-gen/config', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
3165
|
-
const st = document.getElementById('ig-status');
|
|
3166
|
-
if (st) { st.textContent = '✅ Configuration saved'; st.style.color = 'var(--green)'; }
|
|
3167
|
-
} catch(e) {
|
|
3168
|
-
const st = document.getElementById('ig-status');
|
|
3169
|
-
if (st) { st.textContent = '❌ Failed: ' + e.message; st.style.color = 'var(--red)'; }
|
|
3170
|
-
}
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
1091
|
</script>
|
|
3174
1092
|
</body>
|
|
3175
1093
|
</html>
|