refactor: 移除提示词模板页面及相关接口,精简岗位/标签/用户提示词模型

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-16 14:32:21 +08:00
1 parent 838e986670
commit e6662cf673
19 files changed
+55 -729

No files matched your search

@@ -1,258 +0,0 @@
"use client";
import { CheckIcon, Loader2, PencilIcon, PlusIcon, TagIcon, Trash2Icon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { resolveTagColor } from "@/components/workspace/tags/tag-colors";
import {
usePromptTemplates,
useCreatePromptTemplate,
useDeletePromptTemplate,
useUpdatePromptTemplate,
type PromptTemplate,
} from "@/core/custom-prompts";
import { useResourceTags, useSetResourceTags, useTags } from "@/core/tags";
import { cn } from "@/lib/utils";
function errMsg(e: unknown): string {
return String(e instanceof Error ? e.message : e);
}
function TemplateFormDialog({
open,
onOpenChange,
editing,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editing: PromptTemplate | null;
}) {
const createT = useCreatePromptTemplate();
const updateT = useUpdatePromptTemplate();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [syncedFor, setSyncedFor] = useState<string | null>(null);
const syncKey = `${open}:${editing?.id ?? "new"}`;
if (open && syncedFor !== syncKey) {
setTitle(editing?.title ?? "");
setContent(editing?.content ?? "");
setSyncedFor(syncKey);
}
if (!open && syncedFor !== null) setSyncedFor(null);
const pending = createT.isPending || updateT.isPending;
const handleSave = async () => {
if (!content.trim()) return;
try {
if (editing) {
await updateT.mutateAsync({ id: editing.id, input: { title: title.trim(), content } });
} else {
await createT.mutateAsync({ title: title.trim(), content });
}
onOpenChange(false);
} catch (e) {
toast.error(errMsg(e));
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editing ? "编辑提示词模板" : "新增提示词模板"}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium">标题</label>
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="如:理赔标准话术" maxLength={255} />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium">内容</label>
<Textarea value={content} onChange={(e) => setContent(e.target.value)} rows={5} placeholder="提示词正文…" />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
<Button onClick={handleSave} disabled={pending || !content.trim()}>
{pending && <Loader2 className="size-4 animate-spin" />}保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function TemplateTagsDialog({
open,
onOpenChange,
template,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
template: PromptTemplate;
}) {
const { data: tags } = useTags("user_prompt");
const { data: current } = useResourceTags("user_prompt", template.id, open);
const setTags = useSetResourceTags();
const [selected, setSelected] = useState<string[]>([]);
const [syncedFor, setSyncedFor] = useState<string | null>(null);
const syncKey = `${open}:${current ? "ready" : "loading"}`;
if (open && current && syncedFor !== syncKey) {
setSelected(current.map((t) => t.id));
setSyncedFor(syncKey);
}
if (!open && syncedFor !== null) setSyncedFor(null);
const toggle = (id: string) =>
setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
const handleSave = async () => {
try {
await setTags.mutateAsync({ targetType: "user_prompt", targetId: template.id, tagIds: selected });
onOpenChange(false);
} catch (e) {
toast.error(errMsg(e));
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>为模板打标签</DialogTitle>
</DialogHeader>
{!tags || tags.length === 0 ? (
<p className="text-muted-foreground text-sm">
暂无「常用提示词」标签,请先到「标签管理」创建。
</p>
) : (
<div className="flex flex-wrap gap-1.5">
{tags.map((tag) => {
const checked = selected.includes(tag.id);
const color = resolveTagColor(tag.color);
return (
<button
key={tag.id}
type="button"
onClick={() => toggle(tag.id)}
className={cn(
"inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium transition-all",
color.chip,
checked ? "ring-foreground/40 ring-2 ring-offset-1" : "opacity-45 hover:opacity-90",
)}
>
{checked && <CheckIcon className="size-3" />}
{tag.name}
</button>
);
})}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
<Button onClick={handleSave} disabled={setTags.isPending}>
{setTags.isPending && <Loader2 className="size-4 animate-spin" />}保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export function PromptTemplatesPage() {
const { data: templates, isLoading, error } = usePromptTemplates();
const deleteT = useDeletePromptTemplate();
const [formOpen, setFormOpen] = useState(false);
const [tagsOpen, setTagsOpen] = useState(false);
const [editing, setEditing] = useState<PromptTemplate | null>(null);
const [tagging, setTagging] = useState<PromptTemplate | null>(null);
const handleDelete = async (tpl: PromptTemplate) => {
if (!window.confirm(`确认删除模板「${tpl.title || tpl.content.slice(0, 20)}」?已分发给用户的克隆将在下次同步时移除。`)) return;
try {
await deleteT.mutateAsync(tpl.id);
} catch (e) {
toast.error(errMsg(e));
}
};
return (
<div className="flex flex-col gap-4 p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-lg font-semibold">常用提示词模板</h1>
<p className="text-muted-foreground text-sm">
管理员维护提示词模板并打标签;岗位配置了对应标签的用户,会自动加载这些模板到「常用提示词」。
</p>
</div>
<Button size="sm" onClick={() => { setEditing(null); setFormOpen(true); }}>
<PlusIcon className="size-4" />新增模板
</Button>
</div>
{isLoading && (
<div className="text-muted-foreground flex items-center justify-center gap-2 py-12">
<Loader2 className="size-4 animate-spin" />加载中…
</div>
)}
{error && <div className="text-destructive text-sm">{errMsg(error)}</div>}
{!isLoading && !error && templates && templates.length === 0 && (
<div className="text-muted-foreground rounded-lg border border-dashed py-12 text-center text-sm">
还没有模板,点击「新增模板」创建。
</div>
)}
{!isLoading && !error && templates && templates.length > 0 && (
<div className="overflow-hidden rounded-md border">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="text-muted-foreground px-4 py-3 text-left font-medium">标题</th>
<th className="text-muted-foreground px-4 py-3 text-left font-medium">内容</th>
<th className="text-muted-foreground px-4 py-3 text-right font-medium">操作</th>
</tr>
</thead>
<tbody>
{templates.map((tpl, i) => (
<tr key={tpl.id} className={i % 2 === 0 ? "bg-background" : "bg-muted/20"}>
<td className="px-4 py-3 font-medium">{tpl.title || "—"}</td>
<td className="text-muted-foreground max-w-md truncate px-4 py-3">{tpl.content}</td>
<td className="px-4 py-3 text-right">
<div className="flex justify-end gap-1">
<Button variant="ghost" size="icon" onClick={() => { setTagging(tpl); setTagsOpen(true); }} aria-label="打标签">
<TagIcon className="size-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => { setEditing(tpl); setFormOpen(true); }} aria-label="编辑">
<PencilIcon className="size-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => handleDelete(tpl)} aria-label="删除">
<Trash2Icon className="text-destructive size-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<TemplateFormDialog open={formOpen} onOpenChange={setFormOpen} editing={editing} />
{tagging && <TemplateTagsDialog open={tagsOpen} onOpenChange={setTagsOpen} template={tagging} />}
</div>
);
}
@@ -525,6 +525,25 @@ export function InputBox({
</div>
)}
{isNewThread && searchParams.get("mode") !== "skill" && showAdminRecommendedQuestions && (
<div className="flex flex-wrap justify-center gap-2.5 px-4 mb-[10px]">
{recommendedQuestions.map((q) => (
<button
key={q.id}
type="button"
className="rounded-full border border-[#e4e4e7] bg-white px-4 py-2 text-sm text-[#18181b] shadow-none transition-colors hover:bg-[#f4f4f5] cursor-pointer dark:border-slate-600 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700"
onClick={() => {
textInput.setInput(q.content);
setFollowupsHidden(true);
setTimeout(() => requestFormSubmit(), 0);
}}
>
{desensitize(q.title)}
</button>
))}
</div>
)}
{showFollowups && (
<div className="flex items-center justify-center pb-2">
<div className="flex items-center gap-2">
@@ -1103,35 +1122,6 @@ export function InputBox({
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelector>
{isNewThread && searchParams.get("mode") !== "skill" && showAdminRecommendedQuestions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
title="推荐问题"
className="inline-flex items-center gap-1.5 rounded-full border border-[#e4e4e7] bg-background px-3 py-1.5 text-xs text-[#18181b] shadow-sm transition-colors hover:bg-[#f4f4f5] cursor-pointer dark:border-slate-600 dark:bg-slate-700/60 dark:text-slate-200 dark:hover:bg-slate-700"
>
<LightbulbIcon className="size-4 text-amber-500" />
推荐问题
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="end" className="max-h-80 w-72 overflow-y-auto">
{recommendedQuestions.map((q) => (
<DropdownMenuItem
key={q.id}
className="cursor-pointer whitespace-normal"
onSelect={() => {
textInput.setInput(q.content);
setFollowupsHidden(true);
setTimeout(() => requestFormSubmit(), 0);
}}
>
{desensitize(q.title)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
<PromptInputSubmit
className={cn(
"rounded-full border-border bg-background shadow-sm hover:bg-muted dark:bg-blue-500 dark:border-blue-500 dark:text-white dark:hover:bg-blue-400 dark:shadow-none",
@@ -10,7 +10,6 @@ import {
DatabaseZapIcon,
GaugeIcon,
HistoryIcon,
MessageSquareCodeIcon,
ScrollTextIcon,
Settings2Icon,
SettingsIcon,
@@ -217,16 +216,6 @@ export function WorkspaceNavMenu() {
岗位管理
</DropdownMenuItem>
)}
{isAdmin && (
<DropdownMenuItem
onClick={() =>
navigate("/page/workspace/admin/prompt-templates")
}
>
<MessageSquareCodeIcon />
提示词模板
</DropdownMenuItem>
)}
{isAdmin && logSystem?.enabled && logSystem.url && (
<DropdownMenuItem
onClick={() =>