@allbluecn/admin-app 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/package.json +16 -12
  2. package/src/components/admin/admin-user-menu.tsx +2 -4
  3. package/src/components/admin/developer-log.tsx +3 -5
  4. package/src/components/ai-memory/memory-facts-panel.tsx +383 -0
  5. package/src/components/ai-prompts/preview-panel.tsx +165 -0
  6. package/src/components/ai-prompts/prompt-editor.tsx +328 -0
  7. package/src/components/ai-prompts/prompt-table.tsx +141 -0
  8. package/src/components/ai-prompts/prompt-utils.ts +124 -0
  9. package/src/components/ai-prompts/section-editor.tsx +135 -0
  10. package/src/components/ai-prompts/variable-panel.tsx +128 -0
  11. package/src/components/ai-prompts/version-history.tsx +157 -0
  12. package/src/components/ai-provider/provider-form.tsx +6 -10
  13. package/src/components/ai-provider/provider-table.tsx +33 -13
  14. package/src/components/auth/admin-login-form.tsx +1 -1
  15. package/src/components/component/component-form-dialog.tsx +4 -4
  16. package/src/components/component/component-import-dialog.tsx +2 -2
  17. package/src/components/dependency/changelog-dialog.tsx +2 -4
  18. package/src/components/dependency/columns.tsx +4 -8
  19. package/src/components/dependency/dependency-table.tsx +2 -2
  20. package/src/components/dependency/edit-dialog.tsx +7 -11
  21. package/src/components/invitations/invitations-page.tsx +4 -8
  22. package/src/components/knowledge-public/bookmark-form-dialog.tsx +1 -1
  23. package/src/components/knowledge-public/compliance-form-dialog.tsx +1 -1
  24. package/src/components/knowledge-public/legal-form-dialog.tsx +2 -2
  25. package/src/components/layout/sidebar-02/app-sidebar.tsx +17 -9
  26. package/src/components/layout/sidebar-02/nav-main.tsx +6 -14
  27. package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -7
  28. package/src/components/magicui/rainbow-button.tsx +2 -2
  29. package/src/components/market/columns.tsx +2 -4
  30. package/src/components/market/market-app-form.tsx +2 -2
  31. package/src/components/market/market-app-list.tsx +1 -1
  32. package/src/components/oauth-providers/provider-page.tsx +1 -1
  33. package/src/components/ui/alert-dialog.tsx +28 -57
  34. package/src/components/ui/alert.tsx +3 -4
  35. package/src/components/ui/avatar.tsx +5 -28
  36. package/src/components/ui/badge.tsx +19 -34
  37. package/src/components/ui/button.tsx +14 -41
  38. package/src/components/ui/card.tsx +6 -24
  39. package/src/components/ui/checkbox.tsx +4 -26
  40. package/src/components/ui/collapsible.tsx +6 -37
  41. package/src/components/ui/dialog.tsx +30 -60
  42. package/src/components/ui/dropdown-menu.tsx +118 -137
  43. package/src/components/ui/field.tsx +8 -27
  44. package/src/components/ui/input-group.tsx +13 -26
  45. package/src/components/ui/input.tsx +4 -21
  46. package/src/components/ui/label.tsx +3 -27
  47. package/src/components/ui/pagination.tsx +17 -31
  48. package/src/components/ui/select.tsx +101 -76
  49. package/src/components/ui/separator.tsx +4 -27
  50. package/src/components/ui/sheet.tsx +29 -58
  51. package/src/components/ui/sidebar.tsx +119 -117
  52. package/src/components/ui/skeleton.tsx +1 -18
  53. package/src/components/ui/switch.tsx +5 -24
  54. package/src/components/ui/table.tsx +1 -19
  55. package/src/components/ui/textarea.tsx +2 -20
  56. package/src/components/ui/toast.tsx +282 -0
  57. package/src/components/ui/tooltip.tsx +34 -45
  58. package/src/components/user/settings-activity-log.tsx +1 -1
  59. package/src/components/user/user-create-dialog.tsx +2 -2
  60. package/src/components/user/user-filters.tsx +2 -2
  61. package/src/hooks/use-mobile.ts +0 -17
  62. package/src/lib/pacer.ts +21 -0
  63. package/src/lib/utils.ts +1 -23
  64. package/src/paraglide/README.md +1 -1
  65. package/src/paraglide/messages/_index.js +95 -0
  66. package/src/paraglide/messages/admin_ai_memory_action_clearuser1.js +30 -0
  67. package/src/paraglide/messages/admin_ai_memory_action_query.js +30 -0
  68. package/src/paraglide/messages/admin_ai_memory_clear_cancel.js +30 -0
  69. package/src/paraglide/messages/admin_ai_memory_clear_confirm_action.js +30 -0
  70. package/src/paraglide/messages/admin_ai_memory_clear_confirm_description.js +30 -0
  71. package/src/paraglide/messages/admin_ai_memory_clear_confirm_title.js +30 -0
  72. package/src/paraglide/messages/admin_ai_memory_columns_actions.js +30 -0
  73. package/src/paraglide/messages/admin_ai_memory_columns_content.js +30 -0
  74. package/src/paraglide/messages/admin_ai_memory_columns_createdat1.js +30 -0
  75. package/src/paraglide/messages/admin_ai_memory_columns_threadid1.js +30 -0
  76. package/src/paraglide/messages/admin_ai_memory_delete_cancel.js +30 -0
  77. package/src/paraglide/messages/admin_ai_memory_delete_confirm_action.js +30 -0
  78. package/src/paraglide/messages/admin_ai_memory_delete_confirm_description.js +30 -0
  79. package/src/paraglide/messages/admin_ai_memory_delete_confirm_title.js +30 -0
  80. package/src/paraglide/messages/admin_ai_memory_empty.js +30 -0
  81. package/src/paraglide/messages/admin_ai_memory_emptyhint1.js +30 -0
  82. package/src/paraglide/messages/admin_ai_memory_error_nouserid2.js +30 -0
  83. package/src/paraglide/messages/admin_ai_memory_error_query.js +30 -0
  84. package/src/paraglide/messages/admin_ai_memory_error_title.js +30 -0
  85. package/src/paraglide/messages/admin_ai_memory_form_user.js +30 -0
  86. package/src/paraglide/messages/admin_ai_memory_page_description.js +30 -0
  87. package/src/paraglide/messages/admin_ai_memory_page_title.js +30 -0
  88. package/src/paraglide/messages/admin_ai_memory_placeholder_user.js +30 -0
  89. package/src/paraglide/messages/admin_ai_memory_stat_count.js +30 -0
  90. package/src/paraglide/messages/admin_ai_memory_stat_lastwrite1.js +30 -0
  91. package/src/paraglide/messages/admin_ai_memory_toast_cleared.js +30 -0
  92. package/src/paraglide/messages/admin_ai_memory_toast_deleted.js +30 -0
  93. package/src/paraglide/messages/admin_ai_prompts_active.js +30 -0
  94. package/src/paraglide/messages/admin_ai_prompts_back.js +30 -0
  95. package/src/paraglide/messages/admin_ai_prompts_badge_protected.js +30 -0
  96. package/src/paraglide/messages/admin_ai_prompts_cache_hint.js +30 -0
  97. package/src/paraglide/messages/admin_ai_prompts_columns_active.js +30 -0
  98. package/src/paraglide/messages/admin_ai_prompts_columns_key.js +30 -0
  99. package/src/paraglide/messages/admin_ai_prompts_columns_name.js +30 -0
  100. package/src/paraglide/messages/admin_ai_prompts_columns_protected.js +30 -0
  101. package/src/paraglide/messages/admin_ai_prompts_columns_updatedat1.js +30 -0
  102. package/src/paraglide/messages/admin_ai_prompts_columns_version.js +30 -0
  103. package/src/paraglide/messages/admin_ai_prompts_description.js +30 -0
  104. package/src/paraglide/messages/admin_ai_prompts_description_placeholder.js +30 -0
  105. package/src/paraglide/messages/admin_ai_prompts_edit.js +30 -0
  106. package/src/paraglide/messages/admin_ai_prompts_name.js +30 -0
  107. package/src/paraglide/messages/admin_ai_prompts_name_placeholder.js +30 -0
  108. package/src/paraglide/messages/admin_ai_prompts_note_placeholder.js +30 -0
  109. package/src/paraglide/messages/admin_ai_prompts_page_description.js +30 -0
  110. package/src/paraglide/messages/admin_ai_prompts_page_title.js +30 -0
  111. package/src/paraglide/messages/admin_ai_prompts_preview_empty.js +30 -0
  112. package/src/paraglide/messages/admin_ai_prompts_preview_generating.js +30 -0
  113. package/src/paraglide/messages/admin_ai_prompts_preview_hint.js +30 -0
  114. package/src/paraglide/messages/admin_ai_prompts_preview_nocontent1.js +30 -0
  115. package/src/paraglide/messages/admin_ai_prompts_preview_placeholder.js +30 -0
  116. package/src/paraglide/messages/admin_ai_prompts_preview_section.js +30 -0
  117. package/src/paraglide/messages/admin_ai_prompts_preview_title.js +30 -0
  118. package/src/paraglide/messages/admin_ai_prompts_protected_hint.js +30 -0
  119. package/src/paraglide/messages/admin_ai_prompts_rollback.js +30 -0
  120. package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_action.js +30 -0
  121. package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_description.js +30 -0
  122. package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_title.js +30 -0
  123. package/src/paraglide/messages/admin_ai_prompts_save.js +30 -0
  124. package/src/paraglide/messages/admin_ai_prompts_section_add.js +30 -0
  125. package/src/paraglide/messages/admin_ai_prompts_section_id.js +30 -0
  126. package/src/paraglide/messages/admin_ai_prompts_section_movedown1.js +30 -0
  127. package/src/paraglide/messages/admin_ai_prompts_section_moveup1.js +30 -0
  128. package/src/paraglide/messages/admin_ai_prompts_section_order.js +30 -0
  129. package/src/paraglide/messages/admin_ai_prompts_section_remove.js +30 -0
  130. package/src/paraglide/messages/admin_ai_prompts_section_template_placeholder.js +30 -0
  131. package/src/paraglide/messages/admin_ai_prompts_sections_empty.js +30 -0
  132. package/src/paraglide/messages/admin_ai_prompts_sections_hint.js +30 -0
  133. package/src/paraglide/messages/admin_ai_prompts_sections_title.js +30 -0
  134. package/src/paraglide/messages/admin_ai_prompts_toast_activated.js +30 -0
  135. package/src/paraglide/messages/admin_ai_prompts_toast_failed.js +30 -0
  136. package/src/paraglide/messages/admin_ai_prompts_toast_protected_locked.js +30 -0
  137. package/src/paraglide/messages/admin_ai_prompts_toast_rolled_back.js +30 -0
  138. package/src/paraglide/messages/admin_ai_prompts_toast_save_failed.js +30 -0
  139. package/src/paraglide/messages/admin_ai_prompts_toast_saved.js +30 -0
  140. package/src/paraglide/messages/admin_ai_prompts_variable_add.js +30 -0
  141. package/src/paraglide/messages/admin_ai_prompts_variable_declarehint1.js +30 -0
  142. package/src/paraglide/messages/admin_ai_prompts_variable_description_placeholder.js +30 -0
  143. package/src/paraglide/messages/admin_ai_prompts_variable_name_placeholder.js +30 -0
  144. package/src/paraglide/messages/admin_ai_prompts_variable_remove.js +30 -0
  145. package/src/paraglide/messages/admin_ai_prompts_variable_undeclared.js +30 -0
  146. package/src/paraglide/messages/admin_ai_prompts_variables_empty.js +30 -0
  147. package/src/paraglide/messages/admin_ai_prompts_variables_title.js +30 -0
  148. package/src/paraglide/messages/admin_ai_prompts_versions_current.js +30 -0
  149. package/src/paraglide/messages/admin_ai_prompts_versions_empty.js +30 -0
  150. package/src/paraglide/messages/admin_ai_prompts_versions_hint.js +30 -0
  151. package/src/paraglide/messages/admin_ai_prompts_versions_nonote1.js +30 -0
  152. package/src/paraglide/messages/admin_ai_prompts_versions_title.js +30 -0
  153. package/src/paraglide/messages/admin_ai_prompts_versions_unknowncreator1.js +30 -0
  154. package/src/paraglide/messages/admin_ai_table_seedfromseed2.js +30 -0
  155. package/src/paraglide/messages/admin_ai_table_toast_seedcreated1.js +30 -0
  156. package/src/paraglide/messages/admin_ai_table_toast_seedfailed1.js +30 -0
  157. package/src/paraglide/messages/admin_ai_table_toast_seednonew2.js +30 -0
  158. package/src/paraglide/messages/common_monitoring_aiusage_sandbox1.js +30 -0
  159. package/src/paraglide/messages/common_monitoring_aiusage_sandboxvalue2.js +30 -0
  160. package/src/paraglide/messages/common_nav_aiprompts1.js +30 -0
  161. package/src/routeTree.gen.ts +101 -0
  162. package/src/routes/__root.tsx +2 -2
  163. package/src/{components/ui/sonner.tsx → routes/_nav/ai-memory/route.lazy.tsx} +24 -16
  164. package/src/routes/_nav/ai-memory/route.tsx +31 -0
  165. package/src/routes/_nav/ai-prompts/$key/route.lazy.tsx +32 -0
  166. package/src/routes/_nav/ai-prompts/$key/route.tsx +37 -0
  167. package/src/routes/_nav/ai-prompts/index/route.lazy.tsx +47 -0
  168. package/src/routes/_nav/ai-prompts/route.lazy.tsx +26 -0
  169. package/src/routes/_nav/ai-prompts/route.tsx +31 -0
  170. package/src/routes/_nav/compliance/route.lazy.tsx +2 -4
  171. package/src/routes/_nav/components/route.lazy.tsx +1 -1
  172. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +1 -1
  173. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +1 -1
  174. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +1 -1
  175. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +1 -1
  176. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +10 -1
  177. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +1 -1
  178. package/src/routes/_nav/monitoring/health/route.lazy.tsx +1 -1
  179. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +1 -1
  180. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +1 -1
  181. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +1 -1
  182. package/src/routes/_nav/permissions/route.lazy.tsx +2 -2
  183. package/src/routes/_nav/sso/route.lazy.tsx +1 -1
  184. package/src/routes/_nav/users/$userId/route.lazy.tsx +3 -3
  185. package/src/routes/setup/route.lazy.tsx +1 -1
  186. package/src/server/__tests__/prompt-defaults.test.ts +139 -0
  187. package/src/server/admin-ai-usage.ts +22 -4
  188. package/src/server/ai-memory/store.ts +103 -0
  189. package/src/server/serverFns/ai-memory/clear.ts +31 -0
  190. package/src/server/serverFns/ai-memory/delete.ts +31 -0
  191. package/src/server/serverFns/ai-memory/list-users.ts +40 -0
  192. package/src/server/serverFns/ai-memory/list.ts +54 -0
  193. package/src/server/serverFns/ai-prompts/activate.ts +53 -0
  194. package/src/server/serverFns/ai-prompts/get.ts +74 -0
  195. package/src/server/serverFns/ai-prompts/list.ts +55 -0
  196. package/src/server/serverFns/ai-prompts/preview.ts +66 -0
  197. package/src/server/serverFns/ai-prompts/prompt-defaults.ts +63 -0
  198. package/src/server/serverFns/ai-prompts/prompt-schemas.ts +47 -0
  199. package/src/server/serverFns/ai-prompts/rollback.ts +86 -0
  200. package/src/server/serverFns/ai-prompts/save.ts +129 -0
  201. package/src/server/serverFns/ai-provider/seed-from-seed.ts +64 -0
  202. package/src/styles/globals.css +92 -77
  203. package/src/components/layout/sidebar-02/logo.tsx +0 -40
  204. package/src/components/layout/sidebar-02/team-switcher.tsx +0 -106
  205. package/src/components/providers/theme-toggle.tsx +0 -59
