@becrafter/prompt-manager 0.0.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/app/cli/cli.js +17 -0
  4. package/app/cli/commands/start.js +20 -0
  5. package/app/cli/index.js +37 -0
  6. package/app/cli/support/argv.js +7 -0
  7. package/app/cli/support/signals.js +34 -0
  8. package/app/desktop/assets/icon.png +0 -0
  9. package/app/desktop/main.js +496 -0
  10. package/app/desktop/package-lock.json +4091 -0
  11. package/app/desktop/package.json +63 -0
  12. package/examples/prompts/developer/code-review.yaml +32 -0
  13. package/examples/prompts/developer/code_refactoring.yaml +31 -0
  14. package/examples/prompts/developer/doc-generator.yaml +36 -0
  15. package/examples/prompts/developer/error-code-fixer.yaml +35 -0
  16. package/examples/prompts/generator/gen_3d_edu_webpage_html.yaml +117 -0
  17. package/examples/prompts/generator/gen_3d_webpage_html.yaml +75 -0
  18. package/examples/prompts/generator/gen_bento_grid_html.yaml +112 -0
  19. package/examples/prompts/generator/gen_html_web_page.yaml +88 -0
  20. package/examples/prompts/generator/gen_knowledge_card_html.yaml +83 -0
  21. package/examples/prompts/generator/gen_magazine_card_html.yaml +82 -0
  22. package/examples/prompts/generator/gen_mimeng_headline_title.yaml +71 -0
  23. package/examples/prompts/generator/gen_podcast_script.yaml +69 -0
  24. package/examples/prompts/generator/gen_prd_prototype_html.yaml +175 -0
  25. package/examples/prompts/generator/gen_summarize.yaml +157 -0
  26. package/examples/prompts/generator/gen_title.yaml +119 -0
  27. package/examples/prompts/generator/others/api_documentation.yaml +32 -0
  28. package/examples/prompts/generator/others/build_mcp_server.yaml +26 -0
  29. package/examples/prompts/generator/others/project_architecture.yaml +31 -0
  30. package/examples/prompts/generator/others/test_case_generator.yaml +30 -0
  31. package/examples/prompts/generator/others/writing_assistant.yaml +72 -0
  32. package/package.json +54 -0
  33. package/packages/admin-ui/admin.html +4959 -0
  34. package/packages/admin-ui/css/codemirror-theme_xq-light.css +43 -0
  35. package/packages/admin-ui/css/codemirror.css +344 -0
  36. package/packages/admin-ui/js/closebrackets.min.js +8 -0
  37. package/packages/admin-ui/js/codemirror.min.js +8 -0
  38. package/packages/admin-ui/js/js-yaml.min.js +2 -0
  39. package/packages/admin-ui/js/markdown.min.js +8 -0
  40. package/packages/server/config.js +283 -0
  41. package/packages/server/logger.js +55 -0
  42. package/packages/server/manager.js +473 -0
  43. package/packages/server/mcp.js +234 -0
  44. package/packages/server/mcpManager.js +205 -0
  45. package/packages/server/server.js +1001 -0
  46. package/scripts/postinstall.js +34 -0
