diff --git a/frontend-web/src/components/workspace/knowledge-save-trigger.tsx b/frontend-web/src/components/workspace/knowledge-save-trigger.tsx index e6bbcfec..17cd62ba 100644 --- a/frontend-web/src/components/workspace/knowledge-save-trigger.tsx +++ b/frontend-web/src/components/workspace/knowledge-save-trigger.tsx @@ -1,17 +1,21 @@ "use client"; import { BookMarked } from "lucide-react"; +import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { useKnowledgeActions } from "@/core/knowledge/hooks"; +import { ModelNameSelect } from "./model-name-select"; import { useThread } from "./messages/context"; import { Tooltip } from "./tooltip"; export function KnowledgeSaveTrigger({ threadId }: { threadId: string }) { const { thread } = useThread(); const { ingestThread } = useKnowledgeActions(); + // Optional model override for the extraction (null = configured default). + const [model, setModel] = useState(null); if (thread.messages.length === 0) { return null; @@ -23,7 +27,7 @@ export function KnowledgeSaveTrigger({ threadId }: { threadId: string }) { try { await ingestThread.mutateAsync({ threadId, - payload: { background: true }, + payload: { background: true, model_name: model }, }); toast.success("已加入知识库保存队列,正在后台整理…"); } catch (error) { @@ -32,16 +36,24 @@ export function KnowledgeSaveTrigger({ threadId }: { threadId: string }) { }; return ( - - - + triggerClassName="h-8 w-[8.5rem]" + /> + + + + ); } diff --git a/frontend-web/src/components/workspace/model-name-select.tsx b/frontend-web/src/components/workspace/model-name-select.tsx new file mode 100644 index 00000000..327aaa63 --- /dev/null +++ b/frontend-web/src/components/workspace/model-name-select.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useModels } from "@/core/models/hooks"; +import { cn } from "@/lib/utils"; + +/** Sentinel for "use the backend's default/configured model" (no override). */ +const DEFAULT_VALUE = "__default__"; + +/** + * Compact dropdown to optionally pick an LLM model for a one-off action + * (skill-name generation, knowledge ingestion, …). The value is the model + * `name`; `null` means "use the configured default" (no override sent). + */ +export function ModelNameSelect({ + value, + onChange, + disabled, + className, + triggerClassName, + defaultLabel = "默认模型", +}: { + value: string | null; + onChange: (modelName: string | null) => void; + disabled?: boolean; + className?: string; + triggerClassName?: string; + defaultLabel?: string; +}) { + const { models } = useModels(); + return ( + + ); +} diff --git a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx index 7dc278ec..c43b2d1e 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -57,6 +57,7 @@ import { import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ModelNameSelect } from "@/components/workspace/model-name-select"; import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog"; import { TagChipList } from "@/components/workspace/tags/tag-chip"; import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog"; @@ -163,6 +164,8 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { const [viewMode, setViewMode] = useViewMode("skill"); const reconcileMutation = useReconcileCustomSkills(); const fillNamesMutation = useGenerateMissingSkillNameZh(); + // Optional model override for the admin "一键补充中文名" action (null = default). + const [fillModel, setFillModel] = useState(null); // Name + tag filtering is resolved server-side via the /api/skills query. const { skills, isLoading, error } = useSkills({ search, tagIds, sort }); @@ -249,13 +252,18 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { const handleFillNames = async () => { try { - const result = await fillNamesMutation.mutateAsync(); + const result = await fillNamesMutation.mutateAsync(fillModel); const okCount = result.generated.length; const failCount = result.failed.length; if (okCount === 0 && failCount === 0) { toast.info("所有技能均已配置中文名称"); } else if (failCount === 0) { toast.success(`已为 ${okCount} 个技能补充中文名称`); + } else if (result.model_error) { + // Failures came from the model itself — say so explicitly. + toast.warning( + `已补充 ${okCount} 个,${failCount} 个因大模型报错失败:${result.model_error}`, + ); } else { toast.warning(`已补充 ${okCount} 个,${failCount} 个生成失败,可稍后重试或手动填写`); } @@ -354,24 +362,37 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { ? t.settings.skills.rescanSkillsRunning : t.settings.skills.rescanSkills} - + {isAdmin && ( + <> + + + + )}