@allbluecn/admin-app 0.2.6 → 0.3.0

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 (204) 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/messages/_index.js +95 -0
  65. package/src/paraglide/messages/admin_ai_memory_action_clearuser1.js +30 -0
  66. package/src/paraglide/messages/admin_ai_memory_action_query.js +30 -0
  67. package/src/paraglide/messages/admin_ai_memory_clear_cancel.js +30 -0
  68. package/src/paraglide/messages/admin_ai_memory_clear_confirm_action.js +30 -0
  69. package/src/paraglide/messages/admin_ai_memory_clear_confirm_description.js +30 -0
  70. package/src/paraglide/messages/admin_ai_memory_clear_confirm_title.js +30 -0
  71. package/src/paraglide/messages/admin_ai_memory_columns_actions.js +30 -0
  72. package/src/paraglide/messages/admin_ai_memory_columns_content.js +30 -0
  73. package/src/paraglide/messages/admin_ai_memory_columns_createdat1.js +30 -0
  74. package/src/paraglide/messages/admin_ai_memory_columns_threadid1.js +30 -0
  75. package/src/paraglide/messages/admin_ai_memory_delete_cancel.js +30 -0
  76. package/src/paraglide/messages/admin_ai_memory_delete_confirm_action.js +30 -0
  77. package/src/paraglide/messages/admin_ai_memory_delete_confirm_description.js +30 -0
  78. package/src/paraglide/messages/admin_ai_memory_delete_confirm_title.js +30 -0
  79. package/src/paraglide/messages/admin_ai_memory_empty.js +30 -0
  80. package/src/paraglide/messages/admin_ai_memory_emptyhint1.js +30 -0
  81. package/src/paraglide/messages/admin_ai_memory_error_nouserid2.js +30 -0
  82. package/src/paraglide/messages/admin_ai_memory_error_query.js +30 -0
  83. package/src/paraglide/messages/admin_ai_memory_error_title.js +30 -0
  84. package/src/paraglide/messages/admin_ai_memory_form_user.js +30 -0
  85. package/src/paraglide/messages/admin_ai_memory_page_description.js +30 -0
  86. package/src/paraglide/messages/admin_ai_memory_page_title.js +30 -0
  87. package/src/paraglide/messages/admin_ai_memory_placeholder_user.js +30 -0
  88. package/src/paraglide/messages/admin_ai_memory_stat_count.js +30 -0
  89. package/src/paraglide/messages/admin_ai_memory_stat_lastwrite1.js +30 -0
  90. package/src/paraglide/messages/admin_ai_memory_toast_cleared.js +30 -0
  91. package/src/paraglide/messages/admin_ai_memory_toast_deleted.js +30 -0
  92. package/src/paraglide/messages/admin_ai_prompts_active.js +30 -0
  93. package/src/paraglide/messages/admin_ai_prompts_back.js +30 -0
  94. package/src/paraglide/messages/admin_ai_prompts_badge_protected.js +30 -0
  95. package/src/paraglide/messages/admin_ai_prompts_cache_hint.js +30 -0
  96. package/src/paraglide/messages/admin_ai_prompts_columns_active.js +30 -0
  97. package/src/paraglide/messages/admin_ai_prompts_columns_key.js +30 -0
  98. package/src/paraglide/messages/admin_ai_prompts_columns_name.js +30 -0
  99. package/src/paraglide/messages/admin_ai_prompts_columns_protected.js +30 -0
  100. package/src/paraglide/messages/admin_ai_prompts_columns_updatedat1.js +30 -0
  101. package/src/paraglide/messages/admin_ai_prompts_columns_version.js +30 -0
  102. package/src/paraglide/messages/admin_ai_prompts_description.js +30 -0
  103. package/src/paraglide/messages/admin_ai_prompts_description_placeholder.js +30 -0
  104. package/src/paraglide/messages/admin_ai_prompts_edit.js +30 -0
  105. package/src/paraglide/messages/admin_ai_prompts_name.js +30 -0
  106. package/src/paraglide/messages/admin_ai_prompts_name_placeholder.js +30 -0
  107. package/src/paraglide/messages/admin_ai_prompts_note_placeholder.js +30 -0
  108. package/src/paraglide/messages/admin_ai_prompts_page_description.js +30 -0
  109. package/src/paraglide/messages/admin_ai_prompts_page_title.js +30 -0
  110. package/src/paraglide/messages/admin_ai_prompts_preview_empty.js +30 -0
  111. package/src/paraglide/messages/admin_ai_prompts_preview_generating.js +30 -0
  112. package/src/paraglide/messages/admin_ai_prompts_preview_hint.js +30 -0
  113. package/src/paraglide/messages/admin_ai_prompts_preview_nocontent1.js +30 -0
  114. package/src/paraglide/messages/admin_ai_prompts_preview_placeholder.js +30 -0
  115. package/src/paraglide/messages/admin_ai_prompts_preview_section.js +30 -0
  116. package/src/paraglide/messages/admin_ai_prompts_preview_title.js +30 -0
  117. package/src/paraglide/messages/admin_ai_prompts_protected_hint.js +30 -0
  118. package/src/paraglide/messages/admin_ai_prompts_rollback.js +30 -0
  119. package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_action.js +30 -0
  120. package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_description.js +30 -0
  121. package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_title.js +30 -0
  122. package/src/paraglide/messages/admin_ai_prompts_save.js +30 -0
  123. package/src/paraglide/messages/admin_ai_prompts_section_add.js +30 -0
  124. package/src/paraglide/messages/admin_ai_prompts_section_id.js +30 -0
  125. package/src/paraglide/messages/admin_ai_prompts_section_movedown1.js +30 -0
  126. package/src/paraglide/messages/admin_ai_prompts_section_moveup1.js +30 -0
  127. package/src/paraglide/messages/admin_ai_prompts_section_order.js +30 -0
  128. package/src/paraglide/messages/admin_ai_prompts_section_remove.js +30 -0
  129. package/src/paraglide/messages/admin_ai_prompts_section_template_placeholder.js +30 -0
  130. package/src/paraglide/messages/admin_ai_prompts_sections_empty.js +30 -0
  131. package/src/paraglide/messages/admin_ai_prompts_sections_hint.js +30 -0
  132. package/src/paraglide/messages/admin_ai_prompts_sections_title.js +30 -0
  133. package/src/paraglide/messages/admin_ai_prompts_toast_activated.js +30 -0
  134. package/src/paraglide/messages/admin_ai_prompts_toast_failed.js +30 -0
  135. package/src/paraglide/messages/admin_ai_prompts_toast_protected_locked.js +30 -0
  136. package/src/paraglide/messages/admin_ai_prompts_toast_rolled_back.js +30 -0
  137. package/src/paraglide/messages/admin_ai_prompts_toast_save_failed.js +30 -0
  138. package/src/paraglide/messages/admin_ai_prompts_toast_saved.js +30 -0
  139. package/src/paraglide/messages/admin_ai_prompts_variable_add.js +30 -0
  140. package/src/paraglide/messages/admin_ai_prompts_variable_declarehint1.js +30 -0
  141. package/src/paraglide/messages/admin_ai_prompts_variable_description_placeholder.js +30 -0
  142. package/src/paraglide/messages/admin_ai_prompts_variable_name_placeholder.js +30 -0
  143. package/src/paraglide/messages/admin_ai_prompts_variable_remove.js +30 -0
  144. package/src/paraglide/messages/admin_ai_prompts_variable_undeclared.js +30 -0
  145. package/src/paraglide/messages/admin_ai_prompts_variables_empty.js +30 -0
  146. package/src/paraglide/messages/admin_ai_prompts_variables_title.js +30 -0
  147. package/src/paraglide/messages/admin_ai_prompts_versions_current.js +30 -0
  148. package/src/paraglide/messages/admin_ai_prompts_versions_empty.js +30 -0
  149. package/src/paraglide/messages/admin_ai_prompts_versions_hint.js +30 -0
  150. package/src/paraglide/messages/admin_ai_prompts_versions_nonote1.js +30 -0
  151. package/src/paraglide/messages/admin_ai_prompts_versions_title.js +30 -0
  152. package/src/paraglide/messages/admin_ai_prompts_versions_unknowncreator1.js +30 -0
  153. package/src/paraglide/messages/admin_ai_table_seedfromseed2.js +30 -0
  154. package/src/paraglide/messages/admin_ai_table_toast_seedcreated1.js +30 -0
  155. package/src/paraglide/messages/admin_ai_table_toast_seedfailed1.js +30 -0
  156. package/src/paraglide/messages/admin_ai_table_toast_seednonew2.js +30 -0
  157. package/src/paraglide/messages/common_monitoring_aiusage_sandbox1.js +30 -0
  158. package/src/paraglide/messages/common_monitoring_aiusage_sandboxvalue2.js +30 -0
  159. package/src/paraglide/messages/common_nav_aiprompts1.js +30 -0
  160. package/src/routeTree.gen.ts +101 -0
  161. package/src/routes/__root.tsx +2 -2
  162. package/src/{components/ui/sonner.tsx → routes/_nav/ai-memory/route.lazy.tsx} +24 -16
  163. package/src/routes/_nav/ai-memory/route.tsx +31 -0
  164. package/src/routes/_nav/ai-prompts/$key/route.lazy.tsx +32 -0
  165. package/src/routes/_nav/ai-prompts/$key/route.tsx +37 -0
  166. package/src/routes/_nav/ai-prompts/index/route.lazy.tsx +47 -0
  167. package/src/routes/_nav/ai-prompts/route.lazy.tsx +26 -0
  168. package/src/routes/_nav/ai-prompts/route.tsx +31 -0
  169. package/src/routes/_nav/compliance/route.lazy.tsx +2 -4
  170. package/src/routes/_nav/components/route.lazy.tsx +1 -1
  171. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +1 -1
  172. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +1 -1
  173. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +1 -1
  174. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +1 -1
  175. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +10 -1
  176. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +1 -1
  177. package/src/routes/_nav/monitoring/health/route.lazy.tsx +1 -1
  178. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +1 -1
  179. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +1 -1
  180. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +1 -1
  181. package/src/routes/_nav/permissions/route.lazy.tsx +2 -2
  182. package/src/routes/_nav/sso/route.lazy.tsx +1 -1
  183. package/src/routes/_nav/users/$userId/route.lazy.tsx +3 -3
  184. package/src/routes/setup/route.lazy.tsx +1 -1
  185. package/src/server/__tests__/prompt-defaults.test.ts +139 -0
  186. package/src/server/admin-ai-usage.ts +22 -4
  187. package/src/server/ai-memory/store.ts +103 -0
  188. package/src/server/serverFns/ai-memory/clear.ts +31 -0
  189. package/src/server/serverFns/ai-memory/delete.ts +31 -0
  190. package/src/server/serverFns/ai-memory/list-users.ts +40 -0
  191. package/src/server/serverFns/ai-memory/list.ts +54 -0
  192. package/src/server/serverFns/ai-prompts/activate.ts +53 -0
  193. package/src/server/serverFns/ai-prompts/get.ts +74 -0
  194. package/src/server/serverFns/ai-prompts/list.ts +55 -0
  195. package/src/server/serverFns/ai-prompts/preview.ts +66 -0
  196. package/src/server/serverFns/ai-prompts/prompt-defaults.ts +63 -0
  197. package/src/server/serverFns/ai-prompts/prompt-schemas.ts +47 -0
  198. package/src/server/serverFns/ai-prompts/rollback.ts +86 -0
  199. package/src/server/serverFns/ai-prompts/save.ts +129 -0
  200. package/src/server/serverFns/ai-provider/seed-from-seed.ts +64 -0
  201. package/src/styles/globals.css +92 -77
  202. package/src/components/layout/sidebar-02/logo.tsx +0 -40
  203. package/src/components/layout/sidebar-02/team-switcher.tsx +0 -106
  204. package/src/components/providers/theme-toggle.tsx +0 -59
