diff --git a/frontend-web/package.json b/frontend-web/package.json index 1613e744..3b00ed53 100644 --- a/frontend-web/package.json +++ b/frontend-web/package.json @@ -57,6 +57,7 @@ "cmdk": "^1.1.1", "codemirror": "^6.0.2", "date-fns": "^4.1.0", + "dayjs": "^1.11.21", "docx": "^9.5.1", "dotenv": "^17.2.3", "embla-carousel-react": "^8.6.0", diff --git a/frontend-web/src/components/workspace/chats/chat-box.tsx b/frontend-web/src/components/workspace/chats/chat-box.tsx index 9af9b39d..fe9a8262 100644 --- a/frontend-web/src/components/workspace/chats/chat-box.tsx +++ b/frontend-web/src/components/workspace/chats/chat-box.tsx @@ -16,7 +16,11 @@ import { apiFetch } from "@/core/api"; import { getBackendBaseURL } from "@/core/config"; import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings"; import { useSkills } from "@/core/skills/hooks"; -import { normalizeDisplaySources } from "@/core/skill-display/display-adapter"; +import type { SkillDisplayConfig } from "@/core/skills/type"; +import { + displayConfigFromResultSet, + normalizeDisplaySources, +} from "@/core/skill-display/display-adapter"; import { buildSkillDisplayMap, collectRuntimeDisplaySources, @@ -603,9 +607,10 @@ function adaptBackendSourceWithSkillDisplay( if (!match) { return null; } + const display = findDisplayConfigForBackendSource(source, match.display); const normalized = normalizeDisplaySources( - buildPayloadAtPath(match.display.resultPath || "results", [rawItem]), - match.display, + buildPayloadAtPath(display.resultPath || "results", [rawItem]), + display, )[0]; if (!normalized) { return null; @@ -614,13 +619,27 @@ function adaptBackendSourceWithSkillDisplay( ...source, ...normalized, skillName: match.skillName, - mode: match.display.mode, - tableColumns: match.display.tableColumns, + mode: display.mode, + tableColumns: display.tableColumns, index: source.index, raw: source.raw, }; } +function findDisplayConfigForBackendSource( + source: BackendReferenceSource, + display: SkillDisplayConfig, +) { + const resultSetKey = source.resultSetKey; + if (resultSetKey) { + const resultSet = display.resultSets?.find((item) => item.key === resultSetKey); + if (resultSet) { + return displayConfigFromResultSet(display, resultSet); + } + } + return { ...display, resultSets: [] }; +} + function findBackendSourceDisplay( source: BackendReferenceSource, skillDisplayMap: ReturnType, diff --git a/frontend-web/src/components/workspace/messages/message-group.tsx b/frontend-web/src/components/workspace/messages/message-group.tsx index d354373d..b963fd6d 100644 --- a/frontend-web/src/components/workspace/messages/message-group.tsx +++ b/frontend-web/src/components/workspace/messages/message-group.tsx @@ -258,12 +258,12 @@ function ToolCall({ null); const displayItems = displayMatch && result !== undefined - ? normalizeDisplaySources(result, displayMatch.display).map((item, index) => ({ - ...item, - index: index + 1, - skillName: displayMatch.skillName, - mode: displayMatch.display.mode, - })) + ? normalizeDisplaySources(result, displayMatch.display).map((item, index) => ({ + ...item, + index: index + 1, + skillName: displayMatch.skillName, + mode: item.displayMode ?? displayMatch.display.mode, + })) : []; if (displayItems.length > 0) { return ( diff --git a/frontend-web/src/components/workspace/skill-display-adapter-panel.tsx b/frontend-web/src/components/workspace/skill-display-adapter-panel.tsx index 67be155f..c469c1bf 100644 --- a/frontend-web/src/components/workspace/skill-display-adapter-panel.tsx +++ b/frontend-web/src/components/workspace/skill-display-adapter-panel.tsx @@ -17,6 +17,7 @@ import { import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { + displayConfigFromResultSet, emptyDisplayConfig, inferDisplayConfig, normalizeDisplaySources, @@ -27,9 +28,29 @@ import { getSkillDisplaySample, } from "@/core/skills/api"; import { useSetSkillDisplay } from "@/core/skills/hooks"; -import type { Skill, SkillDisplayConfig, SkillDisplayMode, SkillLinkStrategy } from "@/core/skills/type"; +import type { + Skill, + SkillDisplayConfig, + SkillDisplayMode, + SkillDisplayResultSetConfig, + SkillLinkStrategy, +} from "@/core/skills/type"; -const fieldLabels: Array<[keyof SkillDisplayConfig, string]> = [ +type DisplayFieldKey = + | "resultPath" + | "titleField" + | "summaryField" + | "contentField" + | "sourceField" + | "authorField" + | "typeField" + | "timeField" + | "scoreField" + | "urlField" + | "idField" + | "linkTemplate"; + +const fieldLabels: Array<[DisplayFieldKey, string]> = [ ["resultPath", "结果列表路径"], ["titleField", "标题字段"], ["summaryField", "摘要字段"], @@ -71,14 +92,56 @@ function formatSample(sample: Record | null | undefined) { return sample ? JSON.stringify(sample, null, 2) : ""; } +function firstResultSetKey(display: SkillDisplayConfig | null | undefined) { + return display?.resultSets?.[0]?.key ?? null; +} + export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { const queryClient = useQueryClient(); const setDisplay = useSetSkillDisplay(); const lastSkillNameRef = useRef(skill.name); const [draft, setDraft] = useState(() => skill.display ?? emptyDisplayConfig()); + const [activeResultSetKey, setActiveResultSetKey] = useState(() => + firstResultSetKey(skill.display), + ); const [sampleText, setSampleText] = useState(() => formatSample(skill.displaySample)); - const [tableColumnsText, setTableColumnsText] = useState(() => JSON.stringify((skill.display ?? emptyDisplayConfig()).tableColumns ?? [], null, 2)); + const [tableColumnsText, setTableColumnsText] = useState(() => { + const display = skill.display ?? emptyDisplayConfig(); + const resultSet = display.resultSets?.[0]; + return JSON.stringify((resultSet ?? display).tableColumns ?? [], null, 2); + }); const [loadingDbConfig, setLoadingDbConfig] = useState(false); + const resultSets = draft.resultSets ?? []; + const activeResultSet = + resultSets.find((resultSet) => resultSet.key === activeResultSetKey) ?? null; + const editingConfig = activeResultSet ?? draft; + const previewConfig = activeResultSet + ? displayConfigFromResultSet(draft, activeResultSet) + : draft; + + function updateEditingConfig( + key: K, + value: SkillDisplayResultSetConfig[K], + ) { + if (!activeResultSet) { + setDraft(updateConfig(draft, key as keyof SkillDisplayConfig, value as never)); + return; + } + setDraft({ + ...draft, + resultSets: resultSets.map((resultSet) => + resultSet.key === activeResultSet.key + ? { ...resultSet, [key]: value } + : resultSet, + ), + }); + } + + function selectResultSet(key: string) { + const resultSet = resultSets.find((item) => item.key === key) ?? null; + setActiveResultSetKey(resultSet?.key ?? null); + setTableColumnsText(JSON.stringify((resultSet ?? draft).tableColumns ?? [], null, 2)); + } // Echo server state into the form — on mount, on skill switch, AND after // a successful save (the refetched skill object has a new `display` identity). @@ -88,7 +151,9 @@ export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { useEffect(() => { const display = skill.display ?? emptyDisplayConfig(); setDraft(display); - setTableColumnsText(JSON.stringify(display.tableColumns ?? [], null, 2)); + const resultSet = display.resultSets?.[0]; + setActiveResultSetKey(resultSet?.key ?? null); + setTableColumnsText(JSON.stringify((resultSet ?? display).tableColumns ?? [], null, 2)); }, [skill.name, skill.display]); useEffect(() => { @@ -101,8 +166,10 @@ export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { return; } const display = fresh.display ?? emptyDisplayConfig(); + const resultSet = display.resultSets?.[0]; setDraft(display); - setTableColumnsText(JSON.stringify(display.tableColumns ?? [], null, 2)); + setActiveResultSetKey(resultSet?.key ?? null); + setTableColumnsText(JSON.stringify((resultSet ?? display).tableColumns ?? [], null, 2)); setSampleText(formatSample(fresh.displaySample)); queryClient.setQueryData(["skill", skill.name], fresh); queryClient.setQueriesData({ queryKey: ["skills"] }, (current) => @@ -186,8 +253,8 @@ export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { }, [sampleText]); const previewItems = useMemo(() => ( - samplePayload ? normalizeDisplaySources(samplePayload, draft) : [] - ), [draft, samplePayload]); + samplePayload ? normalizeDisplaySources(samplePayload, previewConfig) : [] + ), [previewConfig, samplePayload]); function generateDraft() { if (!samplePayload) { @@ -196,8 +263,14 @@ export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { } const nextDraft = inferDisplayConfig(samplePayload, skill.name); setDraft(nextDraft); - setTableColumnsText(JSON.stringify(nextDraft.tableColumns ?? [], null, 2)); - toast.success("已生成展示适配草稿"); + const resultSet = nextDraft.resultSets?.[0]; + setActiveResultSetKey(resultSet?.key ?? null); + setTableColumnsText(JSON.stringify((resultSet ?? nextDraft).tableColumns ?? [], null, 2)); + toast.success( + resultSet + ? `已识别 ${nextDraft.resultSets?.length ?? 0} 个结果集,可分别配置` + : "已生成展示适配草稿", + ); } async function loadSampleAndGenerate() { @@ -219,8 +292,14 @@ export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { ); const nextDraft = inferDisplayConfig(response.sample, skill.name); setDraft(nextDraft); - setTableColumnsText(JSON.stringify(nextDraft.tableColumns ?? [], null, 2)); - toast.success("已获取样例并生成展示适配"); + const resultSet = nextDraft.resultSets?.[0]; + setActiveResultSetKey(resultSet?.key ?? null); + setTableColumnsText(JSON.stringify((resultSet ?? nextDraft).tableColumns ?? [], null, 2)); + toast.success( + resultSet + ? `已获取样例,并识别 ${nextDraft.resultSets?.length ?? 0} 个结果集` + : "已获取样例并生成展示适配", + ); } catch (err) { generateDraft(); toast.error(err instanceof Error ? err.message : String(err)); @@ -235,8 +314,10 @@ export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { sample: samplePayload ?? undefined, }); const display = saved.display ?? emptyDisplayConfig(); + const resultSet = display.resultSets?.find((item) => item.key === activeResultSetKey) ?? display.resultSets?.[0]; setDraft(display); - setTableColumnsText(JSON.stringify(display.tableColumns ?? [], null, 2)); + setActiveResultSetKey(resultSet?.key ?? null); + setTableColumnsText(JSON.stringify((resultSet ?? display).tableColumns ?? [], null, 2)); setSampleText(formatSample(saved.displaySample)); toast.success("展示适配已保存"); } catch (err) { @@ -248,6 +329,7 @@ export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { try { await setDisplay.mutateAsync({ skillName: skill.name, display: null, sample: null }); setDraft(emptyDisplayConfig()); + setActiveResultSetKey(null); setTableColumnsText(JSON.stringify([], null, 2)); toast.success("展示适配已清除"); } catch (err) { @@ -283,8 +365,8 @@ export function SkillDisplayAdapterPanel({ skill }: { skill: Skill }) { + {resultSets.length > 0 && ( +
+ + {activeResultSet && ( + + )} +
+ )} +
{fieldLabels.map(([field, label]) => ( ))}
- {draft.mode === "table" && ( + {editingConfig.mode === "table" && (