@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.
- package/package.json +16 -12
- package/src/components/admin/admin-user-menu.tsx +2 -4
- package/src/components/admin/developer-log.tsx +3 -5
- package/src/components/ai-memory/memory-facts-panel.tsx +383 -0
- package/src/components/ai-prompts/preview-panel.tsx +165 -0
- package/src/components/ai-prompts/prompt-editor.tsx +328 -0
- package/src/components/ai-prompts/prompt-table.tsx +141 -0
- package/src/components/ai-prompts/prompt-utils.ts +124 -0
- package/src/components/ai-prompts/section-editor.tsx +135 -0
- package/src/components/ai-prompts/variable-panel.tsx +128 -0
- package/src/components/ai-prompts/version-history.tsx +157 -0
- package/src/components/ai-provider/provider-form.tsx +6 -10
- package/src/components/ai-provider/provider-table.tsx +33 -13
- package/src/components/auth/admin-login-form.tsx +1 -1
- package/src/components/component/component-form-dialog.tsx +4 -4
- package/src/components/component/component-import-dialog.tsx +2 -2
- package/src/components/dependency/changelog-dialog.tsx +2 -4
- package/src/components/dependency/columns.tsx +4 -8
- package/src/components/dependency/dependency-table.tsx +2 -2
- package/src/components/dependency/edit-dialog.tsx +7 -11
- package/src/components/invitations/invitations-page.tsx +4 -8
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +1 -1
- package/src/components/knowledge-public/compliance-form-dialog.tsx +1 -1
- package/src/components/knowledge-public/legal-form-dialog.tsx +2 -2
- package/src/components/layout/sidebar-02/app-sidebar.tsx +17 -9
- package/src/components/layout/sidebar-02/nav-main.tsx +6 -14
- package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -7
- package/src/components/magicui/rainbow-button.tsx +2 -2
- package/src/components/market/columns.tsx +2 -4
- package/src/components/market/market-app-form.tsx +2 -2
- package/src/components/market/market-app-list.tsx +1 -1
- package/src/components/oauth-providers/provider-page.tsx +1 -1
- package/src/components/ui/alert-dialog.tsx +28 -57
- package/src/components/ui/alert.tsx +3 -4
- package/src/components/ui/avatar.tsx +5 -28
- package/src/components/ui/badge.tsx +19 -34
- package/src/components/ui/button.tsx +14 -41
- package/src/components/ui/card.tsx +6 -24
- package/src/components/ui/checkbox.tsx +4 -26
- package/src/components/ui/collapsible.tsx +6 -37
- package/src/components/ui/dialog.tsx +30 -60
- package/src/components/ui/dropdown-menu.tsx +118 -137
- package/src/components/ui/field.tsx +8 -27
- package/src/components/ui/input-group.tsx +13 -26
- package/src/components/ui/input.tsx +4 -21
- package/src/components/ui/label.tsx +3 -27
- package/src/components/ui/pagination.tsx +17 -31
- package/src/components/ui/select.tsx +101 -76
- package/src/components/ui/separator.tsx +4 -27
- package/src/components/ui/sheet.tsx +29 -58
- package/src/components/ui/sidebar.tsx +119 -117
- package/src/components/ui/skeleton.tsx +1 -18
- package/src/components/ui/switch.tsx +5 -24
- package/src/components/ui/table.tsx +1 -19
- package/src/components/ui/textarea.tsx +2 -20
- package/src/components/ui/toast.tsx +282 -0
- package/src/components/ui/tooltip.tsx +34 -45
- package/src/components/user/settings-activity-log.tsx +1 -1
- package/src/components/user/user-create-dialog.tsx +2 -2
- package/src/components/user/user-filters.tsx +2 -2
- package/src/hooks/use-mobile.ts +0 -17
- package/src/lib/pacer.ts +21 -0
- package/src/lib/utils.ts +1 -23
- package/src/paraglide/README.md +1 -1
- package/src/paraglide/messages/_index.js +95 -0
- package/src/paraglide/messages/admin_ai_memory_action_clearuser1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_action_query.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_clear_cancel.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_clear_confirm_action.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_clear_confirm_description.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_clear_confirm_title.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_columns_actions.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_columns_content.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_columns_createdat1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_columns_threadid1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_delete_cancel.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_delete_confirm_action.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_delete_confirm_description.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_delete_confirm_title.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_emptyhint1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_error_nouserid2.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_error_query.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_error_title.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_form_user.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_page_description.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_page_title.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_placeholder_user.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_stat_count.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_stat_lastwrite1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_toast_cleared.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_toast_deleted.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_active.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_back.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_badge_protected.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_cache_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_active.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_key.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_name.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_protected.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_updatedat1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_version.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_description.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_description_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_edit.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_name.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_name_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_note_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_page_description.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_page_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_generating.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_nocontent1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_section.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_protected_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_rollback.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_action.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_description.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_save.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_add.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_id.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_movedown1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_moveup1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_order.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_remove.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_template_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_sections_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_sections_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_sections_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_activated.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_failed.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_protected_locked.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_rolled_back.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_save_failed.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_saved.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_add.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_declarehint1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_description_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_name_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_remove.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_undeclared.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variables_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variables_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_current.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_nonote1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_unknowncreator1.js +30 -0
- package/src/paraglide/messages/admin_ai_table_seedfromseed2.js +30 -0
- package/src/paraglide/messages/admin_ai_table_toast_seedcreated1.js +30 -0
- package/src/paraglide/messages/admin_ai_table_toast_seedfailed1.js +30 -0
- package/src/paraglide/messages/admin_ai_table_toast_seednonew2.js +30 -0
- package/src/paraglide/messages/common_monitoring_aiusage_sandbox1.js +30 -0
- package/src/paraglide/messages/common_monitoring_aiusage_sandboxvalue2.js +30 -0
- package/src/paraglide/messages/common_nav_aiprompts1.js +30 -0
- package/src/routeTree.gen.ts +101 -0
- package/src/routes/__root.tsx +2 -2
- package/src/{components/ui/sonner.tsx → routes/_nav/ai-memory/route.lazy.tsx} +24 -16
- package/src/routes/_nav/ai-memory/route.tsx +31 -0
- package/src/routes/_nav/ai-prompts/$key/route.lazy.tsx +32 -0
- package/src/routes/_nav/ai-prompts/$key/route.tsx +37 -0
- package/src/routes/_nav/ai-prompts/index/route.lazy.tsx +47 -0
- package/src/routes/_nav/ai-prompts/route.lazy.tsx +26 -0
- package/src/routes/_nav/ai-prompts/route.tsx +31 -0
- package/src/routes/_nav/compliance/route.lazy.tsx +2 -4
- package/src/routes/_nav/components/route.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +1 -1
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +10 -1
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +1 -1
- package/src/routes/_nav/permissions/route.lazy.tsx +2 -2
- package/src/routes/_nav/sso/route.lazy.tsx +1 -1
- package/src/routes/_nav/users/$userId/route.lazy.tsx +3 -3
- package/src/routes/setup/route.lazy.tsx +1 -1
- package/src/server/__tests__/prompt-defaults.test.ts +139 -0
- package/src/server/admin-ai-usage.ts +22 -4
- package/src/server/ai-memory/store.ts +103 -0
- package/src/server/serverFns/ai-memory/clear.ts +31 -0
- package/src/server/serverFns/ai-memory/delete.ts +31 -0
- package/src/server/serverFns/ai-memory/list-users.ts +40 -0
- package/src/server/serverFns/ai-memory/list.ts +54 -0
- package/src/server/serverFns/ai-prompts/activate.ts +53 -0
- package/src/server/serverFns/ai-prompts/get.ts +74 -0
- package/src/server/serverFns/ai-prompts/list.ts +55 -0
- package/src/server/serverFns/ai-prompts/preview.ts +66 -0
- package/src/server/serverFns/ai-prompts/prompt-defaults.ts +63 -0
- package/src/server/serverFns/ai-prompts/prompt-schemas.ts +47 -0
- package/src/server/serverFns/ai-prompts/rollback.ts +86 -0
- package/src/server/serverFns/ai-prompts/save.ts +129 -0
- package/src/server/serverFns/ai-provider/seed-from-seed.ts +64 -0
- package/src/styles/globals.css +92 -77
- package/src/components/layout/sidebar-02/logo.tsx +0 -40
- package/src/components/layout/sidebar-02/team-switcher.tsx +0 -106
- 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
|
+
}
|