@@ -0,0 +1,135 @@
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/section-editor.tsx
19
+ import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react"
20
+
21
+ import { m } from "@/paraglide/messages.js"
22
+ import { Button } from "@/components/ui/button"
23
+ import { Input } from "@/components/ui/input"
24
+ import { Textarea } from "@/components/ui/textarea"
25
+ import {
26
+ appendSection,
27
+ moveSection,
28
+ removeSection,
29
+ type PromptSectionDTO,
30
+ } from "./prompt-utils"
31
+
32
+ interface SectionEditorProps {
33
+ sections: PromptSectionDTO[]
34
+ onChange: (sections: PromptSectionDTO[]) => void
35
+ }
36
+
37
+ export function SectionEditor({ sections, onChange }: SectionEditorProps) {
38
+ return (
39
+ <div className="space-y-3">
40
+ {sections.length === 0 && (
41
+ <p className="text-sm text-muted-foreground">
42
+ {m["admin-ai_prompts_sections_empty"]()}
43
+ </p>
44
+ )}
45
+
46
+ {sections.map((section, index) => (
47
+ <div
48
+ key={section.id}
49
+ className="rounded-md border border-border p-3 space-y-3"
50
+ >
51
+ <div className="flex items-center gap-2">
52
+ <span className="text-sm font-medium shrink-0">
53
+ {m["admin-ai_prompts_section_id"]()}
54
+ </span>
55
+ <span className="font-mono text-xs text-muted-foreground truncate">
56
+ {section.id}
57
+ </span>
58
+ <span className="flex-1" />
59
+ <Button
60
+ type="button"
61
+ variant="ghost"
62
+ size="icon-xs"
63
+ className="shrink-0"
64
+ disabled={index === 0}
65
+ onClick={() => onChange(moveSection(sections, index, -1))}
66
+ aria-label={m["admin-ai_prompts_section_moveUp"]()}
67
+ >
68
+ <ArrowUp />
69
+ </Button>
70
+ <Button
71
+ type="button"
72
+ variant="ghost"
73
+ size="icon-xs"
74
+ className="shrink-0"
75
+ disabled={index === sections.length - 1}
76
+ onClick={() => onChange(moveSection(sections, index, 1))}
77
+ aria-label={m["admin-ai_prompts_section_moveDown"]()}
78
+ >
79
+ <ArrowDown />
80
+ </Button>
81
+ <Button
82
+ type="button"
83
+ variant="ghost"
84
+ size="icon-xs"
85
+ className="shrink-0 text-destructive hover:text-destructive"
86
+ disabled={sections.length === 1}
87
+ onClick={() => onChange(removeSection(sections, index))}
88
+ aria-label={m["admin-ai_prompts_section_remove"]()}
89
+ >
90
+ <Trash2 />
91
+ </Button>
92
+ </div>
93
+
94
+ <div className="flex items-center gap-3">
95
+ <div className="flex items-center gap-2 shrink-0">
96
+ <span className="text-sm text-muted-foreground">
97
+ {m["admin-ai_prompts_section_order"]()}
98
+ </span>
99
+ <Input
100
+ type="number"
101
+ min={1}
102
+ className="w-16 h-7"
103
+ value={section.order}
104
+ readOnly
105
+ tabIndex={-1}
106
+ />
107
+ </div>
108
+ </div>
109
+
110
+ <Textarea
111
+ className="font-mono text-sm min-h-24 resize-y"
112
+ value={section.template}
113
+ placeholder={m["admin-ai_prompts_section_template_placeholder"]({ placeholder: "{{name}}" })}
114
+ onChange={(e) => {
115
+ const next = sections.slice()
116
+ next[index] = { ...section, template: e.target.value }
117
+ onChange(next)
118
+ }}
119
+ />
120
+ </div>
121
+ ))}
122
+
123
+ <Button
124
+ type="button"
125
+ variant="outline"
126
+ size="sm"
127
+ className="w-full"
128
+ onClick={() => onChange(appendSection(sections))}
129
+ >
130
+ <Plus />
131
+ {m["admin-ai_prompts_section_add"]()}
132
+ </Button>
133
+ </div>
134
+ )
135
+ }
@@ -0,0 +1,128 @@
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/variable-panel.tsx
19
+ import { AlertTriangle, Plus, Trash2 } from "lucide-react"
20
+
21
+ import { m } from "@/paraglide/messages.js"
22
+ import { Button } from "@/components/ui/button"
23
+ import { Input } from "@/components/ui/input"
24
+ import {
25
+ addVariable,
26
+ removeVariable,
27
+ type PromptVariableDTO,
28
+ } from "./prompt-utils"
29
+
30
+ interface VariablePanelProps {
31
+ variables: PromptVariableDTO[]
32
+ onChange: (variables: PromptVariableDTO[]) => void
33
+ usedVariables: string[]
34
+ }
35
+
36
+ export function VariablePanel({
37
+ variables,
38
+ onChange,
39
+ usedVariables,
40
+ }: VariablePanelProps) {
41
+ const declared = new Set(variables.map((v) => v.name).filter(Boolean))
42
+ const undeclared = usedVariables.filter((name) => !declared.has(name))
43
+
44
+ return (
45
+ <div className="space-y-3">
46
+ {variables.length === 0 && undeclared.length === 0 && (
47
+ <p className="text-sm text-muted-foreground">
48
+ {m["admin-ai_prompts_variables_empty"]()}
49
+ </p>
50
+ )}
51
+
52
+ {variables.map((variable, index) => (
53
+ <div
54
+ key={index}
55
+ title={variable.description || undefined}
56
+ className="flex items-center gap-2 rounded-md border border-border p-2"
57
+ >
58
+ <Input
59
+ className="font-mono text-sm flex-1 h-7"
60
+ value={variable.name}
61
+ placeholder={m["admin-ai_prompts_variable_name_placeholder"]()}
62
+ onChange={(e) => {
63
+ const next = variables.slice()
64
+ next[index] = { ...variable, name: e.target.value }
65
+ onChange(next)
66
+ }}
67
+ />
68
+ <Input
69
+ className="flex-1 h-7"
70
+ value={variable.description}
71
+ placeholder={m["admin-ai_prompts_variable_description_placeholder"]()}
72
+ onChange={(e) => {
73
+ const next = variables.slice()
74
+ next[index] = { ...variable, description: e.target.value }
75
+ onChange(next)
76
+ }}
77
+ />
78
+ <Button
79
+ type="button"
80
+ variant="ghost"
81
+ size="icon-xs"
82
+ className="shrink-0 text-destructive hover:text-destructive"
83
+ onClick={() => onChange(removeVariable(variables, index))}
84
+ aria-label={m["admin-ai_prompts_variable_remove"]()}
85
+ >
86
+ <Trash2 />
87
+ </Button>
88
+ </div>
89
+ ))}
90
+
91
+ <Button
92
+ type="button"
93
+ variant="outline"
94
+ size="sm"
95
+ className="w-full"
96
+ onClick={() => onChange(addVariable(variables))}
97
+ >
98
+ <Plus />
99
+ {m["admin-ai_prompts_variable_add"]()}
100
+ </Button>
101
+
102
+ {undeclared.length > 0 && (
103
+ <div className="rounded-md border border-amber-500/40 bg-amber-500/5 p-3 space-y-2">
104
+ <div className="flex items-center gap-2 text-sm text-amber-600 dark:text-amber-400">
105
+ <AlertTriangle className="size-4 shrink-0" />
106
+ <span>{m["admin-ai_prompts_variable_undeclared"]()}</span>
107
+ </div>
108
+ <div className="flex flex-wrap gap-1.5">
109
+ {undeclared.map((name) => (
110
+ <button
111
+ key={name}
112
+ type="button"
113
+ onClick={() =>
114
+ onChange(addVariable([...variables, { name, description: "" }]))
115
+ }
116
+ className="inline-flex items-center gap-1 rounded border border-amber-500/40 bg-background px-1.5 py-0.5 font-mono text-xs text-amber-600 hover:bg-amber-500/10 dark:text-amber-400"
117
+ title={m["admin-ai_prompts_variable_declareHint"]({ name })}
118
+ >
119
+ <Plus className="size-3" />
120
+ {name}
121
+ </button>
122
+ ))}
123
+ </div>
124
+ </div>
125
+ )}
126
+ </div>
127
+ )
128
+ }
@@ -0,0 +1,157 @@
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/version-history.tsx
19
+ import { useState } from "react"
20
+ import { History, Loader2, RotateCcw } from "lucide-react"
21
+ import { formatTimestamp } from "@allbluecn/web-app/lib/datetime"
22
+
23
+ import { m } from "@/paraglide/messages.js"
24
+ import { Badge } from "@/components/ui/badge"
25
+ import { Button } from "@/components/ui/button"
26
+ import {
27
+ Collapsible,
28
+ CollapsibleContent,
29
+ CollapsibleTrigger,
30
+ } from "@/components/ui/collapsible"
31
+ import {
32
+ AlertDialog,
33
+ AlertDialogAction,
34
+ AlertDialogCancel,
35
+ AlertDialogContent,
36
+ AlertDialogDescription,
37
+ AlertDialogFooter,
38
+ AlertDialogHeader,
39
+ AlertDialogTitle,
40
+ } from "@/components/ui/alert-dialog"
41
+ import type { PromptVersionSummary } from "./prompt-utils"
42
+
43
+ interface VersionHistoryProps {
44
+ versions: PromptVersionSummary[]
45
+ currentVersion: number
46
+ onRollback: (version: number) => void
47
+ rollbackPending: boolean
48
+ }
49
+
50
+ export function VersionHistory({
51
+ versions,
52
+ currentVersion,
53
+ onRollback,
54
+ rollbackPending,
55
+ }: VersionHistoryProps) {
56
+ const [rollbackTarget, setRollbackTarget] = useState<number | null>(null)
57
+
58
+ return (
59
+ <Collapsible defaultOpen>
60
+ <CollapsibleTrigger className="flex w-full items-center gap-2 text-sm font-medium">
61
+ <History />
62
+ <span>{m["admin-ai_prompts_versions_title"]()}</span>
63
+ <Badge variant="secondary" className="ml-auto">
64
+ {versions.length}
65
+ </Badge>
66
+ </CollapsibleTrigger>
67
+ <CollapsibleContent className="pt-3 space-y-2">
68
+ <p className="text-xs text-muted-foreground">
69
+ {m["admin-ai_prompts_versions_hint"]()}
70
+ </p>
71
+
72
+ {versions.length === 0 && (
73
+ <p className="text-sm text-muted-foreground">
74
+ {m["admin-ai_prompts_versions_empty"]()}
75
+ </p>
76
+ )}
77
+
78
+ {versions.map((version) => {
79
+ const isCurrent = version.version >= currentVersion
80
+ return (
81
+ <div
82
+ key={version.id}
83
+ className="flex items-center gap-3 rounded-md border border-border p-2.5"
84
+ >
85
+ <Badge
86
+ variant={isCurrent ? "default" : "outline"}
87
+ className="font-mono shrink-0"
88
+ >
89
+ v{version.version}
90
+ </Badge>
91
+ <div className="flex-1 min-w-0">
92
+ <p className="text-sm truncate">
93
+ {version.note ||
94
+ m["admin-ai_prompts_versions_noNote"]()}
95
+ </p>
96
+ <p className="text-xs text-muted-foreground">
97
+ {version.createdBy ?? m["admin-ai_prompts_versions_unknownCreator"]()}
98
+ {" · "}
99
+ {formatTimestamp(version.createdAt)}
100
+ </p>
101
+ </div>
102
+ {isCurrent ? (
103
+ <span className="text-xs text-muted-foreground shrink-0">
104
+ {m["admin-ai_prompts_versions_current"]()}
105
+ </span>
106
+ ) : (
107
+ <Button
108
+ type="button"
109
+ variant="outline"
110
+ size="sm"
111
+ className="shrink-0"
112
+ disabled={rollbackPending}
113
+ onClick={() => setRollbackTarget(version.version)}
114
+ >
115
+ {rollbackPending ? (
116
+ <Loader2 className="animate-spin" />
117
+ ) : (
118
+ <RotateCcw />
119
+ )}
120
+ {m["admin-ai_prompts_rollback"]()}
121
+ </Button>
122
+ )}
123
+ </div>
124
+ )
125
+ })}
126
+ </CollapsibleContent>
127
+
128
+ <AlertDialog open={rollbackTarget !== null} onOpenChange={(open) => { if (!open) setRollbackTarget(null) }}>
129
+ <AlertDialogContent>
130
+ <AlertDialogHeader>
131
+ <AlertDialogTitle>
132
+ {m["admin-ai_prompts_rollback_confirm_title"]({
133
+ version: rollbackTarget ?? 0,
134
+ })}
135
+ </AlertDialogTitle>
136
+ <AlertDialogDescription>
137
+ {m["admin-ai_prompts_rollback_confirm_description"]({
138
+ version: rollbackTarget ?? 0,
139
+ })}
140
+ </AlertDialogDescription>
141
+ </AlertDialogHeader>
142
+ <AlertDialogFooter>
143
+ <AlertDialogCancel>{m.common_common_cancel()}</AlertDialogCancel>
144
+ <AlertDialogAction
145
+ disabled={rollbackPending}
146
+ onClick={() => {
147
+ if (rollbackTarget !== null) onRollback(rollbackTarget)
148
+ }}
149
+ >
150
+ {m["admin-ai_prompts_rollback_confirm_action"]()}
151
+ </AlertDialogAction>
152
+ </AlertDialogFooter>
153
+ </AlertDialogContent>
154
+ </AlertDialog>
155
+ </Collapsible>
156
+ )
157
+ }
@@ -21,7 +21,7 @@ import { m } from '@/paraglide/messages.js'
21
21
  import { useMutation } from "@tanstack/react-query"