@@ -0,0 +1,4959 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Prompt Manager 后台</title>
7
+ <link rel="stylesheet" href="./css/codemirror.css">
8
+ <link rel="stylesheet" href="./css/codemirror-theme_xq-light.css">
9
+ <style>
10
+ :root {
11
+ --primary: #393939;
12
+ --primary-dark: #393939;
13
+ --success: #28a745;
14
+ --danger: #dc3545;
15
+ --warning: #ffc107;
16
+ --dark: #1a1a1a;
17
+ --light: #f8f9fa;
18
+ --gray: #6c757d;
19
+ --gray-light: #dee2e6;
20
+ --border: #e0e0e0;
21
+ --sidebar-bg: #f8f9fa;
22
+ --editor-bg: #263238;
23
+ --preview-bg: #f5f5f5;
24
+ }
25
+
26
+ * {
27
+ box-sizing: border-box;
28
+ margin: 0;
29
+ padding: 0;
30
+ font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
31
+ -webkit-font-smoothing: antialiased;
32
+ -moz-osx-font-smoothing: grayscale;
33
+ text-rendering: optimizeLegibility;
34
+ }
35
+
36
+ body {
37
+ background-color: var(--light);
38
+ color: var(--dark);
39
+ min-height: 100vh;
40
+ display: flex;
41
+ flex-direction: column;
42
+ letter-spacing: -0.01em;
43
+ font-weight: 420;
44
+ }
45
+
46
+ header {
47
+ background-color: white;
48
+ padding: 12px 25px;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
53
+ border-bottom: 1px solid var(--border);
54
+ }
55
+
56
+ .logo {
57
+ font-size: 24px;
58
+ font-weight: 580;
59
+ color: var(--primary);
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 10px;
63
+ letter-spacing: -0.02em;
64
+ }
65
+
66
+ .logo span {
67
+ color: #8e8989;
68
+ font-size: 12px;
69
+ padding: 3px 8px;
70
+ border-radius: 5px;
71
+ font-weight: 450;
72
+ border: 1px solid var(--border);
73
+ letter-spacing: 0;
74
+ }
75
+
76
+ .nav-right {
77
+ display: flex;
78
+ gap: 12px;
79
+ align-items: center;
80
+ position: relative;
81
+ }
82
+
83
+ .user-profile {
84
+ position: relative;
85
+ }
86
+
87
+ .avatar-btn {
88
+ width: 32px;
89
+ height: 32px;
90
+ padding: 0;
91
+ border: none;
92
+ background: rgba(0,0,0,0.02);
93
+ cursor: pointer;
94
+ border-radius: 50%;
95
+ overflow: hidden;
96
+ transition: all 0.2s ease;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ }
101
+
102
+ .avatar-btn:hover {
103
+ transform: translateY(-1px);
104
+ background: rgba(0,0,0,0.04);
105
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
106
+ }
107
+
108
+ .avatar-img {
109
+ width: 26px;
110
+ height: 26px;
111
+ object-fit: cover;
112
+ border-radius: 50%;
113
+ opacity: 0.75;
114
+ transition: opacity 0.2s ease;
115
+ }
116
+
117
+ .avatar-btn:hover .avatar-img {
118
+ opacity: 0.9;
119
+ }
120
+
121
+ .dropdown-menu {
122
+ position: absolute;
123
+ top: calc(100% + 8px);
124
+ right: 0;
125
+ width: 240px;
126
+ background: white;
127
+ border-radius: 12px;
128
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
129
+ opacity: 0;
130
+ visibility: hidden;
131
+ transform: translateY(-8px) scale(0.96);
132
+ transform-origin: top right;
133
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
134
+ z-index: 1000;
135
+ border: 1px solid var(--border);
136
+ }
137
+
138
+ .dropdown-menu.show {
139
+ opacity: 1;
140
+ visibility: visible;
141
+ transform: translateY(0) scale(1);
142
+ }
143
+
144
+ .dropdown-header {
145
+ padding: 16px;
146
+ border-bottom: 1px solid var(--border);
147
+ }
148
+
149
+ .user-info {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 12px;
153
+ }
154
+
155
+ .user-avatar {
156
+ width: 36px;
157
+ height: 36px;
158
+ border-radius: 50%;
159
+ background: var(--light);
160
+ padding: 3px;
161
+ opacity: 0.8;
162
+ }
163
+
164
+ .user-details {
165
+ flex: 1;
166
+ min-width: 0;
167
+ padding: 2px 0;
168
+ }
169
+
170
+ .user-name {
171
+ font-size: 15px;
172
+ font-weight: 500;
173
+ color: var(--dark);
174
+ margin-bottom: 2px;
175
+ }
176
+
177
+ .user-role {
178
+ font-size: 13px;
179
+ color: var(--gray);
180
+ }
181
+
182
+ .dropdown-divider {
183
+ height: 1px;
184
+ background: var(--border);
185
+ margin: 8px 0;
186
+ }
187
+
188
+ .dropdown-item {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 10px;
192
+ width: 100%;
193
+ padding: 12px 16px;
194
+ border: none;
195
+ background: none;
196
+ font-size: 14px;
197
+ color: var(--dark);
198
+ cursor: pointer;
199
+ transition: all 0.2s ease;
200
+ }
201
+
202
+ .dropdown-item:last-child {
203
+ border-radius: 0 0 12px 12px;
204
+ }
205
+
206
+ .dropdown-item:hover {
207
+ background: rgba(0, 0, 0, 0.04);
208
+ }
209
+
210
+ .dropdown-item .dropdown-icon {
211
+ width: 18px;
212
+ height: 18px;
213
+ color: var(--gray);
214
+ }
215
+
216
+ .dropdown-item:hover .dropdown-icon {
217
+ color: var(--primary);
218
+ }
219
+
220
+ .btn {
221
+ padding: 8px 16px;
222
+ border-radius: 6px;
223
+ font-size: 14px;
224
+ font-weight: 500;
225
+ cursor: pointer;
226
+ transition: all 0.2s;
227
+ border: none;
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 6px;
231
+ }
232
+
233
+ .btn-primary {
234
+ background: var(--primary);
235
+ color: white;
236
+ }
237
+
238
+ .btn-primary:hover {
239
+ background: var(--primary-dark);
240
+ }
241
+
242
+ .btn-success {
243
+ background: var(--success);
244
+ color: white;
245
+ }
246
+
247
+ .btn-outline {
248
+ background: transparent;
249
+ border: 1px solid var(--border);
250
+ color: var(--dark);
251
+ }
252
+
253
+ .btn-outline:hover {
254
+ background: var(--light);
255
+ }
256
+
257
+ .btn-light {
258
+ background: white;
259
+ border: 1px solid var(--border);
260
+ color: var(--dark);
261
+ }
262
+
263
+ .btn-light:hover {
264
+ background: var(--light);
265
+ }
266
+
267
+ .btn-dark {
268
+ background: var(--dark);
269
+ color: white;
270
+ }
271
+
272
+ .btn-dark:hover {
273
+ background: #000;
274
+ }
275
+
276
+ .btn-danger {
277
+ background: var(--danger);
278
+ color: white;
279
+ }
280
+
281
+ .btn-danger:hover {
282
+ background: #b71f31;
283
+ }
284
+
285
+ .btn-sm {
286
+ padding: 6px 12px;
287
+ font-size: 13px;
288
+ }
289
+
290
+ #addArgumentBtn {
291
+ padding: 6px 12px;
292
+ font-size: 12px;
293
+ }
294
+
295
+ button#saveBtn{
296
+ padding: 10px 18px;
297
+ font-size: 14px;
298
+ }
299
+
300
+ .btn.loading {
301
+ opacity: 0.6;
302
+ pointer-events: none;
303
+ }
304
+
305
+ .btn-icon {
306
+ padding: 6px;
307
+ width: 32px;
308
+ height: 32px;
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ }
313
+
314
+ main {
315
+ display: flex;
316
+ flex: 1;
317
+ overflow: hidden;
318
+ height: 93%;
319
+ }
320
+
321
+ aside {
322
+ width: 320px;
323
+ background: var(--sidebar-bg);
324
+ border-right: 1px solid var(--border);
325
+ padding: 0;
326
+ display: flex;
327
+ flex-direction: column;
328
+ max-height: 100%;
329
+ overflow-y: auto;
330
+ scrollbar-width: none;
331
+ }
332
+
333
+ .sidebar-header {
334
+ padding: 15px;
335
+ display: flex;
336
+ flex-direction: column;
337
+ gap: 12px;
338
+ background: white;
339
+ border-bottom: 1px solid var(--border);
340
+ }
341
+
342
+ .new-prompt-btn {
343
+ background: var(--primary);
344
+ color: white;
345
+ border: none;
346
+ border-radius: 10px;
347
+ padding: 12px 16px;
348
+ font-size: 15px;
349
+ font-weight: 500;
350
+ cursor: pointer;
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ gap: 8px;
355
+ width: 100%;
356
+ transition: all 0.2s ease;
357
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
358
+ }
359
+
360
+ .new-prompt-btn:hover {
361
+ background: #333;
362
+ transform: translateY(-1px);
363
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
364
+ }
365
+
366
+ .new-prompt-btn::before {
367
+ content: '+';
368
+ font-size: 18px;
369
+ font-weight: 400;
370
+ margin-right: 2px;
371
+ }
372
+
373
+ .search-container {
374
+ display: flex;
375
+ gap: 10px;
376
+ margin-top: 2px;
377
+ }
378
+
379
+ .search-box {
380
+ position: relative;
381
+ flex: 1;
382
+ }
383
+
384
+ .search-box input {
385
+ width: 100%;
386
+ padding: 9px 32px 9px 38px;
387
+ border: 1px solid var(--border);
388
+ border-radius: 8px;
389
+ font-size: 14px;
390
+ transition: all 0.2s ease;
391
+ background: white;
392
+ }
393
+
394
+ .search-box input:focus {
395
+ outline: none;
396
+ border-color: var(--primary);
397
+ box-shadow: 0 0 0 3px rgba(57,57,57,0.1);
398
+ }
399
+
400
+ .search-box::before {
401
+ content: '';
402
+ position: absolute;
403
+ left: 12px;
404
+ top: 50%;
405
+ transform: translateY(-50%);
406
+ width: 16px;
407
+ height: 16px;
408
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") no-repeat center;
409
+ background-size: contain;
410
+ opacity: 0.6;
411
+ }
412
+
413
+ .search-box .clear-btn {
414
+ position: absolute;
415
+ right: 8px;
416
+ top: 50%;
417
+ transform: translateY(-50%);
418
+ width: 20px;
419
+ height: 20px;
420
+ border: none;
421
+ background: transparent;
422
+ cursor: pointer;
423
+ padding: 0;
424
+ display: none;
425
+ align-items: center;
426
+ justify-content: center;
427
+ color: var(--gray);
428
+ opacity: 0.6;
429
+ transition: opacity 0.2s ease;
430
+ }
431
+
432
+ .search-box .clear-btn::before {
433
+ content: '';
434
+ width: 16px;
435
+ height: 16px;
436
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cline x1='15' y1='9' x2='9' y2='15'/%3E%3Cline x1='9' y1='9' x2='15' y2='15'/%3E%3C/svg%3E") no-repeat center;
437
+ background-size: contain;
438
+ }
439
+
440
+ .search-box .clear-btn:hover {
441
+ opacity: 1;
442
+ }
443
+
444
+ .search-box input:not(:placeholder-shown) + .clear-btn {
445
+ display: flex;
446
+ }
447
+
448
+ .folder-btn {
449
+ background: white;
450
+ border: 1px solid var(--border);
451
+ border-radius: 8px;
452
+ width: 38px;
453
+ height: 38px;
454
+ cursor: pointer;
455
+ display: flex;
456
+ align-items: center;
457
+ justify-content: center;
458
+ transition: all 0.2s ease;
459
+ flex-shrink: 0;
460
+ }
461
+
462
+ .folder-btn::before {
463
+ content: '';
464
+ width: 18px;
465
+ height: 18px;
466
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z'/%3E%3Cline x1='12' y1='11' x2='12' y2='17'/%3E%3Cline x1='9' y1='14' x2='15' y2='14'/%3E%3C/svg%3E") no-repeat center;
467
+ background-size: contain;
468
+ opacity: 0.6;
469
+ transition: opacity 0.2s ease;
470
+ }
471
+
472
+ .folder-btn:hover {
473
+ background: #f8f9fa;
474
+ border-color: var(--primary);
475
+ transform: translateY(-1px);
476
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
477
+ }
478
+
479
+ .folder-btn:hover::before {
480
+ opacity: 1;
481
+ }
482
+
483
+ .toast-container {
484
+ position: fixed;
485
+ bottom: 24px;
486
+ right: 24px;
487
+ display: flex;
488
+ flex-direction: column-reverse;
489
+ gap: 12px;
490
+ z-index: 5000;
491
+ align-items: flex-end;
492
+ }
493
+
494
+ .toast {
495
+ min-width: 280px;
496
+ max-width: 360px;
497
+ background: rgba(255, 255, 255, 0.96);
498
+ border-radius: 12px;
499
+ padding: 16px 18px;
500
+ display: flex;
501
+ align-items: flex-start;
502
+ gap: 14px;
503
+ box-shadow: 0 20px 45px rgba(0, 0, 0, 0.18);
504
+ border: 1px solid transparent;
505
+ opacity: 0;
506
+ transform: translateY(16px);
507
+ animation: toast-enter 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
508
+ pointer-events: auto;
509
+ }
510
+
511
+ .toast.toast-leave {
512
+ animation: toast-leave 0.25s ease forwards;
513
+ }
514
+
515
+ .toast-success {
516
+ border-color: rgba(40,167,69,0.35);
517
+ background: linear-gradient(145deg, rgba(40,167,69,0.08), rgba(40,167,69,0.02));
518
+ }
519
+
520
+ .toast-error {
521
+ border-color: rgba(220,53,69,0.35);
522
+ background: linear-gradient(145deg, rgba(220,53,69,0.08), rgba(220,53,69,0.02));
523
+ }
524
+
525
+ .toast-info {
526
+ border-color: rgba(0,102,255,0.3);
527
+ background: linear-gradient(145deg, rgba(0,102,255,0.08), rgba(0,102,255,0.02));
528
+ }
529
+
530
+ .toast-warning {
531
+ border-color: rgba(255,193,7,0.35);
532
+ background: linear-gradient(145deg, rgba(255,193,7,0.1), rgba(255,193,7,0.03));
533
+ }
534
+
535
+ .toast-icon {
536
+ width: 36px;
537
+ height: 36px;
538
+ border-radius: 50%;
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: center;
542
+ font-size: 18px;
543
+ font-weight: 600;
544
+ flex-shrink: 0;
545
+ background: rgba(0,0,0,0.05);
546
+ color: var(--dark);
547
+ }
548
+
549
+ .toast-success .toast-icon {
550
+ background: rgba(40,167,69,0.15);
551
+ color: var(--success);
552
+ }
553
+
554
+ .toast-error .toast-icon {
555
+ background: rgba(220,53,69,0.15);
556
+ color: var(--danger);
557
+ }
558
+
559
+ .toast-info .toast-icon {
560
+ background: rgba(0,102,255,0.15);
561
+ color: var(--primary);
562
+ }
563
+
564
+ .toast-warning .toast-icon {
565
+ background: rgba(255,193,7,0.2);
566
+ color: #b58100;
567
+ }
568
+
569
+ .toast-content {
570
+ flex: 1;
571
+ display: flex;
572
+ flex-direction: column;
573
+ gap: 4px;
574
+ }
575
+
576
+ .toast-title {
577
+ font-size: 15px;
578
+ font-weight: 600;
579
+ color: var(--dark);
580
+ }
581
+
582
+ .toast-message {
583
+ font-size: 13px;
584
+ color: var(--gray);
585
+ line-height: 1.5;
586
+ white-space: pre-line;
587
+ }
588
+
589
+ .toast-close {
590
+ border: none;
591
+ background: transparent;
592
+ color: var(--gray);
593
+ font-size: 18px;
594
+ cursor: pointer;
595
+ padding: 2px;
596
+ line-height: 1;
597
+ margin-left: 8px;
598
+ flex-shrink: 0;
599
+ }
600
+
601
+ .toast-close:hover {
602
+ color: var(--dark);
603
+ }
604
+
605
+ @keyframes toast-enter {
606
+ to {
607
+ opacity: 1;
608
+ transform: translateY(0);
609
+ }
610
+ }
611
+
612
+ @keyframes toast-leave {
613
+ to {
614
+ opacity: 0;
615
+ transform: translateY(12px);
616
+ }
617
+ }
618
+
619
+ .variable-desc {
620
+ font-size: 12px;
621
+ color: var(--gray);
622
+ margin-top: 2px;
623
+ }
624
+
625
+ .group-section {
626
+ margin-bottom: 8px;
627
+ }
628
+
629
+ .group-header {
630
+ padding: 12px 15px;
631
+ font-size: 14px;
632
+ font-weight: 500;
633
+ color: var(--dark);
634
+ display: flex;
635
+ align-items: center;
636
+ justify-content: space-between;
637
+ cursor: pointer;
638
+ border-bottom: 1px solid var(--border);
639
+ }
640
+
641
+ .group-section.disabled {
642
+ position: relative;
643
+ }
644
+
645
+ .group-section.disabled .group-header {
646
+ position: relative;
647
+ color: #858a91;
648
+ background: rgba(0,0,0,0.02);
649
+ box-shadow: inset 0 -1px 0 rgba(0,0,0,0.06);
650
+ }
651
+
652
+ .group-section.disabled .group-header::before {
653
+ content: '';
654
+ position: absolute;
655
+ top: 8px;
656
+ left: 9px;
657
+ width: 40px;
658
+ height: 40px;
659
+ background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc1NicgaGVpZ2h0PSc1Nic+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSdnJyB4MT0nMCcgeTE9JzAnIHgyPScxJyB5Mj0nMSc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjZDVkOWRlJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjYWViNGJjJy8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBvbHlnb24gcG9pbnRzPScwLDAgMCw1NiA1NiwwJyBmaWxsPSd1cmwoI2cpJy8+PHRleHQgeD0nNicgeT0nMjQnIGZvbnQtZmFtaWx5PSdJbnRlcixzeXN0ZW0tdWksU2Vnb2UgVUksSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYnIGZvbnQtc2l6ZT0nMTInIGZpbGw9JyM3ZTdmODInIHRyYW5zZm9ybT0ncm90YXRlKC00NSAxOCAyMCknIGZvbnQtd2VpZ2h0PSc2MDAnPuWGu+e7kzwvdGV4dD48L3N2Zz4=") no-repeat;
660
+ background-size: contain;
661
+ pointer-events: none;
662
+ transform: translate(-8px, -8px);
663
+ }
664
+
665
+ .group-section.disabled .group-header:hover {
666
+ background: rgba(0,0,0,0.04);
667
+ }
668
+
669
+ .group-header:hover {
670
+ background: #f0f0f0;
671
+ }
672
+
673
+
674
+
675
+ .group-content {
676
+ overflow: hidden;
677
+ transition: max-height 0.3s ease-out;
678
+ }
679
+
680
+ .group-content.collapsed {
681
+ max-height: 0;
682
+ }
683
+
684
+ .group-content.expanded {
685
+ max-height: 2400px;
686
+ overflow: visible;
687
+ padding-top: 8px;
688
+ scrollbar-width: none;
689
+ }
690
+
691
+ .group-children {
692
+ display: flex;
693
+ flex-direction: column;
694
+ margin-top: 4px;
695
+ }
696
+
697
+ .group-count {
698
+ background: #9ea2a6;
699
+ color: white;
700
+ font-size: 11px;
701
+ padding: 2px 6px;
702
+ border-radius: 10px;
703
+ }
704
+
705
+ .group-section.disabled .group-count {
706
+ background: rgba(0,0,0,0.35);
707
+ }
708
+
709
+
710
+ .prompt-list {
711
+ padding: 0 15px 15px;
712
+ }
713
+
714
+ .prompt-item {
715
+ padding: 12px;
716
+ margin-bottom: 8px;
717
+ border-radius: 8px;
718
+ cursor: pointer;
719
+ position: relative;
720
+ background: #f5f5f5;
721
+ border: 1px solid transparent;
722
+ transition: all 0.2s;
723
+ margin-right: 10px;
724
+ overflow: hidden;
725
+ }
726
+
727
+ .prompt-info {
728
+ display: flex;
729
+ flex-direction: column;
730
+ justify-content: center;
731
+ width: 100%;
732
+ min-width: 0;
733
+ padding-right: 10px; /* 为 prompt-meta 预留空间 */
734
+ }
735
+
736
+ .prompt-name {
737
+ font-weight: 500;
738
+ margin-bottom: 4px;
739
+ white-space: nowrap;
740
+ overflow: hidden;
741
+ text-overflow: ellipsis;
742
+ }
743
+
744
+ .prompt-meta {
745
+ position: absolute;
746
+ right: 0;
747
+ top: 0;
748
+ bottom: 0;
749
+ width: 50px;
750
+ display: flex;
751
+ align-items: center;
752
+ justify-content: center;
753
+ }
754
+
755
+ .prompt-item:hover {
756
+ background: #eeeeee;
757
+ border-color: var(--primary);
758
+ box-shadow: 0 2px 8px rgba(0,102,255,0.1);
759
+ }
760
+
761
+ .prompt-item.active {
762
+ background: rgba(0,102,255,0.05);
763
+ border-color: var(--primary);
764
+ }
765
+
766
+ .prompt-desc {
767
+ font-size: 12px;
768
+ color: var(--gray);
769
+ line-height: 1.3;
770
+ height: 16px;
771
+ overflow: hidden;
772
+ position: relative;
773
+ text-overflow: ellipsis;
774
+ white-space: nowrap;
775
+ cursor: pointer;
776
+ }
777
+
778
+ .prompt-meta-hint {
779
+ font-size: 11px;
780
+ color: var(--gray);
781
+ margin-top: 4px;
782
+ text-transform: none;
783
+ }
784
+
785
+
786
+
787
+ /* Tooltip样式 */
788
+ .tooltip {
789
+ position: relative;
790
+ }
791
+
792
+ .tooltip::after {
793
+ content: attr(title);
794
+ position: absolute;
795
+ bottom: 100%;
796
+ left: 50%;
797
+ transform: translateX(-50%);
798
+ background: rgba(0, 0, 0, 0.8);
799
+ color: white;
800
+ padding: 8px 12px;
801
+ border-radius: 4px;
802
+ font-size: 12px;
803
+ white-space: pre-wrap;
804
+ word-wrap: break-word;
805
+ max-width: 300px;
806
+ z-index: 1000;
807
+ opacity: 0;
808
+ visibility: hidden;
809
+ transition: opacity 0.2s, visibility 0.2s;
810
+ pointer-events: none;
811
+ }
812
+
813
+ .tooltip:hover::after {
814
+ opacity: 1;
815
+ visibility: visible;
816
+ }
817
+
818
+ /* 启用状态边框样式 */
819
+ .prompt-item.enabled {
820
+ border-left: 4px solid #b8b3b3; /* 浅30%的深灰色,原#333调浅30%约为#666 */
821
+ }
822
+
823
+ /* 选中状态样式 */
824
+ .prompt-item.active {
825
+ border-left: 4px solid #666; /* 选中时的灰色边框 */
826
+ }
827
+
828
+ /* 悬停状态样式 */
829
+ .prompt-item:hover {
830
+ border-color: #666; /* 鼠标悬停时的灰色边框 */
831
+ }
832
+
833
+ /* 操作按钮样式 */
834
+ .prompt-actions {
835
+ display: flex;
836
+ flex-direction: column;
837
+ justify-content: center;
838
+ gap: 8px;
839
+ position: absolute;
840
+ right: 0;
841
+ top: 0;
842
+ bottom: 0;
843
+ width: 100px;
844
+ opacity: 0;
845
+ visibility: hidden;
846
+ padding: 12px 16px;
847
+ transform: translateX(100%);
848
+ transition: all 0.25s ease;
849
+ background: linear-gradient(to left,
850
+ rgba(238, 238, 238, 0.95) 0%,
851
+ rgba(238, 238, 238, 0.9) 30%,
852
+ rgba(238, 238, 238, 0.6) 60%,
853
+ rgba(238, 238, 238, 0) 100%);
854
+ z-index: 1;
855
+ }
856
+
857
+ .prompt-item {
858
+ padding-right: 24px; /* 为操作按钮预留空间 */
859
+ }
860
+
861
+ .prompt-item:hover .prompt-actions {
862
+ opacity: 1;
863
+ visibility: visible;
864
+ transform: translateX(0);
865
+ }
866
+
867
+ .action-btn {
868
+ background: rgba(255, 255, 255, 0.8);
869
+ border: 1px solid var(--border);
870
+ border-radius: 4px;
871
+ padding: 4px;
872
+ margin: -3px;
873
+ font-size: 11px;
874
+ cursor: pointer;
875
+ white-space: nowrap;
876
+ text-align: center;
877
+ transition: all 0.2s ease;
878
+ backdrop-filter: blur(2px);
879
+ width: 100%;
880
+ }
881
+
882
+ .action-btn:hover {
883
+ background: rgba(255, 255, 255, 0.95);
884
+ transform: translateX(-2px);
885
+ box-shadow: 0 1px 23px rgba(0,0,0,0.1);
886
+ }
887
+
888
+ /* 美化目录三角图标 */
889
+ .group-toggle {
890
+ position: relative;
891
+ width: 20px;
892
+ height: 20px;
893
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
894
+ }
895
+
896
+ .group-toggle::before {
897
+ content: '';
898
+ position: absolute;
899
+ width: 16px;
900
+ height: 16px;
901
+ top: 50%;
902
+ left: 50%;
903
+ transform: translate(-50%, -50%);
904
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat center;
905
+ background-size: contain;
906
+ opacity: 0.6;
907
+ transition: opacity 0.2s ease;
908
+ }
909
+
910
+ .group-toggle.collapsed {
911
+ transform: rotate(-90deg);
912
+ }
913
+
914
+ .group-header:hover .group-toggle::before {
915
+ opacity: 1;
916
+ }
917
+
918
+ .prompt-meta {
919
+ display: flex;
920
+ align-items: center;
921
+ gap: 6px;
922
+ }
923
+
924
+ .prompt-tag {
925
+ background: rgba(0,102,255,0.1);
926
+ color: var(--primary);
927
+ font-size: 11px;
928
+ padding: 2px 6px;
929
+ border-radius: 4px;
930
+ font-weight: 500;
931
+ }
932
+
933
+ .prompt-status {
934
+ width: 8px;
935
+ height: 8px;
936
+ border-radius: 50%;
937
+ }
938
+
939
+ .prompt-status.enabled {
940
+ background: var(--success);
941
+ }
942
+
943
+ .prompt-status.disabled {
944
+ background: var(--gray-light);
945
+ }
946
+
947
+ .prompt-list-empty {
948
+ padding: 16px 14px;
949
+ border: 1px dashed rgba(0,0,0,0.12);
950
+ border-radius: 10px;
951
+ color: var(--gray);
952
+ font-size: 13px;
953
+ line-height: 1.6;
954
+ background: rgba(0,0,0,0.02);
955
+ text-align: left;
956
+ }
957
+
958
+ .prompt-list-empty span {
959
+ display: block;
960
+ color: var(--dark);
961
+ font-weight: 550;
962
+ margin-bottom: 4px;
963
+ }
964
+
965
+ .group-modal-content {
966
+ min-width: 460px;
967
+ }
968
+
969
+ .group-modal-tabs {
970
+ display: flex;
971
+ gap: 8px;
972
+ padding: 4px;
973
+ background: rgba(0,0,0,0.03);
974
+ border-radius: 10px;
975
+ margin-bottom: 16px;
976
+ }
977
+
978
+ .group-modal-tab {
979
+ flex: 1;
980
+ border: none;
981
+ background: transparent;
982
+ border-radius: 8px;
983
+ padding: 10px 12px;
984
+ font-size: 14px;
985
+ font-weight: 500;
986
+ color: var(--gray);
987
+ cursor: pointer;
988
+ transition: all 0.2s ease;
989
+ }
990
+
991
+ .group-modal-tab:hover {
992
+ color: var(--primary);
993
+ }
994
+
995
+ .group-modal-tab.active {
996
+ background: white;
997
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
998
+ color: var(--primary);
999
+ font-weight: 580;
1000
+ }
1001
+
1002
+ .group-modal-panel {
1003
+ display: flex;
1004
+ flex-direction: column;
1005
+ gap: 12px;
1006
+ }
1007
+
1008
+ .group-modal-panel.hidden {
1009
+ display: none;
1010
+ }
1011
+
1012
+ .group-modal-hint {
1013
+ font-size: 12px;
1014
+ color: var(--gray);
1015
+ margin-top: 6px;
1016
+ }
1017
+
1018
+ .group-modal-footer {
1019
+ display: flex;
1020
+ gap: 12px;
1021
+ width: 100%;
1022
+ justify-content: flex-end;
1023
+ }
1024
+
1025
+ .group-modal-footer.hidden {
1026
+ display: none;
1027
+ }
1028
+
1029
+ .group-manage-toolbar {
1030
+ display: flex;
1031
+ align-items: center;
1032
+ gap: 10px;
1033
+ }
1034
+
1035
+ .group-manage-toolbar input {
1036
+ flex: 1;
1037
+ padding: 8px 12px;
1038
+ border: 1px solid var(--border);
1039
+ border-radius: 8px;
1040
+ font-size: 14px;
1041
+ transition: all 0.2s ease;
1042
+ }
1043
+
1044
+ .group-manage-toolbar input:focus {
1045
+ outline: none;
1046
+ border-color: var(--primary);
1047
+ box-shadow: 0 0 0 3px rgba(57,57,57,0.08);
1048
+ }
1049
+
1050
+ .group-manage-list {
1051
+ display: flex;
1052
+ flex-direction: column;
1053
+ gap: 10px;
1054
+ max-height: 300px;
1055
+ overflow-y: auto;
1056
+ padding-right: 4px;
1057
+ }
1058
+
1059
+ .group-manage-empty {
1060
+ padding: 18px 12px;
1061
+ text-align: center;
1062
+ color: var(--gray);
1063
+ font-size: 13px;
1064
+ border-radius: 10px;
1065
+ background: rgba(0,0,0,0.04);
1066
+ }
1067
+
1068
+ .group-manage-empty.hidden {
1069
+ display: none;
1070
+ }
1071
+
1072
+ .group-manage-item {
1073
+ --depth: 0;
1074
+ display: flex;
1075
+ align-items: flex-start;
1076
+ justify-content: space-between;
1077
+ gap: 12px;
1078
+ padding: 12px 14px;
1079
+ border: 1px solid rgba(0,0,0,0.08);
1080
+ border-radius: 12px;
1081
+ background: white;
1082
+ transition: all 0.2s ease;
1083
+ }
1084
+
1085
+ .group-manage-item:hover {
1086
+ border-color: rgba(0,0,0,0.15);
1087
+ box-shadow: 0 12px 30px rgba(15,23,42,0.12);
1088
+ }
1089
+
1090
+ .group-manage-item.is-disabled {
1091
+ opacity: 0.85;
1092
+ }
1093
+
1094
+ .group-manage-info {
1095
+ flex: 1;
1096
+ min-width: 0;
1097
+ }
1098
+
1099
+ .group-manage-name {
1100
+ display: flex;
1101
+ align-items: center;
1102
+ gap: 8px;
1103
+ font-size: 14px;
1104
+ font-weight: 560;
1105
+ color: var(--dark);
1106
+ padding-left: calc(var(--depth) * 16px);
1107
+ }
1108
+
1109
+ .group-manage-edit {
1110
+ display: flex;
1111
+ align-items: center;
1112
+ gap: 8px;
1113
+ padding-left: calc(var(--depth) * 16px);
1114
+ }
1115
+
1116
+ .group-manage-path {
1117
+ font-size: 12px;
1118
+ color: var(--gray);
1119
+ margin-top: 4px;
1120
+ word-break: break-all;
1121
+ }
1122
+
1123
+ .group-status-badge {
1124
+ font-size: 11px;
1125
+ padding: 2px 6px;
1126
+ border-radius: 999px;
1127
+ background: rgba(0,0,0,0.06);
1128
+ color: var(--gray);
1129
+ font-weight: 500;
1130
+ margin-left: 6px;
1131
+ }
1132
+
1133
+ .group-status-badge.enabled {
1134
+ background: rgba(40,167,69,0.12);
1135
+ color: var(--success);
1136
+ }
1137
+
1138
+ .group-status-badge.disabled {
1139
+ background: rgba(220,53,69,0.15);
1140
+ color: var(--danger);
1141
+ }
1142
+
1143
+ .group-status-badge.default {
1144
+ background: rgba(0,0,0,0.08);
1145
+ color: var(--dark);
1146
+ }
1147
+
1148
+ .group-manage-actions {
1149
+ display: flex;
1150
+ align-items: center;
1151
+ gap: 8px;
1152
+ }
1153
+
1154
+ .group-manage-action-btn {
1155
+ border: none;
1156
+ background: transparent;
1157
+ padding: 6px 8px;
1158
+ border-radius: 6px;
1159
+ font-size: 13px;
1160
+ color: var(--primary);
1161
+ cursor: pointer;
1162
+ transition: all 0.2s ease;
1163
+ }
1164
+
1165
+ .group-manage-action-btn:hover {
1166
+ background: rgba(0,0,0,0.05);
1167
+ }
1168
+
1169
+ .group-manage-action-btn.danger {
1170
+ color: var(--danger);
1171
+ }
1172
+
1173
+ .group-manage-action-btn[disabled] {
1174
+ cursor: not-allowed;
1175
+ opacity: 0.6;
1176
+ }
1177
+
1178
+ .group-manage-rename-input {
1179
+ width: 100%;
1180
+ padding: 8px 10px;
1181
+ border: 1px solid var(--border);
1182
+ border-radius: 8px;
1183
+ font-size: 14px;
1184
+ transition: all 0.2s ease;
1185
+ }
1186
+
1187
+ .group-manage-rename-input:focus {
1188
+ outline: none;
1189
+ border-color: var(--primary);
1190
+ box-shadow: 0 0 0 3px rgba(57,57,57,0.08);
1191
+ }
1192
+
1193
+
1194
+ .editor-container {
1195
+ flex: 1;
1196
+ display: flex;
1197
+ flex-direction: column;
1198
+ background: white;
1199
+ border-left: 1px solid var(--border);
1200
+ }
1201
+
1202
+ .editor-header {
1203
+ padding: 15px 20px;
1204
+ /* border-bottom: 1px solid var(--border); */
1205
+ display: flex;
1206
+ flex-direction: column;
1207
+ gap: 12px;
1208
+ background: white;
1209
+ }
1210
+
1211
+ .editor-header-top {
1212
+ display: flex;
1213
+ gap: 16px;
1214
+ align-items: center;
1215
+ width: 100%;
1216
+ }
1217
+
1218
+ .editor-header-top input,
1219
+ .prompt-description {
1220
+ padding: 8px 12px;
1221
+ border: 1px solid var(--border);
1222
+ border-radius: 6px;
1223
+ font-size: 14px;
1224
+ background: white;
1225
+ }
1226
+
1227
+ .editor-header-top input {
1228
+ flex: 1;
1229
+ min-width: 0;
1230
+ }
1231
+
1232
+ .editor-header-top input:focus,
1233
+ .prompt-description:focus {
1234
+ outline: none;
1235
+ border-color: var(--primary);
1236
+ }
1237
+
1238
+ .group-selector {
1239
+ position: relative;
1240
+ flex-shrink: 0;
1241
+ }
1242
+
1243
+ .group-selector-btn {
1244
+ display: flex;
1245
+ align-items: center;
1246
+ gap: 8px;
1247
+ min-width: 160px;
1248
+ padding: 8px 12px;
1249
+ border: 1px solid var(--border);
1250
+ border-radius: 6px;
1251
+ background: white;
1252
+ font-size: 14px;
1253
+ color: var(--dark);
1254
+ cursor: pointer;
1255
+ transition: all 0.2s ease;
1256
+ }
1257
+
1258
+ .group-selector-btn:hover {
1259
+ border-color: var(--primary);
1260
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
1261
+ transform: translateY(-1px);
1262
+ }
1263
+
1264
+ .group-selector-label {
1265
+ color: var(--gray);
1266
+ font-size: 12px;
1267
+ letter-spacing: 0;
1268
+ }
1269
+
1270
+ .group-selector-value {
1271
+ font-weight: 500;
1272
+ color: var(--primary);
1273
+ max-width: 160px;
1274
+ overflow: hidden;
1275
+ text-overflow: ellipsis;
1276
+ white-space: nowrap;
1277
+ }
1278
+
1279
+ .group-selector-icon {
1280
+ margin-left: auto;
1281
+ display: flex;
1282
+ align-items: center;
1283
+ justify-content: center;
1284
+ color: var(--gray);
1285
+ }
1286
+
1287
+ .group-selector-input {
1288
+ position: absolute;
1289
+ inset: 0;
1290
+ opacity: 0;
1291
+ pointer-events: none;
1292
+ }
1293
+
1294
+ .prompt-description {
1295
+ width: 100%;
1296
+ min-height: 38px;
1297
+ line-height: 1.5;
1298
+ resize: none;
1299
+ overflow-y: hidden;
1300
+ transition: border-color 0.2s ease;
1301
+ }
1302
+
1303
+ .prompt-description::-webkit-scrollbar {
1304
+ width: 6px;
1305
+ }
1306
+
1307
+ .prompt-description::-webkit-scrollbar-thumb {
1308
+ background: var(--gray-light);
1309
+ border-radius: 3px;
1310
+ }
1311
+
1312
+ .editor-controls {
1313
+ display: flex;
1314
+ align-items: center;
1315
+ gap: 16px;
1316
+ margin-left: auto;
1317
+ }
1318
+
1319
+ .mode-toggle {
1320
+ display: inline-flex;
1321
+ align-items: center;
1322
+ padding: 4px;
1323
+ border-radius: 999px;
1324
+ background: var(--light);
1325
+ border: 1px solid var(--border);
1326
+ }
1327
+
1328
+ .mode-btn {
1329
+ padding: 6px 18px;
1330
+ border: none;
1331
+ background: transparent;
1332
+ border-radius: 999px;
1333
+ font-size: 13px;
1334
+ font-weight: 500;
1335
+ cursor: pointer;
1336
+ color: var(--gray);
1337
+ transition: all 0.2s;
1338
+ }
1339
+
1340
+ .mode-btn.active {
1341
+ background: white;
1342
+ color: var(--primary);
1343
+ box-shadow: 0 1px 4px rgba(0,0,0,0.08);
1344
+ }
1345
+
1346
+ .editor-content {
1347
+ display: flex;
1348
+ flex-direction: column;
1349
+ flex: 1;
1350
+ gap: 16px;
1351
+ margin: 12px 20px 20px;
1352
+ overflow-y: auto;
1353
+ scrollbar-width: none;
1354
+ }
1355
+
1356
+ .arguments-section {
1357
+ padding: 18px 20px;
1358
+ background: white;
1359
+ border: 1px solid var(--border);
1360
+ border-radius: 12px;
1361
+ box-shadow: 0 8px 24px rgba(0,0,0,0.05);
1362
+ display: flex;
1363
+ flex-direction: column;
1364
+ gap: 16px;
1365
+ }
1366
+
1367
+ .arguments-section.has-error {
1368
+ border-color: rgba(220,53,69,0.4);
1369
+ box-shadow: 0 0 0 2px rgba(220,53,69,0.08), 0 8px 24px rgba(220,53,69,0.08);
1370
+ }
1371
+
1372
+ .arguments-header {
1373
+ display: flex;
1374
+ align-items: center;
1375
+ justify-content: space-between;
1376
+ gap: 12px;
1377
+ flex-wrap: wrap;
1378
+ }
1379
+
1380
+ .arguments-title {
1381
+ display: flex;
1382
+ flex-direction: column;
1383
+ gap: 4px;
1384
+ }
1385
+
1386
+ .arguments-title span {
1387
+ font-size: 16px;
1388
+ font-weight: 600;
1389
+ color: var(--dark);
1390
+ }
1391
+
1392
+ .arguments-title small {
1393
+ font-size: 12px;
1394
+ color: var(--gray);
1395
+ }
1396
+
1397
+ .arguments-actions {
1398
+ display: flex;
1399
+ gap: 10px;
1400
+ align-items: center;
1401
+ }
1402
+
1403
+ .arguments-list {
1404
+ display: grid;
1405
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
1406
+ gap: 12px;
1407
+ }
1408
+
1409
+ .arguments-empty {
1410
+ grid-column: 1 / -1;
1411
+ font-size: 13px;
1412
+ color: var(--gray);
1413
+ background: var(--light);
1414
+ border: 1px dashed var(--border);
1415
+ border-radius: 8px;
1416
+ padding: 16px;
1417
+ text-align: center;
1418
+ }
1419
+
1420
+ .argument-card {
1421
+ border: 1px solid var(--border);
1422
+ border-radius: 12px;
1423
+ background: linear-gradient(180deg, rgba(248,249,250,0.9), rgba(255,255,255,0.9));
1424
+ padding: 16px;
1425
+ display: flex;
1426
+ flex-direction: column;
1427
+ gap: 12px;
1428
+ /* min-height: 150px; */
1429
+ position: relative;
1430
+ overflow: hidden;
1431
+ }
1432
+
1433
+ .argument-card.argument-card-unused {
1434
+ border-color: rgba(220,53,69,0.45);
1435
+ box-shadow: 0 0 0 1px rgba(220,53,69,0.18);
1436
+ background: rgba(220,53,69,0.04);
1437
+ }
1438
+
1439
+ .argument-card-header {
1440
+ display: flex;
1441
+ justify-content: space-between;
1442
+ align-items: flex-start;
1443
+ gap: 8px;
1444
+ }
1445
+
1446
+ .argument-name {
1447
+ font-size: 15px;
1448
+ font-weight: 600;
1449
+ color: var(--primary);
1450
+ word-break: break-all;
1451
+ }
1452
+
1453
+ .argument-badges {
1454
+ display: flex;
1455
+ flex-wrap: wrap;
1456
+ gap: 6px;
1457
+ }
1458
+
1459
+ .argument-badge {
1460
+ padding: 2px 8px;
1461
+ border-radius: 999px;
1462
+ font-size: 11px;
1463
+ background: rgba(0,0,0,0.04);
1464
+ color: var(--gray);
1465
+ }
1466
+
1467
+ .argument-required {
1468
+ background: rgba(220,53,69,0.12);
1469
+ color: var(--danger);
1470
+ font-weight: 600;
1471
+ }
1472
+
1473
+ .argument-body {
1474
+ display: flex;
1475
+ flex-direction: column;
1476
+ gap: 6px;
1477
+ color: var(--gray);
1478
+ font-size: 12px;
1479
+ /* margin-bottom: 24px; */
1480
+ }
1481
+
1482
+ .argument-body .argument-description {
1483
+ color: var(--dark);
1484
+ font-size: 13px;
1485
+ line-height: 1.5;
1486
+ display: -webkit-box;
1487
+ -webkit-line-clamp: 3;
1488
+ -webkit-box-orient: vertical;
1489
+ overflow: hidden;
1490
+ }
1491
+
1492
+ .argument-body .argument-placeholder {
1493
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1494
+ background: rgba(0,0,0,0.04);
1495
+ padding: 2px 6px;
1496
+ border-radius: 4px;
1497
+ color: var(--primary);
1498
+ display: inline-block;
1499
+ }
1500
+
1501
+ .argument-actions {
1502
+ display: flex;
1503
+ justify-content: center;
1504
+ gap: 8px;
1505
+ opacity: 0;
1506
+ visibility: hidden;
1507
+ transition: all 0.25s ease;
1508
+ position: absolute;
1509
+ left: 0;
1510
+ right: 0;
1511
+ bottom: 0;
1512
+ padding: 12px;
1513
+ background: linear-gradient(to top,
1514
+ rgba(255, 255, 255, 0.95) 0%,
1515
+ rgba(255, 255, 255, 0.9) 50%,
1516
+ rgba(255, 255, 255, 0) 100%);
1517
+ transform: translateY(100%);
1518
+ }
1519
+
1520
+ .argument-card:hover .argument-actions {
1521
+ opacity: 1;
1522
+ visibility: visible;
1523
+ transform: translateY(0);
1524
+ }
1525
+
1526
+ .argument-action-btn {
1527
+ border: none;
1528
+ background: rgba(0,0,0,0.06);
1529
+ color: var(--primary);
1530
+ font-size: 12px;
1531
+ padding: 6px 12px;
1532
+ border-radius: 6px;
1533
+ cursor: pointer;
1534
+ transition: all 0.2s ease;
1535
+ backdrop-filter: blur(4px);
1536
+ }
1537
+
1538
+ .argument-action-btn:hover {
1539
+ background: rgba(0,0,0,0.1);
1540
+ transform: translateY(-1px);
1541
+ }
1542
+
1543
+ .argument-action-btn.delete {
1544
+ color: var(--danger);
1545
+ background: rgba(220,53,69,0.12);
1546
+ }
1547
+
1548
+ .argument-action-btn.delete:hover {
1549
+ background: rgba(220,53,69,0.18);
1550
+ }
1551
+
1552
+ .modal {
1553
+ position: fixed;
1554
+ inset: 0;
1555
+ background: rgba(17, 17, 17, 0.35);
1556
+ display: flex;
1557
+ align-items: center;
1558
+ justify-content: center;
1559
+ z-index: 1200;
1560
+ padding: 24px;
1561
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1562
+ backdrop-filter: blur(4px);
1563
+ }
1564
+
1565
+ .modal.hidden {
1566
+ opacity: 0;
1567
+ pointer-events: none;
1568
+ }
1569
+
1570
+ .modal.hidden .modal-content {
1571
+ transform: scale(0.95) translateY(10px);
1572
+ }
1573
+
1574
+ .modal-dialog {
1575
+ width: min(420px, 90vw);
1576
+ margin: auto;
1577
+ }
1578
+
1579
+ .modal-content {
1580
+ background: white;
1581
+ border-radius: 16px;
1582
+ box-shadow: 0 25px 50px -12px rgba(0,0,0,0.15);
1583
+ overflow: hidden;
1584
+ transform: scale(1) translateY(0);
1585
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1586
+ border: 1px solid rgba(0,0,0,0.08);
1587
+ }
1588
+
1589
+ .modal-header {
1590
+ padding: 20px 24px;
1591
+ border-bottom: 1px solid var(--border);
1592
+ display: flex;
1593
+ align-items: center;
1594
+ justify-content: space-between;
1595
+ background: linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,0.95));
1596
+ }
1597
+
1598
+ .modal-header h3 {
1599
+ font-size: 18px;
1600
+ font-weight: 580;
1601
+ color: var(--dark);
1602
+ letter-spacing: -0.01em;
1603
+ margin: 0;
1604
+ }
1605
+
1606
+ .modal-close {
1607
+ width: 32px;
1608
+ height: 32px;
1609
+ border: none;
1610
+ background: transparent;
1611
+ border-radius: 8px;
1612
+ color: var(--gray);
1613
+ cursor: pointer;
1614
+ display: flex;
1615
+ align-items: center;
1616
+ justify-content: center;
1617
+ transition: all 0.2s ease;
1618
+ padding: 0;
1619
+ }
1620
+
1621
+ .modal-close:hover {
1622
+ background: rgba(0,0,0,0.04);
1623
+ color: var(--dark);
1624
+ }
1625
+
1626
+ .modal-body {
1627
+ padding: 24px;
1628
+ }
1629
+
1630
+ .modal-footer {
1631
+ padding: 16px 24px;
1632
+ border-top: 1px solid var(--border);
1633
+ display: flex;
1634
+ justify-content: flex-end;
1635
+ gap: 12px;
1636
+ background: rgba(248,249,250,0.65);
1637
+ }
1638
+
1639
+ .group-dropdown {
1640
+ position: absolute;
1641
+ top: calc(100% + 8px);
1642
+ right: 0;
1643
+ width: 420px;
1644
+ max-height: 420px;
1645
+ padding: 16px;
1646
+ border-radius: 14px;
1647
+ background: white;
1648
+ border: 1px solid rgba(0,0,0,0.08);
1649
+ box-shadow: 0 20px 50px rgba(15, 23, 42, 0.18);
1650
+ display: none;
1651
+ flex-direction: column;
1652
+ gap: 14px;
1653
+ z-index: 1200;
1654
+ }
1655
+
1656
+ .group-dropdown.show {
1657
+ display: flex;
1658
+ }
1659
+
1660
+ .group-dropdown-search input {
1661
+ width: 100%;
1662
+ padding: 10px 14px;
1663
+ border: 1px solid var(--border);
1664
+ border-radius: 8px;
1665
+ font-size: 14px;
1666
+ transition: all 0.2s ease;
1667
+ }
1668
+
1669
+ .group-dropdown-search input:focus {
1670
+ outline: none;
1671
+ border-color: var(--primary);
1672
+ box-shadow: 0 0 0 3px rgba(57, 57, 57, 0.08);
1673
+ }
1674
+
1675
+ .group-dropdown-body {
1676
+ display: flex;
1677
+ flex-direction: column;
1678
+ gap: 10px;
1679
+ min-height: 220px;
1680
+ }
1681
+
1682
+ .group-cascader {
1683
+ display: flex;
1684
+ gap: 12px;
1685
+ }
1686
+
1687
+ .group-cascader-column {
1688
+ flex: 1;
1689
+ min-width: 0;
1690
+ display: flex;
1691
+ flex-direction: column;
1692
+ gap: 4px;
1693
+ max-height: 260px;
1694
+ overflow-y: auto;
1695
+ padding-right: 4px;
1696
+ border-right: 1px solid rgba(0,0,0,0.06);
1697
+ }
1698
+
1699
+ .group-cascader-column:last-child {
1700
+ border-right: none;
1701
+ }
1702
+
1703
+ .group-cascader-title {
1704
+ font-size: 12px;
1705
+ color: var(--gray);
1706
+ letter-spacing: 0;
1707
+ padding: 0 6px;
1708
+ }
1709
+
1710
+ .group-cascader-list {
1711
+ display: flex;
1712
+ flex-direction: column;
1713
+ gap: 2px;
1714
+ }
1715
+
1716
+ .group-cascader-empty {
1717
+ padding: 6px 4px;
1718
+ font-size: 12px;
1719
+ color: var(--gray);
1720
+ }
1721
+
1722
+ .group-cascader-item {
1723
+ border: none;
1724
+ background: transparent;
1725
+ text-align: left;
1726
+ padding: 6px 8px;
1727
+ border-radius: 8px;
1728
+ font-size: 13px;
1729
+ color: var(--dark);
1730
+ cursor: pointer;
1731
+ transition: background 0.18s ease, color 0.18s ease, padding-left 0.18s ease;
1732
+ position: relative;
1733
+ }
1734
+
1735
+ .group-cascader-item:hover {
1736
+ background: rgba(0,0,0,0.05);
1737
+ color: var(--primary);
1738
+ padding-left: 12px;
1739
+ }
1740
+
1741
+ .group-cascader-item.active {
1742
+ background: rgba(0,0,0,0.04);
1743
+ color: var(--primary);
1744
+ }
1745
+
1746
+ .group-cascader-label {
1747
+ display: inline-flex;
1748
+ align-items: center;
1749
+ gap: 6px;
1750
+ }
1751
+
1752
+ .group-cascader-path-hint {
1753
+ font-size: 11px;
1754
+ color: var(--gray);
1755
+ letter-spacing: 0;
1756
+ margin-left: 6px;
1757
+ }
1758
+
1759
+ .group-cascader-suffix {
1760
+ margin-left: auto;
1761
+ font-size: 12px;
1762
+ color: var(--gray);
1763
+ transition: color 0.18s ease;
1764
+ display: inline-flex;
1765
+ align-items: center;
1766
+ }
1767
+
1768
+ .group-cascader-item:hover .group-cascader-suffix,
1769
+ .group-cascader-item.active .group-cascader-suffix,
1770
+ .group-cascader-item.selected .group-cascader-suffix {
1771
+ color: var(--primary);
1772
+ }
1773
+
1774
+ .group-cascader-check {
1775
+ margin-left: auto;
1776
+ font-size: 12px;
1777
+ color: var(--primary);
1778
+ display: inline-flex;
1779
+ align-items: center;
1780
+ }
1781
+
1782
+ .group-search-results {
1783
+ display: none;
1784
+ flex-direction: column;
1785
+ gap: 6px;
1786
+ max-height: 280px;
1787
+ overflow-y: auto;
1788
+ padding-right: 4px;
1789
+ }
1790
+
1791
+ .group-search-results.show {
1792
+ display: flex;
1793
+ }
1794
+
1795
+ .group-search-item {
1796
+ border: 1px solid rgba(0,0,0,0.06);
1797
+ border-radius: 10px;
1798
+ padding: 8px 10px;
1799
+ display: flex;
1800
+ flex-direction: column;
1801
+ gap: 4px;
1802
+ cursor: pointer;
1803
+ transition: all 0.2s ease;
1804
+ background: white;
1805
+ text-align: left;
1806
+ font-size: 13px;
1807
+ color: var(--dark);
1808
+ }
1809
+
1810
+ .group-search-item:hover {
1811
+ border-color: var(--primary);
1812
+ box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12);
1813
+ transform: translateY(-1px);
1814
+ }
1815
+
1816
+ .group-search-item.selected {
1817
+ border-color: var(--primary);
1818
+ box-shadow: inset 0 0 0 1px var(--primary);
1819
+ }
1820
+
1821
+ .group-search-path {
1822
+ font-size: 12px;
1823
+ color: var(--gray);
1824
+ letter-spacing: 0;
1825
+ }
1826
+
1827
+ .group-dropdown-empty {
1828
+ padding: 18px 12px;
1829
+ border-radius: 10px;
1830
+ text-align: center;
1831
+ background: rgba(0,0,0,0.03);
1832
+ color: var(--gray);
1833
+ font-size: 13px;
1834
+ }
1835
+
1836
+ .group-dropdown-empty.hidden {
1837
+ display: none;
1838
+ }
1839
+
1840
+ .form-group {
1841
+ margin-bottom: 0;
1842
+ }
1843
+
1844
+ .form-group label {
1845
+ display: block;
1846
+ margin-bottom: 8px;
1847
+ font-size: 14px;
1848
+ font-weight: 500;
1849
+ color: var(--gray);
1850
+ }
1851
+
1852
+ .form-group input {
1853
+ width: 100%;
1854
+ padding: 12px 14px;
1855
+ border: 1.5px solid var(--border);
1856
+ border-radius: 10px;
1857
+ font-size: 15px;
1858
+ transition: all 0.2s ease;
1859
+ background: white;
1860
+ }
1861
+
1862
+ .form-group input:focus {
1863
+ outline: none;
1864
+ border-color: var(--primary);
1865
+ box-shadow: 0 0 0 3px rgba(57,57,57,0.08);
1866
+ }
1867
+
1868
+ .form-group input::placeholder {
1869
+ color: #adb5bd;
1870
+ opacity: 0.8;
1871
+ }
1872
+
1873
+ .argument-modal {
1874
+ position: fixed;
1875
+ inset: 0;
1876
+ background: rgba(17, 17, 17, 0.35);
1877
+ display: flex;
1878
+ align-items: center;
1879
+ justify-content: center;
1880
+ z-index: 1200;
1881
+ padding: 24px;
1882
+ transition: opacity 0.2s ease;
1883
+ }
1884
+
1885
+ .argument-modal.hidden {
1886
+ opacity: 0;
1887
+ pointer-events: none;
1888
+ }
1889
+
1890
+ .argument-modal-dialog {
1891
+ width: min(520px, 100%);
1892
+ background: white;
1893
+ border-radius: 12px;
1894
+ box-shadow: 0 24px 64px rgba(0,0,0,0.18);
1895
+ display: flex;
1896
+ flex-direction: column;
1897
+ overflow: hidden;
1898
+ border: 1px solid rgba(0,0,0,0.06);
1899
+ }
1900
+
1901
+ .argument-modal-header {
1902
+ display: flex;
1903
+ justify-content: space-between;
1904
+ align-items: center;
1905
+ padding: 18px 24px;
1906
+ border-bottom: 1px solid var(--border);
1907
+ }
1908
+
1909
+ .argument-modal-header h3 {
1910
+ font-size: 18px;
1911
+ font-weight: 600;
1912
+ color: var(--dark);
1913
+ }
1914
+
1915
+ .argument-modal-close {
1916
+ border: none;
1917
+ background: transparent;
1918
+ font-size: 24px;
1919
+ line-height: 1;
1920
+ color: var(--gray);
1921
+ cursor: pointer;
1922
+ padding: 4px;
1923
+ }
1924
+
1925
+ .argument-modal-close:hover {
1926
+ color: var(--dark);
1927
+ }
1928
+
1929
+ .argument-modal-body {
1930
+ padding: 20px 24px 8px;
1931
+ }
1932
+
1933
+ .argument-form-grid {
1934
+ display: grid;
1935
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1936
+ gap: 16px 18px;
1937
+ }
1938
+
1939
+ .form-field {
1940
+ display: flex;
1941
+ flex-direction: column;
1942
+ gap: 6px;
1943
+ }
1944
+
1945
+ .form-field.full-width {
1946
+ grid-column: 1 / -1;
1947
+ }
1948
+
1949
+ .form-field-inline {
1950
+ align-self: end;
1951
+ height: 100%;
1952
+ display: flex;
1953
+ align-items: flex-end;
1954
+ padding-top: 26px;
1955
+ }
1956
+
1957
+ .argument-modal label {
1958
+ font-size: 13px;
1959
+ color: var(--gray);
1960
+ font-weight: 500;
1961
+ }
1962
+
1963
+ .argument-modal .required {
1964
+ color: var(--danger);
1965
+ font-weight: 600;
1966
+ font-size: 12px;
1967
+ }
1968
+
1969
+ .argument-modal input[type="text"],
1970
+ .argument-modal textarea {
1971
+ border: 1px solid var(--border);
1972
+ border-radius: 6px;
1973
+ padding: 10px 12px;
1974
+ font-size: 14px;
1975
+ background: white;
1976
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
1977
+ }
1978
+
1979
+ .argument-modal input[type="text"]:focus,
1980
+ .argument-modal textarea:focus {
1981
+ outline: none;
1982
+ border-color: var(--primary);
1983
+ box-shadow: 0 0 0 3px rgba(57,57,57,0.08);
1984
+ }
1985
+
1986
+ .argument-modal textarea {
1987
+ resize: vertical;
1988
+ min-height: 96px;
1989
+ }
1990
+
1991
+ .checkbox-field {
1992
+ display: inline-flex;
1993
+ align-items: center;
1994
+ gap: 8px;
1995
+ font-size: 13px;
1996
+ color: var(--dark);
1997
+ padding: 10px 12px;
1998
+ border: 1px solid var(--border);
1999
+ border-radius: 6px;
2000
+ }
2001
+
2002
+ .checkbox-field input {
2003
+ accent-color: var(--primary);
2004
+ }
2005
+
2006
+ .argument-modal-footer {
2007
+ display: flex;
2008
+ justify-content: flex-end;
2009
+ gap: 12px;
2010
+ padding: 16px 24px 20px;
2011
+ border-top: 1px solid var(--border);
2012
+ background: rgba(248,249,250,0.65);
2013
+ }
2014
+
2015
+ body.modal-open {
2016
+ overflow: hidden;
2017
+ }
2018
+
2019
+ .editor-body {
2020
+ position: relative;
2021
+ flex: 1;
2022
+ box-sizing: border-box;
2023
+ min-height: 0;
2024
+ }
2025
+
2026
+ .workspace-pane {
2027
+ position: absolute;
2028
+ inset: 0;
2029
+ display: flex;
2030
+ flex-direction: column;
2031
+ background: white;
2032
+ transition: opacity 0.25s ease;
2033
+ border-radius: 12px;
2034
+ overflow: hidden;
2035
+ border: 1px solid var(--border);
2036
+ box-shadow: 0 8px 24px rgba(0,0,0,0.05);
2037
+ min-height: 400px;
2038
+ scrollbar-width: none;
2039
+ }
2040
+
2041
+ .workspace-pane.hidden {
2042
+ opacity: 0;
2043
+ visibility: hidden;
2044
+ pointer-events: none;
2045
+ }
2046
+
2047
+ .pane-header {
2048
+ padding: 16px 24px;
2049
+ border-bottom: 1px solid var(--border);
2050
+ background: white;
2051
+ font-size: 14px;
2052
+ font-weight: 600;
2053
+ color: var(--gray);
2054
+ }
2055
+
2056
+ .pane-content {
2057
+ flex: 1;
2058
+ display: flex;
2059
+ flex-direction: column;
2060
+ padding: 0;
2061
+ min-height: 0;
2062
+ }
2063
+
2064
+ .pane-content.preview {
2065
+ background: var(--preview-bg);
2066
+ padding: 24px;
2067
+ }
2068
+
2069
+ .CodeMirror {
2070
+ flex: 1;
2071
+ height: 100%;
2072
+ font-size: 14px;
2073
+ line-height: 1.5;
2074
+ padding-left: 8px;
2075
+ }
2076
+
2077
+ .CodeMirror-gutters {
2078
+ display: none;
2079
+ }
2080
+
2081
+ .CodeMirror-scroll {
2082
+ min-height: 100%;
2083
+ }
2084
+
2085
+ .preview-content {
2086
+ flex: 1;
2087
+ padding: 24px;
2088
+ overflow: auto;
2089
+ background: white;
2090
+ border-radius: 12px;
2091
+ box-shadow: 0 8px 24px rgba(0,0,0,0.08);
2092
+ scrollbar-width: thin;
2093
+ scrollbar-color: var(--gray-light) transparent;
2094
+ }
2095
+
2096
+ /* 预览模式下的 CodeMirror 样式调整 */
2097
+ .preview-content .CodeMirror {
2098
+ height: auto;
2099
+ background: transparent;
2100
+ border: none;
2101
+ padding: 0;
2102
+ font-size: 14px;
2103
+ }
2104
+
2105
+ .preview-content .CodeMirror-scroll {
2106
+ overflow: hidden !important;
2107
+ }
2108
+
2109
+ .preview-content .CodeMirror-sizer {
2110
+ border-right: none !important;
2111
+ }
2112
+
2113
+ .preview-content .CodeMirror-gutters {
2114
+ display: none;
2115
+ }
2116
+
2117
+ .preview-content .CodeMirror-cursor {
2118
+ display: none !important;
2119
+ }
2120
+
2121
+ @media (max-width: 1100px) {
2122
+ .arguments-list {
2123
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
2124
+ }
2125
+ }
2126
+
2127
+ .empty-state {
2128
+ text-align: center;
2129
+ padding: 40px 20px;
2130
+ color: var(--gray);
2131
+ }
2132
+
2133
+ .empty-state h3 {
2134
+ margin-bottom: 8px;
2135
+ font-weight: 500;
2136
+ }
2137
+
2138
+ .empty-state p {
2139
+ font-size: 14px;
2140
+ margin-bottom: 16px;
2141
+ }
2142
+
2143
+ .login-container {
2144
+ position: fixed;
2145
+ top: 0;
2146
+ left: 0;
2147
+ right: 0;
2148
+ bottom: 0;
2149
+ background: linear-gradient(135deg,
2150
+ rgba(255,255,255,0.92),
2151
+ rgba(248,249,250,0.95),
2152
+ rgba(255,255,255,0.98)
2153
+ );
2154
+ display: flex;
2155
+ align-items: center;
2156
+ justify-content: center;
2157
+ z-index: 1000;
2158
+ backdrop-filter: blur(12px);
2159
+ }
2160
+
2161
+ .login-box {
2162
+ width: 400px;
2163
+ background: white;
2164
+ border-radius: 20px;
2165
+ box-shadow: 0 25px 50px -12px rgba(0,0,0,0.15);
2166
+ padding: 48px 40px;
2167
+ text-align: center;
2168
+ border: 1px solid rgba(0,0,0,0.06);
2169
+ position: relative;
2170
+ animation: login-box-show 0.5s cubic-bezier(0.21, 1.02, 0.73, 1) forwards;
2171
+ backdrop-filter: blur(10px);
2172
+ background: linear-gradient(180deg,
2173
+ rgba(255, 255, 255, 0.98) 0%,
2174
+ rgba(255, 255, 255, 0.96) 100%
2175
+ );
2176
+ }
2177
+
2178
+ @keyframes login-box-show {
2179
+ 0% {
2180
+ opacity: 0;
2181
+ transform: translateY(20px);
2182
+ }
2183
+ 100% {
2184
+ opacity: 1;
2185
+ transform: translateY(0);
2186
+ }
2187
+ }
2188
+
2189
+ .login-box h2 {
2190
+ font-size: 28px;
2191
+ font-weight: 600;
2192
+ color: var(--primary);
2193
+ margin-bottom: 36px;
2194
+ position: relative;
2195
+ display: inline-block;
2196
+ }
2197
+
2198
+ .login-box h2::after {
2199
+ content: '';
2200
+ position: absolute;
2201
+ bottom: -12px;
2202
+ left: 50%;
2203
+ transform: translateX(-50%);
2204
+ width: 45px;
2205
+ height: 4px;
2206
+ background: linear-gradient(to right, var(--primary), var(--primary-dark));
2207
+ border-radius: 4px;
2208
+ opacity: 0.8;
2209
+ }
2210
+
2211
+ .login-box .input-group {
2212
+ position: relative;
2213
+ margin-bottom: 24px;
2214
+ width: 100%;
2215
+ }
2216
+
2217
+ .login-box .input-group::before {
2218
+ content: '';
2219
+ position: absolute;
2220
+ left: 16px;
2221
+ top: 50%;
2222
+ transform: translateY(-50%);
2223
+ width: 16px;
2224
+ height: 16px;
2225
+ opacity: 0.5;
2226
+ transition: opacity 0.2s ease;
2227
+ background-size: contain;
2228
+ background-repeat: no-repeat;
2229
+ }
2230
+
2231
+ .login-box .input-group.username::before {
2232
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
2233
+ }
2234
+
2235
+ .login-box .input-group.password::before {
2236
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E");
2237
+ }
2238
+
2239
+ .login-box .input-group:focus-within::before {
2240
+ opacity: 1;
2241
+ }
2242
+
2243
+ .login-box input {
2244
+ width: 100%;
2245
+ padding: 14px 15px 14px 48px;
2246
+ border: 1.5px solid var(--border);
2247
+ border-radius: 12px;
2248
+ font-size: 16px;
2249
+ font-weight: 450;
2250
+ letter-spacing: -0.01em;
2251
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
2252
+ background: rgba(255, 255, 255, 0.9);
2253
+ box-shadow: 0 2px 6px rgba(0,0,0,0.02);
2254
+ }
2255
+
2256
+ .login-box input:focus {
2257
+ outline: none;
2258
+ border-color: var(--primary);
2259
+ border-width: 1.5px;
2260
+ box-shadow: 0 0 0 3px rgba(57,57,57,0.08), 0 2px 8px rgba(0,0,0,0.04);
2261
+ background: white;
2262
+ }
2263
+
2264
+ .login-box input::placeholder {
2265
+ color: #adb5bd;
2266
+ font-size: 15px;
2267
+ font-weight: 450;
2268
+ letter-spacing: -0.01em;
2269
+ opacity: 0.85;
2270
+ }
2271
+
2272
+ .login-box button {
2273
+ width: 100%;
2274
+ padding: 16px 132px;
2275
+ margin-top: 32px;
2276
+ border: none;
2277
+ border-radius: 12px;
2278
+ font-size: 16px;
2279
+ font-weight: 520;
2280
+ background: var(--primary);
2281
+ color: white;
2282
+ cursor: pointer;
2283
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2284
+ position: relative;
2285
+ overflow: hidden;
2286
+ letter-spacing: 0.02em;
2287
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
2288
+ }
2289
+
2290
+ .login-box button::before {
2291
+ content: '';
2292
+ position: absolute;
2293
+ top: 0;
2294
+ left: 0;
2295
+ right: 0;
2296
+ bottom: 0;
2297
+ background: linear-gradient(to right, transparent, rgba(255,255,255,0.1), transparent);
2298
+ transform: translateX(-100%);
2299
+ transition: transform 0.8s ease;
2300
+ }
2301
+
2302
+ .login-box button:hover {
2303
+ background: var(--primary-dark);
2304
+ transform: translateY(-2px);
2305
+ box-shadow: 0 8px 16px rgba(0,0,0,0.12);
2306
+ }
2307
+
2308
+ .login-box button:active {
2309
+ transform: translateY(0);
2310
+ box-shadow: 0 3px 8px rgba(0,0,0,0.08);
2311
+ background: var(--primary);
2312
+ }
2313
+
2314
+ .login-box button:hover::before {
2315
+ transform: translateX(100%);
2316
+ }
2317
+
2318
+ .error-msg {
2319
+ color: var(--danger);
2320
+ font-size: 13px;
2321
+ margin-top: 12px;
2322
+ text-align: center;
2323
+ opacity: 0;
2324
+ transform: translateY(-10px);
2325
+ animation: error-show 0.3s ease forwards;
2326
+ display: flex;
2327
+ align-items: center;
2328
+ justify-content: center;
2329
+ gap: 6px;
2330
+ }
2331
+
2332
+ .error-msg::before {
2333
+ content: '';
2334
+ width: 16px;
2335
+ height: 16px;
2336
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23dc3545' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cline x1='12' y1='8' x2='12' y2='12'/%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'/%3E%3C/svg%3E") no-repeat center;
2337
+ background-size: contain;
2338
+ }
2339
+
2340
+ @keyframes error-show {
2341
+ to {
2342
+ opacity: 1;
2343
+ transform: translateY(0);
2344
+ }
2345
+ }
2346
+
2347
+ .success-msg {
2348
+ color: var(--success);
2349
+ font-size: 13px;
2350
+ margin-top: 12px;
2351
+ text-align: center;
2352
+ display: flex;
2353
+ align-items: center;
2354
+ justify-content: center;
2355
+ gap: 6px;
2356
+ }
2357
+
2358
+ .success-msg::before {
2359
+ content: '';
2360
+ width: 16px;
2361
+ height: 16px;
2362
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2328a745' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 11.08V12a10 10 0 1 1-5.93-9.14'/%3E%3Cpolyline points='22 4 12 14.01 9 11.01'/%3E%3C/svg%3E") no-repeat center;
2363
+ background-size: contain;
2364
+ }
2365
+
2366
+ .loading {
2367
+ opacity: 0.6;
2368
+ pointer-events: none;
2369
+ }
2370
+
2371
+ .status-badge {
2372
+ display: inline-flex;
2373
+ align-items: center;
2374
+ gap: 4px;
2375
+ padding: 4px 8px;
2376
+ border-radius: 12px;
2377
+ font-size: 12px;
2378
+ font-weight: 500;
2379
+ }
2380
+
2381
+ .status-badge.enabled {
2382
+ background: rgba(40,167,69,0.1);
2383
+ color: var(--success);
2384
+ }
2385
+
2386
+ .status-badge.disabled {
2387
+ background: rgba(220,53,69,0.1);
2388
+ color: var(--danger);
2389
+ }
2390
+ </style>
2391
+ </head>
2392
+ <body>
2393
+ <!-- 登录界面 -->
2394
+ <div id="login" class="login-container">
2395
+ <div class="login-box">
2396
+ <h2>Prompt Manager</h2>
2397
+ <div class="input-group username">
2398
+ <input type="text" id="username" placeholder="用户名" />
2399
+ </div>
2400
+ <div class="input-group password">
2401
+ <input type="password" id="password" placeholder="密码" />
2402
+ </div>
2403
+ <button id="loginBtn" class="btn btn-primary">登&nbsp;&nbsp;&nbsp;&nbsp;录</button>
2404
+ </div>
2405
+ </div>
2406
+
2407
+ <!-- 主界面 -->
2408
+ <!-- 类目管理弹窗 -->
2409
+ <div id="newFolderModal" class="modal hidden">
2410
+ <div class="modal-dialog">
2411
+ <div class="modal-content group-modal-content">
2412
+ <div class="modal-header">
2413
+ <h3>类目管理</h3>
2414
+ <button type="button" class="modal-close" onclick="toggleNewFolderModal()">
2415
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
2416
+ <path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
2417
+ </svg>
2418
+ </button>
2419
+ </div>
2420
+ <div class="modal-body">
2421
+ <div class="group-modal-tabs">
2422
+ <button type="button" class="group-modal-tab active" data-tab="create">新增类目</button>
2423
+ <button type="button" class="group-modal-tab" data-tab="manage">管理类目</button>
2424
+ </div>
2425
+ <div class="group-modal-panel" data-panel="create">
2426
+ <div class="form-group">
2427
+ <label>目录名称</label>
2428
+ <input type="text" id="newFolderName" placeholder="请输入目录名称" onkeydown="handleNewFolderKeydown(event)">
2429
+ <div class="group-modal-hint">名称仅支持字母、数字、下划线、短横线和中文,长度不超过64个字符。</div>
2430
+ </div>
2431
+ </div>
2432
+ <div class="group-modal-panel hidden" data-panel="manage">
2433
+ <div class="group-manage-toolbar">
2434
+ <input type="text" id="groupManageSearch" placeholder="搜索类目..." />
2435
+ <button type="button" class="btn btn-light btn-sm" id="groupManageRefreshBtn">刷新</button>
2436
+ </div>
2437
+ <div class="group-manage-list" id="groupManageList"></div>
2438
+ <div class="group-manage-empty hidden" id="groupManageEmpty">暂无类目信息</div>
2439
+ </div>
2440
+ </div>
2441
+ <div class="modal-footer">
2442
+ <div class="group-modal-footer group-modal-footer-create" id="groupModalCreateFooter">
2443
+ <button type="button" class="btn btn-light" onclick="toggleNewFolderModal()">取消</button>
2444
+ <button type="button" class="btn btn-dark" onclick="createNewFolder()">创建</button>
2445
+ </div>
2446
+ <div class="group-modal-footer group-modal-footer-manage hidden" id="groupModalManageFooter">
2447
+ <button type="button" class="btn btn-light" onclick="toggleNewFolderModal(false)">关闭</button>
2448
+ </div>
2449
+ </div>
2450
+ </div>
2451
+ </div>
2452
+ </div>
2453
+
2454
+ <div id="main" style="display: none; height: 100vh; flex-direction: column;">
2455
+ <!-- 顶部导航 -->
2456
+ <header>
2457
+ <div class="logo">
2458
+ Prompt Manager <span>Beta</span>
2459
+ </div>
2460
+ <div class="nav-right">
2461
+ <div class="user-profile">
2462
+ <button class="avatar-btn" id="avatarBtn" aria-haspopup="true" aria-expanded="false">
2463
+ <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%239ca3af'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E" alt="用户头像" class="avatar-img">
2464
+ </button>
2465
+ <div class="dropdown-menu" id="userMenu">
2466
+ <div class="dropdown-header">
2467
+ <div class="user-info">
2468
+ <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%239ca3af'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E" alt="用户头像" class="user-avatar">
2469
+ <div class="user-details">
2470
+ <div class="user-name">管理员</div>
2471
+ <div class="user-role">Admin</div>
2472
+ </div>
2473
+ </div>
2474
+ </div>
2475
+ <div class="dropdown-divider"></div>
2476
+ <button id="logoutBtn" class="dropdown-item">
2477
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="dropdown-icon"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
2478
+ 退出登录
2479
+ </button>
2480
+ </div>
2481
+ </div>
2482
+ </div>
2483
+ </header>
2484
+
2485
+ <!-- 主体内容 -->
2486
+ <main>
2487
+ <!-- 左侧边栏 -->
2488
+ <aside>
2489
+ <div class="sidebar-header">
2490
+ <button id="newPromptBtn" class="new-prompt-btn">
2491
+ 新建 Prompt
2492
+ </button>
2493
+ <div class="search-container">
2494
+ <div class="search-box">
2495
+ <input type="text" id="searchInput" placeholder="搜索提示词..." />
2496
+ <button type="button" class="clear-btn" title="清除搜索"></button>
2497
+ </div>
2498
+ <button id="newGroupBtn" class="folder-btn" title="新建目录">
2499
+ </button>
2500
+ </div>
2501
+ </div>
2502
+
2503
+ <div id="groupList"></div>
2504
+ </aside>
2505
+
2506
+ <!-- 右侧编辑器 -->
2507
+ <div class="editor-container">
2508
+ <div class="editor-header">
2509
+ <div class="editor-header-top">
2510
+ <input type="text" id="promptName" placeholder="Prompt 名称" />
2511
+ <div class="group-selector">
2512
+ <button type="button" id="promptGroupBtn" class="group-selector-btn" aria-haspopup="listbox" aria-expanded="false" aria-controls="promptGroupDropdown">
2513
+ <span class="group-selector-label" style="display: none;">关联类目</span>
2514
+ <span class="group-selector-value" id="promptGroupLabel">default</span>
2515
+ <span class="group-selector-icon" aria-hidden="true">
2516
+ <svg width="14" height="14" viewBox="0 0 20 20" fill="none">
2517
+ <path d="M5 8L10 13L15 8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
2518
+ </svg>
2519
+ </span>
2520
+ </button>
2521
+ <select id="promptGroup" class="group-selector-input" aria-hidden="true" tabindex="-1">
2522
+ <option value="default">default</option>
2523
+ <option value="developer">developer</option>
2524
+ <option value="generator">generator</option>
2525
+ <option value="operation">operation</option>
2526
+ </select>
2527
+ <div class="group-dropdown" id="promptGroupDropdown" role="dialog" aria-modal="false">
2528
+ <div class="group-dropdown-search">
2529
+ <input type="text" id="promptGroupSearch" placeholder="搜索类目..." autocomplete="off" />
2530
+ </div>
2531
+ <div class="group-dropdown-body">
2532
+ <div class="group-cascader" id="promptGroupCascader"></div>
2533
+ <div class="group-search-results" id="promptGroupSearchResults"></div>
2534
+ <div class="group-dropdown-empty hidden" id="promptGroupEmpty">暂无匹配的类目</div>
2535
+ </div>
2536
+ </div>
2537
+ </div>
2538
+ <div class="editor-controls">
2539
+ <div class="mode-toggle">
2540
+ <button id="editModeBtn" class="mode-btn active" data-mode="edit">编辑</button>
2541
+ <button id="previewModeBtn" class="mode-btn" data-mode="preview">预览</button>
2542
+ </div>
2543
+ <button id="saveBtn" class="btn btn-primary btn-sm">
2544
+ 保存
2545
+ </button>
2546
+ </div>
2547
+ </div>
2548
+ <textarea id="promptDescription" class="prompt-description" placeholder="Prompt 描述"></textarea>
2549
+ </div>
2550
+
2551
+ <div class="editor-content">
2552
+ <section class="arguments-section" id="argumentsSection">
2553
+ <div class="arguments-header">
2554
+ <div class="arguments-title">
2555
+ <span>参数配置</span>
2556
+ </div>
2557
+ <div class="arguments-actions">
2558
+ <button id="addArgumentBtn" class="btn btn-outline btn-sm" type="button">
2559
+ 新增
2560
+ </button>
2561
+ </div>
2562
+ </div>
2563
+ <div class="arguments-list" id="argumentsList">
2564
+ <div class="arguments-empty">暂无参数,点击“新增”开始配置</div>
2565
+ </div>
2566
+ </section>
2567
+ <div class="editor-body" id="editorWorkspace">
2568
+ <div class="workspace-pane" id="editorPane">
2569
+ <div class="pane-header">编辑器</div>
2570
+ <div class="pane-content">
2571
+ <textarea id="editor"></textarea>
2572
+ </div>
2573
+ </div>
2574
+ <div class="workspace-pane hidden" id="previewPane">
2575
+ <div class="pane-header">实时预览</div>
2576
+ <div class="pane-content preview">
2577
+ <div class="preview-content" id="previewContent">
2578
+ <p>选择或创建prompt开始编辑</p>
2579
+ </div>
2580
+ </div>
2581
+ </div>
2582
+ </div>
2583
+ </div>
2584
+ </div>
2585
+ </main>
2586
+ </div>
2587
+
2588
+ <!-- 消息提示 -->
2589
+ <div id="toastContainer" class="toast-container"></div>
2590
+
2591
+ <!-- 删除 Prompt 弹窗 -->
2592
+ <div id="deletePromptModal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="deletePromptTitle">
2593
+ <div class="modal-dialog">
2594
+ <div class="modal-content">
2595
+ <div class="modal-header">
2596
+ <h3 id="deletePromptTitle">删除 Prompt</h3>
2597
+ <button type="button" class="modal-close" id="deletePromptCloseBtn" aria-label="关闭">
2598
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
2599
+ <path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
2600
+ </svg>
2601
+ </button>
2602
+ </div>
2603
+ <div class="modal-body">
2604
+ <p style="font-size: 14px; color: var(--gray); line-height: 1.6;">
2605
+ 确认删除 <span id="deletePromptName" style="color: var(--dark); font-weight: 600;"></span> 吗?该操作不可撤销。
2606
+ </p>
2607
+ </div>
2608
+ <div class="modal-footer">
2609
+ <button type="button" class="btn btn-light" id="deletePromptCancelBtn">取消</button>
2610
+ <button type="button" class="btn btn-danger" id="deletePromptConfirmBtn">删除</button>
2611
+ </div>
2612
+ </div>
2613
+ </div>
2614
+ </div>
2615
+
2616
+ <!-- 参数编辑弹窗 -->
2617
+ <div id="argumentModal" class="argument-modal hidden" role="dialog" aria-modal="true" aria-labelledby="argumentModalTitle">
2618
+ <div class="argument-modal-dialog">
2619
+ <div class="argument-modal-header">
2620
+ <h3 id="argumentModalTitle">新增参数</h3>
2621
+ <button type="button" id="argumentModalClose" class="argument-modal-close" aria-label="关闭">×</button>
2622
+ </div>
2623
+ <form id="argumentForm" class="argument-modal-form">
2624
+ <div class="argument-modal-body">
2625
+ <div class="argument-form-grid">
2626
+ <div class="form-field">
2627
+ <label for="argumentNameInput">参数名称 <span class="required">*</span></label>
2628
+ <input type="text" id="argumentNameInput" placeholder="例如:language" />
2629
+ </div>
2630
+ <div class="form-field">
2631
+ <label for="argumentTypeInput">类型</label>
2632
+ <input type="text" id="argumentTypeInput" list="argumentTypeOptions" placeholder="例如:string" />
2633
+ </div>
2634
+ <div class="form-field">
2635
+ <label for="argumentDefaultInput">默认值</label>
2636
+ <input type="text" id="argumentDefaultInput" placeholder="可选" />
2637
+ </div>
2638
+ <div class="form-field form-field-inline">
2639
+ <label class="checkbox-field">
2640
+ <input type="checkbox" id="argumentRequiredInput" />
2641
+ <span>必填</span>
2642
+ </label>
2643
+ </div>
2644
+ <div class="form-field full-width">
2645
+ <label for="argumentDescriptionInput">参数说明</label>
2646
+ <textarea id="argumentDescriptionInput" rows="3" placeholder="用于提示词中的描述"></textarea>
2647
+ </div>
2648
+ </div>
2649
+ </div>
2650
+ <div class="argument-modal-footer">
2651
+ <button type="button" class="btn btn-outline" id="argumentCancelBtn">取消</button>
2652
+ <button type="submit" class="btn btn-primary">保存</button>
2653
+ </div>
2654
+ </form>
2655
+ </div>
2656
+ </div>
2657
+
2658
+ <datalist id="argumentTypeOptions">
2659
+ <option value="string">字符串</option>
2660
+ <option value="number">数字</option>
2661
+ <option value="boolean">布尔值</option>
2662
+ </datalist>
2663
+
2664
+ <script>
2665
+ // 应用状态
2666
+ let currentToken = localStorage.getItem('prompt-admin-token')
2667
+ let currentPrompt = null
2668
+ let currentPromptObject = null
2669
+ let descriptionInputEl = null
2670
+ let allPrompts = []
2671
+ let expandedGroups = new Set()
2672
+ let editor = null
2673
+ let argumentsState = []
2674
+ let unusedArgumentNames = new Set()
2675
+ let editingArgumentIndex = null
2676
+ let argumentModalEl = null
2677
+ let argumentFormEl = null
2678
+ let argumentModalTitleEl = null
2679
+ let argumentNameInput = null
2680
+ let argumentTypeInput = null
2681
+ let argumentRequiredInput = null
2682
+ let argumentDefaultInput = null
2683
+ let argumentDescriptionInput = null
2684
+ let deletePromptModalEl = null
2685
+ let deletePromptNameEl = null
2686
+ let deletePromptConfirmBtn = null
2687
+ let deletePromptCancelBtn = null
2688
+ let deletePromptCloseBtn = null
2689
+ let pendingDeletePromptName = null
2690
+ let pendingDeletePromptPath = null
2691
+ let promptGroupBtnEl = null
2692
+ let promptGroupLabelEl = null
2693
+ let promptGroupDropdownEl = null
2694
+ let promptGroupSearchInput = null
2695
+ let promptGroupCascaderEl = null
2696
+ let promptGroupSearchResultsEl = null
2697
+ let promptGroupEmptyEl = null
2698
+ let groupTreeState = []
2699
+ let cascaderActivePaths = []
2700
+ let isGroupDropdownOpen = false
2701
+ let groupModalActiveTab = 'create'
2702
+ let groupManageListEl = null
2703
+ let groupManageEmptyEl = null
2704
+ let groupManageSearchInputEl = null
2705
+ let groupManageSearchValue = ''
2706
+ let groupManageEditingPath = null
2707
+ const groupManageActionLoading = new Set()
2708
+
2709
+ function refreshModalOpenState() {
2710
+ const hasOpenModal =
2711
+ (argumentModalEl && !argumentModalEl.classList.contains('hidden')) ||
2712
+ (deletePromptModalEl && !deletePromptModalEl.classList.contains('hidden'))
2713
+ if (hasOpenModal) {
2714
+ document.body.classList.add('modal-open')
2715
+ } else {
2716
+ document.body.classList.remove('modal-open')
2717
+ }
2718
+ }
2719
+
2720
+ // API 基础配置
2721
+ const API_BASE = '/api'
2722
+
2723
+ // 动态加载CodeMirror资源
2724
+ function loadScript(src) {
2725
+ return new Promise((resolve, reject) => {
2726
+ const script = document.createElement('script')
2727
+ script.src = src
2728
+ script.onload = resolve
2729
+ script.onerror = () => reject(new Error(`加载失败: ${src}`))
2730
+ document.head.appendChild(script)
2731
+ })
2732
+ }
2733
+
2734
+ // API请求封装
2735
+ async function apiCall(endpoint, options = {}) {
2736
+ const config = {
2737
+ headers: {
2738
+ 'Content-Type': 'application/json',
2739
+ ...options.headers
2740
+ },
2741
+ ...options
2742
+ }
2743
+
2744
+ if (currentToken) {
2745
+ config.headers.Authorization = `Bearer ${currentToken}`
2746
+ }
2747
+
2748
+ try {
2749
+ const response = await fetch(`${API_BASE}${endpoint}`, config)
2750
+
2751
+ if (response.status === 401) {
2752
+ // Token 过期,重新登录
2753
+ localStorage.removeItem('prompt-admin-token')
2754
+ currentToken = null
2755
+ showLogin()
2756
+ throw new Error('登录已过期,请重新登录')
2757
+ }
2758
+
2759
+ if (!response.ok) {
2760
+ const error = await response.text()
2761
+ throw new Error(error || '请求失败')
2762
+ }
2763
+
2764
+ return await response.json()
2765
+ } catch (error) {
2766
+ const handledError = error instanceof Error ? error : new Error(error?.message || '请求失败')
2767
+ handledError.__shown = true
2768
+ console.error('API请求错误:', handledError)
2769
+ showMessage(handledError.message, 'error')
2770
+ throw handledError
2771
+ }
2772
+ }
2773
+
2774
+ // 提示组件
2775
+ function showMessage(message, type = 'success', options = {}) {
2776
+ const container = document.getElementById('toastContainer')
2777
+ if (!container) return
2778
+
2779
+ const normalizedType = ['success', 'error', 'info', 'warning'].includes(type) ? type : 'success'
2780
+ const titleMap = {
2781
+ success: '操作成功',
2782
+ error: '操作失败',
2783
+ info: '提示信息',
2784
+ warning: '注意'
2785
+ }
2786
+ const iconMap = {
2787
+ success: '✓',
2788
+ error: '✕',
2789
+ info: 'ℹ',
2790
+ warning: '!'
2791
+ }
2792
+
2793
+ const toast = document.createElement('div')
2794
+ toast.className = `toast toast-${normalizedType}`
2795
+ toast.setAttribute('role', normalizedType === 'error' ? 'alert' : 'status')
2796
+ toast.setAttribute('aria-live', normalizedType === 'error' ? 'assertive' : 'polite')
2797
+
2798
+ const iconEl = document.createElement('span')
2799
+ iconEl.className = 'toast-icon'
2800
+ iconEl.textContent = options.icon || iconMap[normalizedType]
2801
+
2802
+ const contentEl = document.createElement('div')
2803
+ contentEl.className = 'toast-content'
2804
+
2805
+ const titleText = options.title || titleMap[normalizedType]
2806
+ if (titleText) {
2807
+ const titleEl = document.createElement('div')
2808
+ titleEl.className = 'toast-title'
2809
+ titleEl.textContent = titleText
2810
+ contentEl.appendChild(titleEl)
2811
+ }
2812
+
2813
+ const messageEl = document.createElement('div')
2814
+ messageEl.className = 'toast-message'
2815
+ messageEl.textContent = message
2816
+ contentEl.appendChild(messageEl)
2817
+
2818
+ const closeBtn = document.createElement('button')
2819
+ closeBtn.className = 'toast-close'
2820
+ closeBtn.type = 'button'
2821
+ closeBtn.setAttribute('aria-label', '关闭提示')
2822
+ closeBtn.innerHTML = '&times;'
2823
+
2824
+ toast.appendChild(iconEl)
2825
+ toast.appendChild(contentEl)
2826
+ toast.appendChild(closeBtn)
2827
+ container.appendChild(toast)
2828
+
2829
+ let hideTimer
2830
+ let removed = false
2831
+
2832
+ const removeToast = () => {
2833
+ if (removed) return
2834
+ removed = true
2835
+ toast.classList.add('toast-leave')
2836
+ toast.removeEventListener('mouseenter', pauseTimer)
2837
+ toast.removeEventListener('mouseleave', resumeTimer)
2838
+ setTimeout(() => {
2839
+ if (toast.parentNode === container) {
2840
+ container.removeChild(toast)
2841
+ }
2842
+ }, 250)
2843
+ }
2844
+
2845
+ const pauseTimer = () => clearTimeout(hideTimer)
2846
+ const resumeTimer = () => {
2847
+ clearTimeout(hideTimer)
2848
+ hideTimer = setTimeout(removeToast, Number(options.duration) || 5000)
2849
+ }
2850
+
2851
+ closeBtn.addEventListener('click', () => {
2852
+ pauseTimer()
2853
+ removeToast()
2854
+ })
2855
+
2856
+ toast.addEventListener('mouseenter', pauseTimer)
2857
+ toast.addEventListener('mouseleave', resumeTimer)
2858
+
2859
+ resumeTimer()
2860
+ }
2861
+
2862
+ // 登录功能
2863
+ async function login(username, password) {
2864
+ try {
2865
+ const result = await apiCall('/login', {
2866
+ method: 'POST',
2867
+ body: JSON.stringify({ username, password })
2868
+ })
2869
+
2870
+ currentToken = result.token
2871
+ localStorage.setItem('prompt-admin-token', currentToken)
2872
+ showMain()
2873
+ loadPrompts()
2874
+ } catch (error) {
2875
+ document.getElementById('loginError').textContent = error.message || '登录失败'
2876
+ }
2877
+ }
2878
+
2879
+ // 处理登录页面的回车事件和按钮点击
2880
+ function setupLoginEvents() {
2881
+ const loginForm = document.getElementById('login')
2882
+ const usernameInput = document.getElementById('username')
2883
+ const passwordInput = document.getElementById('password')
2884
+ const loginBtn = document.getElementById('loginBtn')
2885
+
2886
+ // 自动聚焦到用户名输入框
2887
+ usernameInput.focus()
2888
+
2889
+ function handleLogin() {
2890
+ const username = usernameInput.value.trim()
2891
+ const password = passwordInput.value
2892
+ if (username && password) {
2893
+ login(username, password)
2894
+ }
2895
+ }
2896
+
2897
+ // 监听整个页面的回车事件
2898
+ document.addEventListener('keydown', (e) => {
2899
+ if (e.key === 'Enter' && loginForm.style.display !== 'none') {
2900
+ e.preventDefault()
2901
+ handleLogin()
2902
+ }
2903
+ })
2904
+
2905
+ // 登录按钮点击事件
2906
+ loginBtn.addEventListener('click', (e) => {
2907
+ e.preventDefault()
2908
+ handleLogin()
2909
+ })
2910
+
2911
+ // Tab 键切换焦点时的优化
2912
+ usernameInput.addEventListener('keydown', (e) => {
2913
+ if (e.key === 'Enter') {
2914
+ e.preventDefault()
2915
+ if (!usernameInput.value.trim()) {
2916
+ usernameInput.focus()
2917
+ } else {
2918
+ passwordInput.focus()
2919
+ }
2920
+ }
2921
+ })
2922
+
2923
+ passwordInput.addEventListener('keydown', (e) => {
2924
+ if (e.key === 'Enter') {
2925
+ e.preventDefault()
2926
+ if (!passwordInput.value) {
2927
+ passwordInput.focus()
2928
+ } else {
2929
+ handleLogin()
2930
+ }
2931
+ }
2932
+ })
2933
+ }
2934
+
2935
+ // 页面加载完成后初始化事件
2936
+ document.addEventListener('DOMContentLoaded', () => {
2937
+ setupLoginEvents()
2938
+ })
2939
+
2940
+ function escapeHtml(input) {
2941
+ if (input === null || input === undefined) {
2942
+ return ''
2943
+ }
2944
+ return String(input).replace(/[&<>"']/g, (match) => {
2945
+ const map = {
2946
+ '&': '&amp;',
2947
+ '<': '&lt;',
2948
+ '>': '&gt;',
2949
+ '"': '&quot;',
2950
+ "'": '&#39;'
2951
+ }
2952
+ return map[match] || match
2953
+ })
2954
+ }
2955
+
2956
+ function escapeRegExp(input) {
2957
+ return String(input).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
2958
+ }
2959
+
2960
+ // 退出登录
2961
+ function logout() {
2962
+ localStorage.removeItem('prompt-admin-token')
2963
+ currentToken = null
2964
+ showLogin()
2965
+ }
2966
+
2967
+ // 显示登录界面
2968
+ function showLogin() {
2969
+ document.getElementById('login').style.display = 'flex'
2970
+ document.getElementById('main').style.display = 'none'
2971
+ }
2972
+
2973
+ // 显示主界面
2974
+ function showMain() {
2975
+ document.getElementById('login').style.display = 'none'
2976
+ document.getElementById('main').style.display = 'block'
2977
+ }
2978
+
2979
+ // 加载prompt列表
2980
+ async function loadPrompts(search = '', enabledOnly = false, group = null) {
2981
+ try {
2982
+ const queryParams = new URLSearchParams()
2983
+ if (search) queryParams.append('search', search)
2984
+ if (enabledOnly) queryParams.append('enabled', 'true')
2985
+ if (group) queryParams.append('group', group)
2986
+
2987
+ const prompts = await apiCall(`/prompts?${queryParams}`)
2988
+ allPrompts = prompts
2989
+ await renderGroupList(prompts)
2990
+ } catch (error) {
2991
+ console.error('加载prompt列表失败:', error)
2992
+ }
2993
+ }
2994
+
2995
+ function createDefaultPromptObject() {
2996
+ return {
2997
+ name: 'new-prompt',
2998
+ description: '新prompt描述',
2999
+ enabled: true,
3000
+ messages: [
3001
+ {
3002
+ role: 'user',
3003
+ content: {
3004
+ text: `这是一个新的prompt模板\n{{variable1}}`
3005
+ }
3006
+ }
3007
+ ],
3008
+ arguments: [
3009
+ {
3010
+ name: 'variable1',
3011
+ type: 'string',
3012
+ required: false,
3013
+ default: '',
3014
+ description: '示例变量'
3015
+ }
3016
+ ],
3017
+ variables: [
3018
+ {
3019
+ name: 'variable1',
3020
+ type: 'string',
3021
+ required: false,
3022
+ default: '',
3023
+ description: '示例变量'
3024
+ }
3025
+ ]
3026
+ }
3027
+ }
3028
+
3029
+ // 用户头像下拉菜单控制
3030
+ const avatarBtn = document.getElementById('avatarBtn')
3031
+ const userMenu = document.getElementById('userMenu')
3032
+
3033
+ // 点击头像显示/隐藏下拉菜单
3034
+ avatarBtn.addEventListener('click', (e) => {
3035
+ e.stopPropagation()
3036
+ userMenu.classList.toggle('show')
3037
+ avatarBtn.setAttribute('aria-expanded', userMenu.classList.contains('show'))
3038
+ })
3039
+
3040
+ // 点击页面其他地方关闭下拉菜单
3041
+ document.addEventListener('click', (e) => {
3042
+ if (!userMenu.contains(e.target) && !avatarBtn.contains(e.target)) {
3043
+ userMenu.classList.remove('show')
3044
+ avatarBtn.setAttribute('aria-expanded', 'false')
3045
+ }
3046
+ })
3047
+
3048
+ // ESC 键关闭下拉菜单
3049
+ document.addEventListener('keydown', (e) => {
3050
+ if (e.key === 'Escape' && userMenu.classList.contains('show')) {
3051
+ userMenu.classList.remove('show')
3052
+ avatarBtn.setAttribute('aria-expanded', 'false')
3053
+ }
3054
+ })
3055
+
3056
+ function clonePromptObject(obj) {
3057
+ return obj ? JSON.parse(JSON.stringify(obj)) : null
3058
+ }
3059
+
3060
+ function getFirstUserMessage(promptObj) {
3061
+ if (!promptObj || !Array.isArray(promptObj.messages)) return null
3062
+ return promptObj.messages.find(message => message?.role === 'user') || null
3063
+ }
3064
+
3065
+ function buildPromptObjectFromUI() {
3066
+ const nameInput = document.getElementById('promptName')
3067
+ const name = (nameInput?.value || '').trim() || 'new-prompt'
3068
+ const description = descriptionInputEl?.value || ''
3069
+ const markdown = editor ? editor.getValue() : ''
3070
+
3071
+ const base = clonePromptObject(currentPromptObject) || createDefaultPromptObject()
3072
+ base.name = name
3073
+ base.description = description
3074
+
3075
+ if (!Array.isArray(base.messages)) {
3076
+ base.messages = []
3077
+ }
3078
+
3079
+ let userMessage = getFirstUserMessage(base)
3080
+ if (!userMessage) {
3081
+ userMessage = { role: 'user', content: { text: '' } }
3082
+ base.messages.push(userMessage)
3083
+ }
3084
+
3085
+ if (!userMessage.content || typeof userMessage.content !== 'object') {
3086
+ userMessage.content = {}
3087
+ }
3088
+
3089
+ userMessage.content.text = markdown
3090
+
3091
+ const sanitizedArguments = Array.isArray(argumentsState)
3092
+ ? argumentsState
3093
+ .map(arg => {
3094
+ const nameValue = (arg.name || '').trim()
3095
+ if (!nameValue) return null
3096
+ const typeValue = (arg.type || '').trim()
3097
+ const descriptionValue = (arg.description || '').trim()
3098
+ const defaultValue = arg.default
3099
+ const argumentResult = {
3100
+ name: nameValue,
3101
+ type: typeValue || 'string',
3102
+ required: Boolean(arg.required)
3103
+ }
3104
+ if (descriptionValue) {
3105
+ argumentResult.description = descriptionValue
3106
+ }
3107
+ if (defaultValue !== undefined && defaultValue !== null) {
3108
+ const defaultText = String(defaultValue).trim()
3109
+ if (defaultText) {
3110
+ argumentResult.default = defaultText
3111
+ }
3112
+ }
3113
+ return argumentResult
3114
+ })
3115
+ .filter(Boolean)
3116
+ : []
3117
+
3118
+ base.arguments = sanitizedArguments
3119
+
3120
+ return base
3121
+ }
3122
+
3123
+ function adjustDescriptionHeight() {
3124
+ if (!descriptionInputEl) return
3125
+
3126
+ const style = window.getComputedStyle(descriptionInputEl)
3127
+ const lineHeight = parseFloat(style.lineHeight) || 20
3128
+ const padding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom) || 0
3129
+ const border = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth) || 0
3130
+ const maxHeight = lineHeight * 4 + padding + border
3131
+ const baseHeight = lineHeight + padding + border
3132
+
3133
+ descriptionInputEl.style.height = 'auto'
3134
+ const contentHeight = descriptionInputEl.scrollHeight
3135
+ const newHeight = Math.max(baseHeight, Math.min(maxHeight, contentHeight))
3136
+ descriptionInputEl.style.height = `${newHeight}px`
3137
+ descriptionInputEl.style.overflowY = descriptionInputEl.scrollHeight > maxHeight ? 'auto' : 'hidden'
3138
+ }
3139
+
3140
+ function normalizeArgument(argument = {}) {
3141
+ return {
3142
+ name: argument?.name ? String(argument.name) : '',
3143
+ description: argument?.description ? String(argument.description) : '',
3144
+ type: argument?.type ? String(argument.type) : 'string',
3145
+ required: Boolean(argument?.required),
3146
+ default: argument?.default ?? ''
3147
+ }
3148
+ }
3149
+
3150
+ function setArgumentsState(list = []) {
3151
+ argumentsState = Array.isArray(list) ? list.map(item => normalizeArgument(item)) : []
3152
+ unusedArgumentNames = new Set()
3153
+ const section = document.getElementById('argumentsSection')
3154
+ if (section) {
3155
+ section.classList.remove('has-error')
3156
+ }
3157
+ renderArgumentsEditor()
3158
+ }
3159
+
3160
+ function removeArgument(index) {
3161
+ if (index < 0 || index >= argumentsState.length) return
3162
+ argumentsState.splice(index, 1)
3163
+ setUnusedArgumentHighlights([])
3164
+ }
3165
+
3166
+ function renderArgumentsEditor() {
3167
+ const listEl = document.getElementById('argumentsList')
3168
+ if (!listEl) return
3169
+
3170
+ listEl.innerHTML = ''
3171
+
3172
+ if (!argumentsState.length) {
3173
+ listEl.innerHTML = '<div class="arguments-empty">暂无参数,点击“新增参数”开始配置</div>'
3174
+ return
3175
+ }
3176
+
3177
+ const fragment = document.createDocumentFragment()
3178
+
3179
+ argumentsState.forEach((rawArgument, index) => {
3180
+ const argument = normalizeArgument(rawArgument)
3181
+ const displayName = argument.name?.trim() || `参数 ${index + 1}`
3182
+ const normalizedName = argument.name?.trim() || ''
3183
+ const safeType = escapeHtml(argument.type || 'string')
3184
+ const safeDefault = argument.default ? escapeHtml(String(argument.default)) : ''
3185
+ const safeDescription = argument.description ? escapeHtml(argument.description) : ''
3186
+
3187
+ const card = document.createElement('div')
3188
+ card.className = 'argument-card'
3189
+ card.dataset.index = index
3190
+
3191
+ if (normalizedName && unusedArgumentNames.has(normalizedName)) {
3192
+ card.classList.add('argument-card-unused')
3193
+ }
3194
+
3195
+ const badgeParts = [`<span class="argument-badge">类型:${safeType || 'string'}</span>`]
3196
+ if (argument.required) {
3197
+ badgeParts.push('<span class="argument-badge argument-required">必填</span>')
3198
+ }
3199
+ if (safeDefault) {
3200
+ badgeParts.push(`<span class="argument-badge">默认:${safeDefault}</span>`)
3201
+ }
3202
+
3203
+ const placeholderSnippet = normalizedName
3204
+ ? `<div>变量占位:<span class="argument-placeholder">{{${escapeHtml(normalizedName)}}}</span></div>`
3205
+ : ''
3206
+
3207
+ card.innerHTML = `
3208
+ <div class="argument-card-header">
3209
+ <div class="argument-name">${escapeHtml(displayName)}</div>
3210
+ <div class="argument-badges">${badgeParts.join('')}</div>
3211
+ </div>
3212
+ <div class="argument-body">
3213
+ ${safeDescription ? `<div class="argument-description">${safeDescription}</div>` : '<div class="argument-description" style="color: var(--gray);">暂无说明</div>'}
3214
+ ${placeholderSnippet}
3215
+ </div>
3216
+ <div class="argument-actions">
3217
+ <button type="button" class="argument-action-btn edit" data-action="edit" data-index="${index}">编辑</button>
3218
+ <button type="button" class="argument-action-btn delete" data-action="delete" data-index="${index}">删除</button>
3219
+ </div>
3220
+ `
3221
+
3222
+ fragment.appendChild(card)
3223
+ })
3224
+
3225
+ listEl.appendChild(fragment)
3226
+ }
3227
+
3228
+ function setUnusedArgumentHighlights(names = []) {
3229
+ unusedArgumentNames = new Set(Array.isArray(names) ? names.map(name => String(name).trim()).filter(Boolean) : [])
3230
+ const section = document.getElementById('argumentsSection')
3231
+ if (section) {
3232
+ section.classList.toggle('has-error', unusedArgumentNames.size > 0)
3233
+ }
3234
+ renderArgumentsEditor()
3235
+ }
3236
+
3237
+ function openDeletePromptModal(promptName, relativePath) {
3238
+ if (!deletePromptModalEl) return
3239
+ pendingDeletePromptName = promptName
3240
+ pendingDeletePromptPath = relativePath || null
3241
+ if (deletePromptNameEl) {
3242
+ deletePromptNameEl.textContent = `“${promptName}”`
3243
+ }
3244
+ deletePromptModalEl.classList.remove('hidden')
3245
+ deletePromptModalEl.setAttribute('aria-hidden', 'false')
3246
+ refreshModalOpenState()
3247
+ if (deletePromptConfirmBtn) {
3248
+ requestAnimationFrame(() => deletePromptConfirmBtn.focus())
3249
+ }
3250
+ }
3251
+
3252
+ function closeDeletePromptModal() {
3253
+ if (!deletePromptModalEl) return
3254
+ deletePromptModalEl.classList.add('hidden')
3255
+ deletePromptModalEl.setAttribute('aria-hidden', 'true')
3256
+ refreshModalOpenState()
3257
+ pendingDeletePromptName = null
3258
+ pendingDeletePromptPath = null
3259
+ }
3260
+
3261
+ async function confirmDeletePrompt() {
3262
+ if (!pendingDeletePromptName) {
3263
+ closeDeletePromptModal()
3264
+ return
3265
+ }
3266
+ const promptToDelete = pendingDeletePromptName
3267
+ const pathToDelete = pendingDeletePromptPath
3268
+ closeDeletePromptModal()
3269
+ await deletePrompt(promptToDelete, pathToDelete)
3270
+ }
3271
+
3272
+ function openArgumentModal(index = null) {
3273
+ if (!argumentModalEl) return
3274
+ editingArgumentIndex = Number.isInteger(index) && index >= 0 ? index : null
3275
+ const isEdit = editingArgumentIndex !== null && argumentsState[editingArgumentIndex]
3276
+ const payload = isEdit ? normalizeArgument(argumentsState[editingArgumentIndex]) : normalizeArgument({ type: 'string' })
3277
+
3278
+ if (argumentModalTitleEl) {
3279
+ argumentModalTitleEl.textContent = isEdit ? '编辑参数' : '新增参数'
3280
+ }
3281
+ if (argumentNameInput) {
3282
+ argumentNameInput.value = payload.name || ''
3283
+ }
3284
+ if (argumentTypeInput) {
3285
+ argumentTypeInput.value = payload.type || 'string'
3286
+ }
3287
+ if (argumentRequiredInput) {
3288
+ argumentRequiredInput.checked = Boolean(payload.required)
3289
+ }
3290
+ if (argumentDefaultInput) {
3291
+ const modalDefaultValue = payload.default
3292
+ argumentDefaultInput.value = modalDefaultValue === undefined || modalDefaultValue === null ? '' : String(modalDefaultValue)
3293
+ }
3294
+ if (argumentDescriptionInput) {
3295
+ argumentDescriptionInput.value = payload.description || ''
3296
+ }
3297
+
3298
+ argumentModalEl.classList.remove('hidden')
3299
+ argumentModalEl.setAttribute('aria-hidden', 'false')
3300
+ refreshModalOpenState()
3301
+
3302
+ requestAnimationFrame(() => {
3303
+ if (argumentNameInput) {
3304
+ argumentNameInput.focus()
3305
+ argumentNameInput.select()
3306
+ }
3307
+ })
3308
+ }
3309
+
3310
+ function closeArgumentModal() {
3311
+ if (!argumentModalEl) return
3312
+ argumentModalEl.classList.add('hidden')
3313
+ argumentModalEl.setAttribute('aria-hidden', 'true')
3314
+ refreshModalOpenState()
3315
+ editingArgumentIndex = null
3316
+
3317
+ if (argumentFormEl) {
3318
+ argumentFormEl.reset()
3319
+ }
3320
+ if (argumentNameInput) {
3321
+ argumentNameInput.value = ''
3322
+ }
3323
+ if (argumentTypeInput) {
3324
+ argumentTypeInput.value = 'string'
3325
+ }
3326
+ if (argumentRequiredInput) {
3327
+ argumentRequiredInput.checked = false
3328
+ }
3329
+ if (argumentDefaultInput) {
3330
+ argumentDefaultInput.value = ''
3331
+ }
3332
+ if (argumentDescriptionInput) {
3333
+ argumentDescriptionInput.value = ''
3334
+ }
3335
+ }
3336
+
3337
+ function handleArgumentFormSubmit(event) {
3338
+ event.preventDefault()
3339
+ if (!argumentNameInput || !argumentTypeInput) return
3340
+
3341
+ const name = (argumentNameInput.value || '').trim()
3342
+ if (!name) {
3343
+ showMessage('请输入参数名称', 'error')
3344
+ argumentNameInput.focus()
3345
+ return
3346
+ }
3347
+
3348
+ const type = (argumentTypeInput.value || 'string').trim() || 'string'
3349
+ const required = argumentRequiredInput ? argumentRequiredInput.checked : false
3350
+ const defaultValue = argumentDefaultInput ? argumentDefaultInput.value : ''
3351
+ const description = argumentDescriptionInput ? argumentDescriptionInput.value.trim() : ''
3352
+
3353
+ const payload = normalizeArgument({
3354
+ name,
3355
+ type,
3356
+ required,
3357
+ default: defaultValue,
3358
+ description
3359
+ })
3360
+
3361
+ if (editingArgumentIndex !== null && argumentsState[editingArgumentIndex]) {
3362
+ argumentsState[editingArgumentIndex] = payload
3363
+ } else {
3364
+ argumentsState.push(payload)
3365
+ }
3366
+
3367
+ closeArgumentModal()
3368
+ setUnusedArgumentHighlights([])
3369
+ }
3370
+
3371
+ function collectPromptTextContent(promptObject) {
3372
+ const segments = []
3373
+ if (!promptObject || !Array.isArray(promptObject.messages)) {
3374
+ return ''
3375
+ }
3376
+ promptObject.messages.forEach(message => {
3377
+ if (!message) return
3378
+ const content = message.content
3379
+ if (Array.isArray(content)) {
3380
+ content.forEach(item => {
3381
+ if (typeof item === 'string') {
3382
+ segments.push(item)
3383
+ } else if (item && typeof item === 'object') {
3384
+ Object.values(item).forEach(value => {
3385
+ if (typeof value === 'string') {
3386
+ segments.push(value)
3387
+ }
3388
+ })
3389
+ }
3390
+ })
3391
+ } else if (typeof content === 'string') {
3392
+ segments.push(content)
3393
+ } else if (content && typeof content === 'object') {
3394
+ Object.values(content).forEach(value => {
3395
+ if (typeof value === 'string') {
3396
+ segments.push(value)
3397
+ }
3398
+ })
3399
+ }
3400
+ })
3401
+ return segments.join('\n')
3402
+ }
3403
+
3404
+ function findUnusedArguments(promptObject) {
3405
+ const args = Array.isArray(promptObject?.arguments) ? promptObject.arguments : []
3406
+ if (!args.length) return []
3407
+ const combinedContent = collectPromptTextContent(promptObject)
3408
+ return args.filter(arg => {
3409
+ const name = arg?.name?.trim()
3410
+ if (!name) return false
3411
+ const pattern = new RegExp(`{{\\s*${escapeRegExp(name)}\\s*}}`)
3412
+ return !pattern.test(combinedContent)
3413
+ })
3414
+ }
3415
+
3416
+ function handleArgumentListClick(event) {
3417
+ const target = event.target.closest('[data-action]')
3418
+ if (!target) return
3419
+ const action = target.dataset.action
3420
+ const index = Number(target.dataset.index)
3421
+ if (!Number.isInteger(index)) return
3422
+ if (action === 'delete') {
3423
+ const argumentName = argumentsState[index]?.name || `参数 ${index + 1}`
3424
+ const confirmed = window.confirm(`确定要删除参数 "${argumentName}" 吗?`)
3425
+ if (!confirmed) return
3426
+ removeArgument(index)
3427
+ } else if (action === 'edit') {
3428
+ openArgumentModal(index)
3429
+ }
3430
+ }
3431
+
3432
+ function sanitizeGroupId(pathValue = 'default') {
3433
+ return pathValue.replace(/[^a-zA-Z0-9-_]/g, '__') || 'default'
3434
+ }
3435
+
3436
+ function collectAncestorPaths(pathValue) {
3437
+ if (!pathValue) return []
3438
+ const segments = pathValue.split('/')
3439
+ const paths = []
3440
+ let current = ''
3441
+ segments.forEach(segment => {
3442
+ current = current ? `${current}/${segment}` : segment
3443
+ paths.push(current)
3444
+ })
3445
+ return paths
3446
+ }
3447
+
3448
+ function flattenGroupTree(nodes, depth = 0, accumulator = []) {
3449
+ if (!Array.isArray(nodes)) return accumulator
3450
+ nodes.forEach(node => {
3451
+ const value = node?.path || 'default'
3452
+ const name = node?.name || value
3453
+ accumulator.push({
3454
+ value,
3455
+ name,
3456
+ depth,
3457
+ path: value,
3458
+ enabled: node?.enabled !== false
3459
+ })
3460
+ if (Array.isArray(node?.children) && node.children.length) {
3461
+ flattenGroupTree(node.children, depth + 1, accumulator)
3462
+ }
3463
+ })
3464
+ return accumulator
3465
+ }
3466
+
3467
+ function findNodeByPath(nodes, targetPath) {
3468
+ if (!Array.isArray(nodes)) return null
3469
+ for (const node of nodes) {
3470
+ if (!node) continue
3471
+ const nodePath = node.path || 'default'
3472
+ if (nodePath === targetPath) {
3473
+ return node
3474
+ }
3475
+ if (Array.isArray(node.children) && node.children.length) {
3476
+ const found = findNodeByPath(node.children, targetPath)
3477
+ if (found) return found
3478
+ }
3479
+ }
3480
+ return null
3481
+ }
3482
+
3483
+ function setCascaderActivePathsByValue(pathValue = 'default') {
3484
+ const target = pathValue || 'default'
3485
+ const candidates = collectAncestorPaths(target)
3486
+ const validated = []
3487
+ candidates.forEach(path => {
3488
+ if (findNodeByPath(groupTreeState, path) || path === 'default') {
3489
+ validated.push(path)
3490
+ }
3491
+ })
3492
+ cascaderActivePaths = validated.length ? validated : ['default']
3493
+ }
3494
+
3495
+ function renderGroupDropdownContent(keyword = '') {
3496
+ const trimmed = (keyword || '').trim()
3497
+ if (!isGroupDropdownOpen) return
3498
+ if (trimmed) {
3499
+ renderGroupSearchResults(trimmed)
3500
+ } else {
3501
+ renderGroupCascader()
3502
+ }
3503
+ }
3504
+
3505
+ function renderGroupCascader() {
3506
+ if (!promptGroupCascaderEl) return
3507
+ const selectEl = document.getElementById('promptGroup')
3508
+ const currentValue = selectEl?.value || 'default'
3509
+
3510
+ promptGroupCascaderEl.innerHTML = ''
3511
+ promptGroupCascaderEl.style.display = 'flex'
3512
+
3513
+ if (promptGroupSearchResultsEl) {
3514
+ promptGroupSearchResultsEl.classList.remove('show')
3515
+ promptGroupSearchResultsEl.innerHTML = ''
3516
+ }
3517
+
3518
+ if (!Array.isArray(groupTreeState) || !groupTreeState.length) {
3519
+ promptGroupCascaderEl.innerHTML = ''
3520
+ promptGroupCascaderEl.style.display = 'none'
3521
+ if (promptGroupEmptyEl) {
3522
+ promptGroupEmptyEl.classList.remove('hidden')
3523
+ promptGroupEmptyEl.textContent = '暂未配置类目'
3524
+ }
3525
+ return
3526
+ }
3527
+
3528
+ if (promptGroupEmptyEl) {
3529
+ promptGroupEmptyEl.classList.add('hidden')
3530
+ }
3531
+
3532
+ const columns = []
3533
+ let depth = 0
3534
+ let nodes = groupTreeState
3535
+ columns.push({ depth, nodes, title: '全部类目' })
3536
+
3537
+ while (true) {
3538
+ const activePath = cascaderActivePaths[depth]
3539
+ if (!activePath) break
3540
+ const activeNode = findNodeByPath(nodes, activePath)
3541
+ if (activeNode && Array.isArray(activeNode.children) && activeNode.children.length) {
3542
+ depth += 1
3543
+ nodes = activeNode.children
3544
+ columns.push({
3545
+ depth,
3546
+ nodes,
3547
+ title: activeNode.name || activeNode.path || '子类目'
3548
+ })
3549
+ } else {
3550
+ break
3551
+ }
3552
+ }
3553
+
3554
+ columns.forEach(({ depth, nodes, title }) => {
3555
+ const columnEl = document.createElement('div')
3556
+ columnEl.className = 'group-cascader-column'
3557
+
3558
+ const titleEl = document.createElement('div')
3559
+ titleEl.className = 'group-cascader-title'
3560
+ titleEl.textContent = title || '子类目'
3561
+ columnEl.appendChild(titleEl)
3562
+
3563
+ const listEl = document.createElement('div')
3564
+ listEl.className = 'group-cascader-list'
3565
+ columnEl.appendChild(listEl)
3566
+
3567
+ if (!Array.isArray(nodes) || !nodes.length) {
3568
+ const emptyEl = document.createElement('div')
3569
+ emptyEl.className = 'group-cascader-empty'
3570
+ emptyEl.textContent = '无子类目'
3571
+ listEl.appendChild(emptyEl)
3572
+ } else {
3573
+ nodes.forEach(node => {
3574
+ if (!node) return
3575
+ const nodePath = node.path || 'default'
3576
+ const nodeName = node.name || nodePath
3577
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0
3578
+ const isActivePath = cascaderActivePaths[depth] === nodePath
3579
+ const isSelected = currentValue === nodePath
3580
+
3581
+ const itemBtn = document.createElement('button')
3582
+ itemBtn.type = 'button'
3583
+ let itemClass = 'group-cascader-item'
3584
+ if (hasChildren) itemClass += ' has-children'
3585
+ if (isActivePath) itemClass += ' active'
3586
+ if (isSelected) itemClass += ' selected'
3587
+ itemBtn.className = itemClass
3588
+
3589
+ const safeName = escapeHtml(nodeName)
3590
+ const safePath = escapeHtml(nodePath)
3591
+ let suffix = ''
3592
+ if (hasChildren) {
3593
+ suffix = '<span class="group-cascader-suffix">›</span>'
3594
+ if (isSelected) {
3595
+ suffix = `<span class="group-cascader-check">✓</span>${suffix}`
3596
+ }
3597
+ } else if (isSelected) {
3598
+ suffix = '<span class="group-cascader-check">✓</span>'
3599
+ }
3600
+ const hint = nodePath.includes('/') ? `<span class="group-cascader-path-hint">${safePath}</span>` : ''
3601
+
3602
+ itemBtn.innerHTML = `
3603
+ <span class="group-cascader-label">${safeName}${hint}</span>
3604
+ ${suffix}
3605
+ `
3606
+
3607
+ itemBtn.addEventListener('click', () => {
3608
+ handleCascaderItemClick(node, depth)
3609
+ })
3610
+
3611
+ listEl.appendChild(itemBtn)
3612
+ })
3613
+ }
3614
+
3615
+ promptGroupCascaderEl.appendChild(columnEl)
3616
+ })
3617
+ }
3618
+
3619
+ function renderGroupSearchResults(keyword) {
3620
+ if (!promptGroupSearchResultsEl) return
3621
+ const selectEl = document.getElementById('promptGroup')
3622
+ const currentValue = selectEl?.value || 'default'
3623
+ const options = getFilteredGroupOptions(keyword)
3624
+
3625
+ if (promptGroupCascaderEl) {
3626
+ promptGroupCascaderEl.style.display = 'none'
3627
+ }
3628
+
3629
+ promptGroupSearchResultsEl.innerHTML = ''
3630
+
3631
+ if (!options.length) {
3632
+ promptGroupSearchResultsEl.classList.remove('show')
3633
+ if (promptGroupEmptyEl) {
3634
+ promptGroupEmptyEl.classList.remove('hidden')
3635
+ promptGroupEmptyEl.textContent = '没有匹配的类目'
3636
+ }
3637
+ return
3638
+ }
3639
+
3640
+ promptGroupSearchResultsEl.classList.add('show')
3641
+ if (promptGroupEmptyEl) {
3642
+ promptGroupEmptyEl.classList.add('hidden')
3643
+ }
3644
+
3645
+ const fragment = document.createDocumentFragment()
3646
+ options.forEach(option => {
3647
+ const item = document.createElement('button')
3648
+ item.type = 'button'
3649
+ const isSelected = option.value === currentValue
3650
+ item.className = `group-search-item${isSelected ? ' selected' : ''}`
3651
+ const safeName = escapeHtml(option.name)
3652
+ const safePath = escapeHtml(option.path)
3653
+ item.innerHTML = `
3654
+ <span>${safeName}</span>
3655
+ <span class="group-search-path">${safePath}</span>
3656
+ `
3657
+ item.addEventListener('click', () => {
3658
+ selectGroupValue(option.value)
3659
+ closeGroupDropdown()
3660
+ })
3661
+ fragment.appendChild(item)
3662
+ })
3663
+
3664
+ promptGroupSearchResultsEl.appendChild(fragment)
3665
+ }
3666
+
3667
+ function handleCascaderItemClick(node, depth) {
3668
+ const nodePath = node?.path || 'default'
3669
+ const hasChildren = Array.isArray(node?.children) && node.children.length > 0
3670
+ cascaderActivePaths = cascaderActivePaths.slice(0, depth)
3671
+ cascaderActivePaths[depth] = nodePath
3672
+ selectGroupValue(nodePath, !hasChildren)
3673
+ if (hasChildren) {
3674
+ renderGroupCascader()
3675
+ }
3676
+ }
3677
+
3678
+ function updatePromptGroupDisplay() {
3679
+ const selectEl = document.getElementById('promptGroup')
3680
+ if (!selectEl || !promptGroupLabelEl || !promptGroupBtnEl) return
3681
+ const value = selectEl.value || 'default'
3682
+ const option = Array.from(selectEl.options || []).find(opt => opt.value === value)
3683
+ const displayText = option ? option.textContent.trim() : value
3684
+ promptGroupLabelEl.textContent = displayText || value || 'default'
3685
+ promptGroupBtnEl.dataset.value = value
3686
+ }
3687
+
3688
+ function updateGroupSelectOptions(tree) {
3689
+ const selectEl = document.getElementById('promptGroup')
3690
+ if (!selectEl) return
3691
+ const previousValue = selectEl.value || 'default'
3692
+ const optionsData = flattenGroupTree(tree || [])
3693
+ selectEl.innerHTML = ''
3694
+ optionsData.forEach(optionData => {
3695
+ const optionEl = document.createElement('option')
3696
+ optionEl.value = optionData.value
3697
+ optionEl.textContent = optionData.path
3698
+ selectEl.appendChild(optionEl)
3699
+ })
3700
+ const hasPrevious = optionsData.some(optionData => optionData.value === previousValue)
3701
+ selectEl.value = hasPrevious ? previousValue : (optionsData[0]?.value || 'default')
3702
+ updatePromptGroupDisplay()
3703
+ if (isGroupDropdownOpen) {
3704
+ setCascaderActivePathsByValue(selectEl.value || 'default')
3705
+ renderGroupDropdownContent(promptGroupSearchInput?.value || '')
3706
+ }
3707
+ }
3708
+
3709
+ function getFilteredGroupOptions(keyword = '') {
3710
+ const normalized = keyword.trim().toLowerCase()
3711
+ let options = flattenGroupTree(groupTreeState || [])
3712
+ if (!options.length) {
3713
+ const selectEl = document.getElementById('promptGroup')
3714
+ if (selectEl) {
3715
+ options = Array.from(selectEl.options || []).map(opt => {
3716
+ const optionValue = opt.value || 'default'
3717
+ const text = opt.textContent?.trim() || optionValue
3718
+ return {
3719
+ value: optionValue,
3720
+ name: text,
3721
+ depth: 0,
3722
+ path: optionValue
3723
+ }
3724
+ })
3725
+ }
3726
+ }
3727
+ if (!normalized) return options
3728
+ return options.filter(option => {
3729
+ const name = option.name ? option.name.toLowerCase() : ''
3730
+ const path = option.path ? option.path.toLowerCase() : ''
3731
+ return name.includes(normalized) || path.includes(normalized)
3732
+ })
3733
+ }
3734
+
3735
+ function selectGroupValue(value, closeAfterSelection = true) {
3736
+ const selectEl = document.getElementById('promptGroup')
3737
+ if (!selectEl) return
3738
+ const exists = Array.from(selectEl.options || []).some(option => option.value === value)
3739
+ if (!exists) {
3740
+ const optionEl = document.createElement('option')
3741
+ optionEl.value = value
3742
+ optionEl.textContent = value
3743
+ selectEl.appendChild(optionEl)
3744
+ }
3745
+ selectEl.value = value
3746
+ updatePromptGroupDisplay()
3747
+ setCascaderActivePathsByValue(value)
3748
+ if (isGroupDropdownOpen) {
3749
+ if (!closeAfterSelection) {
3750
+ renderGroupDropdownContent(promptGroupSearchInput?.value || '')
3751
+ } else {
3752
+ closeGroupDropdown(true)
3753
+ }
3754
+ }
3755
+ }
3756
+
3757
+ function openGroupDropdown() {
3758
+ if (!promptGroupDropdownEl || isGroupDropdownOpen) return
3759
+ isGroupDropdownOpen = true
3760
+ promptGroupDropdownEl.classList.add('show')
3761
+ promptGroupBtnEl?.setAttribute('aria-expanded', 'true')
3762
+ const selectEl = document.getElementById('promptGroup')
3763
+ setCascaderActivePathsByValue(selectEl?.value || 'default')
3764
+ renderGroupDropdownContent('')
3765
+ if (promptGroupSearchInput) {
3766
+ promptGroupSearchInput.value = ''
3767
+ requestAnimationFrame(() => promptGroupSearchInput.focus())
3768
+ }
3769
+ }
3770
+
3771
+ function closeGroupDropdown(focusButton = false) {
3772
+ if (!promptGroupDropdownEl || !isGroupDropdownOpen) return
3773
+ isGroupDropdownOpen = false
3774
+ promptGroupDropdownEl.classList.remove('show')
3775
+ promptGroupBtnEl?.setAttribute('aria-expanded', 'false')
3776
+ if (promptGroupSearchInput) {
3777
+ promptGroupSearchInput.value = ''
3778
+ }
3779
+ if (promptGroupCascaderEl) {
3780
+ promptGroupCascaderEl.style.display = 'flex'
3781
+ }
3782
+ if (promptGroupSearchResultsEl) {
3783
+ promptGroupSearchResultsEl.classList.remove('show')
3784
+ promptGroupSearchResultsEl.innerHTML = ''
3785
+ }
3786
+ if (promptGroupEmptyEl) {
3787
+ promptGroupEmptyEl.classList.add('hidden')
3788
+ }
3789
+ if (focusButton && promptGroupBtnEl) {
3790
+ promptGroupBtnEl.focus()
3791
+ }
3792
+ }
3793
+
3794
+ function toggleGroupDropdown(force) {
3795
+ const shouldOpen = typeof force === 'boolean' ? force : !isGroupDropdownOpen
3796
+ if (shouldOpen) {
3797
+ openGroupDropdown()
3798
+ } else {
3799
+ closeGroupDropdown()
3800
+ }
3801
+ }
3802
+
3803
+ function setGroupModalTab(tab = 'create') {
3804
+ const nextTab = tab === 'manage' ? 'manage' : 'create'
3805
+ groupModalActiveTab = nextTab
3806
+ const tabButtons = document.querySelectorAll('.group-modal-tab')
3807
+ tabButtons.forEach(btn => {
3808
+ btn.classList.toggle('active', btn.dataset.tab === nextTab)
3809
+ })
3810
+ const panels = document.querySelectorAll('.group-modal-panel')
3811
+ panels.forEach(panel => {
3812
+ panel.classList.toggle('hidden', panel.dataset.panel !== nextTab)
3813
+ })
3814
+ const createFooter = document.getElementById('groupModalCreateFooter')
3815
+ const manageFooter = document.getElementById('groupModalManageFooter')
3816
+ if (createFooter) createFooter.classList.toggle('hidden', nextTab !== 'create')
3817
+ if (manageFooter) manageFooter.classList.toggle('hidden', nextTab !== 'manage')
3818
+ if (nextTab === 'create') {
3819
+ const input = document.getElementById('newFolderName')
3820
+ setTimeout(() => input?.focus(), 120)
3821
+ } else {
3822
+ setTimeout(() => groupManageSearchInputEl?.focus(), 120)
3823
+ renderGroupManageList()
3824
+ }
3825
+ }
3826
+
3827
+ function renderGroupManageList() {
3828
+ if (!groupManageListEl) return
3829
+ const flattened = flattenGroupTree(groupTreeState || [])
3830
+ const keyword = (groupManageSearchValue || '').trim().toLowerCase()
3831
+ const filtered = flattened.filter(item => {
3832
+ if (!item) return false
3833
+ if (!keyword) return true
3834
+ const nameLower = (item.name || '').toLowerCase()
3835
+ const pathLower = (item.path || '').toLowerCase()
3836
+ return nameLower.includes(keyword) || pathLower.includes(keyword)
3837
+ })
3838
+
3839
+ groupManageListEl.innerHTML = ''
3840
+ if (groupManageEmptyEl) {
3841
+ if (!filtered.length) {
3842
+ groupManageEmptyEl.textContent = keyword ? '未找到匹配的类目' : '暂无类目信息'
3843
+ groupManageEmptyEl.classList.remove('hidden')
3844
+ } else {
3845
+ groupManageEmptyEl.classList.add('hidden')
3846
+ }
3847
+ }
3848
+ if (!filtered.length) {
3849
+ return
3850
+ }
3851
+
3852
+ filtered.forEach(item => {
3853
+ const enabled = item.enabled !== false
3854
+ const isDefault = item.path === 'default'
3855
+ const isEditing = groupManageEditingPath === item.path
3856
+ const isBusy = groupManageActionLoading.has(item.path)
3857
+ const safeName = escapeHtml(item.name)
3858
+ const safePath = escapeHtml(item.path)
3859
+ const row = document.createElement('div')
3860
+ row.className = `group-manage-item${enabled ? '' : ' is-disabled'}`
3861
+ row.dataset.path = item.path
3862
+ row.style.setProperty('--depth', item.depth || 0)
3863
+
3864
+ if (isEditing) {
3865
+ row.innerHTML = `
3866
+ <div class="group-manage-info">
3867
+ <div class="group-manage-edit">
3868
+ <input type="text" class="group-manage-rename-input" value="${safeName}" maxlength="64">
3869
+ </div>
3870
+ <div class="group-manage-path">${safePath}</div>
3871
+ </div>
3872
+ <div class="group-manage-actions">
3873
+ <button type="button" class="group-manage-action-btn" data-action="rename-confirm"${isBusy ? ' disabled' : ''}>保存</button>
3874
+ <button type="button" class="group-manage-action-btn" data-action="rename-cancel"${isBusy ? ' disabled' : ''}>取消</button>
3875
+ </div>
3876
+ `
3877
+ } else {
3878
+ const statusBadge = `<span class="group-status-badge ${enabled ? 'enabled' : 'disabled'}">${enabled ? '启用中' : '已冻结'}</span>`
3879
+ const defaultBadge = isDefault ? '<span class="group-status-badge default">默认</span>' : ''
3880
+ const toggleDisabled = isBusy || isDefault
3881
+ const renameDisabled = isBusy || isDefault
3882
+ const deleteDisabled = isBusy || isDefault
3883
+ const toggleText = enabled ? '冻结' : '启用'
3884
+ row.innerHTML = `
3885
+ <div class="group-manage-info">
3886
+ <div class="group-manage-name">${safeName}${statusBadge}${defaultBadge}</div>
3887
+ <div class="group-manage-path">${safePath}</div>
3888
+ </div>
3889
+ <div class="group-manage-actions">
3890
+ <button type="button" class="group-manage-action-btn" data-action="toggle"${toggleDisabled ? ' disabled' : ''}>${toggleText}</button>
3891
+ <button type="button" class="group-manage-action-btn" data-action="rename"${renameDisabled ? ' disabled' : ''}>重命名</button>
3892
+ <button type="button" class="group-manage-action-btn danger" data-action="delete"${deleteDisabled ? ' disabled' : ''}>删除</button>
3893
+ </div>
3894
+ `
3895
+ }
3896
+
3897
+ groupManageListEl.appendChild(row)
3898
+ })
3899
+ }
3900
+
3901
+ async function refreshGroupData() {
3902
+ try {
3903
+ const searchInput = document.getElementById('searchInput')
3904
+ const searchValue = searchInput ? searchInput.value : ''
3905
+ await loadPrompts(searchValue)
3906
+ if (groupModalActiveTab === 'manage') {
3907
+ renderGroupManageList()
3908
+ }
3909
+ } catch (error) {
3910
+ console.error('刷新类目数据失败:', error)
3911
+ }
3912
+ }
3913
+
3914
+ function handleGroupManageSearchInput(event) {
3915
+ groupManageSearchValue = event.target.value || ''
3916
+ renderGroupManageList()
3917
+ }
3918
+
3919
+ function resetGroupManageState() {
3920
+ groupManageSearchValue = ''
3921
+ groupManageEditingPath = null
3922
+ groupManageActionLoading.clear()
3923
+ if (groupManageSearchInputEl) {
3924
+ groupManageSearchInputEl.value = ''
3925
+ }
3926
+ renderGroupManageList()
3927
+ }
3928
+
3929
+ async function handleGroupRename(pathValue, newName) {
3930
+ if (!newName || !newName.trim()) {
3931
+ showMessage('请输入新的类目名称', 'error')
3932
+ return
3933
+ }
3934
+ const trimmedName = newName.trim()
3935
+ if (!/^[a-zA-Z0-9-_\u4e00-\u9fa5]{1,64}$/.test(trimmedName)) {
3936
+ showMessage('名称格式无效,只能包含字母、数字、中划线、下划线和中文', 'error')
3937
+ return
3938
+ }
3939
+ const currentSegments = pathValue.split('/')
3940
+ const currentName = currentSegments[currentSegments.length - 1] || pathValue
3941
+ if (trimmedName === currentName) {
3942
+ groupManageEditingPath = null
3943
+ renderGroupManageList()
3944
+ showMessage('类目名称未变更', 'info')
3945
+ return
3946
+ }
3947
+ groupManageActionLoading.add(pathValue)
3948
+ renderGroupManageList()
3949
+ try {
3950
+ await apiCall('/groups/rename', {
3951
+ method: 'PATCH',
3952
+ body: JSON.stringify({ path: pathValue, newName: trimmedName })
3953
+ })
3954
+ showMessage('类目重命名成功')
3955
+ groupManageEditingPath = null
3956
+ await refreshGroupData()
3957
+ } catch (error) {
3958
+ console.error('重命名类目失败:', error)
3959
+ showMessage(error?.message || '类目重命名失败', 'error')
3960
+ } finally {
3961
+ groupManageActionLoading.delete(pathValue)
3962
+ renderGroupManageList()
3963
+ }
3964
+ }
3965
+
3966
+ async function handleGroupDelete(pathValue, displayName) {
3967
+ const confirmed = window.confirm(`确认删除「${displayName}」吗?该操作不可撤销。`)
3968
+ if (!confirmed) return
3969
+ groupManageActionLoading.add(pathValue)
3970
+ renderGroupManageList()
3971
+ try {
3972
+ await apiCall(`/groups?path=${encodeURIComponent(pathValue)}`, {
3973
+ method: 'DELETE'
3974
+ })
3975
+ showMessage(`已删除 “${displayName}”`)
3976
+ await refreshGroupData()
3977
+ } catch (error) {
3978
+ console.error('删除类目失败:', error)
3979
+ showMessage(error?.message || '删除类目失败', 'error')
3980
+ } finally {
3981
+ groupManageActionLoading.delete(pathValue)
3982
+ renderGroupManageList()
3983
+ }
3984
+ }
3985
+
3986
+ async function handleGroupStatusToggle(pathValue, displayName, nextEnabled) {
3987
+ groupManageActionLoading.add(pathValue)
3988
+ renderGroupManageList()
3989
+ try {
3990
+ const result = await apiCall('/groups/status', {
3991
+ method: 'PATCH',
3992
+ body: JSON.stringify({ path: pathValue, enabled: nextEnabled })
3993
+ })
3994
+ const finalEnabled = result?.enabled !== false
3995
+ showMessage(`“${displayName}” 已${finalEnabled ? '启用' : '冻结'}`)
3996
+ await refreshGroupData()
3997
+ } catch (error) {
3998
+ console.error('更新类目状态失败:', error)
3999
+ showMessage(error?.message || '更新类目状态失败', 'error')
4000
+ } finally {
4001
+ groupManageActionLoading.delete(pathValue)
4002
+ renderGroupManageList()
4003
+ }
4004
+ }
4005
+
4006
+ function handleGroupManageListClick(event) {
4007
+ const actionBtn = event.target.closest('[data-action]')
4008
+ if (!actionBtn) return
4009
+ const item = actionBtn.closest('.group-manage-item')
4010
+ if (!item) return
4011
+ const pathValue = item.dataset.path
4012
+ if (!pathValue) return
4013
+ if (groupManageActionLoading.has(pathValue) && actionBtn.dataset.action !== 'rename-cancel') {
4014
+ return
4015
+ }
4016
+
4017
+ if (actionBtn.dataset.action === 'rename') {
4018
+ groupManageEditingPath = pathValue
4019
+ renderGroupManageList()
4020
+ const selectorPath = pathValue.replace(/"/g, '\\"')
4021
+ const nextRow = groupManageListEl?.querySelector(`[data-path="${selectorPath}"]`)
4022
+ const input = nextRow?.querySelector('.group-manage-rename-input')
4023
+ setTimeout(() => input?.focus(), 60)
4024
+ return
4025
+ }
4026
+
4027
+ if (actionBtn.dataset.action === 'rename-cancel') {
4028
+ groupManageEditingPath = null
4029
+ renderGroupManageList()
4030
+ return
4031
+ }
4032
+
4033
+ if (actionBtn.dataset.action === 'rename-confirm') {
4034
+ const input = item.querySelector('.group-manage-rename-input')
4035
+ const nextName = input ? input.value : ''
4036
+ handleGroupRename(pathValue, nextName)
4037
+ return
4038
+ }
4039
+
4040
+ const safeName = item.querySelector('.group-manage-name')?.firstChild?.textContent?.trim() || pathValue
4041
+
4042
+ if (actionBtn.dataset.action === 'toggle') {
4043
+ const currentNode = findNodeByPath(groupTreeState, pathValue)
4044
+ const isCurrentlyEnabled = currentNode ? currentNode.enabled !== false : true
4045
+ const nextEnabled = !isCurrentlyEnabled
4046
+ handleGroupStatusToggle(pathValue, safeName, nextEnabled)
4047
+ return
4048
+ }
4049
+
4050
+ if (actionBtn.dataset.action === 'delete') {
4051
+ handleGroupDelete(pathValue, safeName)
4052
+ }
4053
+ }
4054
+
4055
+ async function renderGroupList(prompts) {
4056
+ try {
4057
+ const groupTree = await apiCall('/groups')
4058
+ const container = document.getElementById('groupList')
4059
+ container.innerHTML = ''
4060
+
4061
+ const promptMap = new Map()
4062
+ ;(Array.isArray(prompts) ? prompts : []).forEach(prompt => {
4063
+ const pathValue = prompt.groupPath || prompt.group || 'default'
4064
+ if (!promptMap.has(pathValue)) {
4065
+ promptMap.set(pathValue, [])
4066
+ }
4067
+ promptMap.get(pathValue).push(prompt)
4068
+ })
4069
+
4070
+ if (currentPrompt) {
4071
+ const activePath = currentPrompt.groupPath || currentPrompt.group || 'default'
4072
+ collectAncestorPaths(activePath).forEach(pathValue => expandedGroups.add(pathValue))
4073
+ }
4074
+
4075
+ const ensureDefaultNode = nodes => {
4076
+ const hasDefault = nodes.some(node => node.path === 'default')
4077
+ if (!hasDefault) {
4078
+ nodes.unshift({ name: 'default', path: 'default', children: [], enabled: true })
4079
+ }
4080
+ }
4081
+
4082
+ ensureDefaultNode(groupTree)
4083
+ groupTreeState = Array.isArray(groupTree) ? groupTree : []
4084
+ updateGroupSelectOptions(groupTreeState)
4085
+
4086
+ const validPaths = new Set()
4087
+ const collectPaths = nodes => {
4088
+ nodes.forEach(node => {
4089
+ const nodePath = node.path || 'default'
4090
+ validPaths.add(nodePath)
4091
+ if (Array.isArray(node.children) && node.children.length) {
4092
+ collectPaths(node.children)
4093
+ }
4094
+ })
4095
+ }
4096
+ collectPaths(groupTree)
4097
+ expandedGroups = new Set([...expandedGroups].filter(pathValue => validPaths.has(pathValue)))
4098
+
4099
+ const renderNode = (node, parentEl, depth = 0) => {
4100
+ const pathValue = node.path || 'default'
4101
+ const promptsInGroup = promptMap.get(pathValue) || []
4102
+
4103
+ const section = document.createElement('div')
4104
+ section.className = 'group-section'
4105
+ section.dataset.path = pathValue
4106
+ section.dataset.depth = depth
4107
+
4108
+ const header = document.createElement('div')
4109
+ header.className = 'group-header'
4110
+ header.dataset.group = pathValue
4111
+ let headerPadding = 16 + depth * 18
4112
+ if (node.enabled === false) {
4113
+ headerPadding += 32
4114
+ }
4115
+ header.style.paddingLeft = `${headerPadding}px`
4116
+
4117
+ const titleSpan = document.createElement('span')
4118
+ titleSpan.textContent = node.name
4119
+ if (node.enabled === false) {
4120
+ section.classList.add('disabled')
4121
+ }
4122
+ header.appendChild(titleSpan)
4123
+
4124
+ const headerActions = document.createElement('div')
4125
+ headerActions.style.display = 'flex'
4126
+ headerActions.style.alignItems = 'center'
4127
+ headerActions.style.gap = '6px'
4128
+
4129
+ const countEl = document.createElement('span')
4130
+ countEl.className = 'group-count'
4131
+ countEl.id = `group-count-${sanitizeGroupId(pathValue)}`
4132
+ countEl.textContent = promptsInGroup.length
4133
+
4134
+ const toggleEl = document.createElement('span')
4135
+ toggleEl.className = 'group-toggle'
4136
+
4137
+ headerActions.appendChild(countEl)
4138
+ headerActions.appendChild(toggleEl)
4139
+ header.appendChild(headerActions)
4140
+
4141
+ const content = document.createElement('div')
4142
+ content.className = 'group-content collapsed'
4143
+
4144
+ const promptListEl = document.createElement('div')
4145
+ promptListEl.className = 'prompt-list'
4146
+ promptListEl.id = `promptList-${sanitizeGroupId(pathValue)}`
4147
+ content.appendChild(promptListEl)
4148
+
4149
+ renderGroupPromptList(promptListEl, pathValue, promptsInGroup)
4150
+
4151
+ let totalCount = promptsInGroup.length
4152
+
4153
+ if (Array.isArray(node.children) && node.children.length) {
4154
+ const childrenWrap = document.createElement('div')
4155
+ childrenWrap.className = 'group-children'
4156
+ node.children.forEach(child => {
4157
+ const childCount = renderNode(child, childrenWrap, depth + 1)
4158
+ totalCount += childCount
4159
+ })
4160
+ if (childrenWrap.childNodes.length) {
4161
+ content.appendChild(childrenWrap)
4162
+ }
4163
+ }
4164
+
4165
+ countEl.textContent = totalCount
4166
+
4167
+ const shouldExpandByDefault = expandedGroups.size === 0 && depth === 0
4168
+ const isExpanded = expandedGroups.has(pathValue) || shouldExpandByDefault
4169
+ if (shouldExpandByDefault) {
4170
+ expandedGroups.add(pathValue)
4171
+ }
4172
+
4173
+ content.classList.toggle('expanded', isExpanded)
4174
+ content.classList.toggle('collapsed', !isExpanded)
4175
+ toggleEl.classList.toggle('collapsed', !isExpanded)
4176
+
4177
+ header.addEventListener('click', (e) => {
4178
+ const isClickOnActions = e.target.closest('.toggle-btn') || e.target.closest('.delete-btn') || e.target.closest('.prompt-actions')
4179
+ if (!isClickOnActions) {
4180
+ const nextExpanded = !content.classList.contains('expanded')
4181
+ content.classList.toggle('expanded', nextExpanded)
4182
+ content.classList.toggle('collapsed', !nextExpanded)
4183
+ toggleEl.classList.toggle('collapsed', !nextExpanded)
4184
+ if (nextExpanded) {
4185
+ expandedGroups.add(pathValue)
4186
+ } else {
4187
+ expandedGroups.delete(pathValue)
4188
+ }
4189
+ }
4190
+ })
4191
+
4192
+ section.appendChild(header)
4193
+ section.appendChild(content)
4194
+ parentEl.appendChild(section)
4195
+ return totalCount
4196
+ }
4197
+
4198
+ const fragment = document.createDocumentFragment()
4199
+ groupTree.forEach(node => renderNode(node, fragment, 0))
4200
+ container.appendChild(fragment)
4201
+ if (groupModalActiveTab === 'manage') {
4202
+ renderGroupManageList()
4203
+ }
4204
+ } catch (error) {
4205
+ console.error('渲染分组列表失败:', error)
4206
+ }
4207
+ }
4208
+
4209
+ function renderGroupPromptList(container, groupPath, prompts) {
4210
+ container.innerHTML = ''
4211
+
4212
+ const sortedPrompts = [...prompts].sort((a, b) => (a.name || '').localeCompare(b.name || '', 'zh-CN'))
4213
+
4214
+ if (!sortedPrompts.length) {
4215
+ const emptyEl = document.createElement('div')
4216
+ emptyEl.className = 'prompt-list-empty'
4217
+ emptyEl.innerHTML = `
4218
+ <span>该目录暂无 Prompt</span>
4219
+ 请点击左上角“新建 Prompt”按钮创建。
4220
+ `
4221
+ container.appendChild(emptyEl)
4222
+ return
4223
+ }
4224
+
4225
+ sortedPrompts.forEach(prompt => {
4226
+ const item = document.createElement('div')
4227
+ const isActive = currentPrompt?.relativePath
4228
+ ? prompt.relativePath === currentPrompt.relativePath
4229
+ : currentPrompt?.name === prompt.name
4230
+ item.className = `prompt-item ${prompt.enabled ? 'enabled' : ''} ${isActive ? 'active' : ''}`
4231
+ const hasSubGroup = prompt.groupPath && prompt.groupPath.includes('/')
4232
+ const groupHint = hasSubGroup ? prompt.groupPath : (prompt.group || 'default')
4233
+ item.innerHTML = `
4234
+ <div class="prompt-info">
4235
+ <div class="prompt-name">${prompt.name}</div>
4236
+ <div class="prompt-desc" title="${prompt.description || ''}">${prompt.description || ''}</div>
4237
+ ${hasSubGroup ? `<div class="prompt-meta-hint">${groupHint}</div>` : ''}
4238
+ </div>
4239
+ <div class="prompt-meta">
4240
+ <div class="prompt-actions">
4241
+ <button class="action-btn toggle-btn" data-prompt="${prompt.name}" data-path="${prompt.relativePath || ''}">
4242
+ ${prompt.enabled ? '停用' : '启用'}
4243
+ </button>
4244
+ <button class="action-btn delete-btn" data-prompt="${prompt.name}" data-path="${prompt.relativePath || ''}" style="color: var(--danger);">
4245
+ 删除
4246
+ </button>
4247
+ </div>
4248
+ </div>
4249
+ `
4250
+
4251
+ item.addEventListener('click', (e) => {
4252
+ if (!e.target.closest('.prompt-actions') && !e.target.classList.contains('toggle-btn') && !e.target.classList.contains('delete-btn')) {
4253
+ selectPrompt(prompt, e)
4254
+ }
4255
+ })
4256
+
4257
+ const actions = item.querySelector('.prompt-actions')
4258
+ if (actions) {
4259
+ actions.addEventListener('click', (e) => {
4260
+ e.stopPropagation()
4261
+ e.preventDefault()
4262
+ })
4263
+
4264
+ const toggleBtn = item.querySelector('.toggle-btn')
4265
+ const deleteBtn = item.querySelector('.delete-btn')
4266
+
4267
+ if (toggleBtn) {
4268
+ toggleBtn.addEventListener('click', (e) => {
4269
+ e.stopPropagation()
4270
+ e.preventDefault()
4271
+ const targetPath = toggleBtn.getAttribute('data-path')
4272
+ togglePrompt(prompt.name, targetPath)
4273
+ })
4274
+ }
4275
+
4276
+ if (deleteBtn) {
4277
+ deleteBtn.addEventListener('click', (e) => {
4278
+ e.stopPropagation()
4279
+ e.preventDefault()
4280
+ const targetPath = deleteBtn.getAttribute('data-path')
4281
+ openDeletePromptModal(prompt.name, targetPath)
4282
+ })
4283
+ }
4284
+ }
4285
+
4286
+ container.appendChild(item)
4287
+ })
4288
+ }
4289
+
4290
+ async function togglePrompt(promptName, relativePath) {
4291
+ try {
4292
+ const query = relativePath ? `?path=${encodeURIComponent(relativePath)}` : ''
4293
+ const result = await apiCall(`/prompts/${encodeURIComponent(promptName)}/toggle${query}`, {
4294
+ method: 'POST'
4295
+ })
4296
+ const searchInput = document.getElementById('searchInput')
4297
+ const searchValue = searchInput ? searchInput.value : ''
4298
+ await loadPrompts(searchValue)
4299
+ const statusText = result?.enabled ? '已启用' : '已停用'
4300
+ showMessage(`“${promptName}” ${statusText}`, result?.enabled ? 'success' : 'info')
4301
+ if (currentPrompt?.relativePath === relativePath) {
4302
+ currentPrompt.enabled = result?.enabled
4303
+ currentPromptObject = currentPromptObject ? { ...currentPromptObject, enabled: result?.enabled } : currentPromptObject
4304
+ }
4305
+ } catch (error) {
4306
+ console.error('切换prompt状态失败:', error)
4307
+ showMessage('启用状态切换失败: ' + error.message, 'error')
4308
+ }
4309
+ }
4310
+
4311
+ async function deletePrompt(promptName, relativePath) {
4312
+ try {
4313
+ const query = relativePath ? `?path=${encodeURIComponent(relativePath)}` : ''
4314
+ await apiCall(`/prompts/${encodeURIComponent(promptName)}${query}`, {
4315
+ method: 'DELETE'
4316
+ })
4317
+ if (currentPrompt?.relativePath === relativePath || (!currentPrompt?.relativePath && currentPrompt?.name === promptName)) {
4318
+ resetEditor()
4319
+ }
4320
+ const searchInput = document.getElementById('searchInput')
4321
+ const searchValue = searchInput ? searchInput.value : ''
4322
+ await loadPrompts(searchValue)
4323
+ showMessage(`已删除 “${promptName}”`)
4324
+ } catch (error) {
4325
+ console.error('删除prompt失败:', error)
4326
+ showMessage('删除失败: ' + error.message, 'error')
4327
+ }
4328
+ }
4329
+
4330
+
4331
+ // 选择prompt
4332
+ async function selectPrompt(prompt, triggerEvent) {
4333
+ try {
4334
+ const query = prompt.relativePath ? `?path=${encodeURIComponent(prompt.relativePath)}` : ''
4335
+ const promptData = await apiCall(`/prompts/${encodeURIComponent(prompt.name)}${query}`)
4336
+ currentPrompt = {
4337
+ ...promptData,
4338
+ relativePath: promptData.relativePath || prompt.relativePath || null,
4339
+ groupPath: promptData.groupPath || prompt.groupPath || null,
4340
+ group: promptData.group || prompt.group || null
4341
+ }
4342
+
4343
+ let parsedPrompt = null
4344
+ try {
4345
+ parsedPrompt = window.jsyaml?.load(promptData.yaml || '') || null
4346
+ } catch (err) {
4347
+ console.error('解析提示词失败:', err)
4348
+ }
4349
+
4350
+ currentPromptObject = parsedPrompt ? clonePromptObject(parsedPrompt) : createDefaultPromptObject()
4351
+ setArgumentsState(currentPromptObject.arguments || [])
4352
+
4353
+ const nameInput = document.getElementById('promptName')
4354
+ if (nameInput) nameInput.value = currentPromptObject.name || promptData.name || ''
4355
+ const promptGroupSelect = document.getElementById('promptGroup')
4356
+ if (promptGroupSelect) {
4357
+ const resolvedGroup =
4358
+ promptData.groupPath ||
4359
+ promptData.group ||
4360
+ prompt.groupPath ||
4361
+ prompt.group ||
4362
+ 'default'
4363
+
4364
+ const optionExists = Array.from(promptGroupSelect.options || []).some(
4365
+ option => option.value === resolvedGroup
4366
+ )
4367
+ if (!optionExists) {
4368
+ const optionEl = document.createElement('option')
4369
+ optionEl.value = resolvedGroup
4370
+ optionEl.textContent = resolvedGroup
4371
+ promptGroupSelect.appendChild(optionEl)
4372
+ }
4373
+
4374
+ promptGroupSelect.value = resolvedGroup
4375
+ updatePromptGroupDisplay()
4376
+ setCascaderActivePathsByValue(resolvedGroup)
4377
+ if (isGroupDropdownOpen) {
4378
+ renderGroupDropdownContent(promptGroupSearchInput?.value || '')
4379
+ }
4380
+ }
4381
+
4382
+ if (descriptionInputEl) {
4383
+ descriptionInputEl.value = currentPromptObject.description || ''
4384
+ adjustDescriptionHeight()
4385
+ }
4386
+
4387
+ const userMessage = getFirstUserMessage(currentPromptObject)
4388
+ const messageText = userMessage?.content?.text || ''
4389
+ if (editor) {
4390
+ editor.setValue(messageText)
4391
+ }
4392
+
4393
+ // 更新UI状态
4394
+ document.querySelectorAll('.prompt-item').forEach(el => el.classList.remove('active'))
4395
+ const targetItem = triggerEvent?.currentTarget || triggerEvent?.target?.closest('.prompt-item')
4396
+ if (targetItem) {
4397
+ targetItem.classList.add('active')
4398
+ }
4399
+
4400
+ // 更新预览
4401
+ updatePreview(true)
4402
+ } catch (error) {
4403
+ console.error('加载prompt详情失败:', error)
4404
+ }
4405
+ }
4406
+
4407
+
4408
+
4409
+ // 切换工作区模式
4410
+ function setWorkspaceMode(mode) {
4411
+ const isPreview = mode === 'preview'
4412
+ const editorPane = document.getElementById('editorPane')
4413
+ const previewPane = document.getElementById('previewPane')
4414
+ const editModeBtn = document.getElementById('editModeBtn')
4415
+ const previewModeBtn = document.getElementById('previewModeBtn')
4416
+ const argumentsSection = document.getElementById('argumentsSection')
4417
+
4418
+ if (!editorPane || !previewPane || !editModeBtn || !previewModeBtn) return
4419
+
4420
+ editModeBtn.classList.toggle('active', !isPreview)
4421
+ previewModeBtn.classList.toggle('active', isPreview)
4422
+ editorPane.classList.toggle('hidden', isPreview)
4423
+ previewPane.classList.toggle('hidden', !isPreview)
4424
+
4425
+ // 在预览模式下隐藏参数配置区域,编辑模式下显示
4426
+ if (argumentsSection) {
4427
+ argumentsSection.style.display = isPreview ? 'none' : 'flex'
4428
+ }
4429
+
4430
+ if (isPreview) {
4431
+ updatePreview(true)
4432
+ } else {
4433
+ if (editor) {
4434
+ editor.refresh()
4435
+ }
4436
+ // 在切换回编辑模式时清理预览编辑器
4437
+ if (previewEditor) {
4438
+ const previewContent = document.getElementById('previewContent')
4439
+ if (previewContent) {
4440
+ previewContent.innerHTML = '<p>切换到预览模式查看内容</p>'
4441
+ }
4442
+ previewEditor = null
4443
+ }
4444
+ }
4445
+ }
4446
+
4447
+ // 更新预览
4448
+ let previewEditor = null;
4449
+
4450
+ async function updatePreview(force = false) {
4451
+ if (!editor) return
4452
+
4453
+ const previewPane = document.getElementById('previewPane')
4454
+ if (!force && previewPane?.classList.contains('hidden')) {
4455
+ return
4456
+ }
4457
+
4458
+ try {
4459
+ const previewContent = document.getElementById('previewContent')
4460
+ if (!previewContent) return
4461
+
4462
+ const content = editor.getValue()
4463
+
4464
+ // 如果预览编辑器不存在,创建一个
4465
+ if (!previewEditor) {
4466
+ previewContent.innerHTML = ''
4467
+ previewEditor = CodeMirror((elt) => {
4468
+ previewContent.appendChild(elt)
4469
+ }, {
4470
+ value: content,
4471
+ mode: 'markdown',
4472
+ theme: 'xq-light',
4473
+ lineWrapping: true,
4474
+ readOnly: true,
4475
+ lineNumbers: false,
4476
+ cursorHeight: 0
4477
+ })
4478
+ } else {
4479
+ // 更新预览内容
4480
+ previewEditor.setValue(content)
4481
+ }
4482
+
4483
+ // 确保预览区域的 CodeMirror 实例能正确显示
4484
+ setTimeout(() => {
4485
+ if (previewEditor) {
4486
+ previewEditor.refresh()
4487
+ }
4488
+ }, 10)
4489
+
4490
+ } catch (error) {
4491
+ console.error('预览更新失败:', error)
4492
+ document.getElementById('previewContent').innerHTML = '<p style="color: red;">预览生成失败</p>'
4493
+ }
4494
+ }
4495
+
4496
+ // 保存prompt
4497
+ async function savePrompt() {
4498
+ if (!editor) return
4499
+
4500
+ const saveBtn = document.getElementById('saveBtn')
4501
+ if (saveBtn?.disabled) return
4502
+
4503
+ const name = document.getElementById('promptName').value.trim()
4504
+ const group = document.getElementById('promptGroup').value.trim()
4505
+
4506
+ if (!window.jsyaml) {
4507
+ showMessage('资源未准备就绪,请刷新重试', 'error')
4508
+ return
4509
+ }
4510
+
4511
+ if (!name) {
4512
+ showMessage('请输入prompt名称', 'error')
4513
+ return
4514
+ }
4515
+
4516
+ const promptObject = buildPromptObjectFromUI()
4517
+ const unusedArguments = findUnusedArguments(promptObject)
4518
+ if (unusedArguments.length > 0) {
4519
+ const missingNames = unusedArguments.map(arg => arg.name).filter(Boolean)
4520
+ if (missingNames.length) {
4521
+ setUnusedArgumentHighlights(missingNames)
4522
+ const argumentsSection = document.getElementById('argumentsSection')
4523
+ if (argumentsSection) {
4524
+ argumentsSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
4525
+ }
4526
+ showMessage(`以下参数未在内容中使用:${missingNames.join(', ')}`, 'error')
4527
+ const firstMissing = missingNames[0]
4528
+ requestAnimationFrame(() => {
4529
+ const listEl = document.getElementById('argumentsList')
4530
+ if (!listEl) return
4531
+ const targetCard = Array.from(listEl.querySelectorAll('.argument-card')).find(card => {
4532
+ const idx = Number(card.dataset.index)
4533
+ const argName = argumentsState[idx]?.name?.trim()
4534
+ return argName === firstMissing
4535
+ })
4536
+ if (targetCard) {
4537
+ const targetIndex = Number(targetCard.dataset.index)
4538
+ if (Number.isInteger(targetIndex)) {
4539
+ openArgumentModal(targetIndex)
4540
+ }
4541
+ }
4542
+ })
4543
+ return
4544
+ }
4545
+ } else {
4546
+ setUnusedArgumentHighlights([])
4547
+ }
4548
+ const yaml = window.jsyaml.dump(promptObject)
4549
+
4550
+ let originalText = ''
4551
+ if (saveBtn) {
4552
+ originalText = (saveBtn.textContent || '保存').trim()
4553
+ saveBtn.disabled = true
4554
+ saveBtn.classList.add('loading')
4555
+ saveBtn.textContent = '保存中...'
4556
+ }
4557
+
4558
+ try {
4559
+ const result = await apiCall('/prompts', {
4560
+ method: 'POST',
4561
+ body: JSON.stringify({
4562
+ name,
4563
+ group,
4564
+ yaml,
4565
+ relativePath: currentPrompt?.relativePath || null
4566
+ })
4567
+ })
4568
+
4569
+ showMessage('保存成功')
4570
+ currentPromptObject = clonePromptObject(promptObject)
4571
+ const updatedRelativePath = result?.relativePath || currentPrompt?.relativePath || null
4572
+ const pathSegments = updatedRelativePath ? updatedRelativePath.split('/') : []
4573
+ const updatedGroupPath = pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : (pathSegments[0] || currentPrompt?.groupPath || group || 'default')
4574
+ currentPrompt = {
4575
+ ...(currentPrompt || {}),
4576
+ name,
4577
+ relativePath: updatedRelativePath,
4578
+ group: result?.group || group,
4579
+ groupPath: updatedGroupPath
4580
+ }
4581
+ // 重新加载列表,传递搜索参数
4582
+ const searchInput = document.getElementById('searchInput');
4583
+ const searchValue = searchInput ? searchInput.value : '';
4584
+ await loadPrompts(searchValue)
4585
+ } catch (error) {
4586
+ console.error('保存失败:', error)
4587
+ if (!error?.__shown) {
4588
+ showMessage('保存失败: ' + (error?.message || '未知错误'), 'error')
4589
+ }
4590
+ } finally {
4591
+ if (saveBtn) {
4592
+ saveBtn.disabled = false
4593
+ saveBtn.classList.remove('loading')
4594
+ saveBtn.textContent = originalText
4595
+ }
4596
+ }
4597
+ }
4598
+
4599
+ // 重置编辑器
4600
+ function resetEditor() {
4601
+ document.getElementById('promptName').value = ''
4602
+ const groupSelect = document.getElementById('promptGroup')
4603
+ if (groupSelect) {
4604
+ groupSelect.value = 'default'
4605
+ updatePromptGroupDisplay()
4606
+ setCascaderActivePathsByValue('default')
4607
+ if (isGroupDropdownOpen) {
4608
+ renderGroupDropdownContent(promptGroupSearchInput?.value || '')
4609
+ }
4610
+ }
4611
+ if (editor) editor.setValue('')
4612
+ if (descriptionInputEl) {
4613
+ descriptionInputEl.value = ''
4614
+ adjustDescriptionHeight()
4615
+ }
4616
+ setArgumentsState([])
4617
+ currentPrompt = null
4618
+ currentPromptObject = null
4619
+ document.getElementById('previewContent').innerHTML = '<p>选择或创建prompt开始编辑</p>'
4620
+ setWorkspaceMode('edit')
4621
+ }
4622
+
4623
+ // 新建prompt
4624
+ function newPrompt() {
4625
+ currentPrompt = null
4626
+ currentPromptObject = createDefaultPromptObject()
4627
+
4628
+ const nameInput = document.getElementById('promptName')
4629
+ if (nameInput) nameInput.value = currentPromptObject.name
4630
+ const groupSelect = document.getElementById('promptGroup')
4631
+ if (groupSelect) {
4632
+ groupSelect.value = 'default'
4633
+ updatePromptGroupDisplay()
4634
+ setCascaderActivePathsByValue('default')
4635
+ if (isGroupDropdownOpen) {
4636
+ renderGroupDropdownContent(promptGroupSearchInput?.value || '')
4637
+ }
4638
+ }
4639
+
4640
+ const defaultMessage = getFirstUserMessage(currentPromptObject)
4641
+ if (editor) {
4642
+ editor.setValue(defaultMessage?.content?.text || '')
4643
+ }
4644
+
4645
+ setArgumentsState(currentPromptObject.arguments || [])
4646
+
4647
+ if (descriptionInputEl) {
4648
+ descriptionInputEl.value = currentPromptObject.description || ''
4649
+ adjustDescriptionHeight()
4650
+ }
4651
+
4652
+ document.getElementById('previewContent').innerHTML = '<p>选择或创建prompt开始编辑</p>'
4653
+ setWorkspaceMode('edit')
4654
+
4655
+ document.querySelectorAll('.prompt-item').forEach(el => el.classList.remove('active'))
4656
+ updatePreview(true)
4657
+ }
4658
+
4659
+ // 打开/关闭新建目录弹窗
4660
+ function toggleNewFolderModal(show) {
4661
+ const modal = document.getElementById('newFolderModal')
4662
+ const input = document.getElementById('newFolderName')
4663
+ const shouldShow = typeof show === 'boolean' ? show : modal.classList.contains('hidden')
4664
+ modal.classList.toggle('hidden', !shouldShow)
4665
+
4666
+ if (shouldShow) {
4667
+ document.body.style.overflow = 'hidden'
4668
+ if (input) input.value = ''
4669
+ resetGroupManageState()
4670
+ setGroupModalTab('create')
4671
+ } else {
4672
+ document.body.style.overflow = ''
4673
+ if (input) input.value = ''
4674
+ groupManageEditingPath = null
4675
+ groupManageActionLoading.clear()
4676
+ }
4677
+ }
4678
+
4679
+ // 处理目录名输入框的键盘事件
4680
+ function handleNewFolderKeydown(event) {
4681
+ if (event.key === 'Enter') {
4682
+ event.preventDefault()
4683
+ createNewFolder()
4684
+ } else if (event.key === 'Escape') {
4685
+ event.preventDefault()
4686
+ toggleNewFolderModal(false)
4687
+ }
4688
+ }
4689
+
4690
+ // 创建新目录
4691
+ async function createNewFolder() {
4692
+ const input = document.getElementById('newFolderName')
4693
+ const folderName = (input?.value || '').trim()
4694
+
4695
+ if (!folderName) {
4696
+ showMessage('请输入目录名称', 'error')
4697
+ input?.focus()
4698
+ return
4699
+ }
4700
+
4701
+ try {
4702
+ await apiCall('/groups', {
4703
+ method: 'POST',
4704
+ body: JSON.stringify({ name: folderName })
4705
+ })
4706
+
4707
+ showMessage('类目创建成功')
4708
+ toggleNewFolderModal(false)
4709
+ const searchInput = document.getElementById('searchInput')
4710
+ const searchValue = searchInput ? searchInput.value : ''
4711
+ await loadPrompts(searchValue)
4712
+ } catch (error) {
4713
+ console.error('创建目录失败:', error)
4714
+ showMessage(error.message || '创建类目失败', 'error')
4715
+ }
4716
+ }
4717
+
4718
+ // 初始化应用
4719
+ async function initApp() {
4720
+ try {
4721
+ // 样式资源:https://codemirror.net/5/theme/
4722
+ // 效果预览:https://toolshu.com/codemirror-theme-prev#default
4723
+
4724
+ // 并行加载资源
4725
+ await Promise.all([
4726
+ loadScript('./js/codemirror.min.js'),
4727
+ loadScript('./js/markdown.min.js'),
4728
+ loadScript('./js/closebrackets.min.js'),
4729
+ loadScript('./js/js-yaml.min.js')
4730
+ ])
4731
+
4732
+ // 初始化编辑器
4733
+ editor = CodeMirror.fromTextArea(document.getElementById('editor'), {
4734
+ mode: 'markdown',
4735
+ lineNumbers: false,
4736
+ theme: 'xq-light',
4737
+ lineWrapping: true,
4738
+ autoCloseBrackets: true,
4739
+ value: '请选择或创建prompt开始编辑'
4740
+ })
4741
+
4742
+ editor.on('change', () => updatePreview())
4743
+
4744
+ const editModeBtn = document.getElementById('editModeBtn')
4745
+ const previewModeBtn = document.getElementById('previewModeBtn')
4746
+ editModeBtn.addEventListener('click', () => setWorkspaceMode('edit'))
4747
+ previewModeBtn.addEventListener('click', () => setWorkspaceMode('preview'))
4748
+ setWorkspaceMode('edit')
4749
+
4750
+ descriptionInputEl = document.getElementById('promptDescription')
4751
+ if (descriptionInputEl) {
4752
+ descriptionInputEl.addEventListener('input', adjustDescriptionHeight)
4753
+ adjustDescriptionHeight()
4754
+ }
4755
+
4756
+ setArgumentsState([])
4757
+
4758
+ argumentModalEl = document.getElementById('argumentModal')
4759
+ argumentFormEl = document.getElementById('argumentForm')
4760
+ argumentModalTitleEl = document.getElementById('argumentModalTitle')
4761
+ argumentNameInput = document.getElementById('argumentNameInput')
4762
+ argumentTypeInput = document.getElementById('argumentTypeInput')
4763
+ argumentRequiredInput = document.getElementById('argumentRequiredInput')
4764
+ argumentDefaultInput = document.getElementById('argumentDefaultInput')
4765
+ argumentDescriptionInput = document.getElementById('argumentDescriptionInput')
4766
+ deletePromptModalEl = document.getElementById('deletePromptModal')
4767
+ deletePromptNameEl = document.getElementById('deletePromptName')
4768
+ deletePromptConfirmBtn = document.getElementById('deletePromptConfirmBtn')
4769
+ deletePromptCancelBtn = document.getElementById('deletePromptCancelBtn')
4770
+ deletePromptCloseBtn = document.getElementById('deletePromptCloseBtn')
4771
+ promptGroupBtnEl = document.getElementById('promptGroupBtn')
4772
+ promptGroupLabelEl = document.getElementById('promptGroupLabel')
4773
+ promptGroupDropdownEl = document.getElementById('promptGroupDropdown')
4774
+ promptGroupSearchInput = document.getElementById('promptGroupSearch')
4775
+ promptGroupCascaderEl = document.getElementById('promptGroupCascader')
4776
+ promptGroupSearchResultsEl = document.getElementById('promptGroupSearchResults')
4777
+ promptGroupEmptyEl = document.getElementById('promptGroupEmpty')
4778
+ groupManageListEl = document.getElementById('groupManageList')
4779
+ groupManageEmptyEl = document.getElementById('groupManageEmpty')
4780
+ groupManageSearchInputEl = document.getElementById('groupManageSearch')
4781
+ const groupModalTabs = document.querySelectorAll('.group-modal-tab')
4782
+ groupModalTabs.forEach(tabBtn => {
4783
+ tabBtn.addEventListener('click', () => setGroupModalTab(tabBtn.dataset.tab))
4784
+ })
4785
+ const groupManageRefreshBtn = document.getElementById('groupManageRefreshBtn')
4786
+ if (groupManageSearchInputEl) {
4787
+ groupManageSearchInputEl.addEventListener('input', handleGroupManageSearchInput)
4788
+ }
4789
+ if (groupManageRefreshBtn) {
4790
+ groupManageRefreshBtn.addEventListener('click', () => {
4791
+ groupManageEditingPath = null
4792
+ refreshGroupData()
4793
+ })
4794
+ }
4795
+ if (groupManageListEl) {
4796
+ groupManageListEl.addEventListener('click', handleGroupManageListClick)
4797
+ }
4798
+ updatePromptGroupDisplay()
4799
+
4800
+ if (argumentTypeInput && !argumentTypeInput.value) {
4801
+ argumentTypeInput.value = 'string'
4802
+ }
4803
+
4804
+ const addArgumentBtn = document.getElementById('addArgumentBtn')
4805
+ if (addArgumentBtn) {
4806
+ addArgumentBtn.addEventListener('click', () => openArgumentModal(null))
4807
+ }
4808
+
4809
+ const argumentsListEl = document.getElementById('argumentsList')
4810
+ if (argumentsListEl) {
4811
+ argumentsListEl.addEventListener('click', handleArgumentListClick)
4812
+ }
4813
+
4814
+ if (argumentFormEl) {
4815
+ argumentFormEl.addEventListener('submit', handleArgumentFormSubmit)
4816
+ }
4817
+
4818
+ const argumentCancelBtn = document.getElementById('argumentCancelBtn')
4819
+ if (argumentCancelBtn) {
4820
+ argumentCancelBtn.addEventListener('click', closeArgumentModal)
4821
+ }
4822
+
4823
+ const argumentModalClose = document.getElementById('argumentModalClose')
4824
+ if (argumentModalClose) {
4825
+ argumentModalClose.addEventListener('click', closeArgumentModal)
4826
+ }
4827
+
4828
+ if (argumentModalEl) {
4829
+ argumentModalEl.addEventListener('click', (event) => {
4830
+ if (event.target === argumentModalEl) {
4831
+ closeArgumentModal()
4832
+ }
4833
+ })
4834
+ }
4835
+
4836
+ if (promptGroupBtnEl) {
4837
+ promptGroupBtnEl.addEventListener('click', () => toggleGroupDropdown())
4838
+ }
4839
+ if (promptGroupDropdownEl) {
4840
+ promptGroupDropdownEl.addEventListener('mousedown', (event) => {
4841
+ event.stopPropagation()
4842
+ })
4843
+ }
4844
+ if (promptGroupSearchInput) {
4845
+ promptGroupSearchInput.addEventListener('input', (event) => {
4846
+ renderGroupDropdownContent(event.target.value || '')
4847
+ })
4848
+ }
4849
+
4850
+ document.addEventListener('mousedown', (event) => {
4851
+ if (!isGroupDropdownOpen) return
4852
+ if (promptGroupDropdownEl?.contains(event.target) || promptGroupBtnEl?.contains(event.target)) {
4853
+ return
4854
+ }
4855
+ closeGroupDropdown()
4856
+ })
4857
+
4858
+ if (deletePromptConfirmBtn) {
4859
+ deletePromptConfirmBtn.addEventListener('click', confirmDeletePrompt)
4860
+ }
4861
+ const closeDeleteHandlers = [deletePromptCancelBtn, deletePromptCloseBtn]
4862
+ closeDeleteHandlers.forEach(btn => {
4863
+ if (btn) {
4864
+ btn.addEventListener('click', closeDeletePromptModal)
4865
+ }
4866
+ })
4867
+ if (deletePromptModalEl) {
4868
+ deletePromptModalEl.addEventListener('click', (event) => {
4869
+ if (event.target === deletePromptModalEl) {
4870
+ closeDeletePromptModal()
4871
+ }
4872
+ })
4873
+ }
4874
+
4875
+ document.addEventListener('keydown', (event) => {
4876
+ if (event.key === 'Escape') {
4877
+ if (isGroupDropdownOpen) {
4878
+ closeGroupDropdown(true)
4879
+ return
4880
+ }
4881
+ if (argumentModalEl && !argumentModalEl.classList.contains('hidden')) {
4882
+ closeArgumentModal()
4883
+ return
4884
+ }
4885
+ if (deletePromptModalEl && !deletePromptModalEl.classList.contains('hidden')) {
4886
+ closeDeletePromptModal()
4887
+ }
4888
+ }
4889
+ })
4890
+
4891
+ // 绑定事件
4892
+ document.getElementById('loginBtn').addEventListener('click', () => {
4893
+ const username = document.getElementById('username').value.trim()
4894
+ const password = document.getElementById('password').value.trim()
4895
+
4896
+ if (!username || !password) {
4897
+ document.getElementById('loginError').textContent = '请输入用户名和密码'
4898
+ return
4899
+ }
4900
+
4901
+ login(username, password)
4902
+ })
4903
+
4904
+ document.getElementById('logoutBtn').addEventListener('click', logout)
4905
+ document.getElementById('newPromptBtn').addEventListener('click', newPrompt)
4906
+ document.getElementById('newGroupBtn').addEventListener('click', () => toggleNewFolderModal(true))
4907
+ document.getElementById('saveBtn').addEventListener('click', savePrompt)
4908
+
4909
+ // 搜索功能
4910
+ const searchInput = document.getElementById('searchInput')
4911
+ const searchBox = searchInput.closest('.search-box')
4912
+ const clearBtn = searchBox.querySelector('.clear-btn')
4913
+ let searchTimeout
4914
+
4915
+ // 搜索输入事件
4916
+ searchInput.addEventListener('input', () => {
4917
+ clearTimeout(searchTimeout)
4918
+ searchTimeout = setTimeout(() => {
4919
+ loadPrompts(searchInput.value)
4920
+ }, 300)
4921
+ })
4922
+
4923
+ // 清除按钮点击事件
4924
+ if (clearBtn) {
4925
+ clearBtn.addEventListener('click', () => {
4926
+ searchInput.value = ''
4927
+ searchInput.focus()
4928
+ loadPrompts('')
4929
+ })
4930
+ }
4931
+
4932
+ // ESC 键清除搜索
4933
+ searchInput.addEventListener('keydown', (e) => {
4934
+ if (e.key === 'Escape') {
4935
+ searchInput.value = ''
4936
+ loadPrompts('')
4937
+ }
4938
+ })
4939
+
4940
+ // 检查登录状态
4941
+ if (currentToken) {
4942
+ showMain()
4943
+ loadPrompts()
4944
+ } else {
4945
+ showLogin()
4946
+ }
4947
+
4948
+ } catch (err) {
4949
+ console.error('初始化错误:', err)
4950
+ document.getElementById('loginError').textContent = '资源加载失败,请刷新重试'
4951
+ throw err
4952
+ }
4953
+ }
4954
+
4955
+ // 启动应用
4956
+ document.addEventListener('DOMContentLoaded', initApp)
4957
+ </script>
4958
+ </body>
4959
+ </html>