@@ -0,0 +1,328 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // apps/admin/src/components/ai-prompts/prompt-editor.tsx
19
+ import { useCallback, useEffect, useState } from "react"
20
+ import type {
21
+ SystemPromptSectionDTO,
22
+ SystemPromptTemplateDTO,
23
+ } from "@allbluecn/kernel"
24
+ import { useMutation } from "@tanstack/react-query"
25
+ import { Link } from "@tanstack/react-router"
26
+ import { ArrowLeft, Loader2, Save, ShieldCheck } from "lucide-react"
27
+
28
+ import { m } from "@/paraglide/messages.js"
29
+ import { toast } from "@/components/ui/toast"
30
+ import { Badge } from "@/components/ui/badge"
31
+ import { Button } from "@/components/ui/button"
32
+ import { Field, FieldContent, FieldLabel } from "@/components/ui/field"
33
+ import { Input } from "@/components/ui/input"
34
+ import { Switch } from "@/components/ui/switch"
35
+ import { Textarea } from "@/components/ui/textarea"
36
+ import {
37
+ Tooltip,
38
+ TooltipContent,
39
+ TooltipTrigger,
40
+ } from "@/components/ui/tooltip"
41
+ import { activatePromptTemplateFn } from "@/server/serverFns/ai-prompts/activate"
42
+ import { getPromptTemplateFn } from "@/server/serverFns/ai-prompts/get"
43
+ import { rollbackPromptTemplateFn } from "@/server/serverFns/ai-prompts/rollback"
44
+ import { savePromptTemplateFn } from "@/server/serverFns/ai-prompts/save"
45
+ import { SectionEditor } from "./section-editor"
46
+ import { VariablePanel } from "./variable-panel"
47
+ import { PreviewPanel } from "./preview-panel"
48
+ import { VersionHistory } from "./version-history"
49
+ import {
50
+ extractUsedVariables,
51
+ renumberSections,
52
+ type PromptSectionDTO,
53
+ type PromptVariableDTO,
54
+ type PromptVersionSummary,
55
+ } from "./prompt-utils"
56
+
57
+ interface PromptEditorProps {
58
+ template: SystemPromptTemplateDTO
59
+ versions: PromptVersionSummary[]
60
+ }
61
+
62
+ /** 分区编辑态:在 DTO 基础上带「模板中已引用的变量名」,供变量面板展示说明 */
63
+ interface PromptSectionState extends PromptSectionDTO {
64
+ usedVariables?: string[]
65
+ }
66
+
67
+ function toSectionState(
68
+ sections: PromptSectionDTO[],
69
+ usedVariables: string[],
70
+ ): PromptSectionState[] {
71
+ return sections.map((s) => ({ ...s, usedVariables }))
72
+ }
73
+
74
+ export function PromptEditor({ template, versions: initialVersions }: PromptEditorProps) {
75
+ const [sections, setSections] = useState<PromptSectionState[]>(
76
+ toSectionState(template.sections, extractUsedVariables(template.sections)),
77
+ )
78
+ const [variables, setVariables] = useState<PromptVariableDTO[]>(template.variables ?? [])
79
+ const [name, setName] = useState(template.name)
80
+ const [description, setDescription] = useState(template.description ?? "")
81
+ const [note, setNote] = useState("")
82
+ const [expectedVersion, setExpectedVersion] = useState(template.version)
83
+ const [isActive, setIsActive] = useState(template.isActive)
84
+ const [versions, setVersions] = useState<PromptVersionSummary[]>(initialVersions)
85
+
86
+ useEffect(() => {
87
+ setSections(
88
+ toSectionState(template.sections, extractUsedVariables(template.sections)),
89
+ )
90
+ setVariables(template.variables ?? [])
91
+ setName(template.name)
92
+ setDescription(template.description ?? "")
93
+ setNote("")
94
+ setExpectedVersion(template.version)
95
+ setIsActive(template.isActive)
96
+ setVersions(initialVersions)
97
+ }, [template, initialVersions])
98
+
99
+ const applyTemplate = useCallback(
100
+ (tpl: SystemPromptTemplateDTO, list: PromptVersionSummary[]) => {
101
+ setSections(
102
+ toSectionState(tpl.sections, extractUsedVariables(tpl.sections)),
103
+ )
104
+ setVariables(tpl.variables ?? [])
105
+ setName(tpl.name)
106
+ setDescription(tpl.description ?? "")
107
+ setExpectedVersion(tpl.version)
108
+ setIsActive(tpl.isActive)
109
+ setVersions(list)
110
+ },
111
+ [],
112
+ )
113
+
114
+ const refresh = useCallback(async () => {
115
+ const res = await getPromptTemplateFn({ data: { key: template.key } })
116
+ applyTemplate(res.template, res.versions)
117
+ }, [applyTemplate, template.key])
118
+
119
+ const usedVariables = extractUsedVariables(sections)
120
+
121
+ const saveMutation = useMutation({
122
+ mutationFn: () =>
123
+ savePromptTemplateFn({
124
+ data: {
125
+ key: template.key,
126
+ expectedVersion,
127
+ name: name.trim(),
128
+ description: description.trim() ? description : null,
129
+ sections: renumberSections(
130
+ sections.map(({ usedVariables: _used, ...rest }) => rest),
131
+ ),
132
+ variables: variables.filter((v) => v.name.trim() !== ""),
133
+ note: note.trim() || undefined,
134
+ },
135
+ }),
136
+ onSuccess: async () => {
137
+ toast.success(m["admin-ai_prompts_toast_saved"]())
138
+ setNote("")
139
+ await refresh()
140
+ },
141
+ onError: (err) =>
142
+ toast.error(
143
+ err instanceof Error ? err.message : m["admin-ai_prompts_toast_save_failed"](),
144
+ ),
145
+ })
146
+
147
+ const activateMutation = useMutation({
148
+ mutationFn: (next: boolean) =>
149
+ activatePromptTemplateFn({ data: { key: template.key, isActive: next } }),
150
+ onSuccess: (res) => setIsActive(res.isActive),
151
+ onError: (err) =>
152
+ toast.error(
153
+ err instanceof Error ? err.message : m["admin-ai_prompts_toast_failed"](),
154
+ ),
155
+ })
156
+
157
+ const rollbackMutation = useMutation({
158
+ mutationFn: (version: number) =>
159
+ rollbackPromptTemplateFn({
160
+ data: { key: template.key, version, expectedVersion },
161
+ }),
162
+ onSuccess: async () => {
163
+ toast.success(m["admin-ai_prompts_toast_rolled_back"]({ version: rollbackMutation.variables ?? 0 }))
164
+ setNote("")
165
+ await refresh()
166
+ },
167
+ onError: (err) =>
168
+ toast.error(
169
+ err instanceof Error ? err.message : m["admin-ai_prompts_toast_failed"](),
170
+ ),
171
+ })
172
+
173
+ const handleActivate = (checked: boolean) => {
174
+ if (template.isProtected && !checked) {
175
+ toast.error(m["admin-ai_prompts_toast_protected_locked"]())
176
+ return
177
+ }
178
+ activateMutation.mutate(checked)
179
+ }
180
+
181
+ const switchControl = (
182
+ <Switch
183
+ size="sm"
184
+ checked={isActive}
185
+ onCheckedChange={handleActivate}
186
+ disabled={activateMutation.isPending}
187
+ />
188
+ )
189
+
190
+ return (
191
+ <div className="space-y-5">
192
+ <div className="flex flex-wrap items-center gap-3">
193
+ <Button render={<Link to="/ai-prompts" aria-label={m["admin-ai_prompts_back"]()} />} variant="ghost" size="icon-sm" nativeButton={false}>
194
+ <ArrowLeft />
195
+ </Button>
196
+ <div className="min-w-0">
197
+ <div className="flex flex-wrap items-center gap-2">
198
+ <h1 className="text-lg font-semibold truncate">{template.name}</h1>
199
+ <Badge variant="outline" className="font-mono">
200
+ v{expectedVersion}
201
+ </Badge>
202
+ {template.isProtected && (
203
+ <Badge variant="secondary" className="gap-1">
204
+ <ShieldCheck className="size-3.5" />
205
+ {m["admin-ai_prompts_badge_protected"]()}
206
+ </Badge>
207
+ )}
208
+ </div>
209
+ <p className="font-mono text-xs text-muted-foreground">{template.key}</p>
210
+ </div>
211
+
212
+ <div className="ml-auto flex items-center gap-2">
213
+ {template.isProtected ? (
214
+ <Tooltip>
215
+ <TooltipTrigger render={<span className="inline-flex" />}>{switchControl}</TooltipTrigger>
216
+ <TooltipContent>{m["admin-ai_prompts_protected_hint"]()}</TooltipContent>
217
+ </Tooltip>
218
+ ) : (
219
+ switchControl
220
+ )}
221
+ <span className="text-sm text-muted-foreground">
222
+ {m["admin-ai_prompts_active"]()}
223
+ </span>
224
+ </div>
225
+ </div>
226
+
227
+ <div className="grid grid-cols-1 gap-5 xl:grid-cols-2">
228
+ <div className="space-y-5">
229
+ <div className="space-y-3">
230
+ <Field>
231
+ <FieldContent>
232
+ <FieldLabel>{m["admin-ai_prompts_name"]()}</FieldLabel>
233
+ <Input
234
+ value={name}
235
+ placeholder={m["admin-ai_prompts_name_placeholder"]()}
236
+ onChange={(e) => setName(e.target.value)}
237
+ />
238
+ </FieldContent>
239
+ </Field>
240
+ <Field>
241
+ <FieldContent>
242
+ <FieldLabel>{m["admin-ai_prompts_description"]()}</FieldLabel>
243
+ <Textarea
244
+ className="min-h-16 resize-y"
245
+ value={description}
246
+ placeholder={m["admin-ai_prompts_description_placeholder"]()}
247
+ onChange={(e) => setDescription(e.target.value)}
248
+ />
249
+ </FieldContent>
250
+ </Field>
251
+ </div>
252
+
253
+ <div className="space-y-2">
254
+ <div className="flex items-center justify-between">
255
+ <h2 className="text-sm font-medium">
256
+ {m["admin-ai_prompts_sections_title"]()}
257
+ </h2>
258
+ <span className="text-xs text-muted-foreground">
259
+ {m["admin-ai_prompts_sections_hint"]()}
260
+ </span>
261
+ </div>
262
+ <SectionEditor sections={sections} onChange={setSections} />
263
+ </div>
264
+
265
+ <div className="space-y-2">
266
+ <h2 className="text-sm font-medium">
267
+ {m["admin-ai_prompts_variables_title"]()}
268
+ </h2>
269
+ <VariablePanel
270
+ variables={variables}
271
+ onChange={setVariables}
272
+ usedVariables={usedVariables}
273
+ />
274
+ </div>
275
+ </div>
276
+
277
+ <div className="space-y-2">
278
+ <h2 className="text-sm font-medium">
279
+ {m["admin-ai_prompts_preview_title"]()}
280
+ </h2>
281
+ <p className="text-xs text-muted-foreground">
282
+ {m["admin-ai_prompts_preview_hint"]()}
283
+ </p>
284
+ <PreviewPanel
285
+ key={template.key}
286
+ sections={sections}
287
+ variables={variables}
288
+ />
289
+ </div>
290
+ </div>
291
+
292
+ <div className="space-y-2">
293
+ <VersionHistory
294
+ versions={versions}
295
+ currentVersion={expectedVersion}
296
+ rollbackPending={rollbackMutation.isPending}
297
+ onRollback={(version) => rollbackMutation.mutate(version)}
298
+ />
299
+ </div>
300
+
301
+ <div className="sticky bottom-0 flex flex-wrap items-center gap-3 rounded-md border border-border bg-background p-3 shadow-sm">
302
+ <div className="flex-1 min-w-48">
303
+ <Input
304
+ value={note}
305
+ maxLength={200}
306
+ placeholder={m["admin-ai_prompts_note_placeholder"]()}
307
+ onChange={(e) => setNote(e.target.value)}
308
+ />
309
+ </div>
310
+ <span className="text-xs text-muted-foreground">
311
+ {m["admin-ai_prompts_cache_hint"]()}
312
+ </span>
313
+ <Button
314
+ type="button"
315
+ disabled={saveMutation.isPending || !name.trim()}
316
+ onClick={() => saveMutation.mutate()}
317
+ >
318
+ {saveMutation.isPending ? (
319
+ <Loader2 className="animate-spin" />
320
+ ) : (
321
+ <Save />
322
+ )}
323
+ {m["admin-ai_prompts_save"]()}
324
+ </Button>
325
+ </div>
326
+ </div>
327
+ )
328
+ }
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // apps/admin/src/components/ai-prompts/prompt-table.tsx
19
+ import { useMutation } from "@tanstack/react-query"
20
+ import { Link, useRouter } from "@tanstack/react-router"
21
+ import { Edit, ShieldCheck } from "lucide-react"
22
+ import { formatTimestamp } from "@allbluecn/web-app/lib/datetime"
23
+
24
+ import { m } from "@/paraglide/messages.js"
25
+ import { toast } from "@/components/ui/toast"
26
+ import { Badge } from "@/components/ui/badge"
27
+ import { Button } from "@/components/ui/button"
28
+ import { Switch } from "@/components/ui/switch"
29
+ import { DataTable, type AdminColumnDef } from "@/components/data-table/data-table"
30
+ import { activatePromptTemplateFn } from "@/server/serverFns/ai-prompts/activate"
31
+ import type { PromptTemplateRow } from "./prompt-utils"
32
+
33
+ export function PromptTable({ data }: { data: PromptTemplateRow[] }) {
34
+ const router = useRouter()
35
+
36
+ const toggleMutation = useMutation({
37
+ mutationFn: (vars: { key: string; isActive: boolean }) =>
38
+ activatePromptTemplateFn({ data: vars }),
39
+ onSuccess: async () => {
40
+ toast.success(m["admin-ai_prompts_toast_activated"]())
41
+ await router.invalidate()
42
+ },
43
+ onError: (err) =>
44
+ toast.error(
45
+ err instanceof Error ? err.message : m["admin-ai_prompts_toast_failed"](),
46
+ ),
47
+ })
48
+
49
+ const columns: AdminColumnDef<PromptTemplateRow>[] = [
50
+ {
51
+ accessorKey: "key",
52
+ header: m["admin-ai_prompts_columns_key"](),
53
+ meta: { style: { width: "15%" } },
54
+ cell: ({ row }: any) => (
55
+ <span className="font-mono text-sm">{row.original.key}</span>
56
+ ),
57
+ },
58
+ {
59
+ accessorKey: "name",
60
+ header: m["admin-ai_prompts_columns_name"](),
61
+ cell: ({ row }: any) => (
62
+ <span className="font-medium">{row.original.name}</span>
63
+ ),
64
+ },
65
+ {
66
+ accessorKey: "version",
67
+ header: m["admin-ai_prompts_columns_version"](),
68
+ meta: { style: { width: "7rem" } },
69
+ cell: ({ row }: any) => (
70
+ <Badge variant="outline" className="font-mono">
71
+ v{row.original.version}
72
+ </Badge>
73
+ ),
74
+ },
75
+ {
76
+ accessorKey: "isActive",
77
+ header: m["admin-ai_prompts_columns_active"](),
78
+ meta: { style: { width: "6rem" } },
79
+ cell: ({ row }: any) => (
80
+ <div onClick={(e) => e.stopPropagation()}>
81
+ <Switch
82
+ checked={row.original.isActive}
83
+ onCheckedChange={(checked: boolean) =>
84
+ toggleMutation.mutate({
85
+ key: row.original.key,
86
+ isActive: checked,
87
+ })
88
+ }
89
+ disabled={toggleMutation.isPending}
90
+ />
91
+ </div>
92
+ ),
93
+ },
94
+ {
95
+ accessorKey: "isProtected",
96
+ header: m["admin-ai_prompts_columns_protected"](),
97
+ meta: { style: { width: "6rem" } },
98
+ cell: ({ row }: any) =>
99
+ row.original.isProtected ? (
100
+ <Badge variant="secondary" className="gap-1">
101
+ <ShieldCheck className="size-3.5" />
102
+ {m["admin-ai_prompts_badge_protected"]()}
103
+ </Badge>
104
+ ) : (
105
+ <span className="text-muted-foreground">—</span>
106
+ ),
107
+ },
108
+ {
109
+ accessorKey: "updatedAt",
110
+ header: m["admin-ai_prompts_columns_updatedAt"](),
111
+ meta: { style: { width: "9rem" } },
112
+ cell: ({ row }: any) => (
113
+ <span className="text-sm text-muted-foreground">
114
+ {formatTimestamp(row.original.updatedAt)}
115
+ </span>
116
+ ),
117
+ },
118
+ {
119
+ accessorKey: "id",
120
+ header: () => null,
121
+ meta: { style: { width: "4rem" } },
122
+ cell: ({ row }: any) => (
123
+ <div onClick={(e) => e.stopPropagation()}>
124
+ <Button render={<Link to="/ai-prompts/$key" params={{ key: row.original.key }} />} variant="ghost" size="icon-xs" aria-label={m["admin-ai_prompts_edit"]({ key: row.original.key })} nativeButton={false}>
125
+ <Edit />
126
+ </Button>
127
+ </div>
128
+ ),
129
+ },
130
+ ]
131
+
132
+ return (
133
+ <DataTable
134
+ data={data}
135
+ columns={columns}
136
+ onRowClick={(row) =>
137
+ router.navigate({ to: "/ai-prompts/$key", params: { key: row.key } })
138
+ }
139
+ />
140
+ )
141
+ }
@@ -0,0 +1,124 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // apps/admin/src/components/ai-prompts/prompt-utils.ts
19
+ import type {
20
+ PromptVariableDefDTO,
21
+ SystemPromptSectionDTO,
22
+ SystemPromptTemplateDTO,
23
+ } from "@allbluecn/kernel"
24
+
25
+ export type PromptSectionDTO = SystemPromptSectionDTO
26
+ export type PromptVariableDTO = PromptVariableDefDTO
27
+
28
+ export interface PromptTemplateRow {
29
+ id: string
30
+ key: string
31
+ name: string
32
+ version: number
33
+ isActive: boolean
34
+ isProtected: boolean
35
+ updatedAt: string
36
+ }
37
+
38
+ export interface PromptVersionSummary {
39
+ id: string
40
+ version: number
41
+ note: string | null
42
+ createdBy: string | null
43
+ createdAt: string
44
+ }
45
+
46
+ export interface PromptEditorData {
47
+ template: SystemPromptTemplateDTO
48
+ versions: PromptVersionSummary[]
49
+ }
50
+
51
+ export interface PreviewSectionResult {
52
+ id: string
53
+ order: number
54
+ text: string
55
+ }
56
+
57
+ export type PreviewResult =
58
+ | { ok: true; sections: PreviewSectionResult[] }
59
+ | { ok: false; error: string }
60
+
61
+ const TEMPLATE_VAR_RE = /\{\{\s*([^{}]+?)\s*\}\}/g
62
+ const VALID_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
63
+
64
+ /** 从分区模板提取已引用的变量名(与 kernel interpolateTemplate 的校验规则一致) */
65
+ export function extractUsedVariables(
66
+ sections: PromptSectionDTO[],
67
+ ): string[] {
68
+ const used = new Set<string>()
69
+ for (const section of sections) {
70
+ for (const match of section.template.matchAll(TEMPLATE_VAR_RE)) {
71
+ const name = match[1].trim()
72
+ if (VALID_VAR_NAME_RE.test(name)) used.add(name)
73
+ }
74
+ }
75
+ return [...used]
76
+ }
77
+
78
+ /** 新增分区:id 不可改(按 id 过滤依赖它),order 顺延到末尾 */
79
+ export function appendSection(sections: PromptSectionDTO[]): PromptSectionDTO[] {
80
+ const nextOrder = sections.reduce((max, s) => Math.max(max, s.order), 0) + 1
81
+ return [...sections, { id: `sec_${Date.now()}`, order: nextOrder, template: "" }]
82
+ }
83
+
84
+ /** 上下移动:交换数组位置后整体重排 order,保证 order 与展示顺序一致 */
85
+ export function moveSection(
86
+ sections: PromptSectionDTO[],
87
+ index: number,
88
+ delta: -1 | 1,
89
+ ): PromptSectionDTO[] {
90
+ const target = index + delta
91
+ if (target < 0 || target >= sections.length) return sections
92
+ const next = sections.slice()
93
+ ;[next[index], next[target]] = [next[target], next[index]]
94
+ return renumberSections(next)
95
+ }
96
+
97
+ export function removeSection(
98
+ sections: PromptSectionDTO[],
99
+ index: number,
100
+ ): PromptSectionDTO[] {
101
+ return renumberSections(sections.filter((_, i) => i !== index))
102
+ }
103
+
104
+ export function renumberSections(
105
+ sections: PromptSectionDTO[],
106
+ ): PromptSectionDTO[] {
107
+ return sections
108
+ .slice()
109
+ .sort((a, b) => a.order - b.order)
110
+ .map((s, i) => ({ ...s, order: i + 1 }))
111
+ }
112
+
113
+ export function addVariable(
114
+ variables: PromptVariableDTO[],
115
+ ): PromptVariableDTO[] {
116
+ return [...variables, { name: "", description: "" }]
117
+ }
118
+
119
+ export function removeVariable(
120
+ variables: PromptVariableDTO[],
121
+ index: number,
122
+ ): PromptVariableDTO[] {
123
+ return variables.filter((_, i) => i !== index)
124
+ }