@hina114514/chaite 1.9.6 → 1.9.8

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.
@@ -3,105 +3,322 @@
3
3
  <head>
4
4
  <meta charset="UTF-8"/>
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
6
- <title>Chaite Dashboard</title>
6
+ <title>Chaite</title>
7
7
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
8
8
  <style>
9
- *{margin:0;padding:0;box-sizing:border-box}
9
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
10
+
10
11
  :root{
11
- --bg:#0c0e14;--surface:#141720;--surface2:#1a1e2a;--surface3:#222738;
12
- --border:#2a2f42;--text:#e0e4ef;--text2:#8b92a8;--accent:#6c8cff;--accent2:#4a6aef;
13
- --green:#4ade80;--red:#f87171;--yellow:#facc15;--purple:#a78bfa;
14
- --radius:10px;--shadow:0 2px 12px rgba(0,0,0,.4);
12
+ --c-bg:#090c15;
13
+ --c-surface:rgba(18,22,40,0.7);
14
+ --c-surface2:rgba(26,30,52,0.8);
15
+ --c-surface3:rgba(38,42,66,0.6);
16
+ --c-border:rgba(255,255,255,0.06);
17
+ --c-border2:rgba(255,255,255,0.1);
18
+ --c-text:#e4e7f0;
19
+ --c-dim:#7e83a0;
20
+ --c-faint:#52577a;
21
+ --c-accent:#5b8def;
22
+ --c-accent2:rgba(91,141,239,0.15);
23
+ --c-green:#4ade80;
24
+ --c-green2:rgba(74,222,128,0.12);
25
+ --c-red:#f87171;
26
+ --c-red2:rgba(248,113,113,0.12);
27
+ --c-yellow:#facc15;
28
+ --c-purple:#a78bfa;
29
+ --r:12px;
30
+ --r2:20px;
31
+ --ease: cubic-bezier(.16,1,.3,1);
32
+ --ease2: cubic-bezier(.7,0,.3,1);
15
33
  }
16
- body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;min-height:100vh}
17
- a{color:var(--accent);text-decoration:none}
18
- a:hover{text-decoration:underline}
19
- .app{max-width:1280px;margin:0 auto;padding:20px}
20
34
 
21
- /* Login */
22
- .login-wrap{display:flex;align-items:center;justify-content:center;min-height:100vh}
23
- .login-box{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:48px 40px;width:380px;text-align:center;box-shadow:var(--shadow)}
24
- .login-box h1{font-size:24px;margin-bottom:8px}
25
- .login-box p{color:var(--text2);font-size:14px;margin-bottom:32px}
26
- .login-box .spinner{display:inline-block;width:18px;height:18px;border:2px solid var(--text2);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
27
- .login-box .err{color:var(--red);font-size:13px;margin-bottom:12px;min-height:20px}
28
- @keyframes spin{to{transform:rotate(360deg)}}
35
+ body{
36
+ background:var(--c-bg);
37
+ color:var(--c-text);
38
+ font-family: 'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
39
+ font-size:14px;
40
+ line-height:1.5;
41
+ min-height:100vh;
42
+ overflow-x:hidden;
43
+ }
44
+ body::before{
45
+ content:'';position:fixed;inset:0;z-index:-1;
46
+ background:
47
+ radial-gradient(ellipse 80% 60% at 0% 0%,rgba(91,141,239,.06),transparent),
48
+ radial-gradient(ellipse 60% 50% at 100% 100%,rgba(91,141,239,.04),transparent);
49
+ pointer-events:none;
50
+ }
29
51
 
30
- /* Header */
31
- .header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:24px}
32
- .header h1{font-size:20px;display:flex;align-items:center;gap:8px}
33
- .header h1 span{color:var(--accent)}
34
- .header .meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
35
- .badge{background:var(--surface2);border:1px solid var(--border);border-radius:20px;padding:3px 12px;font-size:12px;color:var(--text2)}
36
- .badge.ok{border-color:var(--green);color:var(--green)}
37
- .badge.err{border-color:var(--red);color:var(--red)}
52
+ #app{min-height:100vh}
53
+
54
+ /* Login */
55
+ .login-wrap{
56
+ display:flex;align-items:center;justify-content:center;min-height:100vh;
57
+ padding:20px;
58
+ }
59
+ .login-card{
60
+ width:100%;max-width:400px;
61
+ background:var(--c-surface);
62
+ border:1px solid var(--c-border);
63
+ border-radius:var(--r2);
64
+ padding:48px 36px;
65
+ backdrop-filter:blur(20px);
66
+ -webkit-backdrop-filter:blur(20px);
67
+ text-align:center;
68
+ animation: fadeUp .6s var(--ease) both;
69
+ }
70
+ .login-card h1{
71
+ font-size:28px;font-weight:700;letter-spacing:-.5px;margin-bottom:4px;
72
+ background:linear-gradient(135deg,#5b8def,#8b5cf6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;
73
+ }
74
+ .login-card .sub{color:var(--c-dim);font-size:13px;margin-bottom:28px}
75
+ .login-card input{
76
+ width:100%;padding:11px 14px;border-radius:10px;border:1px solid var(--c-border);
77
+ background:rgba(255,255,255,.03);color:var(--c-text);font-size:14px;outline:none;
78
+ transition:border-color .2s;
79
+ }
80
+ .login-card input:focus{border-color:var(--c-accent)}
81
+ .login-card .err{color:var(--c-red);font-size:13px;margin:8px 0;min-height:20px}
82
+ .login-card .hint{font-size:11px;color:var(--c-faint);margin-top:14px}
83
+ .login-card .hint code{background:var(--c-surface2);padding:2px 6px;border-radius:4px;font-size:11px}
38
84
 
39
85
  /* Buttons */
40
- .btn{background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:6px 14px;font-size:13px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .15s}
41
- .btn:hover{background:var(--surface3);border-color:var(--accent)}
42
- .btn:disabled{opacity:.5;cursor:not-allowed}
43
- .btn.primary{background:var(--accent2);border-color:var(--accent);color:#fff}
44
- .btn.primary:hover{background:var(--accent)}
45
- .btn.danger{border-color:var(--red);color:var(--red)}
46
- .btn.danger:hover{background:rgba(248,113,113,.15)}
47
- .btn.sm{padding:3px 10px;font-size:12px}
86
+ .btn{
87
+ display:inline-flex;align-items:center;justify-content:center;gap:6px;
88
+ padding:7px 14px;border-radius:8px;font-size:12.5px;font-weight:500;
89
+ border:1px solid var(--c-border);color:var(--c-text);
90
+ background:var(--c-surface2);cursor:pointer;
91
+ transition:all .2s var(--ease);
92
+ white-space:nowrap;
93
+ }
94
+ .btn:hover{background:var(--c-surface3);border-color:var(--c-border2);transform:translateY(-1px)}
95
+ .btn:active{transform:translateY(0) scale(.98)}
96
+ .btn:disabled{opacity:.4;pointer-events:none}
97
+ .btn.pri{
98
+ background:var(--c-accent);border-color:var(--c-accent);color:#fff;
99
+ box-shadow:0 2px 12px rgba(91,141,239,.25);
100
+ }
101
+ .btn.pri:hover{box-shadow:0 4px 20px rgba(91,141,239,.4);background:#6b9df5}
102
+ .btn.danger{color:var(--c-red);border-color:rgba(248,113,113,.25)}
103
+ .btn.danger:hover{background:var(--c-red2)}
104
+ .btn.sm{padding:4px 10px;font-size:11px;border-radius:6px}
105
+ .btn.ghost{background:transparent;border-color:transparent;color:var(--c-dim)}
106
+ .btn.ghost:hover{color:var(--c-text);background:rgba(255,255,255,.04)}
107
+ .btn .spin{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite}
108
+
109
+ /* Layout */
110
+ .layout{display:flex;min-height:100vh}
111
+ .sidebar{
112
+ width:220px;min-width:220px;
113
+ background:var(--c-surface);border-right:1px solid var(--c-border);
114
+ display:flex;flex-direction:column;
115
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
116
+ transition:transform .35s var(--ease);
117
+ z-index:50;
118
+ }
119
+ .sidebar-logo{padding:24px 20px 20px}
120
+ .sidebar-logo h2{
121
+ font-size:22px;font-weight:700;letter-spacing:-.5px;
122
+ background:linear-gradient(135deg,#5b8def,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;
123
+ }
124
+ .sidebar-logo .ver{font-size:11px;color:var(--c-faint);margin-top:2px}
125
+
126
+ .sidebar-nav{flex:1;padding:8px 12px}
127
+ .nav-item{
128
+ display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:8px;
129
+ font-size:13px;color:var(--c-dim);cursor:pointer;
130
+ transition:all .15s var(--ease);margin-bottom:2px;
131
+ position:relative;
132
+ }
133
+ .nav-item:hover{color:var(--c-text);background:var(--c-surface2)}
134
+ .nav-item.active{color:var(--c-accent);background:var(--c-accent2)}
135
+ .nav-item .icon{width:18px;text-align:center;font-size:15px;line-height:1}
136
+ .nav-item .badge-mini{
137
+ margin-left:auto;font-size:10px;padding:1px 6px;border-radius:8px;
138
+ background:var(--c-accent2);color:var(--c-accent);
139
+ }
140
+
141
+ .sidebar-footer{padding:16px 20px;border-top:1px solid var(--c-border);font-size:11px;color:var(--c-faint)}
142
+ .sidebar-footer .dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:6px}
143
+ .sidebar-footer .dot.ok{background:var(--c-green);box-shadow:0 0 6px var(--c-green)}
144
+
145
+ .main{flex:1;overflow-x:hidden;display:flex;flex-direction:column}
146
+ .topbar{
147
+ padding:16px 28px;display:flex;align-items:center;justify-content:space-between;
148
+ border-bottom:1px solid var(--c-border);gap:12px;
149
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
150
+ position:sticky;top:0;z-index:40;background:rgba(9,12,21,.8);
151
+ }
152
+ .topbar .t{font-size:15px;font-weight:600;color:var(--c-text)}
153
+ .topbar .meta{display:flex;align-items:center;gap:10px}
154
+ .topbar .refresh-text{font-size:11px;color:var(--c-faint)}
155
+
156
+ .content{padding:24px 28px 40px;flex:1}
157
+
158
+ /* Toast */
159
+ .toast{
160
+ position:fixed;top:20px;right:20px;z-index:200;
161
+ padding:10px 18px;border-radius:10px;font-size:13px;
162
+ animation:slideRight .35s var(--ease) both;
163
+ backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
164
+ }
165
+ .toast.ok{background:rgba(74,222,128,.9);color:#000}
166
+ .toast.err{background:rgba(248,113,113,.9);color:#fff}
167
+
168
+ /* Mobile menu toggle */
169
+ .menu-toggle{
170
+ display:none;width:36px;height:36px;align-items:center;justify-content:center;
171
+ border:none;background:none;color:var(--c-text);cursor:pointer;font-size:20px;
172
+ }
173
+ .sidebar-overlay{display:none;position:fixed;inset:0;z-index:45;background:rgba(0,0,0,.5)}
48
174
 
49
175
  /* Cards */
50
- .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:16px;box-shadow:var(--shadow)}
51
- .card h3{font-size:15px;margin-bottom:14px;display:flex;align-items:center;gap:8px}
52
- .card h3 .dot{width:8px;height:8px;border-radius:50%}
176
+ .card{
177
+ background:var(--c-surface);border:1px solid var(--c-border);
178
+ border-radius:var(--r2);padding:22px;margin-bottom:16px;
179
+ backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
180
+ animation: fadeUp .45s var(--ease) both;
181
+ }
182
+ .card:nth-child(1){animation-delay:0s}
183
+ .card:nth-child(2){animation-delay:.06s}
184
+ .card:nth-child(3){animation-delay:.12s}
185
+ .card:nth-child(4){animation-delay:.18s}
186
+ .card:nth-child(5){animation-delay:.24s}
187
+ .card-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
188
+ .card-hd h3{font-size:14.5px;font-weight:600;display:flex;align-items:center;gap:8px}
189
+ .card-hd h3 .dot{width:7px;height:7px;border-radius:50%}
53
190
 
54
- /* Stat cards */
55
- .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px}
56
- .stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:center}
57
- .stat .val{font-size:28px;font-weight:700;color:var(--accent)}
58
- .stat .label{font-size:12px;color:var(--text2);margin-top:4px}
191
+ /* Stat pills */
192
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:20px}
193
+ .stat{
194
+ background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r2);
195
+ padding:18px 20px;text-align:center;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
196
+ transition:transform .2s var(--ease),border-color .2s var(--ease);
197
+ animation: fadeUp .4s var(--ease) both;
198
+ }
199
+ .stat:hover{transform:translateY(-3px);border-color:var(--c-accent)}
200
+ .stat:nth-child(1){animation-delay:0s}
201
+ .stat:nth-child(2){animation-delay:.05s}
202
+ .stat:nth-child(3){animation-delay:.1s}
203
+ .stat:nth-child(4){animation-delay:.15s}
204
+ .stat:nth-child(5){animation-delay:.2s}
205
+ .stat:nth-child(6){animation-delay:.25s}
206
+ .stat .num{font-size:26px;font-weight:700;color:var(--c-accent)}
207
+ .stat .lbl{font-size:11px;color:var(--c-dim);margin-top:4px;text-transform:uppercase;letter-spacing:.5px}
59
208
 
