@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
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@allbluecn/admin-app",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "@allbluecn/database": "0.8.0",
9
- "@allbluecn/kernel": "0.8.0",
10
- "@allbluecn/web-app": "0.12.0",
8
+ "@allbluecn/database": "0.9.0",
9
+ "@allbluecn/kernel": "0.9.0",
10
+ "@allbluecn/web-app": "0.17.1",
11
11
  "@auth/core": "^0.41.2",
12
- "@lobehub/icons": "^5.16.0",
12
+ "@base-ui/react": "^1.8.0",
13
+ "@fontsource-variable/inter": "^5.3.0",
14
+ "@lobehub/icons": "^5.19.3",
13
15
  "@prisma/client": "^7.8.0",
14
16
  "@radix-ui/react-avatar": "^1.1.11",
15
17
  "@radix-ui/react-collapsible": "^1.1.12",
@@ -22,6 +24,7 @@
22
24
  "@tabler/icons-react": "^3.44.0",
23
25
  "@tanstack/charts": "^0.16.0",
24
26
  "@tanstack/react-form": "2.0.0-alpha.2",
27
+ "@tanstack/react-pacer": "^0.23.0",
25
28
  "@tanstack/react-query": "^5.102.8",
26
29
  "@tanstack/react-router": "1.170.32",
27
30
  "@tanstack/react-router-ssr-query": "^1.167.2",
@@ -32,18 +35,19 @@
32
35
  "bcryptjs": "^3.0.3",
33
36
  "class-variance-authority": "^0.7.1",
34
37
  "clsx": "^2.1.1",
38
+ "cn": "^0.3.0",
35
39
  "date-fns": "^4.4.0",
36
- "lucide-react": "^1.43.0",
37
- "motion": "^12.42.2",
40
+ "lucide-react": "^1.47.0",
41
+ "motion": "^13.4.0",
38
42
  "next-themes": "^0.4.6",
39
43
  "radix-ui": "^1.4.3",
40
44
  "react": "^19.2.4",
41
45
  "react-dom": "^19.2.4",
42
46
  "react-markdown": "^10.1.0",
43
47
  "remark-gfm": "^4.0.1",
44
- "sonner": "^2.0.7",
45
48
  "start-authjs": "^1.0.2",
46
49
  "tailwind-merge": "^3.5.0",
50
+ "tw-animate-css": "^1.4.0",
47
51
  "zod": "^4.4.3"
48
52
  },
49
53
  "devDependencies": {
@@ -51,7 +55,7 @@
51
55
  "@eslint/js": "^9.39.4",
52
56
  "@inlang/paraglide-js": "^2.25.0",
53
57
  "@inlang/plugin-message-format": "^4.4.4",
54
- "@tailwindcss/vite": "^4.3.2",
58
+ "@tailwindcss/vite": "^4.3.3",
55
59
  "@tanstack/react-query-devtools": "5.102.8",
56
60
  "@tanstack/react-router-devtools": "1.167.1",
57
61
  "@testing-library/jest-dom": "7.0.1",
@@ -65,10 +69,10 @@
65
69
  "eslint": "^9",
66
70
  "jsdom": "^29.1.1",
67
71
  "nitro": "3.0.260903-beta",
68
- "shadcn": "^4.15.0",
69
- "tailwindcss": "^4",
72
+ "shadcn": "^4.21.0",
73
+ "tailwindcss": "^4.3.3",
70
74
  "typescript": "^7",
71
- "vite": "^8.1.5",
75
+ "vite": "^8.3.0",
72
76
  "vitest": "5.0.0",
73
77
  "wrangler": "^4.99.0"
74
78
  },
