chore: update ji branch work

This commit is contained in:
吉鑫升 committed 2026-06-04 17:46:11 +08:00
1 parent 6c30e649e3
commit cbd07c9d1b
9 files changed
+188 -124

No files matched your search

@@ -222,6 +222,7 @@ export function AgentManageDialog({
key={agent.id}
value={`${agent.name} ${agent.description ?? ""}`}
onSelect={() => void add(agent)}
className="flex min-w-0 items-center overflow-hidden"
>
<BotIcon className="text-muted-foreground mr-2 size-4 shrink-0" />
<div className="flex min-w-0 flex-1 flex-col">
@@ -120,7 +120,7 @@ export function AgentSelector({
value={`${agent.name} ${agent.description ?? ""}`}
onSelect={() => handleSelect(agent)}
className={cn(
"flex items-center gap-2",
"flex min-w-0 items-center gap-2 overflow-hidden",
agent.id === selectedAgentId && "bg-accent",
)}
>
@@ -29,6 +29,7 @@ import {
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useAgents } from "@/core/agents/hooks";
import { getStoredAuth } from "@/core/auth";
import {
useCreateCustomPrompt,
useCustomPrompts,
@@ -42,16 +43,6 @@ import {
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
/** Sentinel select value for "no agent association" (global prompt). */
const NO_AGENT = "__global__";
/**
* Bar above the chat input showing the user's saved prompts as centered,
* clickable chips. When there are too many to fit, they become a horizontal
* carousel (arrows appear). A gear pinned to the far right opens the manage
* dialog. Prompts are bound to the account (server-backed), and this coexists
* with the admin-configured global prompt prefix.
*/
export function CustomPromptBar({
onInsert,
className,
@@ -59,19 +50,13 @@ export function CustomPromptBar({
}: {
onInsert: (content: string) => void;
className?: string;
/**
* The agent currently selected for this chat (or null on the general
* new-chat page). Agent-associated prompts only show when their ``agent_id``
* matches; un-associated (global) prompts always show.
*/
currentAgentId?: string | null;
}) {
const allPrompts = useCustomPrompts();
const prompts = allPrompts.filter(
(p) => !p.agent_id || p.agent_id === currentAgentId,
(prompt) => !prompt.agent_id || prompt.agent_id === currentAgentId,
);
const [manageOpen, setManageOpen] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const [canLeft, setCanLeft] = useState(false);
const [canRight, setCanRight] = useState(false);
@@ -87,7 +72,7 @@ export function CustomPromptBar({
updateArrows();
window.addEventListener("resize", updateArrows);
return () => window.removeEventListener("resize", updateArrows);
}, [updateArrows, prompts]);
}, [prompts, updateArrows]);
const scrollByDir = (dir: 1 | -1) => {
scrollRef.current?.scrollBy({ left: dir * 220, behavior: "smooth" });
@@ -97,7 +82,7 @@ export function CustomPromptBar({
return (
<>
<div className={cn("flex items-center gap-1", className)}>
<div className={cn("flex min-w-0 items-center gap-1", className)}>
{prompts.length > 0 && (
<>
{overflowing && (
@@ -116,7 +101,7 @@ export function CustomPromptBar({
<div
ref={scrollRef}
onScroll={updateArrows}
className="flex-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
className="min-w-0 flex-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
<div className="mx-auto flex w-max items-center gap-1.5 px-0.5 py-0.5">
{prompts.map((prompt) => (
@@ -150,7 +135,7 @@ export function CustomPromptBar({
)}
</>
)}
{prompts.length === 0 && <div className="flex-1" />}
{prompts.length === 0 && <div className="min-w-0 flex-1" />}
<Button
type="button"
variant="ghost"
@@ -183,34 +168,33 @@ function ManagePromptsDialog({
const { agents } = useAgents();
const preferences = useUserPreferences();
const updatePreferences = useUpdateUserPreferences();
const currentUserId = getStoredAuth()?.user_id ?? null;
const [editingId, setEditingId] = useState<string | null>(null);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [agentId, setAgentId] = useState<string>(NO_AGENT);
const [agentId, setAgentId] = useState<string | undefined>(undefined);
const saving = createPrompt.isPending || updatePrompt.isPending;
const agentNameById = (id?: string | null) =>
id ? agents.find((a) => a.id === id)?.name ?? null : null;
// Only offer the agents the user has favorited (收藏到「我的」) for
// association — the full catalog is huge, mostly irrelevant, and overflowed
// the dropdown. The agent a prompt is *already* bound to is kept in the list
// even if it is no longer a favorite, so editing never silently drops it.
const selectableAgents = useMemo(() => {
const favorites = agents.filter((a) => a.is_favorite);
if (agentId !== NO_AGENT && !favorites.some((a) => a.id === agentId)) {
const bound = agents.find((a) => a.id === agentId);
if (bound) return [bound, ...favorites];
const owned = currentUserId
? agents.filter((agent) => agent.user_id === currentUserId)
: [];
if (agentId && !owned.some((agent) => agent.id === agentId)) {
const bound = agents.find((agent) => agent.id === agentId);
if (bound) return [bound, ...owned];
}
return favorites;
}, [agents, agentId]);
return owned;
}, [agents, agentId, currentUserId]);
const agentNameById = (id?: string | null) =>
id ? agents.find((agent) => agent.id === id)?.name ?? null : null;
const resetForm = () => {
setEditingId(null);
setTitle("");
setContent("");
setAgentId(NO_AGENT);
setAgentId(undefined);
};
const handleSubmit = async () => {
@@ -219,19 +203,23 @@ function ManagePromptsDialog({
toast.error("提示词内容不能为空");
return;
}
const boundAgentId = agentId === NO_AGENT ? null : agentId;
if (!agentId) {
toast.error("请选择关联智能体");
return;
}
try {
if (editingId) {
await updatePrompt.mutateAsync({
id: editingId,
input: { title: trimmedTitle, content, agent_id: boundAgentId },
input: { title: trimmedTitle, content, agent_id: agentId },
});
toast.success("提示词已更新");
} else {
await createPrompt.mutateAsync({
title: trimmedTitle,
content,
agent_id: boundAgentId,
agent_id: agentId,
});
toast.success("提示词已添加");
}
@@ -242,12 +230,12 @@ function ManagePromptsDialog({
};
const handleEdit = (id: string) => {
const target = prompts.find((p) => p.id === id);
const target = prompts.find((prompt) => prompt.id === id);
if (!target) return;
setEditingId(id);
setTitle(target.title);
setContent(target.content);
setAgentId(target.agent_id || NO_AGENT);
setAgentId(target.agent_id || undefined);
};
const handleToggleHideAdmin = async (next: boolean) => {
@@ -262,6 +250,7 @@ function ManagePromptsDialog({
try {
await deletePrompt.mutateAsync(id);
if (editingId === id) resetForm();
toast.success("提示词已删除");
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
@@ -279,7 +268,7 @@ function ManagePromptsDialog({
<DialogHeader>
<DialogTitle>管理常用提示词</DialogTitle>
<DialogDescription>
保存常用的提示词,保存后会以小标签形式显示在输入框上方,点击即可填入。提示词与你的账号绑定,可跨设备使用。
保存常用提示词后,会显示在输入框上方。提示词与账号绑定,可跨设备使用。
</DialogDescription>
</DialogHeader>
@@ -287,41 +276,44 @@ function ManagePromptsDialog({
<div className="min-w-0">
<p className="text-sm font-medium">只用我自己的提示词</p>
<p className="text-muted-foreground mt-0.5 text-xs">
开启后,输入框不再显示管理员配置的提示词(提示词前缀)开关,本账号仅使用自己保存的提示词。
开启后,输入框不再显示管理员配置的提示词前缀开关,本账号仅使用自己保存的提示词。
</p>
</div>
<Switch
checked={preferences.hide_admin_prompt_prefix}
disabled={updatePreferences.isPending}
onCheckedChange={(v) => void handleToggleHideAdmin(v)}
onCheckedChange={(value) => void handleToggleHideAdmin(value)}
/>
</div>
<div className="space-y-3 rounded-md border p-3">
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
onChange={(event) => setTitle(event.target.value)}
placeholder="名称(可选,例如:周报总结)"
/>
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
onChange={(event) => setContent(event.target.value)}
placeholder="提示词内容"
className="min-h-28"
/>
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-xs">关联智能体(可选)</p>
<div className="flex min-w-0 flex-col gap-1">
<p className="text-muted-foreground text-xs">关联智能体</p>
<Select value={agentId} onValueChange={setAgentId}>
<SelectTrigger className="w-full min-w-0 max-w-full overflow-hidden [&>span]:min-w-0 [&>span]:flex-1 [&>span]:truncate [&>span]:text-left">
<SelectValue placeholder="不关联(所有对话可见)" />
<SelectValue placeholder="请选择已配置的智能体" />
</SelectTrigger>
<SelectContent
position="popper"
className="max-h-[40vh] w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)]"
className="max-h-[40vh] w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)] overflow-hidden"
>
<SelectItem value={NO_AGENT}>不关联(所有对话可见)</SelectItem>
{selectableAgents.map((agent) => (
<SelectItem key={agent.id} value={agent.id} className="pr-8">
<SelectItem
key={agent.id}
value={agent.id}
className="max-w-full pr-8"
>
<span className="block max-w-full truncate">
{desensitize(agent.name)}
</span>
@@ -331,13 +323,18 @@ function ManagePromptsDialog({
</Select>
<p className="text-muted-foreground text-xs">
{selectableAgents.length === 0
? "仅可关联你收藏到「我的」的常用智能体;当前还没有收藏任何智能体。"
: "关联后,仅在选中该智能体的对话里显示此提示词。这里只列出你收藏的常用智能体。"}
? "当前账号还没有已配置的智能体。"
: "关联后,仅在选中该智能体的对话里显示此提示词。"}
</p>
</div>
<div className="flex justify-end gap-2">
{editingId && (
<Button variant="outline" size="sm" onClick={resetForm} disabled={saving}>
<Button
variant="outline"
size="sm"
onClick={resetForm}
disabled={saving}
>
取消编辑
</Button>
)}