60
209
  /* Table */
210
+ .tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
61
211
  .tbl{width:100%;border-collapse:collapse;font-size:13px}
62
- .tbl th{text-align:left;padding:10px 12px;color:var(--text2);font-weight:600;border-bottom:1px solid var(--border);font-size:12px;text-transform:uppercase;letter-spacing:.5px}
63
- .tbl td{padding:10px 12px;border-bottom:1px solid var(--border)}
64
- .tbl tr:hover td{background:var(--surface2)}
65
- .pill{display:inline-block;padding:2px 10px;border-radius:12px;font-size:11px;font-weight:600}
66
- .pill.on{background:rgba(74,222,128,.15);color:var(--green)}
67
- .pill.off{background:rgba(248,113,113,.12);color:var(--red)}
68
- .pill.model{background:rgba(108,140,255,.12);color:var(--accent);margin:2px 3px 2px 0}
69
-
70
- /* Tabs */
71
- .tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:1px solid var(--border)}
72
- .tab{padding:10px 20px;font-size:13px;cursor:pointer;color:var(--text2);border-bottom:2px solid transparent;transition:all .15s}
73
- .tab:hover{color:var(--text)}
74
- .tab.active{color:var(--accent);border-bottom-color:var(--accent)}
212
+ .tbl th{text-align:left;padding:10px 12px;color:var(--c-faint);font-weight:500;border-bottom:1px solid var(--c-border);font-size:11px;text-transform:uppercase;letter-spacing:.5px}
213
+ .tbl td{padding:10px 12px;border-bottom:1px solid var(--c-border)}
214
+ .tbl tbody tr{transition:background .15s}
215
+ .tbl tbody tr:hover{background:var(--c-surface2)}
216
+
217
+ /* Pills */
218
+ .pill{display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:500}
219
+ .pill.on{background:var(--c-green2);color:var(--c-green)}
220
+ .pill.off{background:var(--c-red2);color:var(--c-red)}
221
+ .pill.model{background:var(--c-accent2);color:var(--c-accent);margin:2px 3px 2px 0}
222
+ .pill.tag{background:rgba(167,139,250,.12);color:var(--c-purple);margin:2px 3px 2px 0}
223
+ .pill.warn{background:rgba(250,204,21,.12);color:var(--c-yellow)}
224
+
225
+ /* Config accordion */
226
+ .cfg-sec{margin-bottom:10px;border:1px solid var(--c-border);border-radius:var(--r);overflow:hidden;transition:border-color .2s}
227
+ .cfg-sec:hover{border-color:var(--c-border2)}
228
+ .cfg-head{
229
+ display:flex;align-items:center;justify-content:space-between;padding:12px 16px;
230
+ background:var(--c-surface2);cursor:pointer;font-size:13px;font-weight:600;
231
+ transition:background .15s;user-select:none;
232
+ }
233
+ .cfg-head:hover{background:var(--c-surface3)}
234
+ .cfg-head .arr{transition:transform .25s var(--ease);font-size:10px;color:var(--c-faint)}
235
+ .cfg-head .arr.open{transform:rotate(90deg)}
236
+ .cfg-body{
237
+ padding:14px 16px;display:grid;grid-template-columns:repeat(2,1fr);gap:8px;
238
+ animation: slideDown .25s var(--ease) both;
239
+ }
240
+ .cfg-fld{padding:6px 10px;background:rgba(255,255,255,.02);border-radius:6px}
241
+ .cfg-fld .k{font-size:10px;color:var(--c-faint);display:block;margin-bottom:3px;letter-spacing:.3px}
242
+ .cfg-fld .v{word-break:break-all;font-size:12.5px}
243
+ .cfg-fld .v code{color:var(--c-green)}
244
+ .cfg-fld .v .num{color:var(--c-accent)}
245
+ .cfg-json-btn{margin-top:16px}
75
246
 
76
247
  /* Modal */
77
- .modal-mask{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:100}
78
- .modal{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:28px;width:520px;max-height:85vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.5)}
79
- .modal h3{margin-bottom:20px;font-size:17px}
80
- .form-row{margin-bottom:14px}
81
- .form-row label{display:block;font-size:12px;color:var(--text2);margin-bottom:5px}
82
- .form-row input,.form-row select,.form-row textarea{width:100%;padding:8px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:inherit}
83
- .form-row textarea{min-height:80px;resize:vertical}
84
- .form-row input:focus,.form-row select:focus,.form-row textarea:focus{outline:none;border-color:var(--accent)}
248
+ .modal-mask{
249
+ position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;
250
+ background:rgba(0,0,0,.55);padding:20px;
251
+ animation: fadeIn .2s both;
252
+ backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);
253
+ }
254
+ .modal{
255
+ background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r2);
256
+ padding:28px;width:100%;max-width:520px;max-height:85vh;overflow-y:auto;
257
+ backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);
258
+ animation: scaleIn .3s var(--ease) both;
259
+ }
260
+ .modal h3{font-size:17px;font-weight:600;margin-bottom:20px}
261
+ .f-row{margin-bottom:14px}
262
+ .f-row label{display:block;font-size:11px;color:var(--c-dim);margin-bottom:5px;letter-spacing:.3px}
263
+ .f-row input,.f-row select,.f-row textarea{
264
+ width:100%;padding:9px 12px;background:rgba(255,255,255,.03);border:1px solid var(--c-border);
265
+ border-radius:8px;color:var(--c-text);font-size:13px;font-family:inherit;
266
+ transition:border-color .2s;
267
+ }
268
+ .f-row input:focus,.f-row select:focus,.f-row textarea:focus{outline:none;border-color:var(--c-accent)}
269
+ .f-row textarea{min-height:80px;resize:vertical}
270
+ .f-row textarea.mono{font-family:'SF Mono',Fira Code,monospace;font-size:12px}
85
271
  .modal-btns{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}
86
272
 
