feat: 会商链条启停与深度研究报告结构,并完善问答展示与 8BF 返回。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
be4851555d
commit
0ce060693b
42 files changed
+5320
-3247
No files matched your search
@@ -165,7 +165,7 @@ export const ReasoningTrigger = memo(
|
||||
export type ReasoningContentProps = ComponentProps<
|
||||
typeof CollapsibleContent
|
||||
> & {
|
||||
children: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const ReasoningContent = memo(
|
||||
@@ -178,7 +178,11 @@ export const ReasoningContent = memo(
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
||||
{typeof children === "string" ? (
|
||||
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
BotIcon,
|
||||
BriefcaseBusinessIcon,
|
||||
CheckIcon,
|
||||
DatabaseIcon,
|
||||
GlobeIcon,
|
||||
@@ -83,6 +84,7 @@ interface AgentDraft {
|
||||
/** 会商席位「技能识别强化」开关。 */
|
||||
seatSkillDirective: boolean;
|
||||
knowledgeBaseIds: string[];
|
||||
jobResponsibilities: string;
|
||||
}
|
||||
|
||||
function draftFromAgent(agent: Agent): AgentDraft {
|
||||
@@ -97,6 +99,7 @@ function draftFromAgent(agent: Agent): AgentDraft {
|
||||
templateName: agent.template_name ?? "",
|
||||
seatSkillDirective: agent.seat_skill_directive ?? false,
|
||||
knowledgeBaseIds: agent.llmwiki_knowledge_base_ids ?? [],
|
||||
jobResponsibilities: agent.job_responsibilities ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -338,6 +341,8 @@ export function AgentDetailPanel({
|
||||
request.template_name = draft.templateName.trim();
|
||||
if (draft.seatSkillDirective !== (agent.seat_skill_directive ?? false))
|
||||
request.seat_skill_directive = draft.seatSkillDirective;
|
||||
if (draft.jobResponsibilities.trim() !== (agent.job_responsibilities ?? ""))
|
||||
request.job_responsibilities = draft.jobResponsibilities.trim();
|
||||
if (
|
||||
JSON.stringify(draft.knowledgeBaseIds) !==
|
||||
JSON.stringify(agent.llmwiki_knowledge_base_ids ?? [])
|
||||
@@ -720,6 +725,39 @@ export function AgentDetailPanel({
|
||||
</section>
|
||||
|
||||
|
||||
{/* ── 岗位职责 ───────────────────────────────────────────────────── */}
|
||||
<Section
|
||||
icon={<BriefcaseBusinessIcon className="size-3.5" />}
|
||||
title="岗位职责"
|
||||
>
|
||||
{editing && draft ? (
|
||||
<Textarea
|
||||
value={draft.jobResponsibilities}
|
||||
onChange={(e) =>
|
||||
setDraft({ ...draft, jobResponsibilities: e.currentTarget.value })
|
||||
}
|
||||
placeholder="描述该智能体对应岗位的职责…"
|
||||
className="min-h-20 text-sm"
|
||||
/>
|
||||
) : agent.job_responsibilities?.trim() ? (
|
||||
<p className="text-muted-foreground text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{desensitize(agent.job_responsibilities)}
|
||||
</p>
|
||||
) : canEdit ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={beginEdit}
|
||||
className="text-muted-foreground hover:text-foreground hover:border-border w-full rounded-lg border border-dashed px-3 py-4 text-center text-xs transition-colors"
|
||||
>
|
||||
<PlusIcon className="mr-1 inline size-3.5 align-[-2px]" />
|
||||
添加岗位职责
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-muted-foreground/60 text-sm">暂无岗位职责</p>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
|
||||
{/* ── 常用问题 ───────────────────────────────────────────────────── */}
|
||||
{(showCommonQuestions || editing || questions.length > 0 || canEdit) && (
|
||||
<Section
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
ExternalLinkIcon,
|
||||
FileIcon,
|
||||
Loader2Icon,
|
||||
PencilIcon,
|
||||
SearchIcon,
|
||||
SparklesIcon,
|
||||
} from 'lucide-react';
|
||||
PencilIcon,
|
||||
SearchIcon,
|
||||
SparklesIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type AnchorHTMLAttributes,
|
||||
type ImgHTMLAttributes,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
type AnchorHTMLAttributes,
|
||||
type ImgHTMLAttributes,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
|
||||
import { Loader } from '@/components/ai-elements/loader';
|
||||
@@ -695,6 +695,17 @@ function MessageContent_({
|
||||
const withCitations = applyInlineCitations(rawContent ?? '', displaySources);
|
||||
return stripIncompleteCitationMarker(withCitations);
|
||||
}, [rawContent, isHuman, displaySources]);
|
||||
const reasoningToDisplay = useMemo(() => {
|
||||
if (!reasoningContent) return '';
|
||||
const withCitations = applyInlineCitations(reasoningContent, displaySources, {
|
||||
appendSummary: false,
|
||||
});
|
||||
return stripIncompleteCitationMarker(withCitations);
|
||||
}, [reasoningContent, displaySources]);
|
||||
const markdownRehypePlugins = useMemo(
|
||||
() => [...rehypePlugins, [rehypeKatex, { output: 'html' as const }]],
|
||||
[rehypePlugins],
|
||||
);
|
||||
const parsedHumanContent = useMemo(
|
||||
() =>
|
||||
isHuman
|
||||
@@ -724,23 +735,6 @@ function MessageContent_({
|
||||
);
|
||||
}
|
||||
|
||||
// Reasoning-only AI message (no main response content yet)
|
||||
if (!isHuman && reasoningContent && !rawContent && !multiRoundTrace) {
|
||||
return (
|
||||
<AIElementMessageContent className={className}>
|
||||
<Reasoning isStreaming={isLoading}>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{reasoningContent}</ReasoningContent>
|
||||
</Reasoning>
|
||||
<MessageTokenUsage
|
||||
enabled={tokenUsageEnabled}
|
||||
isLoading={isLoading}
|
||||
message={message}
|
||||
/>
|
||||
</AIElementMessageContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (isHuman) {
|
||||
// Only the user's own question carries a delete affordance, and only once
|
||||
// it exists server-side (real, non-optimistic id). Deleting it cascades to
|
||||
@@ -776,25 +770,43 @@ function MessageContent_({
|
||||
<AIElementMessageContent className={className}>
|
||||
{filesList}
|
||||
<div data-reference-batch-id={message.id} className="contents">
|
||||
{reasoningToDisplay && !multiRoundTrace && (
|
||||
<Reasoning isStreaming={isLoading && !contentToDisplay}>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>
|
||||
{isSingleRoundRag || displaySources.length > 0 ? (
|
||||
<MarkdownContent
|
||||
content={reasoningToDisplay}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={markdownRehypePlugins}
|
||||
components={components}
|
||||
className="text-muted-foreground text-sm"
|
||||
/>
|
||||
) : (
|
||||
reasoningToDisplay
|
||||
)}
|
||||
</ReasoningContent>
|
||||
</Reasoning>
|
||||
)}
|
||||
{multiRoundTrace ? (
|
||||
<MultiRoundRagContent
|
||||
trace={multiRoundTrace}
|
||||
content={contentToDisplay}
|
||||
displaySources={displaySources}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: 'html' }]]}
|
||||
rehypePlugins={markdownRehypePlugins}
|
||||
components={components}
|
||||
/>
|
||||
) : (
|
||||
) : contentToDisplay ? (
|
||||
<AssistantDisplayContent
|
||||
content={contentToDisplay}
|
||||
displaySources={displaySources}
|
||||
isLoading={isLoading}
|
||||
renderSkillDisplayBlocks={false}
|
||||
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: 'html' }]]}
|
||||
rehypePlugins={markdownRehypePlugins}
|
||||
components={components}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
{(isSingleRoundRag || isMultiRoundRag) && displaySources.length > 0 && (
|
||||
<FrontendRagSourcesPanel
|
||||
@@ -840,7 +852,7 @@ function FrontendRagSourcesPanel({
|
||||
const [open, setOpen] = useState(false);
|
||||
if (sources.length === 0) return null;
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen} className="mt-3 w-full max-w-5xl">
|
||||
<Collapsible open={open} onOpenChange={setOpen} className="mt-3 w-full max-w-5xl">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
@@ -946,17 +958,17 @@ function MultiRoundRagContent({
|
||||
isLoading: boolean;
|
||||
rehypePlugins: MarkdownContentProps['rehypePlugins'];
|
||||
components: MarkdownContentProps['components'];
|
||||
}) {
|
||||
const [open, setOpen] = useState(!trace.isComplete);
|
||||
useEffect(() => {
|
||||
// Keep the active turn expanded so each new retrieval is visible as it
|
||||
// arrives, then match GeneralQA by collapsing the completed trace. The
|
||||
// header still carries the total round count for quick scanning.
|
||||
setOpen(!trace.isComplete);
|
||||
}, [trace.isComplete]);
|
||||
}) {
|
||||
const [open, setOpen] = useState(!trace.isComplete);
|
||||
useEffect(() => {
|
||||
// Keep the active turn expanded so each new retrieval is visible as it
|
||||
// arrives, then match GeneralQA by collapsing the completed trace. The
|
||||
// header still carries the total round count for quick scanning.
|
||||
setOpen(!trace.isComplete);
|
||||
}, [trace.isComplete]);
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-0 max-w-5xl space-y-4 overflow-hidden">
|
||||
<div className="w-full min-w-0 max-w-5xl space-y-4 overflow-hidden">
|
||||
<section className="border-border/70 overflow-hidden rounded-xl border bg-muted/20">
|
||||
<button
|
||||
type="button"
|
||||
@@ -970,42 +982,43 @@ function MultiRoundRagContent({
|
||||
<Loader2Icon className="text-primary size-4 shrink-0 animate-spin" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">多轮检索思考过程</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{trace.isComplete
|
||||
? `共 ${trace.rounds.length} 轮推理与检索,可展开查看细节`
|
||||
: '正在分析问题并检索资料'}
|
||||
</div>
|
||||
<div className="text-sm font-medium">多轮检索思考过程</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{trace.isComplete
|
||||
? `共 ${trace.rounds.length} 轮推理与检索,可展开查看细节`
|
||||
: '正在分析问题并检索资料'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={cn('text-muted-foreground size-4 transition-transform', !open && '-rotate-90')}
|
||||
/>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="border-border/60 space-y-3 border-t px-4 py-3">
|
||||
{trace.rounds.map((round, index) => (
|
||||
<div
|
||||
key={round.id}
|
||||
className={cn(
|
||||
'relative pl-5',
|
||||
index < trace.rounds.length - 1 && 'pb-1',
|
||||
)}
|
||||
>
|
||||
{index < trace.rounds.length - 1 && (
|
||||
<span className="bg-border absolute bottom-0 left-[7px] top-8 w-px" />
|
||||
)}
|
||||
<span className="border-primary bg-background absolute left-0 top-4 size-3.5 rounded-full border-2" />
|
||||
<MultiRoundRagRoundCard
|
||||
index={index + 1}
|
||||
round={round}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{open && (
|
||||
<div className="border-border/60 space-y-3 border-t px-4 py-3">
|
||||
{trace.rounds.map((round, index) => (
|
||||
<div
|
||||
key={round.id}
|
||||
className={cn(
|
||||
'relative pl-5',
|
||||
index < trace.rounds.length - 1 && 'pb-1',
|
||||
)}
|
||||
>
|
||||
{index < trace.rounds.length - 1 && (
|
||||
<span className="bg-border absolute bottom-0 left-[7px] top-8 w-px" />
|
||||
)}
|
||||
<span className="border-primary bg-background absolute left-0 top-4 size-3.5 rounded-full border-2" />
|
||||
<MultiRoundRagRoundCard
|
||||
index={index + 1}
|
||||
round={round}
|
||||
displaySources={displaySources}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section className="border-border/70 rounded-xl border p-4">
|
||||
@@ -1030,240 +1043,249 @@ function MultiRoundRagContent({
|
||||
);
|
||||
}
|
||||
|
||||
function MultiRoundRagRoundCard({
|
||||
function MultiRoundRagRoundCard({
|
||||
index,
|
||||
round,
|
||||
displaySources,
|
||||
isLoading,
|
||||
rehypePlugins,
|
||||
components,
|
||||
}: {
|
||||
index: number;
|
||||
round: MultiRoundRagRound;
|
||||
displaySources: RuntimeDisplaySource[];
|
||||
isLoading: boolean;
|
||||
rehypePlugins: MarkdownContentProps['rehypePlugins'];
|
||||
components: MarkdownContentProps['components'];
|
||||
}) {
|
||||
const [open, setOpen] = useState(!round.query || round.isSearching);
|
||||
useEffect(() => {
|
||||
// A newly created round stays expanded to expose its thought and loading
|
||||
// query. Once that query has returned, retain only the round header until
|
||||
// the user asks to inspect the completed detail again.
|
||||
setOpen(!round.query || round.isSearching);
|
||||
}, [round.id, round.isSearching, round.query]);
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<article className="border-border/60 bg-background/80 overflow-hidden rounded-xl border shadow-sm">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-muted/40 focus-visible:ring-ring flex min-h-11 w-full min-w-0 items-center gap-2 px-3 py-2.5 text-left text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset"
|
||||
>
|
||||
<span className="bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold">
|
||||
{index}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{round.title}</span>
|
||||
{round.isSearching ? (
|
||||
<span className="bg-primary/10 text-primary inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-1 text-[11px] font-medium">
|
||||
<Loader2Icon className="size-3 motion-reduce:animate-none animate-spin" />
|
||||
检索中
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground shrink-0 text-[11px]">已完成</span>
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'text-muted-foreground size-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none',
|
||||
!open && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="border-border/50 border-t px-3 pb-3">
|
||||
<div className="pt-3">
|
||||
{round.thought && (
|
||||
<MultiRoundRagDetailSection
|
||||
title="思考内容"
|
||||
icon={<SparklesIcon className="text-primary size-4" />}
|
||||
>
|
||||
<MarkdownContent
|
||||
content={round.thought}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
className="text-muted-foreground text-sm"
|
||||
/>
|
||||
</MultiRoundRagDetailSection>
|
||||
)}
|
||||
{round.body && (
|
||||
<section className="border-border/50 mt-3 rounded-lg border p-3">
|
||||
<div className="text-muted-foreground mb-2 text-xs font-medium">本轮正文</div>
|
||||
<MarkdownContent
|
||||
content={round.body}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
className="text-sm"
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
{(round.query || round.isSearching) && (
|
||||
<div
|
||||
className={cn(
|
||||
'border-primary/20 bg-primary/5 text-primary mt-3 inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1.5 text-xs',
|
||||
round.isSearching && 'animate-pulse motion-reduce:animate-none',
|
||||
)}
|
||||
aria-live={round.isSearching ? 'polite' : undefined}
|
||||
>
|
||||
{round.isSearching ? (
|
||||
<Loader2Icon className="size-3.5 shrink-0 motion-reduce:animate-none animate-spin" />
|
||||
) : (
|
||||
<SearchIcon className="size-3.5 shrink-0" />
|
||||
)}
|
||||
<span className="shrink-0 text-muted-foreground">检索词</span>
|
||||
<span className="min-w-0 truncate font-medium">
|
||||
{round.query || '正在生成检索词…'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(round.query || round.isSearching || round.retrievals.length > 0) && (
|
||||
<MultiRoundRagDetailSection
|
||||
defaultOpen={!round.isSearching}
|
||||
title="检索内容"
|
||||
icon={<FileIcon className="text-primary size-3.5" />}
|
||||
suffix={<span className="text-muted-foreground text-xs">{round.retrievals.length} 条</span>}
|
||||
>
|
||||
{round.isSearching ? (
|
||||
<div className="text-muted-foreground flex items-center gap-2 py-1 text-sm">
|
||||
<Loader2Icon className="text-primary size-4 motion-reduce:animate-none animate-spin" />
|
||||
正在召回相关内容…
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{round.retrievals.length > 0 ? (
|
||||
round.retrievals.map((item, resultIndex) => (
|
||||
<MultiRoundRagResultCard
|
||||
key={`${item.id ?? item.title}-${resultIndex}`}
|
||||
item={item}
|
||||
index={resultIndex + 1}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted-foreground py-1 text-sm">
|
||||
本轮未召回相关资料。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</MultiRoundRagDetailSection>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</article>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiRoundRagDetailSection({
|
||||
title,
|
||||
icon,
|
||||
suffix,
|
||||
defaultOpen = true,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
suffix?: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
useEffect(() => setOpen(defaultOpen), [defaultOpen]);
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<section className="border-border/50 bg-muted/20 mt-3 overflow-hidden rounded-lg border">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-muted/40 focus-visible:ring-ring flex min-h-11 w-full min-w-0 items-center gap-2 px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset"
|
||||
>
|
||||
{icon}
|
||||
<span className="min-w-0 flex-1 text-sm font-medium">{title}</span>
|
||||
{suffix}
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'text-muted-foreground size-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none',
|
||||
!open && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="border-border/50 border-t px-3 pb-3 pt-2.5">
|
||||
{children}
|
||||
</CollapsibleContent>
|
||||
</section>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiRoundRagResultCard({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: MultiRoundRagRound['retrievals'][number];
|
||||
index: number;
|
||||
}) {
|
||||
const [open, setOpen] = useState(index === 1);
|
||||
const sourceMeta = [item.source, item.time].filter(Boolean).join(' · ');
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<div className="border-border/60 hover:border-primary/30 bg-background rounded-lg border transition-colors">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-muted/40 focus-visible:ring-ring flex min-h-11 w-full min-w-0 items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset"
|
||||
>
|
||||
<span className="bg-primary/10 text-primary flex size-5 shrink-0 items-center justify-center rounded-md text-[11px] font-semibold">
|
||||
{index}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium" title={item.title}>
|
||||
{item.title}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'text-muted-foreground size-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none',
|
||||
!open && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="border-border/50 border-t px-3 pb-3 pt-2.5">
|
||||
<p className="text-muted-foreground text-sm leading-6 whitespace-pre-wrap">
|
||||
{item.content || '暂无资料摘要'}
|
||||
</p>
|
||||
{(sourceMeta || item.url) && (
|
||||
<div className="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
|
||||
{sourceMeta && <span>{sourceMeta}</span>}
|
||||
{item.url && (
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
查看原文
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
}) {
|
||||
const [open, setOpen] = useState(!round.query || round.isSearching);
|
||||
useEffect(() => {
|
||||
// A newly created round stays expanded to expose its thought and loading
|
||||
// query. Once that query has returned, retain only the round header until
|
||||
// the user asks to inspect the completed detail again.
|
||||
setOpen(!round.query || round.isSearching);
|
||||
}, [round.id, round.isSearching, round.query]);
|
||||
const thoughtContent = useMemo(
|
||||
() =>
|
||||
applyInlineCitations(round.thought, displaySources, {
|
||||
appendSummary: false,
|
||||
}),
|
||||
[displaySources, round.thought],
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<article className="border-border/60 bg-background/80 overflow-hidden rounded-xl border shadow-sm">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-muted/40 focus-visible:ring-ring flex min-h-11 w-full min-w-0 items-center gap-2 px-3 py-2.5 text-left text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset"
|
||||
>
|
||||
<span className="bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold">
|
||||
{index}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{round.title}</span>
|
||||
{round.isSearching ? (
|
||||
<span className="bg-primary/10 text-primary inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-1 text-[11px] font-medium">
|
||||
<Loader2Icon className="size-3 motion-reduce:animate-none animate-spin" />
|
||||
检索中
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground shrink-0 text-[11px]">已完成</span>
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'text-muted-foreground size-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none',
|
||||
!open && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="border-border/50 border-t px-3 pb-3">
|
||||
<div className="pt-3">
|
||||
{round.thought && (
|
||||
<MultiRoundRagDetailSection
|
||||
title="思考内容"
|
||||
icon={<SparklesIcon className="text-primary size-4" />}
|
||||
>
|
||||
<MarkdownContent
|
||||
content={thoughtContent}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
className="text-muted-foreground text-sm"
|
||||
/>
|
||||
</MultiRoundRagDetailSection>
|
||||
)}
|
||||
{round.body && (
|
||||
<section className="border-border/50 mt-3 rounded-lg border p-3">
|
||||
<div className="text-muted-foreground mb-2 text-xs font-medium">本轮正文</div>
|
||||
<MarkdownContent
|
||||
content={round.body}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
className="text-sm"
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
{(round.query || round.isSearching) && (
|
||||
<div
|
||||
className={cn(
|
||||
'border-primary/20 bg-primary/5 text-primary mt-3 inline-flex max-w-full items-center gap-1.5 rounded-full border px-2.5 py-1.5 text-xs',
|
||||
round.isSearching && 'animate-pulse motion-reduce:animate-none',
|
||||
)}
|
||||
aria-live={round.isSearching ? 'polite' : undefined}
|
||||
>
|
||||
{round.isSearching ? (
|
||||
<Loader2Icon className="size-3.5 shrink-0 motion-reduce:animate-none animate-spin" />
|
||||
) : (
|
||||
<SearchIcon className="size-3.5 shrink-0" />
|
||||
)}
|
||||
<span className="shrink-0 text-muted-foreground">检索词</span>
|
||||
<span className="min-w-0 truncate font-medium">
|
||||
{round.query || '正在生成检索词…'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(round.query || round.isSearching || round.retrievals.length > 0) && (
|
||||
<MultiRoundRagDetailSection
|
||||
defaultOpen={!round.isSearching}
|
||||
title="检索内容"
|
||||
icon={<FileIcon className="text-primary size-3.5" />}
|
||||
suffix={<span className="text-muted-foreground text-xs">{round.retrievals.length} 条</span>}
|
||||
>
|
||||
{round.isSearching ? (
|
||||
<div className="text-muted-foreground flex items-center gap-2 py-1 text-sm">
|
||||
<Loader2Icon className="text-primary size-4 motion-reduce:animate-none animate-spin" />
|
||||
正在召回相关内容…
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{round.retrievals.length > 0 ? (
|
||||
round.retrievals.map((item, resultIndex) => (
|
||||
<MultiRoundRagResultCard
|
||||
key={`${item.id ?? item.title}-${resultIndex}`}
|
||||
item={item}
|
||||
index={resultIndex + 1}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted-foreground py-1 text-sm">
|
||||
本轮未召回相关资料。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</MultiRoundRagDetailSection>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</article>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiRoundRagDetailSection({
|
||||
title,
|
||||
icon,
|
||||
suffix,
|
||||
defaultOpen = true,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
suffix?: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
useEffect(() => setOpen(defaultOpen), [defaultOpen]);
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<section className="border-border/50 bg-muted/20 mt-3 overflow-hidden rounded-lg border">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-muted/40 focus-visible:ring-ring flex min-h-11 w-full min-w-0 items-center gap-2 px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset"
|
||||
>
|
||||
{icon}
|
||||
<span className="min-w-0 flex-1 text-sm font-medium">{title}</span>
|
||||
{suffix}
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'text-muted-foreground size-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none',
|
||||
!open && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="border-border/50 border-t px-3 pb-3 pt-2.5">
|
||||
{children}
|
||||
</CollapsibleContent>
|
||||
</section>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiRoundRagResultCard({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: MultiRoundRagRound['retrievals'][number];
|
||||
index: number;
|
||||
}) {
|
||||
const [open, setOpen] = useState(index === 1);
|
||||
const sourceMeta = [item.source, item.time].filter(Boolean).join(' · ');
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<div className="border-border/60 hover:border-primary/30 bg-background rounded-lg border transition-colors">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:bg-muted/40 focus-visible:ring-ring flex min-h-11 w-full min-w-0 items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset"
|
||||
>
|
||||
<span className="bg-primary/10 text-primary flex size-5 shrink-0 items-center justify-center rounded-md text-[11px] font-semibold">
|
||||
{index}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium" title={item.title}>
|
||||
{item.title}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'text-muted-foreground size-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none',
|
||||
!open && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="border-border/50 border-t px-3 pb-3 pt-2.5">
|
||||
<p className="text-muted-foreground text-sm leading-6 whitespace-pre-wrap">
|
||||
{item.content || '暂无资料摘要'}
|
||||
</p>
|
||||
{(sourceMeta || item.url) && (
|
||||
<div className="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
|
||||
{sourceMeta && <span>{sourceMeta}</span>}
|
||||
{item.url && (
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
查看原文
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSingleRoundRagSourceUrl(
|
||||
source: RuntimeDisplaySource | null,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DatabaseZapIcon,
|
||||
GaugeIcon,
|
||||
HistoryIcon,
|
||||
ListTreeIcon,
|
||||
MessageCircleQuestionIcon,
|
||||
ScrollTextIcon,
|
||||
Settings2Icon,
|
||||
@@ -226,6 +227,16 @@ export function WorkspaceNavMenu() {
|
||||
技能管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/* 与全量模式左下角「设置和更多」下拉里的菜单管理同入口(管理员):
|
||||
简洁模式没有全局侧边栏,这里放一份保证两种模式都能进菜单管理。 */}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/strategy/admin/menu-management")}
|
||||
>
|
||||
<ListTreeIcon />
|
||||
菜单管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/tags")}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user