@becrafter/prompt-manager 0.1.2 → 0.1.9

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 (110) hide show
  1. package/README.md +304 -121
  2. package/app/cli/commands/start.js +65 -4
  3. package/app/cli/support/argv.js +6 -0
  4. package/env.example +32 -0
  5. package/package.json +36 -7
  6. package/packages/server/api/admin.routes.js +409 -1
  7. package/packages/server/api/open.routes.js +7 -2
  8. package/packages/server/api/tool.routes.js +479 -0
  9. package/packages/server/app.js +97 -25
  10. package/packages/server/configs/models/built-in/bigmodel.yaml +6 -0
  11. package/packages/server/configs/models/providers.yaml +50 -0
  12. package/packages/server/configs/templates/built-in/general-iteration.yaml +60 -0
  13. package/packages/server/configs/templates/built-in/general-optimize.yaml +63 -0
  14. package/packages/server/configs/templates/built-in/output-format-optimize.yaml +95 -0
  15. package/packages/server/mcp/heartbeat-patch.js +73 -0
  16. package/packages/server/mcp/mcp.server.js +63 -314
  17. package/packages/server/mcp/prompt.handler.js +26 -0
  18. package/packages/server/mcp/thinking-toolkit.handler.js +380 -0
  19. package/packages/server/package.json +35 -3
  20. package/packages/server/server.js +114 -12
  21. package/packages/server/services/TerminalService.js +498 -0
  22. package/packages/server/services/WebSocketService.js +484 -0
  23. package/packages/server/services/manager.js +38 -7
  24. package/packages/server/services/model.service.js +473 -0
  25. package/packages/server/services/optimization.service.js +457 -0
  26. package/packages/server/services/template.service.js +333 -0
  27. package/packages/server/toolm/tool-description-generator-optimized.service.js +5 -2
  28. package/packages/server/toolm/tool-sync.service.js +47 -3
  29. package/packages/server/utils/config.js +8 -1
  30. package/packages/server/utils/port-checker.js +63 -0
  31. package/packages/server/utils/util.js +27 -0
  32. package/IFLOW.md +0 -175
  33. package/app/desktop/assets/app.1.png +0 -0
  34. package/app/desktop/assets/app.png +0 -0
  35. package/app/desktop/assets/icons/icon.icns +0 -0
  36. package/app/desktop/assets/icons/icon.ico +0 -0
  37. package/app/desktop/assets/icons/icon.png +0 -0
  38. package/app/desktop/assets/icons/tray.png +0 -0
  39. package/app/desktop/assets/templates/about.html +0 -147
  40. package/app/desktop/assets/tray.1.png +0 -0
  41. package/app/desktop/assets/tray.png +0 -0
  42. package/app/desktop/main.js +0 -241
  43. package/app/desktop/package-lock.json +0 -5026
  44. package/app/desktop/package.json +0 -100
  45. package/app/desktop/preload.js +0 -7
  46. package/app/desktop/src/core/error-handler.js +0 -108
  47. package/app/desktop/src/core/event-emitter.js +0 -84
  48. package/app/desktop/src/core/logger.js +0 -108
  49. package/app/desktop/src/core/state-manager.js +0 -125
  50. package/app/desktop/src/services/module-loader.js +0 -214
  51. package/app/desktop/src/services/runtime-manager.js +0 -301
  52. package/app/desktop/src/services/service-manager.js +0 -169
  53. package/app/desktop/src/services/update-manager.js +0 -267
  54. package/app/desktop/src/ui/about-dialog-manager.js +0 -208
  55. package/app/desktop/src/ui/admin-window-manager.js +0 -757
  56. package/app/desktop/src/ui/splash-manager.js +0 -253
  57. package/app/desktop/src/ui/tray-manager.js +0 -186
  58. package/app/desktop/src/utils/icon-manager.js +0 -133
  59. package/app/desktop/src/utils/path-utils.js +0 -58
  60. package/app/desktop/src/utils/resource-paths.js +0 -49
  61. package/app/desktop/src/utils/resource-sync.js +0 -260
  62. package/app/desktop/src/utils/runtime-sync.js +0 -241
  63. package/app/desktop/src/utils/template-renderer.js +0 -284
  64. package/app/desktop/src/utils/version-utils.js +0 -59
  65. package/examples/prompts/developer/code-review.yaml +0 -32
  66. package/examples/prompts/developer/code_refactoring.yaml +0 -31
  67. package/examples/prompts/developer/doc-generator.yaml +0 -36
  68. package/examples/prompts/developer/error-code-fixer.yaml +0 -35
  69. package/examples/prompts/engineer/engineer-professional.yaml +0 -92
  70. package/examples/prompts/engineer/laowang-engineer.yaml +0 -132
  71. package/examples/prompts/engineer/nekomata-engineer.yaml +0 -123
  72. package/examples/prompts/engineer/ojousama-engineer.yaml +0 -124
  73. package/examples/prompts/generator/gen_3d_edu_webpage_html.yaml +0 -117
  74. package/examples/prompts/generator/gen_3d_webpage_html.yaml +0 -75
  75. package/examples/prompts/generator/gen_bento_grid_html.yaml +0 -112
  76. package/examples/prompts/generator/gen_html_web_page.yaml +0 -88
  77. package/examples/prompts/generator/gen_knowledge_card_html.yaml +0 -83
  78. package/examples/prompts/generator/gen_magazine_card_html.yaml +0 -82
  79. package/examples/prompts/generator/gen_mimeng_headline_title.yaml +0 -71
  80. package/examples/prompts/generator/gen_podcast_script.yaml +0 -69
  81. package/examples/prompts/generator/gen_prd_prototype_html.yaml +0 -175
  82. package/examples/prompts/generator/gen_summarize.yaml +0 -157
  83. package/examples/prompts/generator/gen_title.yaml +0 -119
  84. package/examples/prompts/generator/others/api_documentation.yaml +0 -32
  85. package/examples/prompts/generator/others/build_mcp_server.yaml +0 -26
  86. package/examples/prompts/generator/others/project_architecture.yaml +0 -31
  87. package/examples/prompts/generator/others/test_case_generator.yaml +0 -30
  88. package/examples/prompts/generator/others/writing_assistant.yaml +0 -72
  89. package/examples/prompts/recommend/human_3-0_growth_diagnostic_coach_prompt.yaml +0 -105
  90. package/examples/prompts/workflow/sixstep-workflow.yaml +0 -192
  91. package/packages/admin-ui/.babelrc +0 -3
  92. package/packages/admin-ui/admin.html +0 -412
  93. package/packages/admin-ui/css/codemirror-theme_xq-light.css +0 -43
  94. package/packages/admin-ui/css/codemirror.css +0 -344
  95. package/packages/admin-ui/css/main.css +0 -2592
  96. package/packages/admin-ui/css/recommended-prompts.css +0 -610
  97. package/packages/admin-ui/package-lock.json +0 -6973
  98. package/packages/admin-ui/package.json +0 -36
  99. package/packages/admin-ui/src/codemirror.js +0 -53
  100. package/packages/admin-ui/src/index.js +0 -3188
  101. package/packages/admin-ui/webpack.config.js +0 -76
  102. package/packages/server/toolm/test-tools.js +0 -264
  103. package/scripts/build-icons.js +0 -135
  104. package/scripts/build.sh +0 -57
  105. package/scripts/postinstall.js +0 -34
  106. package/scripts/surge/CNAME +0 -1
  107. package/scripts/surge/README.md +0 -47
  108. package/scripts/surge/package-lock.json +0 -34
  109. package/scripts/surge/package.json +0 -20
  110. package/scripts/surge/sync-to-surge.js +0 -151