87
- /* Toast */
88
- .toast{position:fixed;top:20px;right:20px;padding:10px 20px;border-radius:8px;font-size:13px;z-index:200;animation:slideIn .3s}
89
- .toast.ok{background:var(--green);color:#000}
90
- .toast.err{background:var(--red);color:#fff}
91
- @keyframes slideIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
273
+ /* Page transition */
274
+ .page-enter{animation: fadeUp .35s var(--ease) both}
275
+ @keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
276
+ @keyframes fadeIn{from{opacity:0}to{opacity:1}}
277
+ @keyframes scaleIn{from{opacity:0;transform:scale(.96) translateY(8px)}to{opacity:1;transform:none}}
278
+ @keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:none}}
279
+ @keyframes slideDown{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
280
+ @keyframes spin{to{transform:rotate(360deg)}}
281
+ @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
282
+
283
+ .skeleton{
284
+ background:linear-gradient(90deg,var(--c-surface2) 25%,var(--c-surface3) 50%,var(--c-surface2) 75%);
285
+ background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:6px;
286
+ }
287
+ .skel-row{height:14px;margin-bottom:8px;width:70%}
288
+ .skel-row.short{width:40%}
289
+
290
+ .mono{font-family:'SF Mono',Fira Code,monospace}
291
+ code{font-family:'SF Mono',Fira Code,monospace;font-size:12px}
92
292
 
93
293
  /* Responsive */
94
294
  @media(max-width:768px){
95
- .stats{grid-template-columns:repeat(2,1fr)}
96
- .modal{width:95%;margin:10px}
97
- .header{flex-direction:column;align-items:flex-start}
295
+ .sidebar{
296
+ position:fixed;left:0;top:0;bottom:0;transform:translateX(-100%);
297
+ z-index:50;width:260px;
298
+ }
299
+ .sidebar.open{transform:translateX(0)}
300
+ .sidebar-overlay.show{display:block}
301
+ .menu-toggle{display:flex}
302
+ .topbar{padding:12px 16px}
303
+ .content{padding:16px}
304
+ .stats{grid-template-columns:repeat(2,1fr);gap:8px}
305
+ .stat{padding:14px 12px}
306
+ .stat .num{font-size:22px}
307
+ .card{padding:16px}
308
+ .cfg-body{grid-template-columns:1fr}
309
+ .modal{max-width:95%;padding:20px}
310
+ }
311
+ @media(max-width:480px){
312
+ .stats{grid-template-columns:1fr 1fr}
313
+ .topbar .meta{gap:6px}
314
+ .topbar .refresh-text{display:none}
98
315
  }
99
316
  </style>
100
317
  </head>
101
318
  <body>
102
319
  <div id="app"></div>
103
320
  <script>
104
- const{createApp,ref,reactive,computed,onMounted,onUnmounted,watch}=Vue
321
+ const{createApp,ref,reactive,computed,onMounted,onUnmounted}=Vue
105
322
 
106
323
  const API=(path,opt={})=>{
107
324
  const t=localStorage.getItem('chaite_jwt')
@@ -116,11 +333,18 @@ createApp({
116
333
  // Auth
117
334
  const jwt=ref(localStorage.getItem('chaite_jwt')||'')
118
335
  const loggedIn=ref(!!jwt.value)
336
+ const loginInput=ref('')
119
337
  const loginLoading=ref(false)
120
338
  const loginError=ref('')
121
339
 
122
- // State
123
- const tab=ref('overview')
340
+ // UI state
341
+ const page=ref('dashboard')
342
+ const sidebarOpen=ref(false)
343
+ const loading=ref(true)
344
+ const refreshTime=ref('')
345
+ let timer=null
346
+
347
+ // Data
124
348
  const health=ref(null)
125
349
  const stats=ref(null)
126
350
  const channels=ref([])
@@ -129,15 +353,19 @@ createApp({
129
353
  const processors=ref([])
130
354
  const triggers=ref([])
131
355
  const toolGroups=ref([])
132
- const loading=ref(true)
133
- const refreshTime=ref('')
134
- let timer=null
356
+ const config=ref({})
357
+ const conversations=ref([])
358
+ const convDetail=ref(null)
135
359
 
136
360
  // Modal
137
- const modal=ref(null) // 'channel'|'preset'|'tool'|'processor'|'trigger'|'toolGroup'|null
138
- const editing=ref(null) // object being edited
361
+ const modal=ref(null)
362
+ const editing=ref(null)
139
363
  const form=ref({})
140
364
 
365
+ // Config edit
366
+ const editingConfig=ref(false)
367
+ const configForm=ref('{}')
368
+
141
369
  // Toast
142
370
  const toastMsg=ref('')
143
371
  const toastType=ref('ok')
@@ -150,7 +378,7 @@ createApp({
150
378
  try{
151
379
  const url=new URL(location.href)
152
380
  const urlToken=url.searchParams.get('token')
153
- const token=urlToken||prompt('Enter access token:')
381
+ const token=urlToken||loginInput.value
154
382
  if(!token){loginLoading.value=false;return}
155
383
  const d=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token})}).then(r=>r.json())
156
384
  if(d.data?.token){
@@ -161,14 +389,13 @@ createApp({
161
389
  }catch(e){loginError.value='Network error'}
162
390
  finally{loginLoading.value=false}
163
391
  }
164
-
165
392
  function logout(){jwt.value='';localStorage.removeItem('chaite_jwt');loggedIn.value=false;clearInterval(timer)}
166
393
 
167
- // Data fetching
394
+ // Data fetch
168
395
  async function refreshAll(){
169
396
  loading.value=true
170
397
  try{
171
- const[h,s,c,t,p,pr,tr,tg]=await Promise.all([
398
+ const[h,s,c,t,p,pr,tr,tg,cfg,cv]=await Promise.all([
172
399
  API('/api/system/health').catch(()=>({})),
173
400
  API('/api/system/stats').catch(()=>({})),
174
401
  API('/api/channels/list').catch(()=>({})),
@@ -177,6 +404,8 @@ createApp({
177
404
  API('/api/processors/list').catch(()=>({})),
178
405
  API('/api/triggers/list').catch(()=>({})),
179
406
  API('/api/toolGroups/list').catch(()=>({})),
407
+ API('/api/config').catch(()=>({})),
408
+ API('/api/state/conversations/list').catch(()=>({})),
180
409
  ])
181
410
  health.value=h.data||null
182
411
  stats.value=s.data||null
@@ -186,78 +415,183 @@ createApp({
186
415
  processors.value=pr.data||[]
187
416
  triggers.value=tr.data||[]
188
417
  toolGroups.value=tg.data||[]
418
+ config.value=cfg.data||{}
419
+ conversations.value=cv.data||[]
189
420
  refreshTime.value=new Date().toLocaleTimeString()
190
421
  }catch(e){console.error(e)}
191
422
  finally{loading.value=false}
192
423
  }
193
424
 
194
- // CRUD helpers
425
+ function navigate(p){page.value=p;sidebarOpen.value=false}
426
+
427
+ // Helpers
428
+ function uptimeStr(s){
429
+ if(!s)return '-'
430
+ const h=Math.floor(s/3600),m=Math.floor(s%3600/60),sec=s%60
431
+ return h>0?`${h}h ${m}m ${sec}s`:`${m}m ${sec}s`
432
+ }
433
+ function fmtNum(n){if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return String(n||0)}
434
+ function fLen(s,n){return s&&s.length>n?s.slice(0,n)+'...':s||''}
435
+
436
+ // Channel test
195
437
  async function testChannel(id){
196
438
  const d=await API('/api/system/test-channel',{method:'POST',body:JSON.stringify({channelId:id})})
197
- if(d.data?.status==='ok')toast(`✅ ${d.data.channelName}${d.data.latency}ms`)
198
- else toast(`❌ ${d.data?.error||d.message}`,'err')
439
+ if(d.data?.status==='ok')toast(''+d.data.channelName+''+d.data.latency+'ms')
440
+ else toast(''+(d.data?.error||d.message),'err')
199
441
  }
200
442
 
443
+ // CRUD
201
444
  function openModal(type,item=null){
202
- modal.value=type
203
- editing.value=item
204
- if(item)form.value=JSON.parse(JSON.stringify(item))
445
+ modal.value=type;editing.value=item
446
+ if(item){
447
+ form.value=JSON.parse(JSON.stringify(item))
448
+ // 为 channel 初始化 JSON 字段
449
+ if(type==='channel'){
450
+ form.value._models = (form.value.models||[]).join(', ')
451
+ form.value._responseModalities = JSON.stringify(form.value.sendMessageOption?.responseModalities||[])
452
+ form.value._safetySettings = JSON.stringify(form.value.sendMessageOption?.safetySettings||[],null,2)
453
+ // 确保 sendMessageOption 存在
454
+ if(!form.value.sendMessageOption){
455
+ form.value.sendMessageOption = {
456
+ model:'',
457
+ temperature:0.8,
458
+ maxToken:4096,
459
+ systemOverride:'',
460
+ enableReasoning:false,
461
+ reasoningEffort:'high',
462
+ reasoningBudgetTokens:0,
463
+ responseModalities:[],
464
+ safetySettings:[],
465
+ toolCallLimit:{maxConsecutiveCalls:8,maxConsecutiveIdenticalCalls:2}
466
+ }
467
+ }
468
+ }
469
+ }
205
470
  else{
206
- const defaults={
207
- channel:{name:'',adapterType:'openai',type:'openai',models:[''],options:{baseUrl:'',apiKey:''},status:'enabled',weight:1,priority:0},
208
- preset:{name:'',prefix:'',sendMessageOption:{model:'',temperature:0.8,maxToken:4096,systemOverride:''}},
471
+ form.value={
472
+ channel:{name:'',adapterType:'openai',type:'openai',models:[''],options:{baseUrl:'',apiKey:''},status:'enabled',weight:1,priority:0,sendMessageOption:{model:'',temperature:0.8,maxToken:4096,systemOverride:'',enableReasoning:false,reasoningEffort:'high',reasoningBudgetTokens:0,responseModalities:[],safetySettings:[],toolCallLimit:{maxConsecutiveCalls:8,maxConsecutiveIdenticalCalls:2}}},
473
+ preset:{name:'',prefix:'',sendMessageOption:{model:'',temperature:0.8,maxToken:4096,systemOverride:'',enableReasoning:false,reasoningEffort:'high',disableHistoryRead:false,disableHistorySave:false}},
209
474
  tool:{name:'',description:'',code:''},
210
475
  processor:{name:'',type:'pre',description:'',code:''},
211
476
  trigger:{name:'',description:'',code:''},
212
477
  toolGroup:{name:'',description:'',toolIds:[],status:'enabled',isDefault:false},
213
- }
214
- form.value=defaults[type]||{}
478
+ }[type]||{}
215
479
  }
216
480
  }
217
481
  function closeModal(){modal.value=null;editing.value=null;form.value={}}
218
482
 
219
483
  async function saveItem(){
220
- const type=modal.value
221
484
  const apiMap={channel:'/api/channels',preset:'/api/preset',tool:'/api/tools',processor:'/api/processors',trigger:'/api/triggers',toolGroup:'/api/toolGroups'}
222
485
  try{
486
+ const type=modal.value
223
487
  const id=editing.value?.id
224
488
  const url=apiMap[type]+(id?'/'+id:'')
225
- const method=id?'PUT':'POST'
226
- const d=await API(url,{method,body:JSON.stringify(form.value)})
489
+
490
+ // 处理 channel 的 JSON 字段
491
+ if(type==='channel'){
492
+ const formData = JSON.parse(JSON.stringify(form.value))
493
+
494
+ // 处理 models 字段
495
+ if(formData._models){
496
+ formData.models = formData._models.split(',').map(m=>m.trim()).filter(m=>m)
497
+ delete formData._models
498
+ }
499
+
500
+ // 处理 responseModalities
501
+ if(formData._responseModalities){
502
+ try{
503
+ formData.sendMessageOption.responseModalities = JSON.parse(formData._responseModalities)
504
+ }catch(e){
505
+ // 如果解析失败,保持原样
506
+ }
507
+ delete formData._responseModalities
508
+ }
509
+
510
+ // 处理 safetySettings
511
+ if(formData._safetySettings){
512
+ try{
513
+ formData.sendMessageOption.safetySettings = JSON.parse(formData._safetySettings)
514
+ }catch(e){
515
+ // 如果解析失败,保持原样
516
+ }
517
+ delete formData._safetySettings
518
+ }
519
+
520
+ form.value = formData
521
+ }
522
+
523
+ const d=await API(url,{method:id?'PUT':'POST',body:JSON.stringify(form.value)})
227
524
  if(d.data!==undefined){toast('Saved');closeModal();refreshAll()}
228
525
  else toast(d.message||'Error','err')
229
526
  }catch(e){toast(e.message,'err')}
230
527
  }
231
528
 
232
529
  async function deleteItem(type,id){
233
- if(!confirm('Delete this item?'))return
530
+ if(!confirm('Delete?'))return
234
531
  const apiMap={channel:'/api/channels',preset:'/api/preset',tool:'/api/tools',processor:'/api/processors',trigger:'/api/triggers',toolGroup:'/api/toolGroups'}
532
+ try{await API(apiMap[type]+'/'+id,{method:'DELETE'});toast('Deleted');refreshAll()}
533
+ catch(e){toast(e.message,'err')}
534
+ }
535
+
536
+ // Config
537
+ const expandedCfg=ref({})
538
+ const SKIP_CONFIG_KEYS=new Set(['version','_saveOrigin','authKey','cloudBaseUrl','cloudApiKey'])
539
+
540
+ const cfgSections=computed(()=>{
541
+ const secs=[]
542
+ const order=['basic','bym','llm','management','chaite','memory','update']
543
+ for(const key of order){
544
+ const v=config.value[key]
545
+ if(v!==undefined) secs.push({key,label:key.charAt(0).toUpperCase()+key.slice(1),fields:typeof v==='object'?v:{_value:v},isObj:typeof v==='object'&&!Array.isArray(v)})
546
+ }
547
+ // Remaining keys not in order
548
+ for(const[k,v]of Object.entries(config.value)){
549
+ if(order.includes(k)||SKIP_CONFIG_KEYS.has(k))continue
550
+ secs.push({key:k,label:k,fields:typeof v==='object'?v:{_value:v},isObj:typeof v==='object'&&!Array.isArray(v)})
551
+ }
552
+ return secs
553
+ })
554
+ function toggleCfg(k){expandedCfg.value[k]=!expandedCfg.value[k]}
555
+ function editConfig(){configForm.value=JSON.stringify(config.value,null,2);editingConfig.value=true}
556
+ async function saveConfig(){
235
557
  try{
236
- await API(apiMap[type]+'/'+id,{method:'DELETE'})
237
- toast('Deleted');refreshAll()
238
- }catch(e){toast(e.message,'err')}
558
+ const obj=JSON.parse(configForm.value)
559
+ const d=await API('/api/config',{method:'POST',body:JSON.stringify(obj)})
560
+ if(d.data!==undefined){toast('Config saved');editingConfig.value=false;refreshAll()}
561
+ else toast(d.message||'Error','err')
562
+ }catch(e){toast('Invalid JSON: '+e.message,'err')}
239
563
  }
240
564
 
241
- function uptimeStr(s){
242
- if(!s)return '-'
243
- const h=Math.floor(s/3600),m=Math.floor(s%3600/60),sec=s%60
244
- return h>0?`${h}h ${m}m ${sec}s`:`${m}m ${sec}s`
565
+ // Conversations
566
+ async function viewConv(uid){
567
+ try{const d=await API('/api/state/'+encodeURIComponent(uid));convDetail.value=d.data||null}
568
+ catch(e){toast(e.message,'err')}
245
569
  }
246
- function fmtNum(n){if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return String(n||0)}
570
+ async function clearConv(uid){
571
+ if(!confirm('Clear state for '+uid+'?'))return
572
+ try{await API('/api/state/'+encodeURIComponent(uid),{method:'DELETE'});toast('Cleared');refreshAll()}
573
+ catch(e){toast(e.message,'err')}
574
+ }
575
+
576
+ const isHealthy=computed(()=>health.value?.status==='ok')
577
+ const channelsOk=computed(()=>channels.value.filter(c=>c.status==='enabled').length)
578
+ const loggedInC=computed(()=>!!jwt.value)
247
579
 
248
580
  onMounted(()=>{
249
581
  if(loggedIn.value)refreshAll()
250
- timer=setInterval(()=>{if(logged.value)refreshAll()},30000)
582
+ timer=setInterval(()=>{if(loggedIn.value)refreshAll()},30000)
251
583
  })
252
584
  onUnmounted(()=>clearInterval(timer))
253
585
 
254
- const loggedIn=computed(()=>!!jwt.value)
255
-
256
586
  return{
257
- jwt,loggedIn,loginLoading,loginError,doLogin,logout,
258
- tab,health,stats,channels,tools,presets,processors,triggers,toolGroups,loading,refreshTime,
587
+ jwt,loggedIn,loginInput,loginLoading,loginError,doLogin,logout,
588
+ page,sidebarOpen,navigate,loading,refreshTime,isHealthy,
589
+ health,stats,channels,tools,presets,processors,triggers,toolGroups,config,conversations,convDetail,
590
+ channelsOk,
259
591
  modal,editing,form,openModal,closeModal,saveItem,deleteItem,testChannel,
260
- toastMsg,toastType,uptimeStr,fmtNum,refreshAll,
592
+ expandedCfg,cfgSections,toggleCfg,editConfig,editingConfig,configForm,saveConfig,SKIP_CONFIG_KEYS,
593
+ viewConv,clearConv,
594
+ toastMsg,toastType,uptimeStr,fmtNum,fLen,refreshAll,
261
595
  }
262
596
  },
263
597
  template:`
@@ -266,265 +600,529 @@ createApp({
266
600
 
267
601
  <!-- Login -->
268
602
  <div v-if="!loggedIn" class="login-wrap">
269
- <div class="login-box">
270
- <h1>⚡ <span style="color:var(--accent)">Chaite</span></h1>
271
- <p>Management Dashboard</p>
603
+ <div class="login-card">
604
+ <h1>Chaite</h1>
605
+ <p class="sub">Management Console</p>
272
606
  <div v-if="!loginLoading">
273
- <div style="margin-bottom:16px">
274
- <input v-model="loginInput" type="password" placeholder="Paste access token here..."
275
- @keyup.enter="doLogin" autofocus
276
- style="width:100%;padding:10px 14px;border-radius:8px;border:1px solid var(--border);background:var(--surface2);color:var(--text);font-size:14px;outline:none"/>
277
- </div>
607
+ <input v-model="loginInput" type="password" placeholder="Access token..."
608
+ @keyup.enter="doLogin" autofocus/>
278
609
  <div class="err">{{loginError}}</div>
279
- <button class="btn primary" @click="doLogin" style="width:100%;padding:10px">Login</button>
280
- <p style="margin-top:16px;font-size:12px;color:var(--text2)">Or append <code>?token=xxx</code> to URL</p>
610
+ <button class="btn pri" @click="doLogin" style="width:100%;padding:10px;margin-top:4px">Sign In</button>
611
+ <p class="hint">Or visit with <code>?token=xxx</code> in URL</p>
281
612
  </div>
282
- <div v-else><div class="spinner"></div></div>
613
+ <div v-else style="padding:20px"><div class="spin" style="display:inline-block"></div></div>
283
614
  </div>
284
615
  </div>
285
616
 
286
- <!-- Dashboard -->
287
- <div v-else class="app">
288
- <!-- Header -->
289
- <div class="header">
290
- <h1>⚡ <span>Chaite</span> Dashboard</h1>
291
- <div class="meta">
292
- <span class="badge" v-if="refreshTime">↻ {{refreshTime}}</span>
293
- <span class="badge ok" v-if="health?.status==='ok'">Healthy</span>
294
- <span class="badge err" v-else-if="health">Error</span>
295
- <button class="btn" @click="refreshAll" :disabled="loading">
296
- <span v-if="loading" style="display:inline-block;width:14px;height:14px;border:2px solid var(--text2);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite"></span>
297
- <span v-else>↻ Refresh</span>
298
- </button>
299
- <button class="btn" @click="logout">Logout</button>
617
+ <!-- Main Layout -->
618
+ <div v-else class="layout">
619
+
620
+ <!-- Sidebar overlay (mobile) -->
621
+ <div :class="['sidebar-overlay',sidebarOpen&&'show']" @click="sidebarOpen=false"></div>
622
+
623
+ <!-- Sidebar -->
624
+ <aside :class="['sidebar',sidebarOpen&&'open']">
625
+ <div class="sidebar-logo">
626
+ <h2>Chaite</h2>
627
+ <div class="ver">v{{health?.version||'...'}}</div>
300
628
  </div>
301
- </div>
629
+ <nav class="sidebar-nav">
630
+ <div :class="['nav-item',page==='dashboard'&&'active']" @click="navigate('dashboard')">
631
+ <span class="icon">◈</span>Overview
632
+ <span class="badge-mini" v-if="health">{{health.channels?.total||0}}ch</span>
633
+ </div>
634
+ <div :class="['nav-item',page==='channels'&&'active']" @click="navigate('channels')">
635
+ <span class="icon">▤</span>Channels
636
+ <span class="badge-mini" v-if="channelsOk">{{channelsOk}}</span>
637
+ </div>
638
+ <div :class="['nav-item',page==='tools'&&'active']" @click="navigate('tools')">
639
+ <span class="icon">⚙</span>Tools
640
+ <span class="badge-mini" v-if="tools.length">{{tools.length}}</span>
641
+ </div>
642
+ <div :class="['nav-item',page==='presets'&&'active']" @click="navigate('presets')">
643
+ <span class="icon">✎</span>Presets
644
+ </div>
645
+ <div :class="['nav-item',page==='processors'&&'active']" @click="navigate('processors')">
646
+ <span class="icon">⇄</span>Processors
647
+ </div>
648
+ <div :class="['nav-item',page==='triggers'&&'active']" @click="navigate('triggers')">
649
+ <span class="icon">⚡</span>Triggers
650
+ </div>
651
+ <div :class="['nav-item',page==='groups'&&'active']" @click="navigate('groups')">
652
+ <span class="icon">☰</span>Groups
653
+ </div>
654
+ <div style="margin:8px 0;border-top:1px solid var(--c-border)"></div>
655
+ <div :class="['nav-item',page==='conversations'&&'active']" @click="navigate('conversations')">
656
+ <span class="icon">◎</span>Sessions
657
+ <span class="badge-mini" v-if="conversations.length">{{conversations.length}}</span>
658
+ </div>
659
+ <div :class="['nav-item',page==='config'&&'active']" @click="navigate('config')">
660
+ <span class="icon">⚙</span>Config
661
+ </div>
662
+ </nav>
663
+ <div class="sidebar-footer">
664
+ <span :class="['dot',isHealthy?'ok':'']"></span>
665
+ {{isHealthy?'Online':'Offline'}}
666
+ <br><span style="font-size:10px">port {{health?.system?.platform||'-'}}</span>
667
+ </div>
668
+ </aside>
302
669
 
303
- <!-- Stats -->
304
- <div class="stats" v-if="health">
305
- <div class="stat"><div class="val">{{uptimeStr(health.uptime)}}</div><div class="label">Uptime</div></div>
306
- <div class="stat"><div class="val">{{health.channels?.total||0}}</div><div class="label">Channels</div></div>
307
- <div class="stat"><div class="val">{{health.models?.count||0}}</div><div class="label">Models</div></div>
308
- <div class="stat"><div class="val">{{health.tools?.count||0}}</div><div class="label">Tools</div></div>
309
- <div class="stat"><div class="val">{{health.system?.processMemory||0}}MB</div><div class="label">Memory</div></div>
310
- <div class="stat"><div class="val">{{fmtNum(stats?.summary?.totalCalls)}}</div><div class="label">Total Calls</div></div>
311
- </div>
670
+ <!-- Main -->
671
+ <div class="main">
672
+ <!-- Top bar -->
673
+ <header class="topbar">
674
+ <div style="display:flex;align-items:center;gap:12px">
675
+ <button class="menu-toggle" @click="sidebarOpen=!sidebarOpen">☰</button>
676
+ <span class="t">
677
+ {{page==='dashboard'?'Overview':page==='conversations'?'Sessions':page==='groups'?'Groups':page.charAt(0).toUpperCase()+page.slice(1)}}
678
+ </span>
679
+ </div>
680
+ <div class="meta">
681
+ <span class="refresh-text" v-if="refreshTime">↻ {{refreshTime}}</span>
682
+ <button class="btn ghost" @click="refreshAll" :disabled="loading">
683
+ <span v-if="loading" class="spin"></span>
684
+ <span v-else>↻</span>
685
+ </button>
686
+ <button class="btn ghost" @click="logout">⏻</button>
687
+ </div>
688
+ </header>
312
689
 
313
- <!-- Tabs -->
314
- <div class="tabs">
315
- <div :class="['tab',tab==='overview'&&'active']" @click="tab='overview'">Overview</div>
316
- <div :class="['tab',tab==='channels'&&'active']" @click="tab='channels'">Channels</div>
317
- <div :class="['tab',tab==='tools'&&'active']" @click="tab='tools'">Tools</div>
318
- <div :class="['tab',tab==='presets'&&'active']" @click="tab='presets'">Presets</div>
319
- <div :class="['tab',tab==='processors'&&'active']" @click="tab='processors'">Processors</div>
320
- <div :class="['tab',tab==='triggers'&&'active']" @click="tab='triggers'">Triggers</div>
321
- <div :class="['tab',tab==='groups'&&'active']" @click="tab='groups'">Groups</div>
322
- </div>
690
+ <div class="content" :key="page">
691
+
692
+ <!-- ========== DASHBOARD ========== -->
693
+ <div v-if="page==='dashboard'" class="page-enter">
694
+ <div class="stats" v-if="health">
695
+ <div class="stat"><div class="num">{{uptimeStr(health.uptime)}}</div><div class="lbl">Uptime</div></div>
696
+ <div class="stat"><div class="num">{{health.channels?.total||0}}</div><div class="lbl">Channels</div></div>
697
+ <div class="stat"><div class="num">{{health.models?.count||0}}</div><div class="lbl">Models</div></div>
698
+ <div class="stat"><div class="num">{{health.tools?.count||0}}</div><div class="lbl">Tools</div></div>
699
+ <div class="stat"><div class="num">{{health.system?.processMemory||0}}M</div><div class="lbl">Memory</div></div>
700
+ <div class="stat"><div class="num">{{fmtNum(stats?.summary?.totalCalls)}}</div><div class="lbl">Total Calls</div></div>
701
+ </div>
702
+
703
+ <div v-if="!loading&&!health" class="card" style="text-align:center;color:var(--c-dim);padding:48px">No data — is the server running?</div>
704
+
705
+ <div class="card" v-if="health">
706
+ <div class="card-hd"><h3><span class="dot" style="background:var(--c-green)"></span>System</h3></div>
707
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:13px">
708
+ <div><span style="color:var(--c-dim)">Version</span><br>{{health.version}}</div>
709
+ <div><span style="color:var(--c-dim)">Node</span><br>{{health.system?.nodeVersion}}</div>
710
+ <div><span style="color:var(--c-dim)">Platform</span><br>{{health.system?.platform}} {{health.system?.arch}}</div>
711
+ <div><span style="color:var(--c-dim)">CPUs</span><br>{{health.system?.cpus}}</div>
712
+ <div><span style="color:var(--c-dim)">Heap</span><br>{{health.system?.heapUsed}}M / {{health.system?.processMemory}}M</div>
713
+ <div><span style="color:var(--c-dim)">Sys Mem</span><br>{{health.system?.freeMemory}}M free</div>
714
+ </div>
715
+ </div>
323
716
 
324
- <!-- Overview -->
325
- <div v-if="tab==='overview'">
326
- <div class="card" v-if="health">
327
- <h3><span class="dot" style="background:var(--green)"></span> System</h3>
328
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:13px">
329
- <div><span style="color:var(--text2)">Version:</span> {{health.version}}</div>
330
- <div><span style="color:var(--text2)">Node:</span> {{health.system?.nodeVersion}}</div>
331
- <div><span style="color:var(--text2)">Platform:</span> {{health.system?.platform}} {{health.system?.arch}}</div>
332
- <div><span style="color:var(--text2)">CPUs:</span> {{health.system?.cpus}}</div>
333
- <div><span style="color:var(--text2)">Heap:</span> {{health.system?.heapUsed}}MB / {{health.system?.processMemory}}MB</div>
334
- <div><span style="color:var(--text2)">System Mem:</span> {{health.system?.freeMemory}}MB free / {{health.system?.totalMemory}}MB</div>
717
+ <div class="card" v-if="health?.models?.list?.length">
718
+ <div class="card-hd"><h3>Models</h3></div>
719
+ <div><span v-for="m in health.models.list" class="pill model">{{m}}</span></div>
720
+ </div>
721
+
722
+ <div class="card" v-if="stats?.perModel&&Object.keys(stats.perModel).length">
723
+ <div class="card-hd"><h3>Usage by Model</h3></div>
724
+ <div class="tbl-wrap"><table class="tbl">
725
+ <thead><tr><th>Model</th><th>Calls</th><th>Tokens</th></tr></thead>
726
+ <tbody>
727
+ <tr v-for="(v,k) in stats.perModel" :key="k">
728
+ <td><span class="pill model">{{k}}</span></td>
729
+ <td>{{fmtNum(v.calls)}}</td><td>{{fmtNum(v.tokens)}}</td>
730
+ </tr>
731
+ </tbody>
732
+ </table></div>
733
+ </div>
734
+
735
+ <div class="card" v-if="stats?.perChannel&&Object.keys(stats.perChannel).length">
736
+ <div class="card-hd"><h3>Usage by Channel</h3></div>
737
+ <div class="tbl-wrap"><table class="tbl">
738
+ <thead><tr><th>Channel</th><th>Calls</th><th>Tokens</th><th>Models</th></tr></thead>
739
+ <tbody>
740
+ <tr v-for="(v,k) in stats.perChannel" :key="k">
741
+ <td>{{k}}</td><td>{{fmtNum(v.calls)}}</td><td>{{fmtNum(v.tokens)}}</td>
742
+ <td><span v-for="m in (v.models||[])" class="pill model">{{m}}</span></td>
743
+ </tr>
744
+ </tbody>
745
+ </table></div>
746
+ </div>
747
+
748
+ <div class="card" v-if="conversations.length">
749
+ <div class="card-hd"><h3>{{conversations.length}} Active Sessions</h3></div>
750
+ <span v-for="c in conversations" class="pill tag">{{c.userId}}</span>
751
+ </div>
335
752
  </div>
336
- </div>
337
753
 
338
- <div class="card" v-if="health?.models?.list?.length">
339
- <h3>Available Models</h3>
340
- <div><span v-for="m in health.models.list" class="pill model">{{m}}</span></div>
341
- </div>
754
+ <!-- ========== CHANNELS ========== -->
755
+ <div v-if="page==='channels'" class="page-enter">
756
+ <div class="card">
757
+ <div class="card-hd">
758
+ <h3>Channels</h3>
759
+ <button class="btn pri sm" @click="openModal('channel')">+ New</button>
760
+ </div>
761
+ <div class="tbl-wrap"><table class="tbl">
762
+ <thead><tr><th>Status</th><th>Name</th><th>Adapter</th><th>Models</th><th>Priority</th><th>Calls</th><th></th></tr></thead>
763
+ <tbody>
764
+ <tr v-for="ch in channels" :key="ch.id">
765
+ <td><span :class="['pill',ch.status==='enabled'?'on':'off']">{{ch.status}}</span></td>
766
+ <td>{{ch.name}}</td><td>{{ch.adapterType}}</td>
767
+ <td><span v-for="m in (ch.models||[])" class="pill model">{{m}}</span></td>
768
+ <td>{{ch.priority}}</td><td>{{fmtNum(ch.statistics?.callTimes)}}</td>
769
+ <td>
770
+ <button class="btn sm" @click="testChannel(ch.id)">Test</button>
771
+ <button class="btn sm" @click="openModal('channel',ch)">Edit</button>
772
+ <button class="btn sm danger" @click="deleteItem('channel',ch.id)">Del</button>
773
+ </td>
774
+ </tr>
775
+ <tr v-if="!channels.length"><td colspan="7" style="color:var(--c-dim);text-align:center;padding:32px">No channels</td></tr>
776
+ </tbody>
777
+ </table></div>
778
+ </div>
779
+ </div>
342
780
 
343
- <div class="card" v-if="stats">
344
- <h3>Usage by Model</h3>
345
- <table class="tbl">
346
- <tr><th>Model</th><th>Calls</th><th>Tokens</th></tr>
347
- <tr v-for="(v,k) in stats.perModel" :key="k">
348
- <td><span class="pill model">{{k}}</span></td>
349
- <td>{{fmtNum(v.calls)}}</td>
350
- <td>{{fmtNum(v.tokens)}}</td>
351
- </tr>
352
- <tr v-if="!Object.keys(stats.perModel||{}).length"><td colspan="3" style="color:var(--text2)">No usage data</td></tr>
353
- </table>
354
- </div>
355
- </div>
781
+ <!-- ========== TOOLS ========== -->
782
+ <div v-if="page==='tools'" class="page-enter">
783
+ <div class="card">
784
+ <div class="card-hd">
785
+ <h3>Tools</h3>
786
+ <button class="btn pri sm" @click="openModal('tool')">+ New</button>
787
+ </div>
788
+ <div class="tbl-wrap"><table class="tbl">
789
+ <thead><tr><th>Name</th><th>Description</th><th></th></tr></thead>
790
+ <tbody>
791
+ <tr v-for="t in tools" :key="t.id">
792
+ <td>{{t.name||t.id}}</td>
793
+ <td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{t.description}}</td>
794
+ <td>
795
+ <button class="btn sm" @click="openModal('tool',t)">Edit</button>
796
+ <button class="btn sm danger" @click="deleteItem('tool',t.id)">Del</button>
797
+ </td>
798
+ </tr>
799
+ <tr v-if="!tools.length"><td colspan="3" style="color:var(--c-dim);text-align:center;padding:32px">No tools</td></tr>
800
+ </tbody>
801
+ </table></div>
802
+ </div>
803
+ </div>
356
804
 
357
- <!-- Channels -->
358
- <div v-if="tab==='channels'">
359
- <div class="card">
360
- <h3 style="justify-content:space-between">Channels <button class="btn primary sm" @click="openModal('channel')">+ New</button></h3>
361
- <table class="tbl">
362
- <tr><th>Status</th><th>Name</th><th>Adapter</th><th>Models</th><th>Priority</th><th>Calls</th><th>Actions</th></tr>
363
- <tr v-for="ch in channels" :key="ch.id">
364
- <td><span :class="['pill',ch.status==='enabled'?'on':'off']">{{ch.status}}</span></td>
365
- <td>{{ch.name}}</td>
366
- <td>{{ch.adapterType}}</td>
367
- <td><span v-for="m in (ch.models||[])" class="pill model">{{m}}</span></td>
368
- <td>{{ch.priority}}</td>
369
- <td>{{fmtNum(ch.statistics?.callTimes)}}</td>
370
- <td>
371
- <button class="btn sm" @click="testChannel(ch.id)">Test</button>
372
- <button class="btn sm" @click="openModal('channel',ch)">Edit</button>
373
- <button class="btn sm danger" @click="deleteItem('channel',ch.id)">Del</button>
374
- </td>
375
- </tr>
376
- <tr v-if="!channels.length"><td colspan="7" style="color:var(--text2)">No channels</td></tr>
377
- </table>
378
- </div>
379
- </div>
805
+ <!-- ========== PRESETS ========== -->
806
+ <div v-if="page==='presets'" class="page-enter">
807
+ <div class="card">
808
+ <div class="card-hd">
809
+ <h3>Presets</h3>
810
+ <button class="btn pri sm" @click="openModal('preset')">+ New</button>
811
+ </div>
812
+ <div class="tbl-wrap"><table class="tbl">
813
+ <thead><tr><th>Name</th><th>Prefix</th><th>Model</th><th>Temp</th><th></th></tr></thead>
814
+ <tbody>
815
+ <tr v-for="p in presets" :key="p.id">
816
+ <td>{{p.name}}</td>
817
+ <td><code>{{p.prefix}}</code></td>
818
+ <td><span class="pill model">{{p.sendMessageOption?.model||'-'}}</span></td>
819
+ <td>{{p.sendMessageOption?.temperature}}</td>
820
+ <td>
821
+ <button class="btn sm" @click="openModal('preset',p)">Edit</button>
822
+ <button class="btn sm danger" @click="deleteItem('preset',p.id)">Del</button>
823
+ </td>
824
+ </tr>
825
+ <tr v-if="!presets.length"><td colspan="5" style="color:var(--c-dim);text-align:center;padding:32px">No presets</td></tr>
826
+ </tbody>
827
+ </table></div>
828
+ </div>
829
+ </div>
380
830
 
381
- <!-- Tools -->
382
- <div v-if="tab==='tools'">
383
- <div class="card">
384
- <h3 style="justify-content:space-between">Tools <button class="btn primary sm" @click="openModal('tool')">+ New</button></h3>
385
- <table class="tbl">
386
- <tr><th>Name</th><th>Description</th><th>Actions</th></tr>
387
- <tr v-for="t in tools" :key="t.id">
388
- <td>{{t.name||t.id}}</td>
389
- <td style="max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{t.description}}</td>
390
- <td>
391
- <button class="btn sm" @click="openModal('tool',t)">Edit</button>
392
- <button class="btn sm danger" @click="deleteItem('tool',t.id)">Del</button>
393
- </td>
394
- </tr>
395
- <tr v-if="!tools.length"><td colspan="3" style="color:var(--text2)">No tools</td></tr>
396
- </table>
397
- </div>
398
- </div>
831
+ <!-- ========== PROCESSORS ========== -->
832
+ <div v-if="page==='processors'" class="page-enter">
833
+ <div class="card">
834
+ <div class="card-hd">
835
+ <h3>Processors</h3>
836
+ <button class="btn pri sm" @click="openModal('processor')">+ New</button>
837
+ </div>
838
+ <div class="tbl-wrap"><table class="tbl">
839
+ <thead><tr><th>Name</th><th>Type</th><th>Description</th><th></th></tr></thead>
840
+ <tbody>
841
+ <tr v-for="p in processors" :key="p.id">
842
+ <td>{{p.name}}</td>
843
+ <td><span class="pill" :style="{background:p.type==='pre'?'rgba(167,139,250,.12)':'rgba(250,204,21,.12)',color:p.type==='pre'?'var(--c-purple)':'var(--c-yellow)'}">{{p.type}}</span></td>
844
+ <td>{{p.description}}</td>
845
+ <td>
846
+ <button class="btn sm" @click="openModal('processor',p)">Edit</button>
847
+ <button class="btn sm danger" @click="deleteItem('processor',p.id)">Del</button>
848
+ </td>
849
+ </tr>
850
+ <tr v-if="!processors.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">No processors</td></tr>
851
+ </tbody>
852
+ </table></div>
853
+ </div>
854
+ </div>
399
855
 
400
- <!-- Presets -->
401
- <div v-if="tab==='presets'">
402
- <div class="card">
403
- <h3 style="justify-content:space-between">Presets <button class="btn primary sm" @click="openModal('preset')">+ New</button></h3>
404
- <table class="tbl">
405
- <tr><th>Name</th><th>Prefix</th><th>Model</th><th>Temp</th><th>Actions</th></tr>
406
- <tr v-for="p in presets" :key="p.id">
407
- <td>{{p.name}}</td>
408
- <td><code>{{p.prefix}}</code></td>
409
- <td><span class="pill model">{{p.sendMessageOption?.model||'-'}}</span></td>
410
- <td>{{p.sendMessageOption?.temperature}}</td>
411
- <td>
412
- <button class="btn sm" @click="openModal('preset',p)">Edit</button>
413
- <button class="btn sm danger" @click="deleteItem('preset',p.id)">Del</button>
414
- </td>
415
- </tr>
416
- <tr v-if="!presets.length"><td colspan="5" style="color:var(--text2)">No presets</td></tr>
417
- </table>
418
- </div>
419
- </div>
856
+ <!-- ========== TRIGGERS ========== -->
857
+ <div v-if="page==='triggers'" class="page-enter">
858
+ <div class="card">
859
+ <div class="card-hd">
860
+ <h3>Triggers</h3>
861
+ <button class="btn pri sm" @click="openModal('trigger')">+ New</button>
862
+ </div>
863
+ <div class="tbl-wrap"><table class="tbl">
864
+ <thead><tr><th>Name</th><th>Description</th><th></th></tr></thead>
865
+ <tbody>
866
+ <tr v-for="t in triggers" :key="t.id">
867
+ <td>{{t.name||t.id}}</td><td>{{t.description}}</td>
868
+ <td>
869
+ <button class="btn sm" @click="openModal('trigger',t)">Edit</button>
870
+ <button class="btn sm danger" @click="deleteItem('trigger',t.id)">Del</button>
871
+ </td>
872
+ </tr>
873
+ <tr v-if="!triggers.length"><td colspan="3" style="color:var(--c-dim);text-align:center;padding:32px">No triggers</td></tr>
874
+ </tbody>
875
+ </table></div>
876
+ </div>
877
+ </div>
420
878
 
421
- <!-- Processors -->
422
- <div v-if="tab==='processors'">
423
- <div class="card">
424
- <h3 style="justify-content:space-between">Processors <button class="btn primary sm" @click="openModal('processor')">+ New</button></h3>
425
- <table class="tbl">
426
- <tr><th>Name</th><th>Type</th><th>Description</th><th>Actions</th></tr>
427
- <tr v-for="p in processors" :key="p.id">
428
- <td>{{p.name}}</td>
429
- <td><span class="pill" :style="{background:p.type==='pre'?'rgba(167,139,250,.15)':'rgba(250,204,21,.15)',color:p.type==='pre'?'var(--purple)':'var(--yellow)'}">{{p.type}}</span></td>
430
- <td>{{p.description}}</td>
431
- <td>
432
- <button class="btn sm" @click="openModal('processor',p)">Edit</button>
433
- <button class="btn sm danger" @click="deleteItem('processor',p.id)">Del</button>
434
- </td>
435
- </tr>
436
- <tr v-if="!processors.length"><td colspan="4" style="color:var(--text2)">No processors</td></tr>
437
- </table>
438
- </div>
439
- </div>
879
+ <!-- ========== GROUPS ========== -->
880
+ <div v-if="page==='groups'" class="page-enter">
881
+ <div class="card">
882
+ <div class="card-hd">
883
+ <h3>Tool Groups</h3>
884
+ <button class="btn pri sm" @click="openModal('toolGroup')">+ New</button>
885
+ </div>
886
+ <div class="tbl-wrap"><table class="tbl">
887
+ <thead><tr><th>Name</th><th>Description</th><th>Default</th><th></th></tr></thead>
888
+ <tbody>
889
+ <tr v-for="g in toolGroups" :key="g.id">
890
+ <td>{{g.name}}</td><td>{{g.description}}</td>
891
+ <td><span v-if="g.isDefault" class="pill on">Default</span></td>
892
+ <td>
893
+ <button class="btn sm" @click="openModal('toolGroup',g)">Edit</button>
894
+ <button class="btn sm danger" @click="deleteItem('toolGroup',g.id)">Del</button>
895
+ </td>
896
+ </tr>
897
+ <tr v-if="!toolGroups.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">No groups</td></tr>
898
+ </tbody>
899
+ </table></div>
900
+ </div>
901
+ </div>
440
902
 
441
- <!-- Triggers -->
442
- <div v-if="tab==='triggers'">
443
- <div class="card">
444
- <h3 style="justify-content:space-between">Triggers <button class="btn primary sm" @click="openModal('trigger')">+ New</button></h3>
445
- <table class="tbl">
446
- <tr><th>Name</th><th>Description</th><th>Actions</th></tr>
447
- <tr v-for="t in triggers" :key="t.id">
448
- <td>{{t.name||t.id}}</td>
449
- <td>{{t.description}}</td>
450
- <td>
451
- <button class="btn sm" @click="openModal('trigger',t)">Edit</button>
452
- <button class="btn sm danger" @click="deleteItem('trigger',t.id)">Del</button>
453
- </td>
454
- </tr>
455
- <tr v-if="!triggers.length"><td colspan="3" style="color:var(--text2)">No triggers</td></tr>
456
- </table>
903
+ <!-- ========== SESSIONS ========== -->
904
+ <div v-if="page==='conversations'" class="page-enter">
905
+ <div class="card">
906
+ <div class="card-hd"><h3>Active Sessions</h3></div>
907
+ <div class="tbl-wrap"><table class="tbl">
908
+ <thead><tr><th>User ID</th><th>Conversation</th><th>Model</th><th></th></tr></thead>
909
+ <tbody>
910
+ <tr v-for="c in conversations" :key="c.userId">
911
+ <td>{{c.userId}}</td>
912
+ <td><code style="font-size:11px">{{c.conversationId||'-'}}</code></td>
913
+ <td><span v-if="c.currentModel" class="pill model">{{c.currentModel}}</span><span v-else style="color:var(--c-dim)">-</span></td>
914
+ <td>
915
+ <button class="btn sm" @click="viewConv(c.userId)">View</button>
916
+ <button class="btn sm danger" @click="clearConv(c.userId)">Clear</button>
917
+ </td>
918
+ </tr>
919
+ <tr v-if="!conversations.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">No active sessions</td></tr>
920
+ </tbody>
921
+ </table></div>
922
+ </div>
923
+ </div>
924
+
925
+ <!-- ========== CONFIG ========== -->
926
+ <div v-if="page==='config'" class="page-enter">
927
+ <div class="card">
928
+ <div class="card-hd"><h3>Plugin Configuration</h3></div>
929
+ <div v-if="cfgSections.length">
930
+ <div v-for="sec in cfgSections" :key="sec.key" class="cfg-sec">
931
+ <div class="cfg-head" @click="toggleCfg(sec.key)">
932
+ <span>{{sec.label}} <span style="font-weight:400;color:var(--c-faint);font-size:11px">· {{Object.keys(sec.fields||{}).length}} items</span></span>
933
+ <span :class="['arr',expandedCfg[sec.key]&&'open']">▶</span>
934
+ </div>
935
+ <div v-if="expandedCfg[sec.key]" class="cfg-body">
936
+ <div v-for="(fv,fk) in sec.fields" :key="fk" class="cfg-fld">
937
+ <span class="k">{{fk}}</span>
938
+ <span class="v">
939
+ <template v-if="typeof fv==='boolean'"><span :class="['pill',fv?'on':'off']">{{fv}}</span></template>
940
+ <template v-else-if="typeof fv==='number'"><span class="num">{{fv}}</span></template>
941
+ <template v-else-if="typeof fv==='string'&&fv.length<=80"><code>{{fv||'""'}}</code></template>
942
+ <template v-else-if="typeof fv==='string'"><code :title="fv">{{fLen(fv,60)}}</code></template>
943
+ <template v-else><span style="color:var(--c-faint);font-size:11px">obj</span></template>
944
+ </span>
945
+ </div>
946
+ </div>
947
+ </div>
948
+ </div>
949
+ <div v-else style="color:var(--c-dim);font-size:13px;padding:20px 0">No config data</div>
950
+ <div class="cfg-json-btn"><button class="btn" @click="editConfig">Edit JSON</button></div>
951
+ </div>
952
+ </div>
953
+
954
+ </div><!-- .content -->
955
+ </div><!-- .main -->
956
+ </div><!-- .layout -->
957
+
958
+ <!-- ===== MODALS ===== -->
959
+
960
+ <!-- CRUD Modal -->
961
+ <div v-if="modal" class="modal-mask" @click.self="closeModal">
962
+ <div class="modal">
963
+ <h3>{{editing?'Edit':'New'}} {{modal}}</h3>
964
+
965
+ <template v-if="modal==='channel'">
966
+ <div class="f-row"><label>Name</label><input v-model="form.name"/></div>
967
+ <div class="f-row"><label>Adapter</label><select v-model="form.adapterType"><option>openai</option><option>gemini</option><option>claude</option></select></div>
968
+ <div class="f-row"><label>Models (comma separated)</label><input v-model="form._models" :placeholder="(form.models||[]).join(', ')"/></div>
969
+ <div class="f-row"><label>Base URL</label><input v-model="form.options.baseUrl"/></div>
970
+ <div class="f-row"><label>API Key</label><input v-model="form.options.apiKey" type="password"/></div>
971
+ <div class="f-row"><label>Priority</label><input v-model.number="form.priority" type="number"/></div>
972
+ <div class="f-row"><label>Weight</label><input v-model.number="form.weight" type="number"/></div>
973
+ <div class="f-row"><label>Status</label><select v-model="form.status"><option>enabled</option><option>disabled</option></select></div>
974
+
975
+ <!-- 模型高级配置 -->
976
+ <div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--c-border)">
977
+ <h4 style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--c-accent)">Model Configuration</h4>
978
+
979
+ <div class="f-row"><label>Model Name</label><input v-model="form.sendMessageOption.model"/></div>
980
+ <div class="f-row"><label>Temperature</label><input v-model.number="form.sendMessageOption.temperature" type="number" step="0.1" min="0" max="2"/></div>
981
+ <div class="f-row"><label>Max Token</label><input v-model.number="form.sendMessageOption.maxToken" type="number" min="1"/></div>
982
+ <div class="f-row"><label>System Prompt</label><textarea v-model="form.sendMessageOption.systemOverride" placeholder="Override system prompt..."></textarea></div>
983
+
984
+ <!-- Gemini 思考配置 -->
985
+ <div v-if="form.adapterType==='gemini'" style="margin-top:12px;padding:12px;background:var(--c-surface2);border-radius:8px">
986
+ <h5 style="font-size:13px;font-weight:600;margin-bottom:8px;color:var(--c-purple)">Gemini Thinking Configuration</h5>
987
+ <div class="f-row">
988
+ <label>Enable Reasoning</label>
989
+ <select v-model="form.sendMessageOption.enableReasoning">
990
+ <option :value="false">Disabled</option>
991
+ <option :value="true">Enabled</option>
992
+ </select>
993
+ </div>
994
+ <div class="f-row" v-if="form.sendMessageOption.enableReasoning">
995
+ <label>Thinking Level</label>
996
+ <select v-model="form.sendMessageOption.reasoningEffort">
997
+ <option value="minimal">Minimal (Low Latency)</option>
998
+ <option value="low">Low</option>
999
+ <option value="medium">Medium (Balanced)</option>
1000
+ <option value="high">High (Deep Reasoning)</option>
1001
+ </select>
1002
+ </div>
1003
+ <div class="f-row" v-if="form.sendMessageOption.enableReasoning">
1004
+ <label>Reasoning Budget Tokens</label>
1005
+ <input v-model.number="form.sendMessageOption.reasoningBudgetTokens" type="number" min="0" placeholder="0 for auto"/>
1006
+ </div>
1007
+ </div>
1008
+
1009
+ <!-- 工具调用限制 -->
1010
+ <div style="margin-top:12px;padding:12px;background:var(--c-surface2);border-radius:8px">
1011
+ <h5 style="font-size:13px;font-weight:600;margin-bottom:8px;color:var(--c-yellow)">Tool Call Limits</h5>
1012
+ <div class="f-row">
1013
+ <label>Max Consecutive Calls</label>
1014
+ <input v-model.number="form.sendMessageOption.toolCallLimit.maxConsecutiveCalls" type="number" min="1"/>
1015
+ </div>
1016
+ <div class="f-row">
1017
+ <label>Max Consecutive Identical Calls</label>
1018
+ <input v-model.number="form.sendMessageOption.toolCallLimit.maxConsecutiveIdenticalCalls" type="number" min="1"/>
1019
+ </div>
1020
+ </div>
1021
+
1022
+ <!-- 高级配置(JSON 格式) -->
1023
+ <div style="margin-top:12px">
1024
+ <div class="f-row">
1025
+ <label>Response Modalities (JSON array)</label>
1026
+ <input v-model="form._responseModalities" :placeholder="JSON.stringify(form.sendMessageOption.responseModalities||[])"/>
1027
+ </div>
1028
+ <div class="f-row">
1029
+ <label>Safety Settings (JSON array)</label>
1030
+ <textarea v-model="form._safetySettings" class="mono" style="min-height:60px" :placeholder="JSON.stringify(form.sendMessageOption.safetySettings||[],null,2)"></textarea>
1031
+ </div>
1032
+ </div>
1033
+ </div>
1034
+ </template>
1035
+
1036
+ <template v-if="modal==='preset'">
1037
+ <div class="f-row"><label>Name</label><input v-model="form.name"/></div>
1038
+ <div class="f-row"><label>Prefix</label><input v-model="form.prefix"/></div>
1039
+ <div class="f-row"><label>Model</label><input v-model="form.sendMessageOption.model"/></div>
1040
+ <div class="f-row"><label>Temperature</label><input v-model.number="form.sendMessageOption.temperature" type="number" step="0.1" min="0" max="2"/></div>
1041
+ <div class="f-row"><label>Max Token</label><input v-model.number="form.sendMessageOption.maxToken" type="number" min="1"/></div>
1042
+ <div class="f-row"><label>System Prompt</label><textarea v-model="form.sendMessageOption.systemOverride" placeholder="System prompt override..."></textarea></div>
1043
+
1044
+ <!-- 高级配置 -->
1045
+ <div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--c-border)">
1046
+ <h4 style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--c-accent)">Advanced Configuration</h4>
1047
+
1048
+ <div class="f-row">
1049
+ <label>Enable Reasoning</label>
1050
+ <select v-model="form.sendMessageOption.enableReasoning">
1051
+ <option :value="false">Disabled</option>
1052
+ <option :value="true">Enabled</option>
1053
+ </select>
1054
+ </div>
1055
+
1056
+ <div class="f-row" v-if="form.sendMessageOption.enableReasoning">
1057
+ <label>Reasoning Effort</label>
1058
+ <select v-model="form.sendMessageOption.reasoningEffort">
1059
+ <option value="minimal">Minimal</option>
1060
+ <option value="low">Low</option>
1061
+ <option value="medium">Medium</option>
1062
+ <option value="high">High</option>
1063
+ </select>
1064
+ </div>
1065
+
1066
+ <div class="f-row">
1067
+ <label>Disable History Read</label>
1068
+ <select v-model="form.sendMessageOption.disableHistoryRead">
1069
+ <option :value="false">No</option>
1070
+ <option :value="true">Yes</option>
1071
+ </select>
1072
+ </div>
1073
+
1074
+ <div class="f-row">
1075
+ <label>Disable History Save</label>
1076
+ <select v-model="form.sendMessageOption.disableHistorySave">
1077
+ <option :value="false">No</option>
1078
+ <option :value="true">Yes</option>
1079
+ </select>
1080
+ </div>
1081
+ </div>
1082
+ </template>
1083
+
1084
+ <template v-if="modal==='tool'||modal==='processor'||modal==='trigger'">
1085
+ <div class="f-row"><label>Name</label><input v-model="form.name"/></div>
1086
+ <div class="f-row" v-if="modal==='processor'"><label>Type</label><select v-model="form.type"><option>pre</option><option>post</option></select></div>
1087
+ <div class="f-row"><label>Description</label><input v-model="form.description"/></div>
1088
+ <div class="f-row"><label>Code</label><textarea v-model="form.code" class="mono" style="min-height:200px"></textarea></div>
1089
+ </template>
1090
+
1091
+ <template v-if="modal==='toolGroup'">
1092
+ <div class="f-row"><label>Name</label><input v-model="form.name"/></div>
1093
+ <div class="f-row"><label>Description</label><input v-model="form.description"/></div>
1094
+ <div class="f-row"><label>Tool IDs (comma)</label><input v-model="form._toolIds" :placeholder="(form.toolIds||[]).join(', ')"/></div>
1095
+ <div class="f-row"><label>Status</label><select v-model="form.status"><option>enabled</option><option>disabled</option></select></div>
1096
+ </template>
1097
+
1098
+ <div class="modal-btns">
1099
+ <button class="btn" @click="closeModal">Cancel</button>
1100
+ <button class="btn pri" @click="saveItem">Save</button>
457
1101
  </div>
458
1102
  </div>
1103
+ </div>
459
1104
 
460
- <!-- Tool Groups -->
461
- <div v-if="tab==='groups'">
462
- <div class="card">
463
- <h3 style="justify-content:space-between">Tool Groups <button class="btn primary sm" @click="openModal('toolGroup')">+ New</button></h3>
464
- <table class="tbl">
465
- <tr><th>Name</th><th>Description</th><th>Default</th><th>Actions</th></tr>
466
- <tr v-for="g in toolGroups" :key="g.id">
467
- <td>{{g.name}}</td>
468
- <td>{{g.description}}</td>
469
- <td><span v-if="g.isDefault" class="pill on">Default</span></td>
470
- <td>
471
- <button class="btn sm" @click="openModal('toolGroup',g)">Edit</button>
472
- <button class="btn sm danger" @click="deleteItem('toolGroup',g.id)">Del</button>
473
- </td>
474
- </tr>
475
- <tr v-if="!toolGroups.length"><td colspan="4" style="color:var(--text2)">No groups</td></tr>
476
- </table>
1105
+ <!-- Config JSON Edit Modal -->
1106
+ <div v-if="editingConfig" class="modal-mask" @click.self="editingConfig=false">
1107
+ <div class="modal">
1108
+ <h3>Edit Configuration</h3>
1109
+ <div class="f-row"><label>JSON</label><textarea v-model="configForm" class="mono" style="min-height:360px"></textarea></div>
1110
+ <div class="modal-btns">
1111
+ <button class="btn" @click="editingConfig=false">Cancel</button>
1112
+ <button class="btn pri" @click="saveConfig">Save</button>
477
1113
  </div>
478
1114
  </div>
1115
+ </div>
479
1116
 
480
- <!-- Modal -->
481
- <div v-if="modal" class="modal-mask" @click.self="closeModal">
482
- <div class="modal">
483
- <h3>{{editing?'Edit':'New'}} {{modal}}</h3>
484
-
485
- <!-- Channel form -->
486
- <template v-if="modal==='channel'">
487
- <div class="form-row"><label>Name</label><input v-model="form.name"/></div>
488
- <div class="form-row"><label>Adapter Type</label><select v-model="form.adapterType"><option>openai</option><option>gemini</option><option>claude</option></select></div>
489
- <div class="form-row"><label>Models (comma separated)</label><input v-model="form._models" :placeholder="(form.models||[]).join(', ')"/></div>
490
- <div class="form-row"><label>Base URL</label><input v-model="form.options.baseUrl"/></div>
491
- <div class="form-row"><label>API Key</label><input v-model="form.options.apiKey" type="password"/></div>
492
- <div class="form-row"><label>Priority</label><input v-model.number="form.priority" type="number"/></div>
493
- <div class="form-row"><label>Weight</label><input v-model.number="form.weight" type="number"/></div>
494
- <div class="form-row"><label>Status</label><select v-model="form.status"><option>enabled</option><option>disabled</option></select></div>
495
- </template>
496
-
497
- <!-- Preset form -->
498
- <template v-if="modal==='preset'">
499
- <div class="form-row"><label>Name</label><input v-model="form.name"/></div>
500
- <div class="form-row"><label>Prefix</label><input v-model="form.prefix"/></div>
501
- <div class="form-row"><label>Model</label><input v-model="form.sendMessageOption.model"/></div>
502
- <div class="form-row"><label>Temperature</label><input v-model.number="form.sendMessageOption.temperature" type="number" step="0.1"/></div>
503
- <div class="form-row"><label>Max Token</label><input v-model.number="form.sendMessageOption.maxToken" type="number"/></div>
504
- <div class="form-row"><label>System Prompt</label><textarea v-model="form.sendMessageOption.systemOverride"></textarea></div>
505
- </template>
506
-
507
- <!-- Tool / Processor / Trigger form -->
508
- <template v-if="modal==='tool'||modal==='processor'||modal==='trigger'">
509
- <div class="form-row"><label>Name</label><input v-model="form.name"/></div>
510
- <div class="form-row" v-if="modal==='processor'"><label>Type</label><select v-model="form.type"><option>pre</option><option>post</option></select></div>
511
- <div class="form-row"><label>Description</label><input v-model="form.description"/></div>
512
- <div class="form-row"><label>Code</label><textarea v-model="form.code" style="min-height:200px;font-family:monospace;font-size:12px"></textarea></div>
513
- </template>
514
-
515
- <!-- Tool Group form -->
516
- <template v-if="modal==='toolGroup'">
517
- <div class="form-row"><label>Name</label><input v-model="form.name"/></div>
518
- <div class="form-row"><label>Description</label><input v-model="form.description"/></div>
519
- <div class="form-row"><label>Tool IDs (comma separated)</label><input v-model="form._toolIds" :placeholder="(form.toolIds||[]).join(', ')"/></div>
520
- <div class="form-row"><label>Status</label><select v-model="form.status"><option>enabled</option><option>disabled</option></select></div>
521
- </template>
522
-
523
- <div class="modal-btns">
524
- <button class="btn" @click="closeModal">Cancel</button>
525
- <button class="btn primary" @click="saveItem">Save</button>
526
- </div>
527
- </div>
1117
+ <!-- Session Detail Modal -->
1118
+ <div v-if="convDetail" class="modal-mask" @click.self="convDetail=null">
1119
+ <div class="modal">
1120
+ <h3>Session: {{convDetail.userId}}</h3>
1121
+ <div class="f-row"><label>Conversation ID</label><input :value="convDetail.current?.conversationId||'-'" readonly/></div>
1122
+ <div class="f-row"><label>Model</label><input :value="convDetail.current?.model||'-'" readonly/></div>
1123
+ <div class="f-row"><label>Preset</label><input :value="convDetail.current?.presetId||'-'" readonly/></div>
1124
+ <div class="f-row"><label>Full State</label><textarea readonly :value="JSON.stringify(convDetail,null,2)" class="mono" style="min-height:200px"></textarea></div>
1125
+ <div class="modal-btns"><button class="btn" @click="convDetail=null">Close</button></div>
528
1126
  </div>
529
1127
  </div>
530
1128
  `