@@ -83,11 +83,9 @@ export function AdminUserMenu() {
83
83
  </div>
84
84
  </SidebarMenuButton>
85
85
  <Tooltip>
86
- <TooltipTrigger asChild>
87
- <SidebarMenuAction onClick={handleExitAdmin} className="hover:text-red-500 h-7 w-8 flex items-center justify-center">
86
+ <TooltipTrigger render={<SidebarMenuAction onClick={handleExitAdmin} className="hover:text-red-500 h-7 w-8 flex items-center justify-center" />}>
88
87
  <LogOut className="h-4 w-4" />
89
- </SidebarMenuAction>
90
- </TooltipTrigger>
88
+ </TooltipTrigger>
91
89
  <TooltipContent>{m.common_login_exitAdmin()}</TooltipContent>
92
90
  </Tooltip>
93
91
  </SidebarMenuItem>
@@ -4,7 +4,7 @@ import { Route } from '@/routes/_nav/developer-logs/route'
4
4
  import type { DeveloperLogEntry } from '@allbluecn/kernel'
5
5
  import { useQueryClient } from '@tanstack/react-query'
6
6
  import { useServerFn } from '@tanstack/react-start'
7
- import { toast } from 'sonner'
7
+ import { toast } from '@/components/ui/toast'
8
8
  import {
9
9
  Plus,
10
10
  Pencil,
@@ -316,11 +316,9 @@ function DeveloperLogPage() {
316
316
  <Pencil className="size-3.5" />
317
317
  </Button>
318
318
  <AlertDialog>
319
- <AlertDialogTrigger asChild>
320
- <Button variant="ghost" size="icon" className="size-7 text-destructive">
319
+ <AlertDialogTrigger render={<Button variant="ghost" size="icon" className="size-7 text-destructive" />}>
321
320
  <Trash2 className="size-3.5" />
322
- </Button>
323
- </AlertDialogTrigger>
321
+ </AlertDialogTrigger>
324
322
  <AlertDialogContent>
325
323
  <AlertDialogHeader>
326
324
  <AlertDialogTitle>{m.common_button_delete()}</AlertDialogTitle>
@@ -0,0 +1,383 @@
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-memory/memory-facts-panel.tsx
19
+ import { useMemo, useState } from "react"
20
+ import {
21
+ Eraser,
22
+ Loader2,
23
+ RotateCw,
24
+ Search,
25
+ TriangleAlert,
26
+ Trash2,
27
+ } from "lucide-react"
28
+
29
+ import { m } from "@/paraglide/messages.js"
30
+ import { formatTimestamp } from "@allbluecn/web-app/lib/datetime"
31
+ import { toast } from "@/components/ui/toast"
32
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
33
+ import { Badge } from "@/components/ui/badge"
34
+ import { Button } from "@/components/ui/button"
35
+ import { Card, CardContent } from "@/components/ui/card"
36
+ import {
37
+ AlertDialog,
38
+ AlertDialogAction,
39
+ AlertDialogCancel,
40
+ AlertDialogContent,
41
+ AlertDialogDescription,
42
+ AlertDialogFooter,
43
+ AlertDialogHeader,
44
+ AlertDialogTitle,
45
+ } from "@/components/ui/alert-dialog"
46
+ import {
47
+ Select,
48
+ SelectContent,
49
+ SelectItem,
50
+ SelectTrigger,
51
+ SelectValue,
52
+ } from "@/components/ui/select"
53
+ import { DataTable, type AdminColumnDef } from "@/components/data-table/data-table"
54
+ import { listMemoryFactsFn } from "@/server/serverFns/ai-memory/list"
55
+ import { deleteMemoryFactFn } from "@/server/serverFns/ai-memory/delete"
56
+ import { clearMemoryFactsFn } from "@/server/serverFns/ai-memory/clear"
57
+
58
+ export interface MemoryFactRow {
59
+ id: string
60
+ content: string
61
+ threadId: string
62
+ createdAt: string
63
+ }
64
+
65
+ export interface MemoryUserOption {
66
+ id: string
67
+ username: string
68
+ email: string
69
+ createdAt: string
70
+ }
71
+
72
+ function MemoryFactsTable({
73
+ data,
74
+ deletingId,
75
+ onDelete,
76
+ }: {
77
+ data: MemoryFactRow[]
78
+ deletingId: string | null
79
+ onDelete: (row: MemoryFactRow) => void
80
+ }) {
81
+ const columns = useMemo<Array<AdminColumnDef<MemoryFactRow>>>(
82
+ () => [
83
+ {
84
+ accessorKey: "content",
85
+ header: m["admin-ai-memory_columns_content"](),
86
+ cell: ({ row }: any) => {
87
+ const fact = row.original as MemoryFactRow
88
+ return (
89
+ <div className="flex min-w-0 flex-col gap-1">
90
+ <Badge
91
+ variant="outline"
92
+ className="self-start font-mono text-xs"
93
+ >
94
+ {fact.id}
95
+ </Badge>
96
+ <div className="whitespace-pre-wrap break-words text-sm">
97
+ {fact.content}
98
+ </div>
99
+ </div>
100
+ )
101
+ },
102
+ },
103
+ {
104
+ accessorKey: "threadId",
105
+ header: m["admin-ai-memory_columns_threadId"](),
106
+ meta: { style: { width: "12rem" } },
107
+ cell: ({ row }: any) => (
108
+ <span className="break-all font-mono text-xs">
109
+ {row.original.threadId}
110
+ </span>
111
+ ),
112
+ },
113
+ {
114
+ accessorKey: "createdAt",
115
+ header: m["admin-ai-memory_columns_createdAt"](),
116
+ meta: { style: { width: "9rem" } },
117
+ cell: ({ row }: any) => (
118
+ <span className="whitespace-nowrap text-muted-foreground">
119
+ {formatTimestamp(row.original.createdAt)}
120
+ </span>
121
+ ),
122
+ },
123
+ {
124
+ id: "actions",
125
+ header: m["admin-ai-memory_columns_actions"](),
126
+ meta: { style: { width: "4rem" } },
127
+ cell: ({ row }: any) => {
128
+ const fact = row.original as MemoryFactRow
129
+ return (
130
+ <Button
131
+ size="sm"
132
+ variant="ghost"
133
+ className="shrink-0"
134
+ disabled={deletingId !== null}
135
+ onClick={() => onDelete(fact)}
136
+ >
137
+ {deletingId === fact.id ? (
138
+ <Loader2 className="animate-spin" />
139
+ ) : (
140
+ <Trash2 />
141
+ )}
142
+ </Button>
143
+ )
144
+ },
145
+ },
146
+ ],
147
+ [deletingId],
148
+ )
149
+
150
+ return <DataTable columns={columns} data={data} />
151
+ }
152
+
153
+ export function MemoryFactsPanel({
154
+ users,
155
+ }: {
156
+ users: Array<MemoryUserOption>
157
+ }) {
158
+ const [userId, setUserId] = useState<string | undefined>(undefined)
159
+ const [facts, setFacts] = useState<MemoryFactRow[]>([])
160
+ const [count, setCount] = useState(0)
161
+ const [lastWriteAt, setLastWriteAt] = useState<string | null>(null)
162
+ const [loading, setLoading] = useState(false)
163
+ const [error, setError] = useState<string | null>(null)
164
+ const [deletingId, setDeletingId] = useState<string | null>(null)
165
+ const [pendingDelete, setPendingDelete] = useState<MemoryFactRow | null>(
166
+ null,
167
+ )
168
+ const [confirmingClear, setConfirmingClear] = useState(false)
169
+
170
+ const selectedUser = users.find((user) => user.id === userId)
171
+
172
+ async function handleQuery() {
173
+ if (!userId) {
174
+ setError(m["admin-ai-memory_error_noUserId"]())
175
+ return
176
+ }
177
+ setLoading(true)
178
+ setError(null)
179
+ try {
180
+ const result = await listMemoryFactsFn({ data: { userId } })
181
+ setFacts(result.facts)
182
+ setCount(result.count)
183
+ setLastWriteAt(result.lastWriteAt)
184
+ } catch (err) {
185
+ setError(
186
+ err instanceof Error ? err.message : m["admin-ai-memory_error_query"](),
187
+ )
188
+ } finally {
189
+ setLoading(false)
190
+ }
191
+ }
192
+
193
+ async function handleDelete(row: MemoryFactRow, onClose: () => void) {
194
+ if (!userId) return
195
+ setDeletingId(row.id)
196
+ try {
197
+ await deleteMemoryFactFn({ data: { id: row.id } })
198
+ toast.success(m["admin-ai-memory_toast_deleted"]())
199
+ const next = facts.filter((item) => item.id !== row.id)
200
+ setFacts(next)
201
+ setCount(Math.max(0, count - 1))
202
+ onClose()
203
+ } catch (err) {
204
+ toast.error(
205
+ err instanceof Error ? err.message : m["admin-ai-memory_error_query"](),
206
+ )
207
+ } finally {
208
+ setDeletingId(null)
209
+ }
210
+ }
211
+
212
+ async function handleClear() {
213
+ if (!userId) return
214
+ setLoading(true)
215
+ try {
216
+ const result = await clearMemoryFactsFn({ data: { userId } })
217
+ toast.success(m["admin-ai-memory_toast_cleared"]({ count: result.deleted }))
218
+ setFacts([])
219
+ setCount(0)
220
+ setLastWriteAt(null)
221
+ } catch (err) {
222
+ toast.error(
223
+ err instanceof Error ? err.message : m["admin-ai-memory_error_query"](),
224
+ )
225
+ } finally {
226
+ setLoading(false)
227
+ setConfirmingClear(false)
228
+ }
229
+ }
230
+
231
+ return (
232
+ <div className="flex flex-col gap-4">
233
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-end">
234
+ <div className="flex flex-1 flex-col gap-1">
235
+ <span className="text-sm text-muted-foreground">
236
+ {m["admin-ai-memory_form_user"]()}
237
+ </span>
238
+ <Select
239
+ value={userId}
240
+ onValueChange={(v) => setUserId(v ?? "")}
241
+ disabled={loading}
242
+ >
243
+ <SelectTrigger className="w-full">
244
+ <SelectValue placeholder={m["admin-ai-memory_placeholder_user"]()} />
245
+ </SelectTrigger>
246
+ <SelectContent>
247
+ {users.map((user) => (
248
+ <SelectItem key={user.id} value={user.id}>
249
+ {`${user.username} · ${user.id}`}
250
+ </SelectItem>
251
+ ))}
252
+ </SelectContent>
253
+ </Select>
254
+ </div>
255
+ <Button onClick={() => void handleQuery()} disabled={loading || !userId}>
256
+ {loading ? (
257
+ <Loader2 className="animate-spin" />
258
+ ) : (
259
+ <Search />
260
+ )}
261
+ {m["admin-ai-memory_action_query"]()}
262
+ </Button>
263
+ </div>
264
+
265
+ {selectedUser ? (
266
+ <Card>
267
+ <CardContent className="flex flex-wrap items-center gap-x-5 gap-y-1 pt-4 text-sm">
268
+ <span className="text-muted-foreground">
269
+ {m["admin-ai-memory_stat_count"]()}
270
+ <span className="ml-1.5 font-medium text-foreground">
271
+ {count}
272
+ </span>
273
+ </span>
274
+ <span className="text-muted-foreground">
275
+ {m["admin-ai-memory_stat_lastWrite"]()}
276
+ <span className="ml-1.5 text-foreground">
277
+ {lastWriteAt ? formatTimestamp(lastWriteAt) : "—"}
278
+ </span>
279
+ </span>
280
+ <div className="ml-auto">
281
+ <Button
282
+ variant="outline"
283
+ size="sm"
284
+ disabled={loading || count === 0}
285
+ onClick={() => setConfirmingClear(true)}
286
+ >
287
+ <Eraser />
288
+ {m["admin-ai-memory_action_clearUser"]()}
289
+ </Button>
290
+ </div>
291
+ </CardContent>
292
+ </Card>
293
+ ) : null}
294
+
295
+ {error ? (
296
+ <Alert variant="destructive">
297
+ <TriangleAlert />
298
+ <AlertTitle>{m["admin-ai-memory_error_title"]()}</AlertTitle>
299
+ <AlertDescription>{error}</AlertDescription>
300
+ </Alert>
301
+ ) : null}
302
+
303
+ <Card>
304
+ <CardContent className="pt-4">
305
+ {loading ? (
306
+ <div className="flex h-28 items-center justify-center text-muted-foreground">
307
+ <Loader2 className="animate-spin" />
308
+ </div>
309
+ ) : facts.length === 0 ? (
310
+ <div className="flex h-28 items-center justify-center text-sm text-muted-foreground">
311
+ {userId
312
+ ? m["admin-ai-memory_empty"]()
313
+ : m["admin-ai-memory_emptyHint"]()}
314
+ </div>
315
+ ) : (
316
+ <MemoryFactsTable
317
+ data={facts}
318
+ deletingId={deletingId}
319
+ onDelete={(row) => setPendingDelete(row)}
320
+ />
321
+ )}
322
+ </CardContent>
323
+ </Card>
324
+
325
+ <AlertDialog open={pendingDelete !== null} onOpenChange={(open) => { if (!open) setPendingDelete(null) }}>
326
+ <AlertDialogContent>
327
+ <AlertDialogHeader>
328
+ <AlertDialogTitle>
329
+ {m["admin-ai-memory_delete_confirm_title"]()}
330
+ </AlertDialogTitle>
331
+ <AlertDialogDescription>
332
+ {m["admin-ai-memory_delete_confirm_description"]()}
333
+ </AlertDialogDescription>
334
+ </AlertDialogHeader>
335
+ <AlertDialogFooter>
336
+ <AlertDialogCancel>
337
+ {m["admin-ai-memory_delete_cancel"]()}
338
+ </AlertDialogCancel>
339
+ <AlertDialogAction
340
+ disabled={deletingId !== null}
341
+ onClick={(event) => {
342
+ if (!pendingDelete) return
343
+ event.preventDefault()
344
+ void handleDelete(pendingDelete, () => setPendingDelete(null))
345
+ }}
346
+ >
347
+ {m["admin-ai-memory_delete_confirm_action"]()}
348
+ </AlertDialogAction>
349
+ </AlertDialogFooter>
350
+ </AlertDialogContent>
351
+ </AlertDialog>
352
+
353
+ <AlertDialog
354
+ open={confirmingClear}
355
+ onOpenChange={(open) => {
356
+ if (!open) setConfirmingClear(false)
357
+ }}
358
+ >
359
+ <AlertDialogContent>
360
+ <AlertDialogHeader>
361
+ <AlertDialogTitle>
362
+ {m["admin-ai-memory_clear_confirm_title"]()}
363
+ </AlertDialogTitle>
364
+ <AlertDialogDescription>
365
+ {m["admin-ai-memory_clear_confirm_description"]()}
366
+ </AlertDialogDescription>
367
+ </AlertDialogHeader>
368
+ <AlertDialogFooter>
369
+ <AlertDialogCancel>
370
+ {m["admin-ai-memory_clear_cancel"]()}
371
+ </AlertDialogCancel>
372
+ <AlertDialogAction
373
+ disabled={loading}
374
+ onClick={() => void handleClear()}
375
+ >
376
+ {m["admin-ai-memory_clear_confirm_action"]()}
377
+ </AlertDialogAction>
378
+ </AlertDialogFooter>
379
+ </AlertDialogContent>
380
+ </AlertDialog>
381
+ </div>
382
+ )
383
+ }
@@ -0,0 +1,165 @@
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/preview-panel.tsx
19
+ import { useCallback, useEffect, useRef, useState } from "react"
20
+ import { AlertTriangle, Loader2 } from "lucide-react"
21
+
22
+ import { m } from "@/paraglide/messages.js"
23
+ import { Input } from "@/components/ui/input"
24
+ import {
25
+ PROMPT_PREVIEW_DEBOUNCE_MS,
26
+ useDebouncedCallback,
27
+ } from "@/lib/pacer"
28
+ import { previewPromptFn } from "@/server/serverFns/ai-prompts/preview"
29
+ import {
30
+ extractUsedVariables,
31
+ type PreviewResult,
32
+ type PromptSectionDTO,
33
+ type PromptVariableDTO,
34
+ } from "./prompt-utils"
35
+
36
+ interface PreviewPanelProps {
37
+ key: string
38
+ sections: PromptSectionDTO[]
39
+ variables: PromptVariableDTO[]
40
+ }
41
+
42
+ export function PreviewPanel({
43
+ key,
44
+ sections,
45
+ variables,
46
+ }: PreviewPanelProps) {
47
+ const declared = variables.map((v) => v.name).filter(Boolean)
48
+ const allVarNames = [
49
+ ...declared,
50
+ ...extractUsedVariables(sections).filter((n) => !declared.includes(n)),
51
+ ]
52
+ const varDescriptions = new Map(
53
+ variables
54
+ .filter((v) => v.name && v.description)
55
+ .map((v) => [v.name, v.description as string]),
56
+ )
57
+
58
+ const [vars, setVars] = useState<Record<string, string>>({})
59
+ const [result, setResult] = useState<PreviewResult | null>(null)
60
+ const [pending, setPending] = useState(false)
61
+
62
+ const mountedRef = useRef(true)
63
+ useEffect(() => {
64
+ mountedRef.current = true
65
+ return () => {
66
+ mountedRef.current = false
67
+ }
68
+ }, [])
69
+
70
+ const latestRef = useRef({ sections, vars })
71
+ latestRef.current = { sections, vars }
72
+
73
+ const runPreview = useCallback((sections: PromptSectionDTO[], vars: Record<string, string>) => {
74
+ setPending(true)
75
+ previewPromptFn({ data: { key, sections, vars } })
76
+ .then((res) => {
77
+ if (mountedRef.current) setResult(res)
78
+ })
79
+ .catch(() => {
80
+ if (mountedRef.current) setResult(null)
81
+ })
82
+ .finally(() => {
83
+ if (mountedRef.current) setPending(false)
84
+ })
85
+ }, [key])
86
+
87
+ const debouncedPreview = useDebouncedCallback(() => {
88
+ const { sections, vars } = latestRef.current
89
+ runPreview(sections, vars)
90
+ }, { wait: PROMPT_PREVIEW_DEBOUNCE_MS })
91
+
92
+ useEffect(() => {
93
+ debouncedPreview()
94
+ }, [sections, vars, debouncedPreview])
95
+
96
+ return (
97
+ <div className="space-y-3">
98
+ {allVarNames.length > 0 && (
99
+ <div className="space-y-2">
100
+ {allVarNames.map((name) => (
101
+ <div key={name} className="space-y-1">
102
+ <div className="flex items-center gap-2">
103
+ <Input
104
+ className="font-mono text-sm flex-1 h-7"
105
+ value={vars[name] ?? ""}
106
+ placeholder={m["admin-ai_prompts_preview_placeholder"]({ name })}
107
+ onChange={(e) =>
108
+ setVars((prev) => ({ ...prev, [name]: e.target.value }))
109
+ }
110
+ />
111
+ </div>
112
+ {varDescriptions.get(name) && (
113
+ <p className="pl-1 text-xs text-muted-foreground">
114
+ {varDescriptions.get(name)}
115
+ </p>
116
+ )}
117
+ </div>
118
+ ))}
119
+ </div>
120
+ )}
121
+
122
+ {pending && (
123
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
124
+ <Loader2 className="size-3.5 animate-spin" />
125
+ <span>{m["admin-ai_prompts_preview_generating"]()}</span>
126
+ </div>
127
+ )}
128
+
129
+ {!pending && result && !result.ok && (
130
+ <div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-2.5 text-sm text-destructive">
131
+ <AlertTriangle className="size-4 shrink-0 mt-0.5" />
132
+ <span>{result.error}</span>
133
+ </div>
134
+ )}
135
+
136
+ {!pending && result?.ok && (
137
+ <div className="space-y-2">
138
+ {result.sections.length === 0 && (
139
+ <p className="text-sm text-muted-foreground">
140
+ {m["admin-ai_prompts_preview_empty"]()}
141
+ </p>
142
+ )}
143
+ {result.sections.map((section, index) => (
144
+ <div
145
+ key={section.id}
146
+ className="rounded-md border border-border p-3"
147
+ >
148
+ <div className="mb-2 flex items-center justify-between">
149
+ <span className="font-mono text-xs text-muted-foreground">
150
+ {section.id}
151
+ </span>
152
+ <span className="text-xs text-muted-foreground">
153
+ {m["admin-ai_prompts_preview_section"]({ index: index + 1 })}
154
+ </span>
155
+ </div>
156
+ <pre className="font-mono text-sm whitespace-pre-wrap break-words">
157
+ {section.text || m["admin-ai_prompts_preview_noContent"]()}
158
+ </pre>
159
+ </div>
160
+ ))}
161
+ </div>
162
+ )}
163
+ </div>
164
+ )
165
+ }