@@ -1,3188 +0,0 @@
1
- // src/index.js - 主应用入口文件
2
-
3
- // 导入样式
4
- import '../css/main.css';
5
- import '../css/recommended-prompts.css';
6
-
7
- // 导入 CodeMirror 相关功能
8
- import { initCodeMirror } from './codemirror';
9
-
10
- // 导入 CodeMirror 5 组件用于预览编辑器
11
- import CodeMirror from 'codemirror';
12
- import 'codemirror/mode/markdown/markdown';
13
- import 'codemirror/addon/edit/closebrackets';
14
- import 'codemirror/addon/edit/matchbrackets';
15
-
16
- // 应用状态
17
- let currentToken = localStorage.getItem('prompt-admin-token');
18
- let currentPrompt = null;
19
- let currentPromptObject = null;
20
- let descriptionInputEl = null;
21
- let allPrompts = [];
22
- let expandedGroups = new Set();
23
- let editor = null;
24
- let argumentsState = [];
25
- let unusedArgumentNames = new Set();
26
- let editingArgumentIndex = null;
27
- let argumentModalEl = null;
28
- let argumentFormEl = null;
29
- let argumentModalTitleEl = null;
30
- let argumentNameInput = null;
31
- let argumentTypeInput = null;
32
- let argumentRequiredInput = null;
33
- let argumentDefaultInput = null;
34
- let argumentDescriptionInput = null;
35
- let deletePromptModalEl = null;
36
- let deletePromptNameEl = null;
37
- let deletePromptConfirmBtn = null;
38
- let deletePromptCancelBtn = null;
39
- let deletePromptCloseBtn = null;
40
- let pendingDeletePromptName = null;
41
- let pendingDeletePromptPath = null;
42
- let promptGroupBtnEl = null;
43
- let promptGroupLabelEl = null;
44
- let promptGroupDropdownEl = null;
45
- let promptGroupSearchInput = null;
46
- let promptGroupCascaderEl = null;
47
- let promptGroupSearchResultsEl = null;
48
- let promptGroupEmptyEl = null;
49
- let groupTreeState = [];
50
- let cascaderActivePaths = [];
51
- let isGroupDropdownOpen = false;
52
- let groupModalActiveTab = 'create';
53
- let groupManageListEl = null;
54
- let groupManageEmptyEl = null;
55
- let groupManageSearchInputEl = null;
56
- let groupManageSearchValue = '';
57
- let groupManageEditingPath = null;
58
- const groupManageActionLoading = new Set();
59
- // 是否需要认证(默认不需要,直到从服务器获取配置)
60
- let requireAuth = false;
61
-
62
- const API_HOST = 'http://localhost:5621';
63
-
64
- // API 基础配置
65
- const API_BASE = `${API_HOST}/adminapi`;
66
- const API_SURGE = `${API_HOST}/surge/`;
67
-
68
- // 提示组件
69
- function showMessage(message, type = 'success', options = {}) {
70
- const container = document.getElementById('toastContainer');
71
- if (!container) return;
72
-
73
- const normalizedType = ['success', 'error', 'info', 'warning'].includes(type) ? type : 'success';
74
- const titleMap = {
75
- success: '操作成功',
76
- error: '操作失败',
77
- info: '提示信息',
78
- warning: '注意'
79
- };
80
- const iconMap = {
81
- success: '✓',
82
- error: '✕',
83
- info: 'ℹ',
84
- warning: '!'
85
- };
86
-
87
- const toast = document.createElement('div');
88
- toast.className = `toast toast-${normalizedType}`;
89
- toast.setAttribute('role', normalizedType === 'error' ? 'alert' : 'status');
90
- toast.setAttribute('aria-live', normalizedType === 'error' ? 'assertive' : 'polite');
91
-
92
- const iconEl = document.createElement('span');
93
- iconEl.className = 'toast-icon';
94
- iconEl.textContent = options.icon || iconMap[normalizedType];
95
-
96
- const contentEl = document.createElement('div');
97
- contentEl.className = 'toast-content';
98
-
99
- const titleText = options.title || titleMap[normalizedType];
100
- if (titleText) {
101
- const titleEl = document.createElement('div');
102
- titleEl.className = 'toast-title';
103
- titleEl.textContent = titleText;
104
- contentEl.appendChild(titleEl);
105
- }
106
-
107
- const messageEl = document.createElement('div');
108
- messageEl.className = 'toast-message';
109
- messageEl.textContent = message;
110
- contentEl.appendChild(messageEl);
111
-
112
- const closeBtn = document.createElement('button');
113
- closeBtn.className = 'toast-close';
114
- closeBtn.type = 'button';
115
- closeBtn.setAttribute('aria-label', '关闭提示');
116
- closeBtn.innerHTML = '×';
117
-
118
- toast.appendChild(iconEl);
119
- toast.appendChild(contentEl);
120
- toast.appendChild(closeBtn);
121
- container.appendChild(toast);
122
-
123
- let hideTimer;
124
- let removed = false;
125
-
126
- const removeToast = () => {
127
- if (removed) return;
128
- removed = true;
129
- toast.classList.add('toast-leave');
130
- toast.removeEventListener('mouseenter', pauseTimer);
131
- toast.removeEventListener('mouseleave', resumeTimer);
132
- setTimeout(() => {
133
- if (toast.parentNode === container) {
134
- container.removeChild(toast);
135
- }
136
- }, 250);
137
- };
138
-
139
- const pauseTimer = () => clearTimeout(hideTimer);
140
- const resumeTimer = () => {
141
- clearTimeout(hideTimer);
142
- hideTimer = setTimeout(removeToast, Number(options.duration) || 5000);
143
- };
144
-
145
- closeBtn.addEventListener('click', () => {
146
- pauseTimer();
147
- removeToast();
148
- });
149
-
150
- toast.addEventListener('mouseenter', pauseTimer);
151
- toast.addEventListener('mouseleave', resumeTimer);
152
-
153
- resumeTimer();
154
- }
155
-
156
- // API请求封装
157
- async function apiCall(endpoint, options = {}) {
158
- const config = {
159
- headers: {
160
- 'Content-Type': 'application/json',
161
- ...options.headers
162
- },
163
- ...options
164
- };
165
-
166
- // 如果需要认证且有token,则添加认证头
167
- if (requireAuth && currentToken) {
168
- config.headers.Authorization = `Bearer ${currentToken}`;
169
- }
170
-
171
- try {
172
- const response = await fetch(`${API_BASE}${endpoint}`, config);
173
-
174
- if (response.status === 401 && requireAuth) {
175
- // Token 过期,重新登录
176
- localStorage.removeItem('prompt-admin-token');
177
- currentToken = null;
178
- showLogin();
179
- throw new Error('登录已过期,请重新登录');
180
- }
181
-
182
- if (!response.ok) {
183
- const error = await response.text();
184
- throw new Error(error || '请求失败');
185
- }
186
-
187
- return await response.json();
188
- } catch (error) {
189
- // 区分网络错误和其他错误
190
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
191
- // 网络错误
192
- console.error('网络请求失败:', error);
193
- showMessage('网络连接失败,请检查服务器是否运行', 'error');
194
- throw new Error('网络连接失败,请检查服务器是否运行');
195
- } else {
196
- const handledError = error instanceof Error ? error : new Error(error?.message || '请求失败');
197
- handledError.__shown = true;
198
- console.error('API请求错误:', handledError);
199
- showMessage(handledError.message, 'error');
200
- throw handledError;
201
- }
202
- }
203
- }
204
-
205
- // 登录功能
206
- async function login(username, password) {
207
- try {
208
- const result = await apiCall('/login', {
209
- method: 'POST',
210
- body: JSON.stringify({ username, password })
211
- });
212
-
213
- currentToken = result.token;
214
- localStorage.setItem('prompt-admin-token', currentToken);
215
- showMain();
216
- loadPrompts();
217
- } catch (error) {
218
- document.getElementById('loginError').textContent = error.message || '登录失败';
219
- }
220
- }
221
-
222
- // 获取或创建令牌(在不需要认证时使用)
223
- async function fetchToken() {
224
- try {
225
- const result = await apiCall('/login', {
226
- method: 'POST',
227
- body: JSON.stringify({ username: '', password: '' })
228
- });
229
-
230
- currentToken = result.token;
231
- localStorage.setItem('prompt-admin-token', currentToken);
232
- showMain();
233
- loadPrompts();
234
-
235
- // 隐藏登录界面
236
- const loginElement = document.getElementById('login');
237
- if (loginElement) {
238
- loginElement.style.display = 'none';
239
- }
240
- } catch (error) {
241
- console.error('获取令牌失败:', error);
242
- showMessage('获取访问权限失败', 'error');
243
- // 即使获取令牌失败,也要确保显示主界面
244
- showMain();
245
- loadPrompts();
246
- }
247
- }
248
-
249
- // 检查是否需要认证
250
- async function checkAuthRequirement() {
251
- try {
252
- // 尝试获取服务器配置来判断是否需要认证
253
- const response = await fetch(`${API_BASE}/config`, {
254
- method: 'GET',
255
- headers: {
256
- 'Content-Type': 'application/json'
257
- }
258
- });
259
-
260
- if (response.ok) {
261
- const config = await response.json();
262
- requireAuth = config.requireAuth !== false;
263
- } else {
264
- // 如果获取配置失败,默认不需要认证(降级处理)
265
- requireAuth = false;
266
- }
267
- } catch (error) {
268
- // 如果请求失败,默认不需要认证(降级处理)
269
- console.warn('无法获取服务器配置,使用默认认证设置:', error);
270
- requireAuth = false;
271
- }
272
-
273
- // 根据认证要求设置登录界面的显示
274
- updateLoginDisplay();
275
- }
276
-
277
- // 转义HTML
278
- function escapeHtml(input) {
279
- if (input === null || input === undefined) {
280
- return '';
281
- }
282
- return String(input).replace(/[&<>"]/g, (match) => {
283
- const map = {
284
- '&': '&amp;',
285
- '<': '&lt;',
286
- '>': '&gt;',
287
- '"': '&quot;',
288
- "'": '&#39;'
289
- };
290
- return map[match] || match;
291
- });
292
- }
293
-
294
- // 转义正则表达式
295
- function escapeRegExp(input) {
296
- return String(input).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
297
- }
298
-
299
- // 退出登录
300
- function logout() {
301
- localStorage.removeItem('prompt-admin-token');
302
- currentToken = null;
303
- showLogin();
304
- }
305
-
306
- // 更新登录界面显示状态
307
- function updateLoginDisplay() {
308
- const loginElement = document.getElementById('login');
309
- if (loginElement) {
310
- loginElement.style.display = requireAuth ? 'flex' : 'none';
311
- }
312
-
313
- // 更新头像的状态
314
- const avatarBtn = document.getElementById('avatarBtn');
315
- if (avatarBtn) {
316
- if (requireAuth) {
317
- avatarBtn.classList.remove('no-auth');
318
- avatarBtn.setAttribute('aria-haspopup', 'true');
319
- } else {
320
- avatarBtn.classList.add('no-auth');
321
- avatarBtn.setAttribute('aria-haspopup', 'false');
322
- }
323
- }
324
- }
325
-
326
- // 显示登录界面
327
- function showLogin() {
328
- // 如果不需要认证,直接获取令牌并显示主界面
329
- if (!requireAuth) {
330
- fetchToken().catch(error => {
331
- console.error('自动获取令牌失败:', error);
332
- // 即使获取令牌失败,也要确保显示主界面
333
- showMain();
334
- loadPrompts();
335
- });
336
- return;
337
- }
338
- document.getElementById('login').style.display = 'flex';
339
- document.getElementById('main').style.display = 'none';
340
- }
341
-
342
- // 显示主界面
343
- function showMain() {
344
- document.getElementById('login').style.display = 'none';
345
- document.getElementById('main').style.display = 'block';
346
- }
347
-
348
- // 显示加载中效果
349
- function showLoading() {
350
- const loadingOverlay = document.getElementById('loadingOverlay');
351
- if (loadingOverlay) {
352
- loadingOverlay.classList.remove('hidden');
353
- }
354
- }
355
-
356
- // 隐藏加载中效果
357
- function hideLoading() {
358
- const loadingOverlay = document.getElementById('loadingOverlay');
359
- if (loadingOverlay) {
360
- loadingOverlay.classList.add('hidden');
361
- }
362
- }
363
-
364
- // 加载prompt列表
365
- async function loadPrompts(search = '', enabledOnly = false, group = null) {
366
- try {
367
- // 显示加载中效果
368
- showLoading();
369
-
370
- const queryParams = new URLSearchParams();
371
- if (search) queryParams.append('search', search);
372
- if (enabledOnly) queryParams.append('enabled', 'true');
373
- if (group) queryParams.append('group', group);
374
-
375
- const prompts = await apiCall(`/prompts?${queryParams}`);
376
- allPrompts = prompts;
377
- await renderGroupList(prompts);
378
- } catch (error) {
379
- console.error('加载prompt列表失败:', error);
380
- } finally {
381
- // 隐藏加载中效果
382
- hideLoading();
383
- }
384
- }
385
-
386
- function createDefaultPromptObject() {
387
- return {
388
- name: 'new-prompt',
389
- description: '新prompt描述',
390
- enabled: true,
391
- messages: [
392
- {
393
- role: 'user',
394
- content: {
395
- text: `这是一个新的prompt模板\n{{variable1}}`
396
- }
397
- }
398
- ],
399
- arguments: [
400
- {
401
- name: 'variable1',
402
- type: 'string',
403
- required: false,
404
- default: '',
405
- description: '示例变量'
406
- }
407
- ],
408
- variables: [
409
- {
410
- name: 'variable1',
411
- type: 'string',
412
- required: false,
413
- default: '',
414
- description: '示例变量'
415
- }
416
- ]
417
- };
418
- }
419
-
420
- // 用户头像下拉菜单控制
421
- function setupUserMenu() {
422
- const avatarBtn = document.getElementById('avatarBtn');
423
- const userMenu = document.getElementById('userMenu');
424
-
425
- // 根据是否需要认证设置头像的可点击状态
426
- if (avatarBtn) {
427
- if (requireAuth) {
428
- avatarBtn.classList.remove('no-auth');
429
- avatarBtn.setAttribute('aria-haspopup', 'true');
430
- } else {
431
- avatarBtn.classList.add('no-auth');
432
- avatarBtn.setAttribute('aria-haspopup', 'false');
433
- }
434
- }
435
-
436
- // 点击头像显示/隐藏下拉菜单,但仅在需要认证时才生效
437
- avatarBtn.addEventListener('click', (e) => {
438
- // 如果需要认证,则显示下拉菜单
439
- if (requireAuth) {
440
- e.stopPropagation();
441
- userMenu.classList.toggle('show');
442
- avatarBtn.setAttribute('aria-expanded', userMenu.classList.contains('show'));
443
- }
444
- // 如果不需要认证,则不执行任何操作
445
- });
446
-
447
- // 点击页面其他地方关闭下拉菜单(仅在需要认证时才生效)
448
- document.addEventListener('click', (e) => {
449
- if (requireAuth && !userMenu.contains(e.target) && !avatarBtn.contains(e.target)) {
450
- userMenu.classList.remove('show');
451
- avatarBtn.setAttribute('aria-expanded', 'false');
452
- }
453
- });
454
-
455
- // ESC 键关闭下拉菜单(仅在需要认证时才生效)
456
- document.addEventListener('keydown', (e) => {
457
- if (requireAuth && e.key === 'Escape' && userMenu.classList.contains('show')) {
458
- userMenu.classList.remove('show');
459
- avatarBtn.setAttribute('aria-expanded', 'false');
460
- }
461
- });
462
- }
463
-
464
- function clonePromptObject(obj) {
465
- return obj ? JSON.parse(JSON.stringify(obj)) : null;
466
- }
467
-
468
- function getFirstUserMessage(promptObj) {
469
- if (!promptObj || !Array.isArray(promptObj.messages)) return null;
470
- return promptObj.messages.find(message => message?.role !== '') || null;
471
- }
472
-
473
- function buildPromptObjectFromUI() {
474
- const nameInput = document.getElementById('promptName');
475
- const name = (nameInput?.value || '').trim() || 'new-prompt';
476
- const description = descriptionInputEl?.value || '';
477
- const markdown = editor ? editor.getValue() : '';
478
-
479
- const base = clonePromptObject(currentPromptObject) || createDefaultPromptObject();
480
- base.name = name;
481
- base.description = description;
482
-
483
- if (!Array.isArray(base.messages)) {
484
- base.messages = [];
485
- }
486
-
487
- let userMessage = getFirstUserMessage(base);
488
- if (!userMessage) {
489
- userMessage = { role: 'user', content: { text: '' } };
490
- base.messages.push(userMessage);
491
- }
492
-
493
- if (!userMessage.content || typeof userMessage.content !== 'object') {
494
- userMessage.content = {};
495
- }
496
-
497
- userMessage.content.text = markdown;
498
-
499
- const sanitizedArguments = Array.isArray(argumentsState)
500
- ? argumentsState
501
- .map(arg => {
502
- const nameValue = (arg.name || '').trim();
503
- if (!nameValue) return null;
504
- const typeValue = (arg.type || '').trim();
505
- const descriptionValue = (arg.description || '').trim();
506
- const defaultValue = arg.default;
507
- const argumentResult = {
508
- name: nameValue,
509
- type: typeValue || 'string',
510
- required: Boolean(arg.required)
511
- };
512
- if (descriptionValue) {
513
- argumentResult.description = descriptionValue;
514
- }
515
- if (defaultValue !== undefined && defaultValue !== null) {
516
- const defaultText = String(defaultValue).trim();
517
- if (defaultText) {
518
- argumentResult.default = defaultText;
519
- }
520
- }
521
- return argumentResult;
522
- })
523
- .filter(Boolean)
524
- : [];
525
-
526
- base.arguments = sanitizedArguments;
527
-
528
- return base;
529
- }
530
-
531
- function adjustDescriptionHeight() {
532
- if (!descriptionInputEl) return;
533
-
534
- const style = window.getComputedStyle(descriptionInputEl);
535
- const lineHeight = parseFloat(style.lineHeight) || 20;
536
- const padding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom) || 0;
537
- const border = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth) || 0;
538
- const maxHeight = lineHeight * 4 + padding + border;
539
- const baseHeight = lineHeight + padding + border;
540
-
541
- descriptionInputEl.style.height = 'auto';
542
- const contentHeight = descriptionInputEl.scrollHeight;
543
- const newHeight = Math.max(baseHeight, Math.min(maxHeight, contentHeight));
544
- descriptionInputEl.style.height = `${newHeight}px`;
545
- descriptionInputEl.style.overflowY = descriptionInputEl.scrollHeight > maxHeight ? 'auto' : 'hidden';
546
- }
547
-
548
- function normalizeArgument(argument = {}) {
549
- return {
550
- name: argument?.name ? String(argument.name) : '',
551
- description: argument?.description ? String(argument.description) : '',
552
- type: argument?.type ? String(argument.type) : 'string',
553
- required: Boolean(argument?.required),
554
- default: argument?.default ?? ''
555
- };
556
- }
557
-
558
- function setArgumentsState(list = []) {
559
- argumentsState = Array.isArray(list) ? list.map(item => normalizeArgument(item)) : [];
560
- unusedArgumentNames = new Set();
561
- const section = document.getElementById('argumentsSection');
562
- if (section) {
563
- section.classList.remove('has-error');
564
- }
565
- renderArgumentsEditor();
566
- }
567
-
568
- function removeArgument(index) {
569
- if (index < 0 || index >= argumentsState.length) return;
570
- argumentsState.splice(index, 1);
571
- setUnusedArgumentHighlights([]);
572
- }
573
-
574
- function renderArgumentsEditor() {
575
- const listEl = document.getElementById('argumentsList');
576
- if (!listEl) return;
577
-
578
- listEl.innerHTML = '';
579
-
580
- if (!argumentsState.length) {
581
- listEl.innerHTML = '<div class="arguments-empty">暂无参数,点击"新增参数"开始配置</div>';
582
- return;
583
- }
584
-
585
- const fragment = document.createDocumentFragment();
586
-
587
- argumentsState.forEach((rawArgument, index) => {
588
- const argument = normalizeArgument(rawArgument);
589
- const displayName = argument.name?.trim() || `参数 ${index + 1}`;
590
- const normalizedName = argument.name?.trim() || '';
591
- const safeType = escapeHtml(argument.type || 'string');
592
- const safeDefault = argument.default ? escapeHtml(String(argument.default)) : '';
593
- const safeDescription = argument.description ? escapeHtml(argument.description) : '';
594
-
595
- const card = document.createElement('div');
596
- card.className = 'argument-card';
597
- card.dataset.index = index;
598
-
599
- if (normalizedName && unusedArgumentNames.has(normalizedName)) {
600
- card.classList.add('argument-card-unused');
601
- }
602
-
603
- const badgeParts = [`<span class="argument-badge">类型:${safeType || 'string'}</span>`];
604
- if (argument.required) {
605
- badgeParts.push('<span class="argument-badge argument-required">必填</span>');
606
- }
607
- if (safeDefault) {
608
- badgeParts.push(`<span class="argument-badge">默认:${safeDefault}</span>`);
609
- }
610
-
611
- const placeholderSnippet = normalizedName
612
- ? `<div>变量占位:<span class="argument-placeholder">{{${escapeHtml(normalizedName)}}}</span></div>`
613
- : '';
614
-
615
- card.innerHTML = `
616
- <div class="argument-card-header">
617
- <div class="argument-name">${escapeHtml(displayName)}</div>
618
- <div class="argument-badges">${badgeParts.join('')}</div>
619
- </div>
620
- <div class="argument-body">
621
- ${safeDescription ? `<div class="argument-description">${safeDescription}</div>` : '<div class="argument-description" style="color: var(--gray);">暂无说明</div>'}
622
- ${placeholderSnippet}
623
- </div>
624
- <div class="argument-actions">
625
- <button type="button" class="argument-action-btn edit" data-action="edit" data-index="${index}">编辑</button>
626
- <button type="button" class="argument-action-btn delete" data-action="delete" data-index="${index}">删除</button>
627
- </div>
628
- `;
629
-
630
- fragment.appendChild(card);
631
- });
632
-
633
- listEl.appendChild(fragment);
634
- }
635
-
636
- function setUnusedArgumentHighlights(names = []) {
637
- unusedArgumentNames = new Set(Array.isArray(names) ? names.map(name => String(name).trim()).filter(Boolean) : []);
638
- const section = document.getElementById('argumentsSection');
639
- if (section) {
640
- section.classList.toggle('has-error', unusedArgumentNames.size > 0);
641
- }
642
- renderArgumentsEditor();
643
- }
644
-
645
- function openDeletePromptModal(promptName, relativePath) {
646
- if (!deletePromptModalEl) return;
647
- pendingDeletePromptName = promptName;
648
- pendingDeletePromptPath = relativePath || null;
649
- if (deletePromptNameEl) {
650
- deletePromptNameEl.textContent = `“${promptName}”`;
651
- }
652
- deletePromptModalEl.classList.remove('hidden');
653
- deletePromptModalEl.setAttribute('aria-hidden', 'false');
654
- refreshModalOpenState();
655
- if (deletePromptConfirmBtn) {
656
- requestAnimationFrame(() => deletePromptConfirmBtn.focus());
657
- }
658
- }
659
-
660
- function closeDeletePromptModal() {
661
- if (!deletePromptModalEl) return;
662
- deletePromptModalEl.classList.add('hidden');
663
- deletePromptModalEl.setAttribute('aria-hidden', 'true');
664
- refreshModalOpenState();
665
- pendingDeletePromptName = null;
666
- pendingDeletePromptPath = null;
667
- }
668
-
669
- async function confirmDeletePrompt() {
670
- if (!pendingDeletePromptName) {
671
- closeDeletePromptModal();
672
- return;
673
- }
674
- const promptToDelete = pendingDeletePromptName;
675
- const pathToDelete = pendingDeletePromptPath;
676
- closeDeletePromptModal();
677
- await deletePrompt(promptToDelete, pathToDelete);
678
- }
679
-
680
- function openArgumentModal(index = null) {
681
- if (!argumentModalEl) return;
682
- editingArgumentIndex = Number.isInteger(index) && index >= 0 ? index : null;
683
- const isEdit = editingArgumentIndex !== null && argumentsState[editingArgumentIndex];
684
- const payload = isEdit ? normalizeArgument(argumentsState[editingArgumentIndex]) : normalizeArgument({ type: 'string' });
685
-
686
- if (argumentModalTitleEl) {
687
- argumentModalTitleEl.textContent = isEdit ? '编辑参数' : '新增参数';
688
- }
689
- if (argumentNameInput) {
690
- argumentNameInput.value = payload.name || '';
691
- }
692
- if (argumentTypeInput) {
693
- argumentTypeInput.value = payload.type || 'string';
694
- }
695
- if (argumentRequiredInput) {
696
- argumentRequiredInput.checked = Boolean(payload.required);
697
- }
698
- if (argumentDefaultInput) {
699
- const modalDefaultValue = payload.default;
700
- argumentDefaultInput.value = modalDefaultValue === undefined || modalDefaultValue === null ? '' : String(modalDefaultValue);
701
- }
702
- if (argumentDescriptionInput) {
703
- argumentDescriptionInput.value = payload.description || '';
704
- }
705
-
706
- argumentModalEl.classList.remove('hidden');
707
- argumentModalEl.setAttribute('aria-hidden', 'false');
708
- refreshModalOpenState();
709
-
710
- requestAnimationFrame(() => {
711
- if (argumentNameInput) {
712
- argumentNameInput.focus();
713
- argumentNameInput.select();
714
- }
715
- });
716
- }
717
-
718
- function closeArgumentModal() {
719
- if (!argumentModalEl) return;
720
- argumentModalEl.classList.add('hidden');
721
- argumentModalEl.setAttribute('aria-hidden', 'true');
722
- refreshModalOpenState();
723
- editingArgumentIndex = null;
724
-
725
- if (argumentFormEl) {
726
- argumentFormEl.reset();
727
- }
728
- if (argumentNameInput) {
729
- argumentNameInput.value = '';
730
- }
731
- if (argumentTypeInput) {
732
- argumentTypeInput.value = 'string';
733
- }
734
- if (argumentRequiredInput) {
735
- argumentRequiredInput.checked = false;
736
- }
737
- if (argumentDefaultInput) {
738
- argumentDefaultInput.value = '';
739
- }
740
- if (argumentDescriptionInput) {
741
- argumentDescriptionInput.value = '';
742
- }
743
- }
744
-
745
- function handleArgumentFormSubmit(event) {
746
- event.preventDefault();
747
- if (!argumentNameInput || !argumentTypeInput) return;
748
-
749
- const name = (argumentNameInput.value || '').trim();
750
- if (!name) {
751
- showMessage('请输入参数名称', 'error');
752
- argumentNameInput.focus();
753
- return;
754
- }
755
-
756
- const type = (argumentTypeInput.value || 'string').trim() || 'string';
757
- const required = argumentRequiredInput ? argumentRequiredInput.checked : false;
758
- const defaultValue = argumentDefaultInput ? argumentDefaultInput.value : '';
759
- const description = argumentDescriptionInput ? argumentDescriptionInput.value.trim() : '';
760
-
761
- const payload = normalizeArgument({
762
- name,
763
- type,
764
- required,
765
- default: defaultValue,
766
- description
767
- });
768
-
769
- if (editingArgumentIndex !== null && argumentsState[editingArgumentIndex]) {
770
- argumentsState[editingArgumentIndex] = payload;
771
- } else {
772
- argumentsState.push(payload);
773
- }
774
-
775
- closeArgumentModal();
776
- setUnusedArgumentHighlights([]);
777
- }
778
-
779
- function collectPromptTextContent(promptObject) {
780
- const segments = [];
781
- if (!promptObject || !Array.isArray(promptObject.messages)) {
782
- return '';
783
- }
784
- promptObject.messages.forEach(message => {
785
- if (!message) return;
786
- const content = message.content;
787
- if (Array.isArray(content)) {
788
- content.forEach(item => {
789
- if (typeof item === 'string') {
790
- segments.push(item);
791
- } else if (item && typeof item === 'object') {
792
- Object.values(item).forEach(value => {
793
- if (typeof value === 'string') {
794
- segments.push(value);
795
- }
796
- });
797
- }
798
- });
799
- } else if (typeof content === 'string') {
800
- segments.push(content);
801
- } else if (content && typeof content === 'object') {
802
- Object.values(content).forEach(value => {
803
- if (typeof value === 'string') {
804
- segments.push(value);
805
- }
806
- });
807
- }
808
- });
809
- return segments.join('\n');
810
- }
811
-
812
- function findUnusedArguments(promptObject) {
813
- const args = Array.isArray(promptObject?.arguments) ? promptObject.arguments : [];
814
- if (!args.length) return [];
815
- const combinedContent = collectPromptTextContent(promptObject);
816
- return args.filter(arg => {
817
- const name = arg?.name?.trim();
818
- if (!name) return false;
819
- const pattern = new RegExp(`{{\\s*${escapeRegExp(name)}\\s*}}`);
820
- return !pattern.test(combinedContent);
821
- });
822
- }
823
-
824
- function handleArgumentListClick(event) {
825
- const target = event.target.closest('[data-action]');
826
- if (!target) return;
827
- const action = target.dataset.action;
828
- const index = Number(target.dataset.index);
829
- if (!Number.isInteger(index)) return;
830
- if (action === 'delete') {
831
- const argumentName = argumentsState[index]?.name || `参数 ${index + 1}`;
832
- const confirmed = window.confirm(`确定要删除参数 "${argumentName}" 吗?`);
833
- if (!confirmed) return;
834
- removeArgument(index);
835
- } else if (action === 'edit') {
836
- openArgumentModal(index);
837
- }
838
- }
839
-
840
- function sanitizeGroupId(pathValue = 'default') {
841
- return pathValue.replace(/[^a-zA-Z0-9-_]/g, '__') || 'default';
842
- }
843
-
844
- function collectAncestorPaths(pathValue) {
845
- if (!pathValue) return [];
846
- const segments = pathValue.split('/');
847
- const paths = [];
848
- let current = '';
849
- segments.forEach(segment => {
850
- current = current ? `${current}/${segment}` : segment;
851
- paths.push(current);
852
- });
853
- return paths;
854
- }
855
-
856
- function flattenGroupTree(nodes, depth = 0, accumulator = []) {
857
- if (!Array.isArray(nodes)) return accumulator;
858
- nodes.forEach(node => {
859
- const value = node?.path || 'default';
860
- const name = node?.name || value;
861
- accumulator.push({
862
- value,
863
- name,
864
- depth,
865
- path: value,
866
- enabled: node?.enabled !== false
867
- });
868
- if (Array.isArray(node?.children) && node.children.length) {
869
- flattenGroupTree(node.children, depth + 1, accumulator);
870
- }
871
- });
872
- return accumulator;
873
- }
874
-
875
- function findNodeByPath(nodes, targetPath) {
876
- if (!Array.isArray(nodes)) return null;
877
- for (const node of nodes) {
878
- if (!node) continue;
879
- const nodePath = node.path || 'default';
880
- if (nodePath === targetPath) {
881
- return node;
882
- }
883
- if (Array.isArray(node.children) && node.children.length) {
884
- const found = findNodeByPath(node.children, targetPath);
885
- if (found) return found;
886
- }
887
- }
888
- return null;
889
- }
890
-
891
- function setCascaderActivePathsByValue(pathValue = 'default') {
892
- const target = pathValue || 'default';
893
- const candidates = collectAncestorPaths(target);
894
- const validated = [];
895
- candidates.forEach(path => {
896
- if (findNodeByPath(groupTreeState, path) || path === 'default') {
897
- validated.push(path);
898
- }
899
- });
900
- cascaderActivePaths = validated.length ? validated : ['default'];
901
- }
902
-
903
- function renderGroupDropdownContent(keyword = '') {
904
- const trimmed = (keyword || '').trim();
905
- if (!isGroupDropdownOpen) return;
906
- if (trimmed) {
907
- renderGroupSearchResults(trimmed);
908
- } else {
909
- renderGroupCascader();
910
- }
911
- }
912
-
913
- function renderGroupCascader() {
914
- if (!promptGroupCascaderEl) return;
915
- const selectEl = document.getElementById('promptGroup');
916
- const currentValue = selectEl?.value || 'default';
917
-
918
- promptGroupCascaderEl.innerHTML = '';
919
- promptGroupCascaderEl.style.display = 'flex';
920
-
921
- if (promptGroupSearchResultsEl) {
922
- promptGroupSearchResultsEl.classList.remove('show');
923
- promptGroupSearchResultsEl.innerHTML = '';
924
- }
925
-
926
- if (!Array.isArray(groupTreeState) || !groupTreeState.length) {
927
- promptGroupCascaderEl.innerHTML = '';
928
- promptGroupCascaderEl.style.display = 'none';
929
- if (promptGroupEmptyEl) {
930
- promptGroupEmptyEl.classList.remove('hidden');
931
- promptGroupEmptyEl.textContent = '暂未配置类目';
932
- }
933
- return;
934
- }
935
-
936
- if (promptGroupEmptyEl) {
937
- promptGroupEmptyEl.classList.add('hidden');
938
- }
939
-
940
- const columns = [];
941
- let depth = 0;
942
- let nodes = groupTreeState;
943
- columns.push({ depth, nodes, title: '全部类目' });
944
-
945
- while (true) {
946
- const activePath = cascaderActivePaths[depth];
947
- if (!activePath) break;
948
- const activeNode = findNodeByPath(nodes, activePath);
949
- if (activeNode && Array.isArray(activeNode.children) && activeNode.children.length) {
950
- depth += 1;
951
- nodes = activeNode.children;
952
- columns.push({
953
- depth,
954
- nodes,
955
- title: activeNode.name || activeNode.path || '子类目'
956
- });
957
- } else {
958
- break;
959
- }
960
- }
961
-
962
- columns.forEach(({ depth, nodes, title }) => {
963
- const columnEl = document.createElement('div');
964
- columnEl.className = 'group-cascader-column';
965
-
966
- const titleEl = document.createElement('div');
967
- titleEl.className = 'group-cascader-title';
968
- titleEl.textContent = title || '子类目';
969
- columnEl.appendChild(titleEl);
970
-
971
- const listEl = document.createElement('div');
972
- listEl.className = 'group-cascader-list';
973
- columnEl.appendChild(listEl);
974
-
975
- if (!Array.isArray(nodes) || !nodes.length) {
976
- const emptyEl = document.createElement('div');
977
- emptyEl.className = 'group-cascader-empty';
978
- emptyEl.textContent = '无子类目';
979
- listEl.appendChild(emptyEl);
980
- } else {
981
- nodes.forEach(node => {
982
- if (!node) return;
983
- const nodePath = node.path || 'default';
984
- const nodeName = node.name || nodePath;
985
- const hasChildren = Array.isArray(node.children) && node.children.length > 0;
986
- const isActivePath = cascaderActivePaths[depth] === nodePath;
987
- const isSelected = currentValue === nodePath;
988
-
989
- const itemBtn = document.createElement('button');
990
- itemBtn.type = 'button';
991
- let itemClass = 'group-cascader-item';
992
- if (hasChildren) itemClass += ' has-children';
993
- if (isActivePath) itemClass += ' active';
994
- if (isSelected) itemClass += ' selected';
995
- itemBtn.className = itemClass;
996
-
997
- const safeName = escapeHtml(nodeName);
998
- const safePath = escapeHtml(nodePath);
999
- let suffix = '';
1000
- if (hasChildren) {
1001
- suffix = '<span class="group-cascader-suffix">›</span>';
1002
- if (isSelected) {
1003
- suffix = `<span class="group-cascader-check">✓</span>${suffix}`;
1004
- }
1005
- } else if (isSelected) {
1006
- suffix = '<span class="group-cascader-check">✓</span>';
1007
- }
1008
- const hint = nodePath.includes('/') ? `<span class="group-cascader-path-hint">${safePath}</span>` : '';
1009
-
1010
- itemBtn.innerHTML = `
1011
- <span class="group-cascader-label">${safeName}${hint}</span>
1012
- ${suffix}
1013
- `;
1014
-
1015
- itemBtn.addEventListener('click', () => {
1016
- handleCascaderItemClick(node, depth);
1017
- });
1018
-
1019
- listEl.appendChild(itemBtn);
1020
- });
1021
- }
1022
-
1023
- promptGroupCascaderEl.appendChild(columnEl);
1024
- });
1025
- }
1026
-
1027
- function renderGroupSearchResults(keyword) {
1028
- if (!promptGroupSearchResultsEl) return;
1029
- const selectEl = document.getElementById('promptGroup');
1030
- const currentValue = selectEl?.value || 'default';
1031
- const options = getFilteredGroupOptions(keyword);
1032
-
1033
- if (promptGroupCascaderEl) {
1034
- promptGroupCascaderEl.style.display = 'none';
1035
- }
1036
-
1037
- promptGroupSearchResultsEl.innerHTML = '';
1038
-
1039
- if (!options.length) {
1040
- promptGroupSearchResultsEl.classList.remove('show');
1041
- if (promptGroupEmptyEl) {
1042
- promptGroupEmptyEl.classList.remove('hidden');
1043
- promptGroupEmptyEl.textContent = '没有匹配的类目';
1044
- }
1045
- return;
1046
- }
1047
-
1048
- promptGroupSearchResultsEl.classList.add('show');
1049
- if (promptGroupEmptyEl) {
1050
- promptGroupEmptyEl.classList.add('hidden');
1051
- }
1052
-
1053
- const fragment = document.createDocumentFragment();
1054
- options.forEach(option => {
1055
- const item = document.createElement('button');
1056
- item.type = 'button';
1057
- const isSelected = option.value === currentValue;
1058
- item.className = `group-search-item${isSelected ? ' selected' : ''}`;
1059
- const safeName = escapeHtml(option.name);
1060
- const safePath = escapeHtml(option.path);
1061
- item.innerHTML = `
1062
- <span>${safeName}</span>
1063
- <span class="group-search-path">${safePath}</span>
1064
- `;
1065
- item.addEventListener('click', () => {
1066
- selectGroupValue(option.value);
1067
- closeGroupDropdown();
1068
- });
1069
- fragment.appendChild(item);
1070
- });
1071
-
1072
- promptGroupSearchResultsEl.appendChild(fragment);
1073
- }
1074
-
1075
- function handleCascaderItemClick(node, depth) {
1076
- const nodePath = node?.path || 'default';
1077
- const hasChildren = Array.isArray(node?.children) && node.children.length > 0;
1078
- cascaderActivePaths = cascaderActivePaths.slice(0, depth);
1079
- cascaderActivePaths[depth] = nodePath;
1080
- selectGroupValue(nodePath, !hasChildren);
1081
- if (hasChildren) {
1082
- renderGroupCascader();
1083
- }
1084
- }
1085
-
1086
- function updatePromptGroupDisplay() {
1087
- const selectEl = document.getElementById('promptGroup');
1088
- if (!selectEl || !promptGroupLabelEl || !promptGroupBtnEl) return;
1089
- const value = selectEl.value || 'default';
1090
- const option = Array.from(selectEl.options || []).find(opt => opt.value === value);
1091
- const displayText = option ? option.textContent.trim() : value;
1092
- promptGroupLabelEl.textContent = displayText || value || 'default';
1093
- promptGroupBtnEl.dataset.value = value;
1094
- }
1095
-
1096
- function updateGroupSelectOptions(tree) {
1097
- const selectEl = document.getElementById('promptGroup');
1098
- if (!selectEl) return;
1099
- const previousValue = selectEl.value || 'default';
1100
- const optionsData = flattenGroupTree(tree || []);
1101
- selectEl.innerHTML = '';
1102
- optionsData.forEach(optionData => {
1103
- const optionEl = document.createElement('option');
1104
- optionEl.value = optionData.value;
1105
- optionEl.textContent = optionData.path;
1106
- selectEl.appendChild(optionEl);
1107
- });
1108
- const hasPrevious = optionsData.some(optionData => optionData.value === previousValue);
1109
- selectEl.value = hasPrevious ? previousValue : (optionsData[0]?.value || 'default');
1110
- updatePromptGroupDisplay();
1111
- if (isGroupDropdownOpen) {
1112
- setCascaderActivePathsByValue(selectEl.value || 'default');
1113
- renderGroupDropdownContent(promptGroupSearchInput?.value || '');
1114
- }
1115
- }
1116
-
1117
- function getFilteredGroupOptions(keyword = '') {
1118
- const normalized = keyword.trim().toLowerCase();
1119
- let options = flattenGroupTree(groupTreeState || []);
1120
- if (!options.length) {
1121
- const selectEl = document.getElementById('promptGroup');
1122
- if (selectEl) {
1123
- options = Array.from(selectEl.options || []).map(opt => {
1124
- const optionValue = opt.value || 'default';
1125
- const text = opt.textContent?.trim() || optionValue;
1126
- return {
1127
- value: optionValue,
1128
- name: text,
1129
- depth: 0,
1130
- path: optionValue
1131
- };
1132
- });
1133
- }
1134
- }
1135
- if (!normalized) return options;
1136
- return options.filter(option => {
1137
- const name = option.name ? option.name.toLowerCase() : '';
1138
- const path = option.path ? option.path.toLowerCase() : '';
1139
- return name.includes(normalized) || path.includes(normalized);
1140
- });
1141
- }
1142
-
1143
- function selectGroupValue(value, closeAfterSelection = true) {
1144
- const selectEl = document.getElementById('promptGroup');
1145
- if (!selectEl) return;
1146
- const exists = Array.from(selectEl.options || []).some(option => option.value === value);
1147
- if (!exists) {
1148
- const optionEl = document.createElement('option');
1149
- optionEl.value = value;
1150
- optionEl.textContent = value;
1151
- selectEl.appendChild(optionEl);
1152
- }
1153
- selectEl.value = value;
1154
- updatePromptGroupDisplay();
1155
- setCascaderActivePathsByValue(value);
1156
- if (isGroupDropdownOpen) {
1157
- if (!closeAfterSelection) {
1158
- renderGroupDropdownContent(promptGroupSearchInput?.value || '');
1159
- } else {
1160
- closeGroupDropdown(true);
1161
- }
1162
- }
1163
- }
1164
-
1165
- function openGroupDropdown() {
1166
- if (!promptGroupDropdownEl || isGroupDropdownOpen) return;
1167
- isGroupDropdownOpen = true;
1168
- promptGroupDropdownEl.classList.add('show');
1169
- promptGroupBtnEl?.setAttribute('aria-expanded', 'true');
1170
- const selectEl = document.getElementById('promptGroup');
1171
- setCascaderActivePathsByValue(selectEl?.value || 'default');
1172
- renderGroupDropdownContent('');
1173
- if (promptGroupSearchInput) {
1174
- promptGroupSearchInput.value = '';
1175
- requestAnimationFrame(() => promptGroupSearchInput.focus());
1176
- }
1177
- }
1178
-
1179
- function closeGroupDropdown(focusButton = false) {
1180
- if (!promptGroupDropdownEl || !isGroupDropdownOpen) return;
1181
- isGroupDropdownOpen = false;
1182
- promptGroupDropdownEl.classList.remove('show');
1183
- promptGroupBtnEl?.setAttribute('aria-expanded', 'false');
1184
- if (promptGroupSearchInput) {
1185
- promptGroupSearchInput.value = '';
1186
- }
1187
- if (promptGroupCascaderEl) {
1188
- promptGroupCascaderEl.style.display = 'flex';
1189
- }
1190
- if (promptGroupSearchResultsEl) {
1191
- promptGroupSearchResultsEl.classList.remove('show');
1192
- promptGroupSearchResultsEl.innerHTML = '';
1193
- }
1194
- if (promptGroupEmptyEl) {
1195
- promptGroupEmptyEl.classList.add('hidden');
1196
- }
1197
- if (focusButton && promptGroupBtnEl) {
1198
- promptGroupBtnEl.focus();
1199
- }
1200
- }
1201
-
1202
- function toggleGroupDropdown(force) {
1203
- const shouldOpen = typeof force === 'boolean' ? force : !isGroupDropdownOpen;
1204
- if (shouldOpen) {
1205
- openGroupDropdown();
1206
- } else {
1207
- closeGroupDropdown();
1208
- }
1209
- }
1210
-
1211
- function setGroupModalTab(tab = 'create') {
1212
- const nextTab = tab === 'manage' ? 'manage' : 'create';
1213
- groupModalActiveTab = nextTab;
1214
- const tabButtons = document.querySelectorAll('.group-modal-tab');
1215
- tabButtons.forEach(btn => {
1216
- btn.classList.toggle('active', btn.dataset.tab === nextTab);
1217
- });
1218
- const panels = document.querySelectorAll('.group-modal-panel');
1219
- panels.forEach(panel => {
1220
- panel.classList.toggle('hidden', panel.dataset.panel !== nextTab);
1221
- });
1222
- const createFooter = document.getElementById('groupModalCreateFooter');
1223
- const manageFooter = document.getElementById('groupModalManageFooter');
1224
- if (createFooter) createFooter.classList.toggle('hidden', nextTab !== 'create');
1225
- if (manageFooter) manageFooter.classList.toggle('hidden', nextTab !== 'manage');
1226
- if (nextTab === 'create') {
1227
- const input = document.getElementById('newFolderName');
1228
- setTimeout(() => {
1229
- input?.focus();
1230
- populateParentFolderSelect(); // 填充父级目录选择框
1231
- }, 120);
1232
- } else {
1233
- setTimeout(() => groupManageSearchInputEl?.focus(), 120);
1234
- renderGroupManageList();
1235
- }
1236
- }
1237
-
1238
- function renderGroupManageList() {
1239
- if (!groupManageListEl) return;
1240
- const flattened = flattenGroupTree(groupTreeState || []);
1241
- const keyword = (groupManageSearchValue || '').trim().toLowerCase();
1242
- const filtered = flattened.filter(item => {
1243
- if (!item) return false;
1244
- if (!keyword) return true;
1245
- const nameLower = (item.name || '').toLowerCase();
1246
- const pathLower = (item.path || '').toLowerCase();
1247
- return nameLower.includes(keyword) || pathLower.includes(keyword);
1248
- });
1249
-
1250
- groupManageListEl.innerHTML = '';
1251
- if (groupManageEmptyEl) {
1252
- if (!filtered.length) {
1253
- groupManageEmptyEl.textContent = keyword ? '未找到匹配的类目' : '暂无类目信息';
1254
- groupManageEmptyEl.classList.remove('hidden');
1255
- } else {
1256
- groupManageEmptyEl.classList.add('hidden');
1257
- }
1258
- }
1259
- if (!filtered.length) {
1260
- return;
1261
- }
1262
-
1263
- filtered.forEach(item => {
1264
- const enabled = item.enabled !== false;
1265
- const isDefault = item.path === 'default';
1266
- const isEditing = groupManageEditingPath === item.path;
1267
- const isBusy = groupManageActionLoading.has(item.path);
1268
- const safeName = escapeHtml(item.name);
1269
- const safePath = escapeHtml(item.path);
1270
- const row = document.createElement('div');
1271
- row.className = `group-manage-item${enabled ? '' : ' is-disabled'}`;
1272
- row.dataset.path = item.path;
1273
- row.style.setProperty('--depth', item.depth || 0);
1274
-
1275
- if (isEditing) {
1276
- row.innerHTML = `
1277
- <div class="group-manage-info">
1278
- <div class="group-manage-edit">
1279
- <input type="text" class="group-manage-rename-input" value="${safeName}" maxlength="64">
1280
- </div>
1281
- <div class="group-manage-path">${safePath}</div>
1282
- </div>
1283
- <div class="group-manage-actions">
1284
- <button type="button" class="group-manage-action-btn" data-action="rename-confirm"${isBusy ? ' disabled' : ''}>保存</button>
1285
- <button type="button" class="group-manage-action-btn" data-action="rename-cancel"${isBusy ? ' disabled' : ''}>取消</button>
1286
- </div>
1287
- `;
1288
- } else {
1289
- const statusBadge = `<span class="group-status-badge ${enabled ? 'enabled' : 'disabled'}">${enabled ? '启用中' : '已冻结'}</span>`;
1290
- const defaultBadge = isDefault ? '<span class="group-status-badge default">默认</span>' : '';
1291
- const toggleDisabled = isBusy || isDefault;
1292
- const renameDisabled = isBusy || isDefault;
1293
- const deleteDisabled = isBusy || isDefault;
1294
- const toggleText = enabled ? '冻结' : '启用';
1295
- row.innerHTML = `
1296
- <div class="group-manage-info">
1297
- <div class="group-manage-name">${safeName}${statusBadge}${defaultBadge}</div>
1298
- <div class="group-manage-path">${safePath}</div>
1299
- </div>
1300
- <div class="group-manage-actions">
1301
- <button type="button" class="group-manage-action-btn" data-action="toggle"${toggleDisabled ? ' disabled' : ''}>${toggleText}</button>
1302
- <button type="button" class="group-manage-action-btn" data-action="rename"${renameDisabled ? ' disabled' : ''}>重命名</button>
1303
- <button type="button" class="group-manage-action-btn danger" data-action="delete"${deleteDisabled ? ' disabled' : ''}>删除</button>
1304
- </div>
1305
- `;
1306
- }
1307
-
1308
- groupManageListEl.appendChild(row);
1309
- });
1310
- }
1311
-
1312
- async function refreshGroupData() {
1313
- try {
1314
- // 显示加载中效果
1315
- showLoading();
1316
-
1317
- const searchInput = document.getElementById('searchInput');
1318
- const searchValue = searchInput ? searchInput.value : '';
1319
- await loadPrompts(searchValue);
1320
- if (groupModalActiveTab === 'manage') {
1321
- renderGroupManageList();
1322
- } else if (groupModalActiveTab === 'create') {
1323
- populateParentFolderSelect(); // 更新父级目录选择框
1324
- }
1325
- } catch (error) {
1326
- console.error('刷新类目数据失败:', error);
1327
- } finally {
1328
- // 隐藏加载中效果
1329
- hideLoading();
1330
- }
1331
- }
1332
-
1333
- function handleGroupManageSearchInput(event) {
1334
- groupManageSearchValue = event.target.value || '';
1335
- renderGroupManageList();
1336
- }
1337
-
1338
- function resetGroupManageState() {
1339
- groupManageSearchValue = '';
1340
- groupManageEditingPath = null;
1341
- groupManageActionLoading.clear();
1342
- if (groupManageSearchInputEl) {
1343
- groupManageSearchInputEl.value = '';
1344
- }
1345
- renderGroupManageList();
1346
- }
1347
-
1348
- async function handleGroupRename(pathValue, newName) {
1349
- if (!newName || !newName.trim()) {
1350
- showMessage('请输入新的类目名称', 'error');
1351
- return;
1352
- }
1353
- const trimmedName = newName.trim();
1354
- if (!/^(?![.]{1,2}$)[^\\/:*?"<>|\r\n]{1,64}$/.test(trimmedName)) {
1355
- showMessage('名称格式无效,不能包含 / \\ : * ? \" < > | 或换行,长度需在1-64字符', 'error');
1356
- return;
1357
- }
1358
- const currentSegments = pathValue.split('/');
1359
- const currentName = currentSegments[currentSegments.length - 1] || pathValue;
1360
- if (trimmedName === currentName) {
1361
- groupManageEditingPath = null;
1362
- renderGroupManageList();
1363
- showMessage('类目名称未变更', 'info');
1364
- return;
1365
- }
1366
- groupManageActionLoading.add(pathValue);
1367
- renderGroupManageList();
1368
- try {
1369
- await apiCall('/groups/rename', {
1370
- method: 'PATCH',
1371
- body: JSON.stringify({ path: pathValue, newName: trimmedName })
1372
- });
1373
- showMessage('类目重命名成功');
1374
- groupManageEditingPath = null;
1375
- await refreshGroupData();
1376
- } catch (error) {
1377
- console.error('重命名类目失败:', error);
1378
- showMessage(error?.message || '类目重命名失败', 'error');
1379
- } finally {
1380
- groupManageActionLoading.delete(pathValue);
1381
- renderGroupManageList();
1382
- }
1383
- }
1384
-
1385
- async function handleGroupDelete(pathValue, displayName) {
1386
- const confirmed = window.confirm(`确认删除「${displayName}」吗?该操作不可撤销。`);
1387
- if (!confirmed) return;
1388
- groupManageActionLoading.add(pathValue);
1389
- renderGroupManageList();
1390
- try {
1391
- await apiCall(`/groups?path=${encodeURIComponent(pathValue)}`, {
1392
- method: 'DELETE'
1393
- });
1394
- showMessage(`已删除 “${displayName}”`);
1395
- await refreshGroupData();
1396
- } catch (error) {
1397
- console.error('删除类目失败:', error);
1398
- showMessage(error?.message || '删除类目失败', 'error');
1399
- } finally {
1400
- groupManageActionLoading.delete(pathValue);
1401
- renderGroupManageList();
1402
- }
1403
- }
1404
-
1405
- async function handleGroupStatusToggle(pathValue, displayName, nextEnabled) {
1406
- groupManageActionLoading.add(pathValue);
1407
- renderGroupManageList();
1408
- try {
1409
- const result = await apiCall('/groups/status', {
1410
- method: 'PATCH',
1411
- body: JSON.stringify({ path: pathValue, enabled: nextEnabled })
1412
- });
1413
- const finalEnabled = result?.enabled !== false;
1414
- showMessage(`“${displayName}” 已${finalEnabled ? '启用' : '冻结'}`);
1415
- await refreshGroupData();
1416
- } catch (error) {
1417
- console.error('更新类目状态失败:', error);
1418
- showMessage(error?.message || '更新类目状态失败', 'error');
1419
- } finally {
1420
- groupManageActionLoading.delete(pathValue);
1421
- renderGroupManageList();
1422
- }
1423
- }
1424
-
1425
- function handleGroupManageListClick(event) {
1426
- const actionBtn = event.target.closest('[data-action]');
1427
- if (!actionBtn) return;
1428
- const item = actionBtn.closest('.group-manage-item');
1429
- if (!item) return;
1430
- const pathValue = item.dataset.path;
1431
- if (!pathValue) return;
1432
- if (groupManageActionLoading.has(pathValue) && actionBtn.dataset.action !== 'rename-cancel') {
1433
- return;
1434
- }
1435
-
1436
- if (actionBtn.dataset.action === 'rename') {
1437
- groupManageEditingPath = pathValue;
1438
- renderGroupManageList();
1439
- const selectorPath = pathValue.replace(/"/g, '\\"');
1440
- const nextRow = groupManageListEl?.querySelector(`[data-path="${selectorPath}"]`);
1441
- const input = nextRow?.querySelector('.group-manage-rename-input');
1442
- setTimeout(() => input?.focus(), 60);
1443
- return;
1444
- }
1445
-
1446
- if (actionBtn.dataset.action === 'rename-cancel') {
1447
- groupManageEditingPath = null;
1448
- renderGroupManageList();
1449
- return;
1450
- }
1451
-
1452
- if (actionBtn.dataset.action === 'rename-confirm') {
1453
- const input = item.querySelector('.group-manage-rename-input');
1454
- const nextName = input ? input.value : '';
1455
- handleGroupRename(pathValue, nextName);
1456
- return;
1457
- }
1458
-
1459
- const safeName = item.querySelector('.group-manage-name')?.firstChild?.textContent?.trim() || pathValue;
1460
-
1461
- if (actionBtn.dataset.action === 'toggle') {
1462
- const currentNode = findNodeByPath(groupTreeState, pathValue);
1463
- const isCurrentlyEnabled = currentNode ? currentNode.enabled !== false : true;
1464
- const nextEnabled = !isCurrentlyEnabled;
1465
- handleGroupStatusToggle(pathValue, safeName, nextEnabled);
1466
- return;
1467
- }
1468
-
1469
- if (actionBtn.dataset.action === 'delete') {
1470
- handleGroupDelete(pathValue, safeName);
1471
- }
1472
- }
1473
-
1474
- async function renderGroupList(prompts) {
1475
- try {
1476
- const groupTree = await apiCall('/groups');
1477
- const container = document.getElementById('groupList');
1478
- container.innerHTML = '';
1479
-
1480
- const promptMap = new Map();
1481
- (Array.isArray(prompts) ? prompts : []).forEach(prompt => {
1482
- const pathValue = prompt.groupPath || prompt.group || 'default';
1483
- if (!promptMap.has(pathValue)) {
1484
- promptMap.set(pathValue, []);
1485
- }
1486
- promptMap.get(pathValue).push(prompt);
1487
- });
1488
-
1489
- if (currentPrompt) {
1490
- const activePath = currentPrompt.groupPath || currentPrompt.group || 'default';
1491
- collectAncestorPaths(activePath).forEach(pathValue => expandedGroups.add(pathValue));
1492
- }
1493
-
1494
- const ensureDefaultNode = nodes => {
1495
- const hasDefault = nodes.some(node => node.path === 'default');
1496
- if (!hasDefault) {
1497
- nodes.unshift({ name: 'default', path: 'default', children: [], enabled: true });
1498
- }
1499
- };
1500
-
1501
- ensureDefaultNode(groupTree);
1502
- groupTreeState = Array.isArray(groupTree) ? groupTree : [];
1503
- updateGroupSelectOptions(groupTreeState);
1504
-
1505
- const validPaths = new Set();
1506
- const collectPaths = nodes => {
1507
- nodes.forEach(node => {
1508
- const nodePath = node.path || 'default';
1509
- validPaths.add(nodePath);
1510
- if (Array.isArray(node.children) && node.children.length) {
1511
- collectPaths(node.children);
1512
- }
1513
- });
1514
- };
1515
- collectPaths(groupTree);
1516
- expandedGroups = new Set([...expandedGroups].filter(pathValue => validPaths.has(pathValue)));
1517
-
1518
- const renderNode = (node, parentEl, depth = 0) => {
1519
- const pathValue = node.path || 'default';
1520
- const promptsInGroup = promptMap.get(pathValue) || [];
1521
-
1522
- const section = document.createElement('div');
1523
- section.className = 'group-section';
1524
- section.dataset.path = pathValue;
1525
- section.dataset.depth = depth;
1526
-
1527
- const header = document.createElement('div');
1528
- header.className = 'group-header';
1529
- header.dataset.group = pathValue;
1530
- let headerPadding = 16 + depth * 18;
1531
- if (node.enabled === false) {
1532
- headerPadding += 32;
1533
- }
1534
- header.style.paddingLeft = `${headerPadding}px`;
1535
-
1536
- const titleSpan = document.createElement('span');
1537
- titleSpan.textContent = node.name;
1538
- if (node.enabled === false) {
1539
- section.classList.add('disabled');
1540
- }
1541
- header.appendChild(titleSpan);
1542
-
1543
- const headerActions = document.createElement('div');
1544
- headerActions.style.display = 'flex';
1545
- headerActions.style.alignItems = 'center';
1546
- headerActions.style.gap = '6px';
1547
-
1548
- const countEl = document.createElement('span');
1549
- countEl.className = 'group-count';
1550
- countEl.id = `group-count-${sanitizeGroupId(pathValue)}`;
1551
- countEl.textContent = promptsInGroup.length;
1552
-
1553
- const toggleEl = document.createElement('span');
1554
- toggleEl.className = 'group-toggle';
1555
-
1556
- headerActions.appendChild(countEl);
1557
- headerActions.appendChild(toggleEl);
1558
- header.appendChild(headerActions);
1559
-
1560
- const content = document.createElement('div');
1561
- content.className = 'group-content collapsed';
1562
-
1563
- const promptListEl = document.createElement('div');
1564
- promptListEl.className = 'prompt-list';
1565
- promptListEl.id = `promptList-${sanitizeGroupId(pathValue)}`;
1566
- content.appendChild(promptListEl);
1567
-
1568
- renderGroupPromptList(promptListEl, pathValue, promptsInGroup);
1569
-
1570
- let totalCount = promptsInGroup.length;
1571
-
1572
- if (Array.isArray(node.children) && node.children.length) {
1573
- const childrenWrap = document.createElement('div');
1574
- childrenWrap.className = 'group-children';
1575
- node.children.forEach(child => {
1576
- const childCount = renderNode(child, childrenWrap, depth + 1);
1577
- totalCount += childCount;
1578
- });
1579
- if (childrenWrap.childNodes.length) {
1580
- content.appendChild(childrenWrap);
1581
- }
1582
- }
1583
-
1584
- countEl.textContent = totalCount;
1585
-
1586
- const shouldExpandByDefault = expandedGroups.size === 0 && depth === 0;
1587
- const isExpanded = expandedGroups.has(pathValue) || shouldExpandByDefault;
1588
- if (shouldExpandByDefault) {
1589
- expandedGroups.add(pathValue);
1590
- }
1591
-
1592
- content.classList.toggle('expanded', isExpanded);
1593
- content.classList.toggle('collapsed', !isExpanded);
1594
- toggleEl.classList.toggle('collapsed', !isExpanded);
1595
-
1596
- header.addEventListener('click', (e) => {
1597
- const isClickOnActions = e.target.closest('.toggle-btn') || e.target.closest('.delete-btn') || e.target.closest('.prompt-actions');
1598
- if (!isClickOnActions) {
1599
- const nextExpanded = !content.classList.contains('expanded');
1600
- content.classList.toggle('expanded', nextExpanded);
1601
- content.classList.toggle('collapsed', !nextExpanded);
1602
- toggleEl.classList.toggle('collapsed', !nextExpanded);
1603
- if (nextExpanded) {
1604
- expandedGroups.add(pathValue);
1605
- } else {
1606
- expandedGroups.delete(pathValue);
1607
- }
1608
- }
1609
- });
1610
-
1611
- section.appendChild(header);
1612
- section.appendChild(content);
1613
- parentEl.appendChild(section);
1614
- return totalCount;
1615
- };
1616
-
1617
- const fragment = document.createDocumentFragment();
1618
- groupTree.forEach(node => renderNode(node, fragment, 0));
1619
- container.appendChild(fragment);
1620
- if (groupModalActiveTab === 'manage') {
1621
- renderGroupManageList();
1622
- }
1623
- } catch (error) {
1624
- console.error('渲染分组列表失败:', error);
1625
- }
1626
- }
1627
-
1628
- function renderGroupPromptList(container, groupPath, prompts) {
1629
- container.innerHTML = '';
1630
-
1631
- const sortedPrompts = [...prompts].sort((a, b) => (a.name || '').localeCompare(b.name || '', 'zh-CN'));
1632
-
1633
- if (!sortedPrompts.length) {
1634
- const emptyEl = document.createElement('div');
1635
- emptyEl.className = 'prompt-list-empty';
1636
- emptyEl.innerHTML = `
1637
- <span>该目录暂无 Prompt</span>
1638
- 请点击左上角"新建 Prompt"按钮创建。
1639
- `;
1640
- container.appendChild(emptyEl);
1641
- return;
1642
- }
1643
-
1644
- sortedPrompts.forEach(prompt => {
1645
- const item = document.createElement('div');
1646
- // 页面加载时不自动选中任何prompt,只有在用户点击后才设置为活动状态
1647
- const isActive = false;
1648
- item.className = `prompt-item ${prompt.enabled ? 'enabled' : ''} ${isActive ? 'active' : ''}`;
1649
- const hasSubGroup = prompt.groupPath && prompt.groupPath.includes('/');
1650
- const groupHint = hasSubGroup ? prompt.groupPath : (prompt.group || 'default');
1651
- item.innerHTML = `
1652
- <div class="prompt-info">
1653
- <div class="prompt-name">${prompt.name}</div>
1654
- <div class="prompt-desc" title="${prompt.description || ''}">${prompt.description || ''}</div>
1655
- ${hasSubGroup ? `<div class="prompt-meta-hint">${groupHint}</div>` : ''}
1656
- </div>
1657
- <div class="prompt-meta">
1658
- <div class="prompt-actions">
1659
- <button class="action-btn toggle-btn" data-prompt="${prompt.name}" data-path="${prompt.relativePath || ''}">
1660
- ${prompt.enabled ? '停用' : '启用'}
1661
- </button>
1662
- <button class="action-btn delete-btn" data-prompt="${prompt.name}" data-path="${prompt.relativePath || ''}" style="color: var(--danger);">
1663
- 删除
1664
- </button>
1665
- </div>
1666
- </div>
1667
- `;
1668
-
1669
- item.addEventListener('click', (e) => {
1670
- if (!e.target.closest('.prompt-actions') && !e.target.classList.contains('toggle-btn') && !e.target.classList.contains('delete-btn')) {
1671
- selectPrompt(prompt, e);
1672
- }
1673
- });
1674
-
1675
- const actions = item.querySelector('.prompt-actions');
1676
- if (actions) {
1677
- actions.addEventListener('click', (e) => {
1678
- e.stopPropagation();
1679
- e.preventDefault();
1680
- });
1681
-
1682
- const toggleBtn = item.querySelector('.toggle-btn');
1683
- const deleteBtn = item.querySelector('.delete-btn');
1684
-
1685
- if (toggleBtn) {
1686
- toggleBtn.addEventListener('click', (e) => {
1687
- e.stopPropagation();
1688
- e.preventDefault();
1689
- const targetPath = toggleBtn.getAttribute('data-path');
1690
- togglePrompt(prompt.name, targetPath);
1691
- });
1692
- }
1693
-
1694
- if (deleteBtn) {
1695
- deleteBtn.addEventListener('click', (e) => {
1696
- e.stopPropagation();
1697
- e.preventDefault();
1698
- const targetPath = deleteBtn.getAttribute('data-path');
1699
- openDeletePromptModal(prompt.name, targetPath);
1700
- });
1701
- }
1702
- }
1703
-
1704
- container.appendChild(item);
1705
- });
1706
- }
1707
-
1708
- async function togglePrompt(promptName, relativePath) {
1709
- try {
1710
- const query = relativePath ? `?path=${encodeURIComponent(relativePath)}` : '';
1711
- const result = await apiCall(`/prompts/${encodeURIComponent(promptName)}/toggle${query}`, {
1712
- method: 'POST'
1713
- });
1714
- const searchInput = document.getElementById('searchInput');
1715
- const searchValue = searchInput ? searchInput.value : '';
1716
- await loadPrompts(searchValue);
1717
- const statusText = result?.enabled ? '已启用' : '已停用';
1718
- showMessage(`"${promptName}" ${statusText}`, result?.enabled ? 'success' : 'info');
1719
- if (currentPrompt?.relativePath === relativePath) {
1720
- currentPrompt.enabled = result?.enabled;
1721
- currentPromptObject = currentPromptObject ? { ...currentPromptObject, enabled: result?.enabled } : currentPromptObject;
1722
- }
1723
- } catch (error) {
1724
- console.error('切换prompt状态失败:', error);
1725
- showMessage('启用状态切换失败: ' + error.message, 'error');
1726
- }
1727
- }
1728
-
1729
- async function deletePrompt(promptName, relativePath) {
1730
- try {
1731
- const query = relativePath ? `?path=${encodeURIComponent(relativePath)}` : '';
1732
- await apiCall(`/prompts/${encodeURIComponent(promptName)}${query}`, {
1733
- method: 'DELETE'
1734
- });
1735
- if (currentPrompt?.relativePath === relativePath || (!currentPrompt?.relativePath && currentPrompt?.name === promptName)) {
1736
- resetEditor();
1737
- }
1738
- const searchInput = document.getElementById('searchInput');
1739
- const searchValue = searchInput ? searchInput.value : '';
1740
- await loadPrompts(searchValue);
1741
- showMessage(`已删除 "${promptName}"`);
1742
- } catch (error) {
1743
- console.error('删除prompt失败:', error);
1744
- showMessage('删除失败: ' + error.message, 'error');
1745
- }
1746
- }
1747
-
1748
- // 选择prompt
1749
- async function selectPrompt(prompt, triggerEvent) {
1750
- try {
1751
- // 显示加载中效果
1752
- showLoading();
1753
-
1754
- // 显示prompt编辑区域
1755
- showPromptEditorArea();
1756
-
1757
- const query = prompt.relativePath ? `?path=${encodeURIComponent(prompt.relativePath)}` : '';
1758
- const promptData = await apiCall(`/prompts/${encodeURIComponent(prompt.name)}${query}`);
1759
- currentPrompt = {
1760
- ...promptData,
1761
- relativePath: promptData.relativePath || prompt.relativePath || null,
1762
- groupPath: promptData.groupPath || prompt.groupPath || null,
1763
- group: promptData.group || prompt.group || null
1764
- };
1765
-
1766
- let parsedPrompt = null;
1767
- try {
1768
- parsedPrompt = window.jsyaml?.load(promptData.yaml || '') || null;
1769
- } catch (err) {
1770
- console.error('解析提示词失败:', err);
1771
- }
1772
-
1773
- currentPromptObject = parsedPrompt ? clonePromptObject(parsedPrompt) : createDefaultPromptObject();
1774
- setArgumentsState(currentPromptObject.arguments || []);
1775
-
1776
- const nameInput = document.getElementById('promptName');
1777
- if (nameInput) nameInput.value = currentPromptObject.name || promptData.name || '';
1778
- const promptGroupSelect = document.getElementById('promptGroup');
1779
- if (promptGroupSelect) {
1780
- const resolvedGroup =
1781
- promptData.groupPath ||
1782
- promptData.group ||
1783
- prompt.groupPath ||
1784
- prompt.group ||
1785
- 'default';
1786
-
1787
- const optionExists = Array.from(promptGroupSelect.options || []).some(
1788
- option => option.value === resolvedGroup
1789
- );
1790
- if (!optionExists) {
1791
- const optionEl = document.createElement('option');
1792
- optionEl.value = resolvedGroup;
1793
- optionEl.textContent = resolvedGroup;
1794
- promptGroupSelect.appendChild(optionEl);
1795
- }
1796
-
1797
- promptGroupSelect.value = resolvedGroup;
1798
- updatePromptGroupDisplay();
1799
- setCascaderActivePathsByValue(resolvedGroup);
1800
- if (isGroupDropdownOpen) {
1801
- renderGroupDropdownContent(promptGroupSearchInput?.value || '');
1802
- }
1803
- }
1804
-
1805
- if (descriptionInputEl) {
1806
- descriptionInputEl.value = currentPromptObject.description || '';
1807
- adjustDescriptionHeight();
1808
- }
1809
-
1810
- // 获取用户消息内容,使用更健壮的逻辑
1811
- let messageText = '';
1812
-
1813
- // 首先尝试使用现有的getFirstUserMessage函数
1814
- const userMessage = getFirstUserMessage(currentPromptObject);
1815
-
1816
- // 如果没找到,尝试其他方法获取内容
1817
- if (!userMessage && currentPromptObject.messages && Array.isArray(currentPromptObject.messages)) {
1818
- // 查找role为'user'的消息
1819
- const userMsg = currentPromptObject.messages.find(msg => msg?.role === 'user');
1820
- if (userMsg) {
1821
- // 处理不同的content格式
1822
- if (typeof userMsg.content === 'string') {
1823
- messageText = userMsg.content;
1824
- } else if (userMsg.content?.text) {
1825
- messageText = userMsg.content.text;
1826
- } else if (userMsg.content) {
1827
- // 如果content是对象,尝试转换为字符串
1828
- messageText = JSON.stringify(userMsg.content, null, 2);
1829
- }
1830
- }
1831
- } else if (userMessage) {
1832
- // 处理从getFirstUserMessage获取到的消息
1833
- if (typeof userMessage.content === 'string') {
1834
- messageText = userMessage.content;
1835
- } else if (userMessage.content?.text) {
1836
- messageText = userMessage.content.text;
1837
- } else if (userMessage.content) {
1838
- // 如果content是对象,尝试转换为字符串
1839
- messageText = JSON.stringify(userMessage.content, null, 2);
1840
- }
1841
- }
1842
-
1843
- // 如果还是空,提供默认提示
1844
- if (!messageText) {
1845
- messageText = '';
1846
- }
1847
-
1848
- if (editor) {
1849
- editor.setValue(messageText);
1850
- // 强制刷新编辑器以确保内容显示
1851
- setTimeout(() => {
1852
- if (editor && typeof editor.requestMeasure === 'function') {
1853
- editor.requestMeasure();
1854
- }
1855
- // 再添加一个延迟刷新,确保内容显示
1856
- setTimeout(() => {
1857
- if (editor && typeof editor.dispatch === 'function') {
1858
- editor.dispatch({
1859
- selection: { anchor: 0 }
1860
- });
1861
- }
1862
- }, 50);
1863
- }, 10);
1864
- }
1865
-
1866
- // 更新UI状态
1867
- document.querySelectorAll('.prompt-item').forEach(el => el.classList.remove('active'));
1868
- const targetItem = triggerEvent?.currentTarget || triggerEvent?.target?.closest('.prompt-item');
1869
- if (targetItem) {
1870
- targetItem.classList.add('active');
1871
- }
1872
-
1873
- // 更新预览
1874
- updatePreview(true);
1875
- } catch (error) {
1876
- console.error('加载prompt详情失败:', error);
1877
- } finally {
1878
- // 隐藏加载中效果
1879
- hideLoading();
1880
- }
1881
- }
1882
-
1883
- // 切换工作区模式
1884
- function setWorkspaceMode(mode) {
1885
- // 确保编辑区域是可见的
1886
- showPromptEditorArea();
1887
-
1888
- const isPreview = mode === 'preview';
1889
- const editorPane = document.getElementById('editorPane');
1890
- const previewPane = document.getElementById('previewPane');
1891
- const editModeBtn = document.getElementById('editModeBtn');
1892
- const previewModeBtn = document.getElementById('previewModeBtn');
1893
- const argumentsSection = document.getElementById('argumentsSection');
1894
-
1895
- if (!editorPane || !previewPane || !editModeBtn || !previewModeBtn) return;
1896
-
1897
- editModeBtn.classList.toggle('active', !isPreview);
1898
- previewModeBtn.classList.toggle('active', isPreview);
1899
- editorPane.classList.toggle('hidden', isPreview);
1900
- previewPane.classList.toggle('hidden', !isPreview);
1901
-
1902
- // 在预览模式下隐藏参数配置区域,编辑模式下显示
1903
- if (argumentsSection) {
1904
- argumentsSection.style.display = isPreview ? 'none' : 'flex';
1905
- }
1906
-
1907
- if (isPreview) {
1908
- updatePreview(true);
1909
- } else {
1910
- // 在切换回编辑模式时清理预览编辑器
1911
- if (previewEditor) {
1912
- try {
1913
- // 检查编辑器是否仍然连接到DOM
1914
- if (previewEditor.getTextArea() && previewEditor.getTextArea().parentNode) {
1915
- // 销毁预览编辑器实例
1916
- previewEditor.toTextArea();
1917
- }
1918
- } catch (e) {
1919
- console.warn('Error destroying preview editor:', e);
1920
- // 如果销毁失败,尝试直接清除内容
1921
- const previewContent = document.getElementById('previewContent');
1922
- if (previewContent) {
1923
- previewContent.innerHTML = '<p>切换到预览模式查看内容</p>';
1924
- }
1925
- }
1926
- previewEditor = null;
1927
- }
1928
- }
1929
- }
1930
-
1931
- // 更新预览
1932
- let previewEditor = null;
1933
-
1934
- async function updatePreview(force = false) {
1935
- if (!editor) return;
1936
-
1937
- const previewPane = document.getElementById('previewPane');
1938
- if (!force && previewPane?.classList.contains('hidden')) {
1939
- return;
1940
- }
1941
-
1942
- try {
1943
- const previewContent = document.getElementById('previewContent');
1944
- if (!previewContent) return;
1945
-
1946
- const content = editor.getValue();
1947
-
1948
- // 如果预览编辑器不存在,创建一个
1949
- if (!previewEditor) {
1950
- previewContent.innerHTML = '';
1951
- // 创建 textarea 并转换为 CodeMirror 预览编辑器
1952
- const textarea = document.createElement('textarea');
1953
- textarea.value = content;
1954
- previewContent.appendChild(textarea);
1955
-
1956
- // 使用 CodeMirror 5 创建只读预览
1957
- previewEditor = CodeMirror.fromTextArea(textarea, {
1958
- mode: 'markdown',
1959
- theme: 'xq-light',
1960
- lineNumbers: false,
1961
- lineWrapping: true,
1962
- readOnly: true,
1963
- viewportMargin: Infinity
1964
- });
1965
- } else {
1966
- // 更新预览内容
1967
- previewEditor.setValue(content);
1968
- }
1969
-
1970
- } catch (error) {
1971
- console.error('预览更新失败:', error);
1972
- document.getElementById('previewContent').innerHTML = '<p style="color: red;">预览生成失败</p>';
1973
- }
1974
- }
1975
-
1976
- // 保存prompt
1977
- async function savePrompt() {
1978
- if (!editor) return;
1979
-
1980
- const saveBtn = document.getElementById('saveBtn');
1981
- if (saveBtn?.disabled) return;
1982
-
1983
- const name = document.getElementById('promptName').value.trim();
1984
- const group = document.getElementById('promptGroup').value.trim();
1985
-
1986
- if (!window.jsyaml) {
1987
- showMessage('资源未准备就绪,请刷新重试', 'error');
1988
- return;
1989
- }
1990
-
1991
- if (!name) {
1992
- showMessage('请输入prompt名称', 'error');
1993
- return;
1994
- }
1995
-
1996
- const promptObject = buildPromptObjectFromUI();
1997
- const unusedArguments = findUnusedArguments(promptObject);
1998
- if (unusedArguments.length > 0) {
1999
- const missingNames = unusedArguments.map(arg => arg.name).filter(Boolean);
2000
- if (missingNames.length) {
2001
- setUnusedArgumentHighlights(missingNames);
2002
- const argumentsSection = document.getElementById('argumentsSection');
2003
- if (argumentsSection) {
2004
- argumentsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
2005
- }
2006
- showMessage(`以下参数未在内容中使用:${missingNames.join(', ')}`, 'error');
2007
- const firstMissing = missingNames[0];
2008
- requestAnimationFrame(() => {
2009
- const listEl = document.getElementById('argumentsList');
2010
- if (!listEl) return;
2011
- const targetCard = Array.from(listEl.querySelectorAll('.argument-card')).find(card => {
2012
- const idx = Number(card.dataset.index);
2013
- const argName = argumentsState[idx]?.name?.trim();
2014
- return argName === firstMissing;
2015
- });
2016
- if (targetCard) {
2017
- const targetIndex = Number(targetCard.dataset.index);
2018
- if (Number.isInteger(targetIndex)) {
2019
- openArgumentModal(targetIndex);
2020
- }
2021
- }
2022
- });
2023
- return;
2024
- }
2025
- } else {
2026
- setUnusedArgumentHighlights([]);
2027
- }
2028
- const yaml = window.jsyaml.dump(promptObject);
2029
-
2030
- let originalText = '';
2031
- if (saveBtn) {
2032
- originalText = (saveBtn.textContent || '保存').trim();
2033
- saveBtn.disabled = true;
2034
- saveBtn.classList.add('loading');
2035
- saveBtn.textContent = '保存中...';
2036
- }
2037
-
2038
- try {
2039
- // 显示加载中效果
2040
- showLoading();
2041
-
2042
- const result = await apiCall('/prompts', {
2043
- method: 'POST',
2044
- body: JSON.stringify({
2045
- name,
2046
- group,
2047
- yaml,
2048
- relativePath: currentPrompt?.relativePath || null
2049
- })
2050
- });
2051
-
2052
- showMessage('保存成功');
2053
- currentPromptObject = clonePromptObject(promptObject);
2054
- const updatedRelativePath = result?.relativePath || currentPrompt?.relativePath || null;
2055
- const pathSegments = updatedRelativePath ? updatedRelativePath.split('/') : [];
2056
- const updatedGroupPath = pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : (pathSegments[0] || currentPrompt?.groupPath || group || 'default');
2057
- currentPrompt = {
2058
- ...(currentPrompt || {}),
2059
- name,
2060
- relativePath: updatedRelativePath,
2061
- group: result?.group || group,
2062
- groupPath: updatedGroupPath
2063
- };
2064
- // 重新加载列表,传递搜索参数
2065
- const searchInput = document.getElementById('searchInput');
2066
- const searchValue = searchInput ? searchInput.value : '';
2067
- await loadPrompts(searchValue);
2068
- } catch (error) {
2069
- console.error('保存失败:', error);
2070
- if (!error?.__shown) {
2071
- showMessage('保存失败: ' + (error?.message || '未知错误'), 'error');
2072
- }
2073
- } finally {
2074
- if (saveBtn) {
2075
- saveBtn.disabled = false;
2076
- saveBtn.classList.remove('loading');
2077
- saveBtn.textContent = originalText;
2078
- }
2079
- // 隐藏加载中效果
2080
- hideLoading();
2081
- }
2082
- }
2083
-
2084
- // 重置编辑器
2085
- function resetEditor() {
2086
- document.getElementById('promptName').value = '';
2087
- const groupSelect = document.getElementById('promptGroup');
2088
- if (groupSelect) {
2089
- groupSelect.value = 'default';
2090
- updatePromptGroupDisplay();
2091
- setCascaderActivePathsByValue('default');
2092
- if (isGroupDropdownOpen) {
2093
- renderGroupDropdownContent(promptGroupSearchInput?.value || '');
2094
- }
2095
- }
2096
- if (editor) editor.setValue('');
2097
- if (descriptionInputEl) {
2098
- descriptionInputEl.value = '';
2099
- adjustDescriptionHeight();
2100
- }
2101
- setArgumentsState([]);
2102
- currentPrompt = null;
2103
- currentPromptObject = null;
2104
- document.getElementById('previewContent').innerHTML = '<p>选择或创建prompt开始编辑</p>';
2105
- setWorkspaceMode('edit');
2106
- }
2107
-
2108
- // 新建prompt
2109
- function newPrompt() {
2110
- // 显示加载中效果
2111
- showLoading();
2112
-
2113
- // 显示prompt编辑区域
2114
- showPromptEditorArea();
2115
-
2116
- currentPrompt = null;
2117
- currentPromptObject = createDefaultPromptObject();
2118
-
2119
- const nameInput = document.getElementById('promptName');
2120
- if (nameInput) nameInput.value = currentPromptObject.name;
2121
- const groupSelect = document.getElementById('promptGroup');
2122
- if (groupSelect) {
2123
- groupSelect.value = 'default';
2124
- updatePromptGroupDisplay();
2125
- setCascaderActivePathsByValue('default');
2126
- if (isGroupDropdownOpen) {
2127
- renderGroupDropdownContent(promptGroupSearchInput?.value || '');
2128
- }
2129
- }
2130
-
2131
- const defaultMessage = getFirstUserMessage(currentPromptObject);
2132
- if (editor) {
2133
- editor.setValue(defaultMessage?.content?.text || '');
2134
- }
2135
-
2136
- setArgumentsState(currentPromptObject.arguments || []);
2137
-
2138
- if (descriptionInputEl) {
2139
- descriptionInputEl.value = currentPromptObject.description || '';
2140
- adjustDescriptionHeight();
2141
- }
2142
-
2143
- document.getElementById('previewContent').innerHTML = '<p>选择或创建prompt开始编辑</p>';
2144
- setWorkspaceMode('edit');
2145
-
2146
- document.querySelectorAll('.prompt-item').forEach(el => el.classList.remove('active'));
2147
- updatePreview(true);
2148
-
2149
- // 隐藏加载中效果
2150
- hideLoading();
2151
- }
2152
-
2153
- // 打开/关闭新建目录弹窗
2154
- function toggleNewFolderModal(show) {
2155
- const modal = document.getElementById('newFolderModal');
2156
- const input = document.getElementById('newFolderName');
2157
- const shouldShow = typeof show === 'boolean' ? show : modal.classList.contains('hidden');
2158
- modal.classList.toggle('hidden', !shouldShow);
2159
-
2160
- if (shouldShow) {
2161
- document.body.style.overflow = 'hidden';
2162
- if (input) input.value = '';
2163
- resetGroupManageState();
2164
- setGroupModalTab('create');
2165
- populateParentFolderSelect(); // 填充父级目录选择框
2166
- } else {
2167
- document.body.style.overflow = '';
2168
- if (input) input.value = '';
2169
- groupManageEditingPath = null;
2170
- groupManageActionLoading.clear();
2171
- }
2172
- }
2173
-
2174
- // 将函数挂载到全局window对象,以便HTML中的内联事件可以访问
2175
- window.toggleNewFolderModal = toggleNewFolderModal;
2176
- window.handleNewFolderKeydown = handleNewFolderKeydown;
2177
- window.createNewFolder = createNewFolder;
2178
-
2179
- // 处理目录名输入框的键盘事件
2180
- function handleNewFolderKeydown(event) {
2181
- if (event.key === 'Enter') {
2182
- event.preventDefault();
2183
- createNewFolder();
2184
- } else if (event.key === 'Escape') {
2185
- event.preventDefault();
2186
- toggleNewFolderModal(false);
2187
- }
2188
- }
2189
-
2190
- // 填充父级目录选择框
2191
- function populateParentFolderSelect() {
2192
- const select = document.getElementById('newFolderParent');
2193
- if (!select) return;
2194
-
2195
- // 清空现有选项
2196
- select.innerHTML = '<option value="">根目录</option>';
2197
-
2198
- // 添加所有现有目录作为选项
2199
- const flattenedGroups = flattenGroupTree(groupTreeState || []);
2200
- flattenedGroups.forEach(group => {
2201
- if (group.path !== 'default') { // 排除默认组
2202
- const option = document.createElement('option');
2203
- option.value = group.path;
2204
- option.textContent = group.path;
2205
- select.appendChild(option);
2206
- }
2207
- });
2208
- }
2209
-
2210
- // 创建新目录
2211
- async function createNewFolder() {
2212
- const nameInput = document.getElementById('newFolderName');
2213
- const parentSelect = document.getElementById('newFolderParent');
2214
- const folderName = (nameInput?.value || '').trim();
2215
- const parentPath = (parentSelect?.value || '').trim();
2216
-
2217
- if (!folderName) {
2218
- showMessage('请输入目录名称', 'error');
2219
- nameInput?.focus();
2220
- return;
2221
- }
2222
-
2223
- // 验证目录名称格式
2224
- if (!/^(?![.]{1,2}$)[^\\/:*?"<>|\r\n]{1,64}$/.test(folderName)) {
2225
- showMessage('目录名称格式无效,不能包含 / \\ : * ? \" < > | 或换行,长度需在1-64字符', 'error');
2226
- nameInput?.focus();
2227
- return;
2228
- }
2229
-
2230
- try {
2231
- const requestData = { name: folderName };
2232
- if (parentPath) {
2233
- requestData.parent = parentPath;
2234
- }
2235
-
2236
- await apiCall('/groups', {
2237
- method: 'POST',
2238
- body: JSON.stringify(requestData)
2239
- });
2240
-
2241
- showMessage('类目创建成功');
2242
- toggleNewFolderModal(false);
2243
- const searchInput = document.getElementById('searchInput');
2244
- const searchValue = searchInput ? searchInput.value : '';
2245
- await loadPrompts(searchValue);
2246
- } catch (error) {
2247
- console.error('创建目录失败:', error);
2248
- showMessage(error.message || '创建类目失败', 'error');
2249
- }
2250
- }
2251
-
2252
- // 刷新模态框打开状态
2253
- function refreshModalOpenState() {
2254
- const hasOpenModal =
2255
- (argumentModalEl && !argumentModalEl.classList.contains('hidden')) ||
2256
- (deletePromptModalEl && !deletePromptModalEl.classList.contains('hidden'));
2257
- if (hasOpenModal) {
2258
- document.body.classList.add('modal-open');
2259
- } else {
2260
- document.body.classList.remove('modal-open');
2261
- }
2262
- }
2263
-
2264
- // 初始化应用
2265
- async function initApp() {
2266
- try {
2267
- // 等待 CodeMirror 加载完成
2268
- editor = await initCodeMirror();
2269
-
2270
- const editModeBtn = document.getElementById('editModeBtn');
2271
- const previewModeBtn = document.getElementById('previewModeBtn');
2272
- editModeBtn.addEventListener('click', () => setWorkspaceMode('edit'));
2273
- previewModeBtn.addEventListener('click', () => setWorkspaceMode('preview'));
2274
- setWorkspaceMode('edit');
2275
-
2276
- descriptionInputEl = document.getElementById('promptDescription');
2277
- if (descriptionInputEl) {
2278
- descriptionInputEl.addEventListener('input', adjustDescriptionHeight);
2279
- adjustDescriptionHeight();
2280
- }
2281
-
2282
- setArgumentsState([]);
2283
-
2284
- argumentModalEl = document.getElementById('argumentModal');
2285
- argumentFormEl = document.getElementById('argumentForm');
2286
- argumentModalTitleEl = document.getElementById('argumentModalTitle');
2287
- argumentNameInput = document.getElementById('argumentNameInput');
2288
- argumentTypeInput = document.getElementById('argumentTypeInput');
2289
- argumentRequiredInput = document.getElementById('argumentRequiredInput');
2290
- argumentDefaultInput = document.getElementById('argumentDefaultInput');
2291
- argumentDescriptionInput = document.getElementById('argumentDescriptionInput');
2292
- deletePromptModalEl = document.getElementById('deletePromptModal');
2293
- deletePromptNameEl = document.getElementById('deletePromptName');
2294
- deletePromptConfirmBtn = document.getElementById('deletePromptConfirmBtn');
2295
- deletePromptCancelBtn = document.getElementById('deletePromptCancelBtn');
2296
- deletePromptCloseBtn = document.getElementById('deletePromptCloseBtn');
2297
- promptGroupBtnEl = document.getElementById('promptGroupBtn');
2298
- promptGroupLabelEl = document.getElementById('promptGroupLabel');
2299
- promptGroupDropdownEl = document.getElementById('promptGroupDropdown');
2300
- promptGroupSearchInput = document.getElementById('promptGroupSearch');
2301
- promptGroupCascaderEl = document.getElementById('promptGroupCascader');
2302
- promptGroupSearchResultsEl = document.getElementById('promptGroupSearchResults');
2303
- promptGroupEmptyEl = document.getElementById('promptGroupEmpty');
2304
- groupManageListEl = document.getElementById('groupManageList');
2305
- groupManageEmptyEl = document.getElementById('groupManageEmpty');
2306
- groupManageSearchInputEl = document.getElementById('groupManageSearch');
2307
-
2308
- // 绑定参数表单事件
2309
- if (argumentFormEl) {
2310
- argumentFormEl.addEventListener('submit', handleArgumentFormSubmit);
2311
- }
2312
-
2313
- // 绑定参数列表点击事件
2314
- document.getElementById('argumentsList').addEventListener('click', handleArgumentListClick);
2315
-
2316
- // 绑定参数按钮事件
2317
- document.getElementById('addArgumentBtn').addEventListener('click', () => openArgumentModal());
2318
-
2319
- // 绑定参数模态框事件
2320
- document.getElementById('argumentModalClose').addEventListener('click', closeArgumentModal);
2321
- document.getElementById('argumentCancelBtn').addEventListener('click', closeArgumentModal);
2322
-
2323
- // 绑定删除提示模态框事件
2324
- document.getElementById('deletePromptConfirmBtn').addEventListener('click', confirmDeletePrompt);
2325
- const closeDeleteHandlers = [deletePromptCancelBtn, deletePromptCloseBtn];
2326
- closeDeleteHandlers.forEach(btn => {
2327
- if (btn) {
2328
- btn.addEventListener('click', closeDeletePromptModal);
2329
- }
2330
- });
2331
- if (deletePromptModalEl) {
2332
- deletePromptModalEl.addEventListener('click', (event) => {
2333
- if (event.target === deletePromptModalEl) {
2334
- closeDeletePromptModal();
2335
- }
2336
- });
2337
- }
2338
-
2339
- // 绑定组选择器事件
2340
- promptGroupBtnEl.addEventListener('click', () => toggleGroupDropdown());
2341
- if (promptGroupSearchInput) {
2342
- promptGroupSearchInput.addEventListener('input', () => {
2343
- renderGroupDropdownContent(promptGroupSearchInput.value);
2344
- });
2345
- }
2346
- document.addEventListener('click', (event) => {
2347
- if (!promptGroupDropdownEl.contains(event.target) && !promptGroupBtnEl.contains(event.target)) {
2348
- closeGroupDropdown();
2349
- }
2350
- });
2351
-
2352
- // 绑定键盘事件
2353
- document.addEventListener('keydown', (event) => {
2354
- if (event.key === 'Escape') {
2355
- if (isGroupDropdownOpen) {
2356
- closeGroupDropdown(true);
2357
- return;
2358
- }
2359
- if (argumentModalEl && !argumentModalEl.classList.contains('hidden')) {
2360
- closeArgumentModal();
2361
- return;
2362
- }
2363
- if (deletePromptModalEl && !deletePromptModalEl.classList.contains('hidden')) {
2364
- closeDeletePromptModal();
2365
- }
2366
- }
2367
- });
2368
-
2369
- // 绑定管理类目列表事件
2370
- if (groupManageListEl) {
2371
- groupManageListEl.addEventListener('click', handleGroupManageListClick);
2372
- groupManageSearchInputEl?.addEventListener('input', handleGroupManageSearchInput);
2373
- }
2374
-
2375
- // 绑定新建目录模态框事件
2376
- document.getElementById('groupManageRefreshBtn').addEventListener('click', () => {
2377
- renderGroupManageList();
2378
- });
2379
-
2380
- // 绑定标签页切换事件
2381
- document.querySelectorAll('.group-modal-tab').forEach(tab => {
2382
- tab.addEventListener('click', (e) => {
2383
- const tabName = e.currentTarget.dataset.tab;
2384
- setGroupModalTab(tabName);
2385
- });
2386
- });
2387
-
2388
- // 绑定事件
2389
- document.getElementById('loginBtn').addEventListener('click', () => {
2390
- const username = document.getElementById('username').value.trim();
2391
- const password = document.getElementById('password').value.trim();
2392
-
2393
- if (!username || !password) {
2394
- document.getElementById('loginError').textContent = '请输入用户名和密码';
2395
- return;
2396
- }
2397
-
2398
- login(username, password);
2399
- });
2400
-
2401
- document.getElementById('logoutBtn').addEventListener('click', logout);
2402
- document.getElementById('newPromptBtn').addEventListener('click', newPrompt);
2403
- document.getElementById('newPromptBtnInBlankArea').addEventListener('click', newPrompt);
2404
- document.getElementById('newGroupBtn').addEventListener('click', () => toggleNewFolderModal(true));
2405
- document.getElementById('saveBtn').addEventListener('click', savePrompt);
2406
-
2407
- // 返回列表按钮事件处理
2408
- const backToListBtn = document.getElementById('backToListBtn');
2409
- if (backToListBtn) {
2410
- backToListBtn.addEventListener('click', () => {
2411
- showCustomBlankContent();
2412
- currentPrompt = null;
2413
- currentPromptObject = null;
2414
- });
2415
- }
2416
-
2417
- // 搜索功能
2418
- const searchInput = document.getElementById('searchInput');
2419
- if (searchInput) {
2420
- const searchBox = searchInput.closest('.search-box');
2421
- const clearBtn = searchBox?.querySelector('.clear-btn');
2422
- let searchTimeout;
2423
-
2424
- // 搜索输入事件
2425
- searchInput.addEventListener('input', () => {
2426
- clearTimeout(searchTimeout);
2427
- searchTimeout = setTimeout(() => {
2428
- loadPrompts(searchInput.value);
2429
- }, 300);
2430
- });
2431
-
2432
- // 清除按钮点击事件
2433
- if (clearBtn) {
2434
- clearBtn.addEventListener('click', () => {
2435
- searchInput.value = '';
2436
- searchInput.focus();
2437
- loadPrompts('');
2438
- });
2439
- }
2440
-
2441
- // ESC 键清除搜索
2442
- searchInput.addEventListener('keydown', (e) => {
2443
- if (e.key === 'Escape') {
2444
- searchInput.value = '';
2445
- loadPrompts('');
2446
- }
2447
- });
2448
- }
2449
-
2450
- // 设置用户菜单
2451
- setupUserMenu();
2452
-
2453
- // 重新加载配置后,根据认证要求设置登录界面的显示
2454
- updateLoginDisplay();
2455
-
2456
- } catch (err) {
2457
- console.error('初始化错误:', err);
2458
- document.getElementById('loginError').textContent = '资源加载失败,请刷新重试';
2459
- throw err;
2460
- }
2461
- }
2462
-
2463
- // 启动应用
2464
- document.addEventListener('DOMContentLoaded', async () => {
2465
- try {
2466
- await checkAuthRequirement();
2467
- setupLoginEvents();
2468
-
2469
- // 检查登录状态
2470
- if (currentToken || !requireAuth) {
2471
- showMain();
2472
- showLoading(); // 显示加载中效果
2473
- loadPrompts(); // 先加载提示词列表
2474
- await initApp(); // 然后初始化应用
2475
- // 页面加载后显示自定义的空白内容区域,不显示编辑器
2476
- showCustomBlankContent();
2477
- // 初始化推荐词功能(内联函数定义)
2478
- let recommendedPrompts = [];
2479
- let currentRecommendedPromptIndex = 0;
2480
- let recommendedPromptsPerPage = 4;
2481
- let currentCardWidth = '25%';
2482
-
2483
- // 根据容器宽度动态计算每页显示的卡片数量和宽度
2484
- function calculatePromptsLayout() {
2485
- const container = document.getElementById('recommendedPromptsList');
2486
- if (!container) return { count: 4, cardWidth: '25%' };
2487
-
2488
- const containerWidth = container.offsetWidth;
2489
- const minCardWidth = 240; // 卡片最小宽度
2490
- const maxCardWidth = 320; // 卡片最大宽度
2491
- const gap = 12; // 卡片间隙
2492
-
2493
- // 计算可以显示的卡片数量(最小2列,最大4列)
2494
- let count = Math.floor((containerWidth + gap) / (minCardWidth + gap));
2495
- count = Math.max(2, Math.min(4, count));
2496
-
2497
- // 计算卡片宽度,确保均匀分布
2498
- const totalGap = gap * (count - 1);
2499
- const cardWidth = (containerWidth - totalGap) / count;
2500
-
2501
- // 确保卡片宽度在最小和最大值之间
2502
- const clampedCardWidth = Math.max(minCardWidth, Math.min(maxCardWidth, cardWidth));
2503
-
2504
- // 如果卡片宽度被限制,重新计算实际能显示的卡片数量
2505
- if (clampedCardWidth !== cardWidth) {
2506
- const actualCount = Math.floor((containerWidth + gap) / (clampedCardWidth + gap));
2507
- return { count: Math.max(2, Math.min(4, actualCount)), cardWidth: `${clampedCardWidth}px` };
2508
- }
2509
-
2510
- // 返回百分比宽度以实现均匀分布
2511
- return { count, cardWidth: `${100 / count * (1 - gap / containerWidth * (count - 1))}%` };
2512
- }
2513
-
2514
- // 加载推荐词数据
2515
- async function loadRecommendedPrompts() {
2516
- try {
2517
- // 尝试从API端点获取推荐词数据
2518
- let data = null;
2519
- let apiUrl = null;
2520
-
2521
- // 使用单个API端点 - 使用API_BASE确保请求发送到正确的后端服务器
2522
- const endpoint = '/prompts.json';
2523
-
2524
- try {
2525
- console.log(`尝试从 ${API_SURGE}${endpoint} 加载推荐词数据...`);
2526
- const response = await fetch(`${API_SURGE}${endpoint}`);
2527
- if (response.ok) {
2528
- data = await response.json();
2529
- apiUrl = endpoint;
2530
- console.log(`成功从 ${API_SURGE}${endpoint} 获取数据`);
2531
- }
2532
- } catch (e) {
2533
- console.log(`从 ${API_SURGE}${endpoint} 获取数据失败:`, e.message);
2534
- }
2535
-
2536
- if (data === null) {
2537
- throw new Error('无法从任何API端点获取推荐词数据');
2538
- }
2539
-
2540
- // 确保返回的数据是数组
2541
- if (!Array.isArray(data)) {
2542
- // 如果返回的是对象(如包含prompts数组的对象),尝试提取
2543
- if (data.prompts && Array.isArray(data.prompts)) {
2544
- data = data.prompts;
2545
- } else {
2546
- throw new Error('API返回的数据格式不正确');
2547
- }
2548
- }
2549
-
2550
- // 转换数据格式以匹配现有结构
2551
- // API返回的数据格式: {name, description, tags, path}
2552
- recommendedPrompts = data.map(prompt => ({
2553
- name: prompt.name || prompt.title || '未命名提示词',
2554
- description: prompt.description || prompt.desc || '暂无描述',
2555
- content: prompt.content || '', // 如果没有content字段,后续可以动态加载
2556
- tags: Array.isArray(prompt.tags) ? prompt.tags : (typeof prompt.tags === 'string' ? [prompt.tags] : []),
2557
- group: prompt.group || prompt.category || 'default',
2558
- path: prompt.path // 保留路径信息,用于后续加载完整内容
2559
- }));
2560
-
2561
- console.log('成功加载推荐词数据:', recommendedPrompts);
2562
- const layout = calculatePromptsLayout();
2563
- recommendedPromptsPerPage = layout.count;
2564
- currentCardWidth = layout.cardWidth;
2565
- renderRecommendedPrompts();
2566
- updateRecommendedPromptsNavigation();
2567
- } catch (error) {
2568
- console.error('加载推荐词失败:', error);
2569
- // 如果API调用失败,将推荐提示词数据置空
2570
- recommendedPrompts = [];
2571
-
2572
- // 不展示推荐区域
2573
- const section = document.getElementById('recommendedPromptsSection');
2574
- const blankContent = document.getElementById('customBlankContent');
2575
- if (section) {
2576
- section.classList.add('hidden');
2577
- }
2578
- if (blankContent) {
2579
- blankContent.classList.add('no-recommendation');
2580
- }
2581
- }
2582
- }
2583
-
2584
- // 渲染推荐词卡片
2585
- function renderRecommendedPrompts() {
2586
- const container = document.getElementById('recommendedPromptsList');
2587
- const section = document.getElementById('recommendedPromptsSection');
2588
- const blankContent = document.getElementById('customBlankContent');
2589
- if (!container) return;
2590
-
2591
- const hasData = recommendedPrompts.length > 0;
2592
-
2593
- if (section) {
2594
- section.classList.toggle('hidden', !hasData);
2595
- }
2596
- if (blankContent) {
2597
- blankContent.classList.toggle('no-recommendation', !hasData);
2598
- }
2599
-
2600
- // 清空容器
2601
- container.innerHTML = '';
2602
-
2603
- if (!hasData) {
2604
- return;
2605
- }
2606
-
2607
- // 根据容器宽度动态计算每页显示的卡片数量和宽度
2608
- const layout = calculatePromptsLayout();
2609
- recommendedPromptsPerPage = layout.count;
2610
- currentCardWidth = layout.cardWidth;
2611
-
2612
- // 确保当前页索引有效
2613
- const maxIndex = Math.max(Math.ceil(recommendedPrompts.length / recommendedPromptsPerPage) - 1, 0);
2614
- if (currentRecommendedPromptIndex > maxIndex) {
2615
- currentRecommendedPromptIndex = 0;
2616
- }
2617
-
2618
- // 计算当前页的推荐词
2619
- const startIndex = currentRecommendedPromptIndex * recommendedPromptsPerPage;
2620
- const endIndex = Math.min(startIndex + recommendedPromptsPerPage, recommendedPrompts.length);
2621
- const currentPrompts = recommendedPrompts.slice(startIndex, endIndex);
2622
-
2623
- // 创建卡片元素
2624
- currentPrompts.forEach((prompt, index) => {
2625
- const card = document.createElement('div');
2626
- card.className = 'recommended-prompt-card';
2627
- card.style.width = currentCardWidth;
2628
- card.style.marginRight = '12px';
2629
- if (index === currentPrompts.length - 1) {
2630
- card.style.marginRight = '0';
2631
- }
2632
-
2633
- const hasTags = Array.isArray(prompt.tags) && prompt.tags.length > 0;
2634
- const tagsHtml = hasTags
2635
- ? `
2636
- <div class="card-tags">
2637
- ${prompt.tags.map(tag => `<span class="card-tag">${escapeHtml(tag)}</span>`).join('')}
2638
- </div>
2639
- `
2640
- : '';
2641
- card.innerHTML = `
2642
- <div class="card-header">
2643
- <div class="card-title">${escapeHtml(prompt.name)}</div>
2644
- </div>
2645
- <div class="card-description">${escapeHtml(prompt.description)}</div>
2646
- ${tagsHtml}
2647
- `;
2648
-
2649
- // 添加点击事件
2650
- card.addEventListener('click', () => {
2651
- console.log('Card clicked:', prompt);
2652
- showRecommendedPromptDetail(prompt);
2653
- });
2654
-
2655
- container.appendChild(card);
2656
- });
2657
- }
2658
-
2659
- // 更新推荐词导航按钮状态
2660
- function updateRecommendedPromptsNavigation() {
2661
- const leftBtn = document.getElementById('recommendedPromptsLeft');
2662
- const rightBtn = document.getElementById('recommendedPromptsRight');
2663
-
2664
- if (leftBtn) {
2665
- leftBtn.classList.toggle('disabled', currentRecommendedPromptIndex <= 0);
2666
- }
2667
-
2668
- if (rightBtn) {
2669
- const maxIndex = Math.ceil(recommendedPrompts.length / recommendedPromptsPerPage) - 1;
2670
- rightBtn.classList.toggle('disabled', currentRecommendedPromptIndex >= maxIndex);
2671
- }
2672
- }
2673
-
2674
- // 推荐词导航事件
2675
- function navigateRecommendedPrompts(direction) {
2676
- const maxIndex = Math.ceil(recommendedPrompts.length / recommendedPromptsPerPage) - 1;
2677
-
2678
- if (direction === 'left' && currentRecommendedPromptIndex > 0) {
2679
- currentRecommendedPromptIndex--;
2680
- } else if (direction === 'right' && currentRecommendedPromptIndex < maxIndex) {
2681
- currentRecommendedPromptIndex++;
2682
- }
2683
-
2684
- // 添加滑动动画
2685
- const container = document.getElementById('recommendedPromptsList');
2686
- if (container) {
2687
- container.classList.add('sliding');
2688
- setTimeout(() => {
2689
- renderRecommendedPrompts();
2690
- updateRecommendedPromptsNavigation();
2691
- container.classList.remove('sliding');
2692
- }, 10);
2693
- } else {
2694
- renderRecommendedPrompts();
2695
- updateRecommendedPromptsNavigation();
2696
- }
2697
- }
2698
-
2699
- // 显示推荐词详情
2700
- async function showRecommendedPromptDetail(prompt) {
2701
- console.log('Showing recommended prompt detail:', prompt);
2702
- // 保存当前推荐提示词数据,供同步弹窗使用
2703
- window.currentRecommendedPromptData = prompt;
2704
-
2705
- const modal = document.getElementById('recommendedPromptModal');
2706
- const titleEl = document.getElementById('recommendedPromptTitle');
2707
- const descEl = document.getElementById('recommendedPromptDescription');
2708
- const contentEl = document.getElementById('recommendedPromptContent');
2709
-
2710
- if (modal) {
2711
- console.log('Modal element found, showing modal');
2712
- modal.classList.add('active');
2713
- document.body.style.overflow = 'hidden';
2714
- } else {
2715
- console.error('Modal element not found');
2716
- return;
2717
- }
2718
-
2719
- // 更新标题文本
2720
- const titleTextEl = titleEl?.querySelector('.modal-title-text');
2721
- if (titleTextEl) {
2722
- console.log('Updating title text to:', prompt.name);
2723
- titleTextEl.textContent = prompt.name;
2724
- } else {
2725
- console.warn('Title text element not found');
2726
- }
2727
-
2728
- if (descEl) {
2729
- console.log('Updating description to:', prompt.description);
2730
- descEl.textContent = prompt.description;
2731
- } else {
2732
- console.warn('Description element not found');
2733
- }
2734
-
2735
- // 如果prompt有完整内容,则直接显示
2736
- if (prompt.content && prompt.content.trim() !== '') {
2737
- if (contentEl) {
2738
- console.log('Displaying direct content');
2739
- contentEl.textContent = prompt.content;
2740
- }
2741
- } else {
2742
- // 如果没有完整内容,则尝试从API动态加载
2743
- if (contentEl) {
2744
- console.log('Loading content from API');
2745
- contentEl.textContent = '正在加载内容...';
2746
-
2747
- try {
2748
- // 如果prompt有路径信息,尝试从surge端点加载完整内容
2749
- if (prompt.path) {
2750
- // 尝试从surge端点获取完整提示词内容
2751
- console.log('Fetching content from surge endpoint:', `${API_SURGE}/${prompt.path}`);
2752
- const surgeResponse = await fetch(`${API_SURGE}/${prompt.path}`);
2753
- if (surgeResponse.ok) {
2754
- const fullPrompt = await surgeResponse.json();
2755
- console.log('Received full prompt from surge:', fullPrompt);
2756
- if (fullPrompt && fullPrompt.messages && Array.isArray(fullPrompt.messages)) {
2757
- // 获取所有消息内容,不区分role
2758
- const allContent = fullPrompt.messages.map(msg => {
2759
- if (typeof msg.content === 'string') {
2760
- return msg.content;
2761
- } else if (msg.content?.text) {
2762
- return msg.content.text;
2763
- } else {
2764
- return JSON.stringify(msg.content, null, 2);
2765
- }
2766
- }).join('\n\n---\n\n'); // 用分隔符连接不同消息
2767
-
2768
- contentEl.textContent = allContent;
2769
- }
2770
- }
2771
- } else {
2772
- contentEl.textContent = '暂无详细内容';
2773
- }
2774
- } catch (error) {
2775
- console.error('加载提示词内容失败:', error);
2776
- contentEl.textContent = '加载内容失败: ' + error.message;
2777
- }
2778
- }
2779
- }
2780
-
2781
- console.log('Finished showing recommended prompt detail');
2782
- }
2783
-
2784
- // 隐藏推荐词详情
2785
- function hideRecommendedPromptDetail() {
2786
- console.log('Hiding recommended prompt detail');
2787
- const modal = document.getElementById('recommendedPromptModal');
2788
- if (modal) {
2789
- console.log('Modal element found, hiding modal');
2790
- modal.classList.remove('active');
2791
- document.body.style.overflow = '';
2792
- } else {
2793
- console.error('Modal element not found when trying to hide');
2794
- }
2795
- }
2796
-
2797
- // 显示同步到我的提示词弹窗
2798
- function showSyncPromptModal() {
2799
- const modal = document.getElementById('syncPromptModal');
2800
- const groupSelect = document.getElementById('syncPromptGroup');
2801
- const nameInput = document.getElementById('syncPromptName');
2802
-
2803
- if (nameInput && window.currentRecommendedPromptData) {
2804
- // 设置默认名称为推荐提示词的名称
2805
- nameInput.value = window.currentRecommendedPromptData.name || '';
2806
- }
2807
-
2808
- if (groupSelect) {
2809
- // 清空现有选项
2810
- groupSelect.innerHTML = '<option value="">请选择目录</option>';
2811
-
2812
- // 获取现有目录并填充选项
2813
- const flattenedGroups = flattenGroupTree(groupTreeState || []);
2814
- flattenedGroups.forEach(group => {
2815
- if (group.path !== 'default') { // 排除默认组,因为默认组可以作为目标
2816
- const option = document.createElement('option');
2817
- option.value = group.path;
2818
- option.textContent = group.path;
2819
- groupSelect.appendChild(option);
2820
- }
2821
- });
2822
-
2823
- // 添加默认选项
2824
- const defaultOption = document.createElement('option');
2825
- defaultOption.value = 'default';
2826
- defaultOption.textContent = 'default';
2827
- groupSelect.insertBefore(defaultOption, groupSelect.firstChild);
2828
-
2829
- // 默认选中 default 目录
2830
- groupSelect.value = 'default';
2831
- }
2832
-
2833
- if (modal) {
2834
- modal.classList.add('active');
2835
- document.body.style.overflow = 'hidden';
2836
- }
2837
- }
2838
-
2839
- // 隐藏同步到我的提示词弹窗
2840
- function hideSyncPromptModal() {
2841
- const modal = document.getElementById('syncPromptModal');
2842
- if (modal) {
2843
- modal.classList.remove('active');
2844
- document.body.style.overflow = '';
2845
- }
2846
- }
2847
-
2848
- // 同步推荐词到我的提示词
2849
- async function syncRecommendedPrompt() {
2850
- const groupSelect = document.getElementById('syncPromptGroup');
2851
- const nameInput = document.getElementById('syncPromptName');
2852
- const selectedGroup = groupSelect ? groupSelect.value : '';
2853
- const customName = nameInput ? nameInput.value.trim() : '';
2854
-
2855
- if (!customName) {
2856
- showMessage('请输入提示词名称', 'error');
2857
- return;
2858
- }
2859
-
2860
- if (!selectedGroup) {
2861
- showMessage('请选择目录', 'error');
2862
- return;
2863
- }
2864
-
2865
- const promptData = window.currentRecommendedPromptData;
2866
- if (!promptData) {
2867
- showMessage('没有选择要同步的提示词', 'error');
2868
- return;
2869
- }
2870
-
2871
- try {
2872
- // 显示加载中效果
2873
- showLoading();
2874
-
2875
- // 获取完整的提示词数据
2876
- let fullPromptData = null;
2877
-
2878
- // 如果有路径信息,从surge端点获取完整数据
2879
- if (promptData.path) {
2880
- try {
2881
- const surgeResponse = await fetch(`${API_SURGE}/${promptData.path}`);
2882
- if (surgeResponse.ok) {
2883
- fullPromptData = await surgeResponse.json();
2884
- }
2885
- } catch (error) {
2886
- console.warn('从surge端点获取完整数据失败:', error);
2887
- }
2888
- }
2889
-
2890
- // 如果没有从surge获取到数据,尝试从本地API获取
2891
- if (!fullPromptData) {
2892
- try {
2893
- const localResponse = await apiCall(`/prompts/${encodeURIComponent(promptData.name)}`);
2894
- if (localResponse && localResponse.yaml) {
2895
- fullPromptData = window.jsyaml.load(localResponse.yaml);
2896
- }
2897
- } catch (error) {
2898
- console.warn('从本地API获取完整数据失败:', error);
2899
- }
2900
- }
2901
-
2902
- // 如果仍然没有获取到完整数据,则使用推荐词数据作为基础
2903
- if (!fullPromptData) {
2904
- fullPromptData = {
2905
- name: promptData.name,
2906
- description: promptData.description || '',
2907
- enabled: true,
2908
- messages: [
2909
- {
2910
- role: 'user',
2911
- content: {
2912
- text: promptData.content || promptData.description || ''
2913
- }
2914
- }
2915
- ],
2916
- arguments: promptData.arguments || []
2917
- };
2918
- }
2919
-
2920
- // 使用用户自定义的名称和目录覆盖原数据
2921
- fullPromptData.name = customName;
2922
-
2923
- // 创建包含所有必要信息的提示词对象
2924
- const promptObject = {
2925
- name: customName,
2926
- description: fullPromptData.description || '',
2927
- enabled: fullPromptData.enabled !== false, // 默认启用
2928
- messages: fullPromptData.messages || [],
2929
- arguments: fullPromptData.arguments || []
2930
- };
2931
-
2932
- // 转换为YAML格式
2933
- let yaml;
2934
- try {
2935
- yaml = window.jsyaml.dump(promptObject);
2936
- } catch (error) {
2937
- console.error('生成YAML失败:', error);
2938
- showMessage('生成提示词数据失败', 'error');
2939
- return;
2940
- }
2941
-
2942
- // 调用API保存提示词
2943
- const result = await apiCall('/prompts', {
2944
- method: 'POST',
2945
- body: JSON.stringify({
2946
- name: customName,
2947
- group: selectedGroup,
2948
- yaml: yaml
2949
- })
2950
- });
2951
-
2952
- showMessage('同步成功');
2953
-
2954
- // 隐藏弹窗
2955
- hideSyncPromptModal();
2956
- hideRecommendedPromptDetail();
2957
-
2958
- // 重新加载提示词列表以显示新添加的提示词
2959
- const searchInput = document.getElementById('searchInput');
2960
- const searchValue = searchInput ? searchInput.value : '';
2961
- await loadPrompts(searchValue);
2962
- } catch (error) {
2963
- console.error('同步推荐词失败:', error);
2964
- showMessage('同步失败: ' + (error?.message || '未知错误'), 'error');
2965
- } finally {
2966
- // 隐藏加载中效果
2967
- hideLoading();
2968
- }
2969
- }
2970
-
2971
- // 绑定推荐词相关事件
2972
- function bindRecommendedPromptsEvents() {
2973
- // 左右导航按钮事件
2974
- const leftBtn = document.getElementById('recommendedPromptsLeft');
2975
- const rightBtn = document.getElementById('recommendedPromptsRight');
2976
-
2977
- if (leftBtn) {
2978
- leftBtn.addEventListener('click', () => {
2979
- if (!leftBtn.classList.contains('disabled')) {
2980
- navigateRecommendedPrompts('left');
2981
- }
2982
- });
2983
- }
2984
-
2985
- if (rightBtn) {
2986
- rightBtn.addEventListener('click', () => {
2987
- if (!rightBtn.classList.contains('disabled')) {
2988
- navigateRecommendedPrompts('right');
2989
- }
2990
- });
2991
- }
2992
-
2993
- // 推荐词详情弹窗关闭事件
2994
- const detailCloseBtn = document.getElementById('recommendedPromptClose');
2995
- if (detailCloseBtn) {
2996
- detailCloseBtn.addEventListener('click', hideRecommendedPromptDetail);
2997
- }
2998
-
2999
- // 推荐词详情弹窗背景点击关闭
3000
- const detailModal = document.getElementById('recommendedPromptModal');
3001
- if (detailModal) {
3002
- detailModal.addEventListener('click', (e) => {
3003
- if (e.target === detailModal) {
3004
- hideRecommendedPromptDetail();
3005
- }
3006
- });
3007
- }
3008
-
3009
- // 同步到我的提示词按钮
3010
- const syncBtn = document.getElementById('syncToMyPromptsBtn');
3011
- if (syncBtn) {
3012
- syncBtn.addEventListener('click', showSyncPromptModal);
3013
- }
3014
-
3015
- // 同步弹窗关闭事件
3016
- const syncCloseBtn = document.getElementById('syncPromptClose');
3017
- const syncCancelBtn = document.getElementById('syncPromptCancel');
3018
-
3019
- if (syncCloseBtn) {
3020
- syncCloseBtn.addEventListener('click', hideSyncPromptModal);
3021
- }
3022
-
3023
- if (syncCancelBtn) {
3024
- syncCancelBtn.addEventListener('click', hideSyncPromptModal);
3025
- }
3026
-
3027
- // 同步弹窗背景点击关闭
3028
- const syncModal = document.getElementById('syncPromptModal');
3029
- if (syncModal) {
3030
- syncModal.addEventListener('click', (e) => {
3031
- if (e.target === syncModal) {
3032
- hideSyncPromptModal();
3033
- }
3034
- });
3035
- }
3036
-
3037
- // 确认同步按钮
3038
- const syncConfirmBtn = document.getElementById('syncPromptConfirm');
3039
- if (syncConfirmBtn) {
3040
- syncConfirmBtn.addEventListener('click', syncRecommendedPrompt);
3041
- }
3042
-
3043
- // ESC键关闭弹窗
3044
- document.addEventListener('keydown', (e) => {
3045
- if (e.key === 'Escape') {
3046
- if (detailModal && detailModal.classList.contains('active')) {
3047
- hideRecommendedPromptDetail();
3048
- } else if (syncModal && syncModal.classList.contains('active')) {
3049
- hideSyncPromptModal();
3050
- }
3051
- }
3052
- });
3053
-
3054
- // 窗口大小改变时重新计算每页显示的卡片数量
3055
- let resizeTimeout;
3056
- window.addEventListener('resize', () => {
3057
- clearTimeout(resizeTimeout);
3058
- resizeTimeout = setTimeout(() => {
3059
- const layout = calculatePromptsLayout();
3060
- recommendedPromptsPerPage = layout.count;
3061
- currentCardWidth = layout.cardWidth;
3062
- // 确保当前页索引有效
3063
- const maxIndex = Math.max(Math.ceil(recommendedPrompts.length / recommendedPromptsPerPage) - 1, 0);
3064
- if (currentRecommendedPromptIndex > maxIndex) {
3065
- currentRecommendedPromptIndex = maxIndex;
3066
- }
3067
- renderRecommendedPrompts();
3068
- updateRecommendedPromptsNavigation();
3069
- }, 300);
3070
- });
3071
- }
3072
-
3073
- // 获取推荐词数据
3074
- loadRecommendedPrompts();
3075
- // 绑定推荐词相关事件
3076
- bindRecommendedPromptsEvents();
3077
- } else {
3078
- showLogin();
3079
- }
3080
- } catch (error) {
3081
- console.error('应用初始化失败:', error);
3082
- // 如果初始化失败,显示登录界面作为降级方案
3083
- try {
3084
- showLogin();
3085
- } catch (showLoginError) {
3086
- console.error('显示登录界面也失败:', showLoginError);
3087
- // 作为最后的手段,直接显示主界面
3088
- document.getElementById('login').style.display = 'none';
3089
- document.getElementById('main').style.display = 'block';
3090
- }
3091
- } finally {
3092
- // 隐藏加载中效果
3093
- hideLoading();
3094
- }
3095
- });
3096
-
3097
- // 显示自定义空白内容区域
3098
- function showCustomBlankContent() {
3099
- const customBlankContent = document.getElementById('customBlankContent');
3100
- const promptEditorArea = document.getElementById('promptEditorArea');
3101
-
3102
- if (customBlankContent) {
3103
- customBlankContent.style.display = 'flex';
3104
- }
3105
-
3106
- if (promptEditorArea) {
3107
- promptEditorArea.style.display = 'none';
3108
- }
3109
-
3110
- // 清空编辑器内容
3111
- if (editor) {
3112
- editor.setValue('');
3113
- }
3114
- // 清空其他编辑区域
3115
- const nameInput = document.getElementById('promptName');
3116
- if (nameInput) nameInput.value = '';
3117
- if (descriptionInputEl) {
3118
- descriptionInputEl.value = '';
3119
- adjustDescriptionHeight();
3120
- }
3121
- // 清除所有选中的prompt状态
3122
- document.querySelectorAll('.prompt-item').forEach(el => el.classList.remove('active'));
3123
- // 重置当前prompt状态
3124
- currentPrompt = null;
3125
- currentPromptObject = null;
3126
- setArgumentsState([]);
3127
- }
3128
-
3129
- // 显示prompt编辑区域
3130
- function showPromptEditorArea() {
3131
- const customBlankContent = document.getElementById('customBlankContent');
3132
- const promptEditorArea = document.getElementById('promptEditorArea');
3133
-
3134
- if (customBlankContent) {
3135
- customBlankContent.style.display = 'none';
3136
- }
3137
-
3138
- if (promptEditorArea) {
3139
- promptEditorArea.style.display = 'flex';
3140
- }
3141
- }
3142
-
3143
- // 处理登录页面的回车事件和按钮点击
3144
- function setupLoginEvents() {
3145
- const loginForm = document.getElementById('login');
3146
- const usernameInput = document.getElementById('username');
3147
- const passwordInput = document.getElementById('password');
3148
- const loginBtn = document.getElementById('loginBtn');
3149
-
3150
- // 自动聚焦到用户名输入框
3151
- if (usernameInput) {
3152
- usernameInput.focus();
3153
- }
3154
-
3155
- function handleLogin() {
3156
- const username = usernameInput.value.trim();
3157
- const password = passwordInput.value;
3158
- if (username && password) {
3159
- login(username, password);
3160
- }
3161
- }
3162
-
3163
- // 监听整个页面的回车事件
3164
- document.addEventListener('keydown', (e) => {
3165
- if (e.key === 'Enter' && loginForm.style.display !== 'none') {
3166
- e.preventDefault();
3167
- handleLogin();
3168
- }
3169
- });
3170
-
3171
- // 登录按钮点击事件
3172
- loginBtn.addEventListener('click', (e) => {
3173
- e.preventDefault();
3174
- handleLogin();
3175
- });
3176
-
3177
- // Tab 键切换焦点时的优化
3178
- usernameInput.addEventListener('keydown', (e) => {
3179
- if (e.key === 'Enter') {
3180
- e.preventDefault();
3181
- if (!usernameInput.value.trim()) {
3182
- usernameInput.focus();
3183
- } else {
3184
- passwordInput.focus();
3185
- }
3186
- }
3187
- });
3188
- }