22
22
  import { Link, useNavigate, useRouter } from "@tanstack/react-router"
23
23
  import { ArrowLeft, Loader2, Save } from "lucide-react"
24
- import { toast } from '@/components/ui/sonner'
24
+ import { toast } from '@/components/ui/toast'
25
25
  import {
26
26
  ADAPTER_KINDS,
27
27
  PROVIDER_CATEGORIES,
@@ -142,11 +142,9 @@ export function ProviderForm({
142
142
  return (
143
143
  <div className="mx-auto w-3/5 space-y-6">
144
144
  <div className="sticky top-0 z-20 flex items-center gap-2 border-b bg-background pt-6 pb-3">
145
- <Button asChild variant="ghost" size="icon">
146
- <Link to="/ai-providers" aria-label="返回">
145
+ <Button render={<Link to="/ai-providers" aria-label="返回" />} variant="ghost" size="icon" nativeButton={false}>
147
146
  <ArrowLeft className="size-4" />
148
- </Link>
149
- </Button>
147
+ </Button>
150
148
  <h2 className="text-lg font-semibold">
151
149
  {mode === "create" ? m['admin-ai_form_addProvider']() : m['admin-ai_form_editProvider']()}
152
150
  </h2>
@@ -230,7 +228,7 @@ export function ProviderForm({
230
228
  </FieldContent>
231
229
  <Select
232
230
  value={values.category}
233
- onValueChange={(v: string) => patch({ category: v as ProviderCategory })}
231
+ onValueChange={(v) => patch({ category: (v ?? "") as ProviderCategory })}
234
232
  >
235
233
  <SelectTrigger className="w-40">
236
234
  <SelectValue />
@@ -252,7 +250,7 @@ export function ProviderForm({
252
250
  </FieldContent>
253
251
  <Select
254
252
  value={values.adapterKind}
255
- onValueChange={(v: string) => patch({ adapterKind: v as AdapterKind })}
253
+ onValueChange={(v) => patch({ adapterKind: (v ?? "") as AdapterKind })}
256
254
  >
257
255
  <SelectTrigger className="w-60">
258
256
  <SelectValue />
@@ -416,9 +414,7 @@ export function ProviderForm({
416
414
  </FieldSet>
417
415
 
418
416
  <div className="sticky bottom-0 z-20 flex items-center justify-end gap-3 border-t bg-background pt-6 pb-6">
419
- <Button asChild variant="outline">
420
- <Link to="/ai-providers">{m.common_common_cancel()}</Link>
421
- </Button>
417
+ <Button render={<Link to="/ai-providers" />} variant="outline" nativeButton={false}>{m.common_common_cancel()}</Button>
422
418
  <Button onClick={handleSubmit} disabled={saveMutation.isPending}>
423
419
  {saveMutation.isPending ? (
424
420
  <Loader2 className="size-4 animate-spin" data-icon="inline-start" />
@@ -20,8 +20,8 @@ import { useState } from "react"
20
20
  import { m } from '@/paraglide/messages.js'
21
21
  import { useMutation } from "@tanstack/react-query"
22
22
  import { Link, useRouter } from "@tanstack/react-router"
23
- import { Edit, Plus, Trash2, Users } from "lucide-react"
24
- import { toast } from '@/components/ui/sonner'
23
+ import { Edit, Plus, RefreshCw, Trash2, Users } from "lucide-react"
24
+ import { toast } from '@/components/ui/toast'
25
25
  import { ADAPTER_KINDS, PROVIDER_BADGE_STYLES, type AiProviderRow } from "@allbluecn/kernel"
26
26
  import { Button } from "@/components/ui/button"
27
27
  import { Switch } from "@/components/ui/switch"
@@ -40,6 +40,7 @@ import { DataTable, type AdminColumnDef } from "@/components/data-table/data-tab
40
40
  import { ProviderIcon } from "./provider-icon"
41
41
  import { updateAiProviderAdminFn } from "@/server/serverFns/ai-provider/update"
42
42
  import { deleteAiProviderAdminFn } from "@/server/serverFns/ai-provider/delete"
43
+ import { seedFromSeedFn } from "@/server/serverFns/ai-provider/seed-from-seed"
43
44
 
44
45
  const ADAPTER_LABEL_MAP = new Map(ADAPTER_KINDS.map((k) => [k.value, k.label]))
45
46
 
@@ -71,6 +72,25 @@ export function ProviderTable({ data }: { data: AiProviderRow[] }) {
71
72
  toast.error(err instanceof Error ? err.message : m['admin-ai_table_toast_deleteFailed']()),
72
73
  })
73
74
 
75
+ const seedMutation = useMutation({
76
+ mutationFn: () => seedFromSeedFn({ data: {} }),
77
+ onSuccess: async (result: { created: string[]; skipped: string[] }) => {
78
+ if (result.created.length === 0) {
79
+ toast.info(m['admin-ai_table_toast_seedNoNew']())
80
+ } else {
81
+ toast.success(
82
+ m['admin-ai_table_toast_seedCreated']({
83
+ created: result.created.length,
84
+ skipped: result.skipped.length,
85
+ }),
86
+ )
87
+ }
88
+ await router.invalidate()
89
+ },
90
+ onError: (err) =>
91
+ toast.error(err instanceof Error ? err.message : m['admin-ai_table_toast_seedFailed']()),
92
+ })
93
+
74
94
  const columns: AdminColumnDef<AiProviderRow>[] = [
75
95
  {
76
96
  accessorKey: "displayName",
@@ -171,15 +191,11 @@ export function ProviderTable({ data }: { data: AiProviderRow[] }) {
171
191
  const canDelete = row.original.configCount === 0
172
192
  return (
173
193
  <div className="flex items-center gap-1">
174
- <Button asChild variant="ghost" size="sm">
175
- <Link
176
- to="/ai-providers/$id/edit"
177
- params={{ id: row.original.id }}
178
- >
194
+ <Button render={<Link to="/ai-providers/$id/edit"
195
+ params={{ id: row.original.id }} />} variant="ghost" size="sm" nativeButton={false}>
179
196
  <Edit className="size-4" />
180
197
  {m['admin-ai_table_edit']()}
181
- </Link>
182
- </Button>
198
+ </Button>
183
199
  {canDelete ? (
184
200
  <Button
185
201
  variant="ghost"
@@ -216,12 +232,16 @@ export function ProviderTable({ data }: { data: AiProviderRow[] }) {
216
232
  {m['admin-ai_table_pageDescription']({ count: data.length })}
217
233
  </p>
218
234
  </div>
219
- <Button asChild>
220
- <Link to="/ai-providers/new">
235
+ <div className="flex items-center gap-2">
236
+ <Button variant="outline" size="sm" onClick={() => seedMutation.mutate()} disabled={seedMutation.isPending}>
237
+ <RefreshCw className="size-4" />
238
+ {m['admin-ai_table_seedFromSeed']()}
239
+ </Button>
240
+ <Button render={<Link to="/ai-providers/new" />} nativeButton={false}>
221
241
  <Plus className="size-4" />
222
242
  {m['admin-ai_table_addProvider']()}
223
- </Link>
224
- </Button>
243
+ </Button>
244
+ </div>
225
245
  </div>
226
246
  <DataTable columns={columns} data={data} />
227
247
 
@@ -23,7 +23,7 @@ import { Button } from "@/components/ui/button";
23
23
  import { Checkbox } from "@/components/ui/checkbox";
24
24
  import { Field, FieldLabel } from "@/components/ui/field";
25
25
  import { Card, CardContent } from "@/components/ui/card";
26
- import { toast } from '@/components/ui/sonner';
26
+ import { toast } from '@/components/ui/toast';
27
27
  import { UserAvatar } from "@allbluecn/web-app/user-avatar";
28
28
  import type { AvatarConfig, ShapeType } from "@allbluecn/kernel";
29
29
  import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from "@/components/auth/auth-layout";
@@ -38,7 +38,7 @@ import {
38
38
  } from '@/components/ui/dialog'
39
39
  import { getComponent, ComponentPreview } from '@allbluecn/web-app/prototype'
40
40
  import { createComponentFn, updateComponentFn } from '@/server/serverFns/components/mutations'
41
- import { toast } from '@/components/ui/sonner'
41
+ import { toast } from '@/components/ui/toast'
42
42
  import type { ComponentItem } from './component-table'
43
43
 
44
44
  const CATEGORY_KEYS = ['base', 'block', 'template'] as const
@@ -197,7 +197,7 @@ export function ComponentFormDialog({ open, onOpenChange, component, onSuccess }
197
197
  <Input value={String(val)} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setVal(e.target.value)} className="font-mono text-xs" />
198
198
  </div>
199
199
  ) : prop.type === 'select' && prop.options ? (
200
- <Select value={String(val)} onValueChange={setVal}>
200
+ <Select value={String(val)} onValueChange={(v) => setVal(v ?? "")}>
201
201
  <SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
202
202
  <SelectContent>
203
203
  {prop.options.map((o) => (
@@ -257,7 +257,7 @@ export function ComponentFormDialog({ open, onOpenChange, component, onSuccess }
257
257
 
258
258
  <div className="space-y-1.5">
259
259
  <Label>{m.common_component_form_category()}</Label>
260
- <Select value={category} onValueChange={setCategory}>
260
+ <Select value={category} onValueChange={(v) => setCategory(v ?? "")}>
261
261
  <SelectTrigger className="w-full">
262
262
  <SelectValue />
263
263
  </SelectTrigger>
@@ -271,7 +271,7 @@ export function ComponentFormDialog({ open, onOpenChange, component, onSuccess }
271
271
 
272
272
  <div className="space-y-1.5">
273
273
  <Label>{m.common_component_form_type()}</Label>
274
- <Select value={type} onValueChange={setType}>
274
+ <Select value={type} onValueChange={(v) => setType(v ?? "")}>
275
275
  <SelectTrigger className="w-full">
276
276
  <SelectValue />
277
277
  </SelectTrigger>
@@ -35,7 +35,7 @@ import {
35
35
  DialogTitle,
36
36
  } from '@/components/ui/dialog'
37
37
  import { importComponentFn } from '@/server/serverFns/components/import'
38
- import { toast } from '@/components/ui/sonner'
38
+ import { toast } from '@/components/ui/toast'
39
39
  import { Loader2, Sparkles } from 'lucide-react'
40
40
  import { cn } from '@/lib/utils'
41
41
 
@@ -207,7 +207,7 @@ export function ComponentImportDialog({ open, onOpenChange, onSuccess }: Compone
207
207
  </div>
208
208
  <div className="space-y-1.5">
209
209
  <Label>分类</Label>
210
- <Select value={category} onValueChange={setCategory}>
210
+ <Select value={category} onValueChange={(v) => setCategory(v ?? "")}>
211
211
  <SelectTrigger className="w-full">
212
212
  <SelectValue />
213
213
  </SelectTrigger>
@@ -40,12 +40,10 @@ export function ChangelogDialog({ row }: { row: ThirdPartyDependencyRow }) {
40
40
 
41
41
  return (
42
42
  <Dialog open={open} onOpenChange={setOpen}>
43
- <DialogTrigger asChild>
44
- <Button size="sm" variant="ghost" className="h-7 gap-1 px-2" title={m.common_dependency_table_cell_viewChangelog()}>
43
+ <DialogTrigger render={<Button size="sm" variant="ghost" className="h-7 gap-1 px-2" title={m.common_dependency_table_cell_viewChangelog()} />}>
45
44
  <FileText className="size-3.5" />
46
45
  <span className="text-xs">{m.common_dependency_table_cell_changelogBtn()}</span>
47
- </Button>
48
- </DialogTrigger>
46
+ </DialogTrigger>
49
47
  <DialogContent className="max-h-[85vh] w-200 max-w-4xl overflow-hidden p-0">
50
48
  <DialogHeader className="border-b px-5 py-3">
51
49
  <div className="flex items-center justify-between gap-2 pr-6">
@@ -128,11 +128,9 @@ export function getDependencyColumns(opts: ColumnOpts): ColumnDef<typeof depende
128
128
  if (!desc) return <span className="text-xs text-muted-foreground">—</span>
129
129
  return (
130
130
  <Tooltip>
131
- <TooltipTrigger asChild>
132
- <span className="line-clamp-2 max-w-[280px] text-xs text-muted-foreground">
131
+ <TooltipTrigger render={<span className="line-clamp-2 max-w-[280px] text-xs text-muted-foreground" />}>
133
132
  {desc}
134
- </span>
135
- </TooltipTrigger>
133
+ </TooltipTrigger>
136
134
  <TooltipContent className="max-w-sm">{desc}</TooltipContent>
137
135
  </Tooltip>
138
136
  )
@@ -156,11 +154,9 @@ export function getDependencyColumns(opts: ColumnOpts): ColumnDef<typeof depende
156
154
  if (r.usages.length > 1) {
157
155
  return (
158
156
  <Tooltip>
159
- <TooltipTrigger asChild>
160
- <span className="cursor-help border-b border-dashed border-muted-foreground/50">
157
+ <TooltipTrigger render={<span className="cursor-help border-b border-dashed border-muted-foreground/50" />}>
161
158
  {r.currentVersion}
162
- </span>
163
- </TooltipTrigger>
159
+ </TooltipTrigger>
164
160
  <TooltipContent>
165
161
  <div className="space-y-0.5 text-xs">
166
162
  {r.usages.map((u) => (