refactor: 移除提示词模板页面及相关接口,精简岗位/标签/用户提示词模型
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
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={() =>
|
||||
|
||||
Reference in new issue
Block a user