feat: 完善 8BF 会商第一步产物与智能体列表,第二步对话区对齐第一步样式
抽出共用分析成果卡片,第一步展示第二、三步 Markdown 产物;智能体列表对齐岗位会商选择样式。第二步对话面板改为深蓝顶栏,第一步保留可配置返回按钮。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
30799f17be
commit
5e1765d889
17 files changed
+1254
-230
No files matched your search
@@ -54,6 +54,10 @@ window.__CM_RUNTIME_CONFIG__ = {
|
||||
// 不影响原多智能体会商使用的任务详情接口。
|
||||
VITE_8BF_TASK_API_BASE_URL: "https://ch1.b.uat.4.cn/zncm",
|
||||
|
||||
// 8BF 会商页右上角「返回」按钮的跳转地址(当前窗口 window.location.href)。
|
||||
// 部署后直接改 dist/runtime-config.js 即可,无需重新打包。留空则点击时提示未配置。
|
||||
VITE_8BF_ROUNDTABLE_BACK_URL: "",
|
||||
|
||||
// 佳来模型对话接口。
|
||||
VITE_AIPI_CHAT_URL: "https://ch1.b.uat.4.cn/gpt",
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12" fill="none" aria-hidden="true">
|
||||
<path d="M13 6H1.5M1.5 6L6.2 1.4M1.5 6L6.2 10.6" stroke="#A2E1F9" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 258 B |
@@ -0,0 +1,104 @@
|
||||
import { FileOutput } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import responsibilityIcon from "@/assets/position-roundtable/责任岗位.svg";
|
||||
import resultTimeIcon from "@/assets/position-roundtable/时间.svg";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import "./analysis-results.css";
|
||||
import type { AnalysisResultCardItem } from "./types";
|
||||
|
||||
function formatResultTime(value: string | undefined, fallback: string): string {
|
||||
if (!value) return fallback;
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function AnalysisResultCardList({
|
||||
items,
|
||||
emptyTitle = "暂无交付产物",
|
||||
emptyDescription = "第二步研讨与第三步总结生成 Markdown 后,产物会显示在这里。",
|
||||
padded = true,
|
||||
onOpen,
|
||||
}: {
|
||||
items: AnalysisResultCardItem[];
|
||||
emptyTitle?: string;
|
||||
emptyDescription?: string;
|
||||
padded?: boolean;
|
||||
onOpen: (item: AnalysisResultCardItem) => void;
|
||||
}) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="position-roundtable-analysis-results-empty flex h-full min-h-[260px] flex-col items-center justify-center p-6 text-center">
|
||||
<FileOutput className="mb-3 size-8 text-muted-foreground/50" />
|
||||
<p className="text-sm font-semibold">{emptyTitle}</p>
|
||||
<p className="mt-1 max-w-[240px] text-xs leading-5 text-muted-foreground">
|
||||
{emptyDescription}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("analysis-result-card-list", padded && "p-[15px]")}>
|
||||
{items.map((item, index) => {
|
||||
const displayedTime = formatResultTime(
|
||||
item.updatedAt,
|
||||
item.fallbackTime || "刚刚",
|
||||
);
|
||||
const displayedProductName = item.delivered ? item.productName : "未交付";
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className="position-roundtable-analysis-result-card"
|
||||
onClick={() => {
|
||||
if (!item.delivered) {
|
||||
toast.info(
|
||||
item.unavailableMessage ||
|
||||
`「${desensitize(item.agentName)}」尚未交付可预览的 Markdown 文件。`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
onOpen(item);
|
||||
}}
|
||||
>
|
||||
<div className="position-roundtable-analysis-result-card-body">
|
||||
<div
|
||||
className={cn(
|
||||
"position-roundtable-analysis-result-tag",
|
||||
`position-roundtable-analysis-result-tag--${(index % 6) + 1}`,
|
||||
)}
|
||||
aria-label={desensitize(item.agentName)}
|
||||
>
|
||||
<span>{desensitize(item.agentName)}</span>
|
||||
</div>
|
||||
<p className="position-roundtable-analysis-result-product">
|
||||
{desensitize(displayedProductName)}
|
||||
</p>
|
||||
<footer className="position-roundtable-analysis-result-meta">
|
||||
<span>
|
||||
<img src={responsibilityIcon} alt="" aria-hidden="true" />
|
||||
{desensitize(item.positionName)}
|
||||
</span>
|
||||
<i aria-hidden="true" />
|
||||
<span>
|
||||
<img src={resultTimeIcon} alt="" aria-hidden="true" />
|
||||
{displayedTime}
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FileOutput, Loader2 } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ArtifactFilePreview } from "@/components/workspace/artifacts";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { loadArtifactContent } from "@/core/artifacts/loader";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { Button as TButton } from "tdesign-react";
|
||||
|
||||
import type { AnalysisResultCardItem } from "./types";
|
||||
|
||||
function isArtifactErrorContent(content: string | null | undefined): boolean {
|
||||
const trimmed = content?.trim();
|
||||
if (!trimmed || !trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
const detail = parsed.detail ?? parsed.message ?? parsed.error;
|
||||
return typeof detail === "string" && detail.trim().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackContent(item: AnalysisResultCardItem | null): string {
|
||||
return (
|
||||
item?.artifactContent?.trim() ||
|
||||
item?.summary?.trim() ||
|
||||
"暂无产物正文"
|
||||
);
|
||||
}
|
||||
|
||||
export function AnalysisResultDetailDialog({
|
||||
item,
|
||||
onOpenChange,
|
||||
extraBody,
|
||||
footerStart,
|
||||
}: {
|
||||
item: AnalysisResultCardItem | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
extraBody?: ReactNode;
|
||||
footerStart?: ReactNode;
|
||||
}) {
|
||||
const artifactThreadId = item?.artifactThreadId || null;
|
||||
const artifactPathValue = item?.artifactPath || "";
|
||||
const artifactContentQuery = useQuery({
|
||||
queryKey: [
|
||||
"analysis-results",
|
||||
"deliverable-artifact-content",
|
||||
artifactThreadId,
|
||||
artifactPathValue,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!artifactThreadId || !artifactPathValue) return "";
|
||||
const result = await loadArtifactContent({
|
||||
filepath: artifactPathValue,
|
||||
threadId: artifactThreadId,
|
||||
isMock: false,
|
||||
});
|
||||
return result.content;
|
||||
},
|
||||
enabled: Boolean(item && artifactThreadId && artifactPathValue),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const loadedArtifactContent =
|
||||
typeof artifactContentQuery.data === "string" ? artifactContentQuery.data : "";
|
||||
const loadedArtifactIsError = isArtifactErrorContent(loadedArtifactContent);
|
||||
const artifactContent =
|
||||
loadedArtifactContent.trim() && !loadedArtifactIsError ? loadedArtifactContent : null;
|
||||
const displayedContent = artifactContent || fallbackContent(item);
|
||||
const artifactLoadFailed =
|
||||
(artifactContentQuery.isError || loadedArtifactIsError) &&
|
||||
!item?.artifactContent?.trim();
|
||||
|
||||
return (
|
||||
<Dialog open={Boolean(item)} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
overlayClassName="!z-[6000]"
|
||||
className="!z-[6001] flex max-h-[86vh] w-[900px] !max-w-[calc(100vw-2rem)] flex-col gap-0 overflow-hidden p-0"
|
||||
>
|
||||
<DialogHeader className="shrink-0 border-b px-5 py-4 dark:border-[#2b8ffe]/25">
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<FileOutput className="size-4 text-primary" />
|
||||
交付产物详情
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{!item ? null : (
|
||||
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-extrabold text-slate-900 dark:text-[#e8edf5]">
|
||||
{desensitize(item.productName)}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{desensitize(item.agentName)} · {desensitize(item.positionName)}
|
||||
</p>
|
||||
</div>
|
||||
{extraBody}
|
||||
<div className="mt-4 rounded-xl border border-slate-200 bg-white p-3 dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/65">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-bold text-slate-700 dark:text-[#e8edf5]">产物内容</p>
|
||||
</div>
|
||||
<div className="h-[52vh] min-h-[320px] overflow-hidden rounded-lg border border-slate-100 bg-slate-50/60 dark:border-[#2b8ffe]/20 dark:bg-[#021d3d]/70">
|
||||
{artifactContentQuery.isLoading ? (
|
||||
<div className="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground dark:text-[#8aa0c4]">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
正在加载 Markdown 文件内容…
|
||||
</div>
|
||||
) : (
|
||||
<ArtifactFilePreview
|
||||
content={displayedContent}
|
||||
language="markdown"
|
||||
threadId={artifactThreadId ?? ""}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{artifactLoadFailed && (
|
||||
<p className="mt-2 text-[11px] text-amber-600 dark:text-amber-300">
|
||||
Markdown 文件内容加载失败,当前展示的是已保存正文。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter className="shrink-0 border-t px-5 py-3 dark:border-[#2b8ffe]/25">
|
||||
{footerStart}
|
||||
<TButton variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</TButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
.analysis-result-card-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-card {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 102px;
|
||||
box-sizing: border-box;
|
||||
padding: 40px 12px 12px;
|
||||
border: 1px solid rgba(85, 166, 254, 0.1);
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 98, 217, 0.1);
|
||||
color: #ffffff;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-card:hover,
|
||||
.position-roundtable-analysis-result-card:focus-visible {
|
||||
border-color: #0062d9;
|
||||
background: rgba(0, 98, 217, 0.3);
|
||||
box-shadow: 0 4px 14px rgba(0, 44, 110, 0.18);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-card-body {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-tag {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: -12px;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: calc(100% + 12px);
|
||||
height: 26px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 22px 0 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px 0 0 0;
|
||||
background: rgba(212, 120, 0, 0.5);
|
||||
color: #ffffff;
|
||||
font-family: "Noto Sans S Chinese", "Noto Sans SC", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
clip-path: polygon(0 0, 100% 0, calc(100% - 14px) 100%, 0 100%);
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-tag--1 { background: rgba(212, 120, 0, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--2 { background: rgba(207, 19, 34, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--3 { background: rgba(29, 171, 123, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--4 { background: rgba(132, 85, 230, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--5 { background: rgba(201, 181, 0, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--6 { background: rgba(69, 195, 243, 0.5); }
|
||||
|
||||
.position-roundtable-analysis-result-tag span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-product {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #ffffff;
|
||||
font-family: "Noto Sans S Chinese", "Noto Sans SC", sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-meta {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
color: #80b7ff;
|
||||
font-family: "Noto Sans S Chinese", "Noto Sans SC", sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-meta span {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-meta img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-meta i {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
background: rgba(128, 183, 255, 0.45);
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-results-empty {
|
||||
color: #80b7ff;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { AnalysisResultCardList } from "./AnalysisResultCardList";
|
||||
export { AnalysisResultDetailDialog } from "./AnalysisResultDetailDialog";
|
||||
export type { AnalysisResultCardItem } from "./types";
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface AnalysisResultCardItem {
|
||||
id: string;
|
||||
agentName: string;
|
||||
productName: string;
|
||||
positionName: string;
|
||||
updatedAt?: string;
|
||||
/** Shown when `updatedAt` is missing. */
|
||||
fallbackTime?: string;
|
||||
/** Clickable preview is available. */
|
||||
delivered: boolean;
|
||||
artifactPath?: string;
|
||||
artifactMimeType?: string | null;
|
||||
artifactThreadId?: string | null;
|
||||
artifactContent?: string | null;
|
||||
summary?: string;
|
||||
unavailableMessage?: string;
|
||||
}
|
||||
@@ -91,6 +91,11 @@ function externalApiBase(): string {
|
||||
);
|
||||
}
|
||||
|
||||
/** 8BF 会商页右上角「返回」按钮的跳转地址,来自 runtime-config.js。 */
|
||||
export function getEightBfRoundtableBackUrl(): string {
|
||||
return readRuntimeEnv("VITE_8BF_ROUNDTABLE_BACK_URL") ?? "";
|
||||
}
|
||||
|
||||
function externalUrl(path: string): string {
|
||||
if (/^https?:\/\//i.test(path)) return path;
|
||||
return `${externalApiBase()}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import {
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
Loader2,
|
||||
Search,
|
||||
Settings2,
|
||||
UserRoundCog,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
AnalysisResultCardList,
|
||||
AnalysisResultDetailDialog,
|
||||
type AnalysisResultCardItem,
|
||||
} from "@/components/analysis-results";
|
||||
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
||||
import { useAgent, useAgents, type Agent } from "@/core/agents";
|
||||
import backArrowIcon from "@/assets/eightbf-roundtable/back-arrow.svg";
|
||||
import agentConfigLogo from "@/assets/position-roundtable/agent-config-logo.svg";
|
||||
import collapsePanelIcon from "@/assets/position-roundtable/collapse-panel.svg";
|
||||
import doubleArrowDecoration from "@/assets/position-roundtable/double-arrow-decoration.svg";
|
||||
import { getEightBfRoundtableBackUrl } from "@/eightbf-roundtable/api/task-context";
|
||||
import { useEightBfAnalysisResults } from "@/eightbf-roundtable/lib/collect-md-artifacts";
|
||||
import { PositionIntentInput } from "@/position-roundtable/components/PositionIntentInput";
|
||||
import type { DraftStep3SummarySnapshot } from "@/roundtable-planning/api/drafts";
|
||||
import type { ThreadIdMap } from "@/roundtable-planning/api/multi-agent";
|
||||
import "@/position-roundtable/styles/intent-understanding.css";
|
||||
import type { Step1PanelProps } from "@/roundtable-planning/components/Step1Panel";
|
||||
import { Step1Panel } from "@/roundtable-planning/components/Step1Panel";
|
||||
@@ -31,6 +39,32 @@ export interface EightBfStep1PanelProps extends Step1PanelProps {
|
||||
selectedModel: string;
|
||||
modelOptions: Array<{ label: string; value: string }>;
|
||||
onSelectedModelChange: (model: string) => void;
|
||||
step2Dialogues?: unknown[];
|
||||
step2ThreadIds?: ThreadIdMap | null;
|
||||
step2CoordinatorName?: string | null;
|
||||
step2SeatStubMessages?: Record<string, unknown[]>;
|
||||
step3Summary?: DraftStep3SummarySnapshot | null;
|
||||
}
|
||||
|
||||
export function EightBfRoundtableBackButton() {
|
||||
const handleBack = () => {
|
||||
const url = getEightBfRoundtableBackUrl();
|
||||
if (!url) {
|
||||
toast.warning("尚未配置返回地址,请在 runtime-config.js 填写 VITE_8BF_ROUNDTABLE_BACK_URL");
|
||||
return;
|
||||
}
|
||||
window.location.href = url;
|
||||
};
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="eightbf-roundtable__back-button"
|
||||
onClick={handleBack}
|
||||
>
|
||||
<img src={backArrowIcon} alt="" />
|
||||
返回
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function EightBfStep1Panel({
|
||||
@@ -41,6 +75,11 @@ export function EightBfStep1Panel({
|
||||
modelOptions,
|
||||
onSelectedModelChange,
|
||||
intentReady,
|
||||
step2Dialogues = [],
|
||||
step2ThreadIds = null,
|
||||
step2CoordinatorName = null,
|
||||
step2SeatStubMessages,
|
||||
step3Summary = null,
|
||||
...step1Props
|
||||
}: EightBfStep1PanelProps) {
|
||||
const [rightPanelOpen, setRightPanelOpen] = useState(true);
|
||||
@@ -82,6 +121,7 @@ export function EightBfStep1Panel({
|
||||
>
|
||||
分析结果
|
||||
</button>
|
||||
<EightBfRoundtableBackButton />
|
||||
</div>
|
||||
}
|
||||
rightSidebar={
|
||||
@@ -96,10 +136,14 @@ export function EightBfStep1Panel({
|
||||
<EightBfAgentWorkspace
|
||||
tab={rightTab}
|
||||
chain={chain}
|
||||
intentReady={intentReady}
|
||||
canConfigureChain={canConfigureChain}
|
||||
onConfigureChain={onConfigureChain}
|
||||
onCollapse={() => setRightPanelOpen(false)}
|
||||
step2Dialogues={step2Dialogues}
|
||||
step2ThreadIds={step2ThreadIds}
|
||||
step2CoordinatorName={step2CoordinatorName}
|
||||
step2SeatStubMessages={step2SeatStubMessages}
|
||||
step3Summary={step3Summary}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -223,22 +267,40 @@ export function EightBfHeaderChain({ chainTitle }: { chainTitle?: string }) {
|
||||
function EightBfAgentWorkspace({
|
||||
tab,
|
||||
chain,
|
||||
intentReady,
|
||||
canConfigureChain,
|
||||
onConfigureChain,
|
||||
onCollapse,
|
||||
step2Dialogues,
|
||||
step2ThreadIds,
|
||||
step2CoordinatorName,
|
||||
step2SeatStubMessages,
|
||||
step3Summary,
|
||||
}: {
|
||||
tab: "agents" | "results";
|
||||
chain: BusinessChain | null;
|
||||
intentReady: object | null;
|
||||
canConfigureChain: boolean;
|
||||
onConfigureChain: () => void;
|
||||
onCollapse: () => void;
|
||||
step2Dialogues: unknown[];
|
||||
step2ThreadIds: ThreadIdMap | null;
|
||||
step2CoordinatorName: string | null;
|
||||
step2SeatStubMessages?: Record<string, unknown[]>;
|
||||
step3Summary: DraftStep3SummarySnapshot | null;
|
||||
}) {
|
||||
const { agents, isLoading } = useAgents();
|
||||
const seats = chain?.seats ?? [];
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
const [agentKeyword, setAgentKeyword] = useState("");
|
||||
const { agent: selectedAgent, isLoading: selectedAgentLoading } = useAgent(selectedAgentId);
|
||||
const [detailItem, setDetailItem] = useState<AnalysisResultCardItem | null>(null);
|
||||
const analysisItems = useEightBfAnalysisResults({
|
||||
chain,
|
||||
dialogues: step2Dialogues,
|
||||
threadIds: step2ThreadIds,
|
||||
coordinatorName: step2CoordinatorName,
|
||||
seatStubMessages: step2SeatStubMessages,
|
||||
summaryReport: step3Summary,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedAgentId && !seats.some((seat) => seat.agent_id === selectedAgentId)) {
|
||||
@@ -250,13 +312,22 @@ function EightBfAgentWorkspace({
|
||||
const agentById = new Map(agents.map((agent) => [agent.id, agent]));
|
||||
return seats.map((seat) => ({ seat, agent: agentById.get(seat.agent_id) ?? null }));
|
||||
}, [agents, seats]);
|
||||
const filteredSeatAgents = useMemo(() => {
|
||||
const keyword = agentKeyword.trim().toLowerCase();
|
||||
if (!keyword) return seatAgents;
|
||||
return seatAgents.filter(({ seat, agent }) => {
|
||||
const name = (agent?.name || seat.name || "").toLowerCase();
|
||||
const description = (agent?.description || seat.description || "").toLowerCase();
|
||||
return name.includes(keyword) || description.includes(keyword);
|
||||
});
|
||||
}, [agentKeyword, seatAgents]);
|
||||
const selectedSeat = seats.find((seat) => seat.agent_id === selectedAgentId) ?? null;
|
||||
|
||||
if (tab === "results") {
|
||||
return (
|
||||
<section className="position-roundtable-intent-analysis-results eightbf-roundtable-intent-analysis-results">
|
||||
<div className="position-roundtable-intent-analysis-header">
|
||||
<span>分析结果</span>
|
||||
<span>分析成果列表</span>
|
||||
<button
|
||||
type="button"
|
||||
className="position-roundtable-intent-config-collapse-btn"
|
||||
@@ -268,8 +339,20 @@ function EightBfAgentWorkspace({
|
||||
</button>
|
||||
</div>
|
||||
<div className="position-roundtable-intent-analysis-results-scroll eightbf-roundtable-intent-results-scroll">
|
||||
<EightBfIntentResults intentReady={intentReady} />
|
||||
<AnalysisResultCardList
|
||||
items={analysisItems}
|
||||
emptyTitle="暂无交付产物"
|
||||
emptyDescription="完成第二步研讨与第三步方案总结并生成 Markdown 后,产物会显示在这里。"
|
||||
padded={false}
|
||||
onOpen={setDetailItem}
|
||||
/>
|
||||
</div>
|
||||
<AnalysisResultDetailDialog
|
||||
item={detailItem}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setDetailItem(null);
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -320,11 +403,8 @@ function EightBfAgentWorkspace({
|
||||
|
||||
return (
|
||||
<section className="eightbf-roundtable__agent-list-workspace">
|
||||
<header>
|
||||
<div>
|
||||
<h3>智能体配置</h3>
|
||||
<p>{chain?.title ? `业务链「${chain.title}」已配置的智能体` : "请先配置 8BF 业务链"}</p>
|
||||
</div>
|
||||
<div className="position-roundtable-intent-analysis-header">
|
||||
<span>智能体配置</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{canConfigureChain && (
|
||||
<button type="button" className="eightbf-roundtable__configure-chain-button" onClick={onConfigureChain}>
|
||||
@@ -341,27 +421,41 @@ function EightBfAgentWorkspace({
|
||||
<img src={collapsePanelIcon} alt="" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="eightbf-roundtable__agent-list-search">
|
||||
<Search className="size-3.5" />
|
||||
<span>业务链智能体</span>
|
||||
<span>{seatAgents.length} 个</span>
|
||||
</div>
|
||||
<div className="eightbf-roundtable__agent-list-scroll">
|
||||
<div className="eightbf-roundtable__agent-selector">
|
||||
<header className="eightbf-roundtable__agent-selector-head">
|
||||
<div className="eightbf-roundtable__agent-selector-title">
|
||||
<b>当前岗位智能体</b>
|
||||
<i>{seatAgents.length}</i>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={agentKeyword}
|
||||
onChange={(event) => setAgentKeyword(event.target.value)}
|
||||
placeholder="搜索名称或说明"
|
||||
className="eightbf-roundtable__agent-selector-search"
|
||||
/>
|
||||
</header>
|
||||
{isLoading && seats.length > 0 ? (
|
||||
<div className="eightbf-roundtable__agent-list-loading"><Loader2 className="size-4 animate-spin" /> 正在加载智能体…</div>
|
||||
) : seats.length === 0 ? (
|
||||
<div className="eightbf-roundtable__agent-list-empty">当前业务链未配置智能体</div>
|
||||
<div className="eightbf-roundtable__agent-selector-empty">正在加载智能体…</div>
|
||||
) : filteredSeatAgents.length === 0 ? (
|
||||
<div className="eightbf-roundtable__agent-selector-empty">
|
||||
<p>{seatAgents.length ? "没有符合条件的智能体" : "当前岗位尚未配置智能体"}</p>
|
||||
{!seatAgents.length && canConfigureChain && (
|
||||
<button type="button" onClick={onConfigureChain}>前往业务链配置</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
seatAgents.map(({ seat, agent }, index) => (
|
||||
<EightBfAgentRow
|
||||
key={seat.agent_id}
|
||||
seat={seat}
|
||||
agent={agent}
|
||||
index={index}
|
||||
onSelect={() => setSelectedAgentId(seat.agent_id)}
|
||||
/>
|
||||
))
|
||||
<div className="eightbf-roundtable__agent-selector-list">
|
||||
{filteredSeatAgents.map(({ seat, agent }) => (
|
||||
<EightBfAgentRow
|
||||
key={seat.agent_id}
|
||||
seat={seat}
|
||||
agent={agent}
|
||||
onSelect={() => setSelectedAgentId(seat.agent_id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -371,45 +465,27 @@ function EightBfAgentWorkspace({
|
||||
function EightBfAgentRow({
|
||||
seat,
|
||||
agent,
|
||||
index,
|
||||
onSelect,
|
||||
}: {
|
||||
seat: ChainSeat;
|
||||
agent: Agent | null;
|
||||
index: number;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const name = agent?.name || seat.name || "未命名智能体";
|
||||
const description = agent?.description || seat.description || "查看该智能体的模型、技能与常用问题配置";
|
||||
const description = agent?.description || seat.description || "智能体助手";
|
||||
const avatarText = Array.from(name.trim() || "?")[0]?.toUpperCase() || "?";
|
||||
return (
|
||||
<button type="button" className="eightbf-roundtable__agent-list-row" onClick={onSelect}>
|
||||
<span className="eightbf-roundtable__agent-list-avatar" aria-hidden="true"><Bot /></span>
|
||||
<span className="min-w-0 flex-1 text-left">
|
||||
<button type="button" className="eightbf-roundtable__agent-selector-card" onClick={onSelect}>
|
||||
<span className="eightbf-roundtable__agent-selector-avatar" aria-hidden="true">{avatarText}</span>
|
||||
<span className="eightbf-roundtable__agent-selector-main">
|
||||
<strong>{name}</strong>
|
||||
<small>{description}</small>
|
||||
<p>{description}</p>
|
||||
</span>
|
||||
<span className="eightbf-roundtable__agent-list-index">{String(index + 1).padStart(2, "0")}</span>
|
||||
<i className="eightbf-roundtable__agent-selector-bar" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function EightBfIntentResults({ intentReady }: { intentReady: object | null }) {
|
||||
const sections = intentSections(intentReady);
|
||||
if (sections.length === 0) {
|
||||
return <p className="eightbf-roundtable__intent-results-empty">完成意图识别后,将在这里展示任务意图与关键约束。</p>;
|
||||
}
|
||||
return (
|
||||
<div className="eightbf-roundtable__intent-results-list">
|
||||
{sections.map((section) => (
|
||||
<section key={section.label}>
|
||||
<h3>{section.label}</h3>
|
||||
<ul>{section.items.map((item) => <li key={item}>{item}</li>)}</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function intentSections(intentReady: object | null): Array<{ label: string; items: string[] }> {
|
||||
const source = intentReady && typeof intentReady === "object" ? intentReady as IntentRecord : null;
|
||||
if (!source) return [];
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type { AnalysisResultCardItem } from "@/components/analysis-results";
|
||||
import type { DraftStep3SummarySnapshot } from "@/roundtable-planning/api/drafts";
|
||||
import {
|
||||
listSessionArtifacts,
|
||||
type ThreadIdMap,
|
||||
} from "@/roundtable-planning/api/multi-agent";
|
||||
import type { BusinessChain } from "@/roundtable-planning/lib/business-chain";
|
||||
|
||||
const MD_PATH_RE = /\.md$/i;
|
||||
const SUMMARY_OUTPUT_PATH = "/mnt/user-data/outputs/方案总结报告.md";
|
||||
|
||||
type DialogueLike = {
|
||||
sender?: string;
|
||||
agentThreadId?: string;
|
||||
time?: string;
|
||||
steps?: Array<{ key?: string; args?: Record<string, unknown> }>;
|
||||
};
|
||||
|
||||
interface MdFileHit {
|
||||
path: string;
|
||||
name: string;
|
||||
threadId: string | null;
|
||||
content: string | null;
|
||||
updatedAt?: string;
|
||||
agentName: string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
function isMarkdownPath(path: string): boolean {
|
||||
return MD_PATH_RE.test(path.split("/").pop() ?? path);
|
||||
}
|
||||
|
||||
function filenameOf(path: string): string {
|
||||
return path.split("/").filter(Boolean).at(-1) || path;
|
||||
}
|
||||
|
||||
function asString(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function fileKey(threadId: string | null, path: string): string {
|
||||
return `${threadId ?? ""}::${path}`;
|
||||
}
|
||||
|
||||
function collectPathsFromSteps(
|
||||
steps: DialogueLike["steps"],
|
||||
): Array<{ path: string; content: string | null }> {
|
||||
if (!Array.isArray(steps) || steps.length === 0) return [];
|
||||
const out: Array<{ path: string; content: string | null }> = [];
|
||||
const seen = new Set<string>();
|
||||
const add = (path: string, content: string | null) => {
|
||||
if (!path || seen.has(path) || !isMarkdownPath(path)) return;
|
||||
seen.add(path);
|
||||
out.push({ path, content });
|
||||
};
|
||||
for (const step of steps) {
|
||||
if (step.key === "tool_calling:present_files") {
|
||||
const fps = step.args?.filepaths;
|
||||
if (Array.isArray(fps)) {
|
||||
for (const fp of fps) add(asString(fp), null);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (step.key === "tool_calling:write_file" || step.key === "tool_calling:str_replace") {
|
||||
add(asString(step.args?.path), asString(step.args?.content) || null);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectFromDialogues(dialogues: unknown[]): MdFileHit[] {
|
||||
const hits: MdFileHit[] = [];
|
||||
let order = 0;
|
||||
for (const raw of dialogues) {
|
||||
if (!raw || typeof raw !== "object") continue;
|
||||
const dialogue = raw as DialogueLike;
|
||||
const sender = asString(dialogue.sender) || "智能体";
|
||||
const threadId = asString(dialogue.agentThreadId) || null;
|
||||
for (const file of collectPathsFromSteps(dialogue.steps)) {
|
||||
hits.push({
|
||||
path: file.path,
|
||||
name: filenameOf(file.path),
|
||||
threadId,
|
||||
content: file.content,
|
||||
updatedAt: asString(dialogue.time) || undefined,
|
||||
agentName: sender,
|
||||
order: order++,
|
||||
});
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
function collectFromStubs(stubs: Record<string, unknown[]> | undefined): MdFileHit[] {
|
||||
const hits: MdFileHit[] = [];
|
||||
for (const [threadId, messages] of Object.entries(stubs ?? {})) {
|
||||
if (!Array.isArray(messages)) continue;
|
||||
for (const message of messages) {
|
||||
if (!message || typeof message !== "object") continue;
|
||||
const calls = (message as { tool_calls?: unknown }).tool_calls;
|
||||
if (!Array.isArray(calls)) continue;
|
||||
for (const call of calls) {
|
||||
if (!call || typeof call !== "object") continue;
|
||||
const tool = call as { name?: unknown; args?: Record<string, unknown> };
|
||||
if (asString(tool.name) !== "write_file") continue;
|
||||
const path = asString(tool.args?.path);
|
||||
if (!path || !isMarkdownPath(path)) continue;
|
||||
hits.push({
|
||||
path,
|
||||
name: filenameOf(path),
|
||||
threadId,
|
||||
content: asString(tool.args?.content) || null,
|
||||
agentName: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
function mergeHits(groups: MdFileHit[][]): Map<string, MdFileHit> {
|
||||
const merged = new Map<string, MdFileHit>();
|
||||
for (const group of groups) {
|
||||
for (const hit of group) {
|
||||
const key = fileKey(hit.threadId, hit.path);
|
||||
const prev = merged.get(key);
|
||||
if (!prev) {
|
||||
merged.set(key, hit);
|
||||
continue;
|
||||
}
|
||||
merged.set(key, {
|
||||
path: hit.path || prev.path,
|
||||
name: hit.name || prev.name,
|
||||
threadId: hit.threadId || prev.threadId,
|
||||
content: hit.content || prev.content,
|
||||
updatedAt: hit.updatedAt || prev.updatedAt,
|
||||
agentName: prev.agentName || hit.agentName,
|
||||
order: Math.max(prev.order ?? 0, hit.order ?? 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function latestHit(hits: MdFileHit[]): MdFileHit | null {
|
||||
if (hits.length === 0) return null;
|
||||
return hits.reduce((latest, hit) => {
|
||||
const latestTime = Date.parse(latest.updatedAt ?? "") || 0;
|
||||
const hitTime = Date.parse(hit.updatedAt ?? "") || 0;
|
||||
if (hitTime !== latestTime) return hitTime > latestTime ? hit : latest;
|
||||
return (hit.order ?? 0) >= (latest.order ?? 0) ? hit : latest;
|
||||
});
|
||||
}
|
||||
|
||||
function pendingSeatCard(seat: BusinessChain["seats"][number]): AnalysisResultCardItem {
|
||||
const agentName = seat.name?.trim() || seat.agent_id;
|
||||
return {
|
||||
id: `eightbf:seat:${seat.agent_id}:pending`,
|
||||
agentName,
|
||||
productName: `待交付:${agentName} Markdown报告`,
|
||||
positionName: agentName,
|
||||
delivered: false,
|
||||
fallbackTime: "待完成",
|
||||
unavailableMessage: `「${agentName}」尚未交付可预览的 Markdown 文件。完成第二步研讨后,这里会直接展示产物。`,
|
||||
};
|
||||
}
|
||||
|
||||
function hitToCard(
|
||||
hit: MdFileHit,
|
||||
positionName: string,
|
||||
id: string,
|
||||
): AnalysisResultCardItem {
|
||||
return {
|
||||
id,
|
||||
agentName: hit.agentName || positionName,
|
||||
productName: hit.name,
|
||||
positionName,
|
||||
updatedAt: hit.updatedAt,
|
||||
fallbackTime: "刚刚",
|
||||
delivered: true,
|
||||
artifactPath: hit.path,
|
||||
artifactMimeType: "text/markdown",
|
||||
artifactThreadId: hit.threadId,
|
||||
artifactContent: hit.content,
|
||||
};
|
||||
}
|
||||
|
||||
export function collectEightBfAnalysisResults({
|
||||
chain,
|
||||
dialogues,
|
||||
threadIds,
|
||||
coordinatorName,
|
||||
seatStubMessages,
|
||||
summaryReport,
|
||||
remoteFiles = [],
|
||||
}: {
|
||||
chain: BusinessChain | null;
|
||||
dialogues: unknown[];
|
||||
threadIds: ThreadIdMap | null;
|
||||
coordinatorName: string | null;
|
||||
seatStubMessages?: Record<string, unknown[]>;
|
||||
summaryReport: DraftStep3SummarySnapshot | null;
|
||||
remoteFiles?: MdFileHit[];
|
||||
}): AnalysisResultCardItem[] {
|
||||
const merged = mergeHits([
|
||||
collectFromDialogues(dialogues),
|
||||
collectFromStubs(seatStubMessages),
|
||||
]);
|
||||
const seats = chain?.seats ?? [];
|
||||
const coordinator = coordinatorName?.trim() || "";
|
||||
const items: AnalysisResultCardItem[] = [];
|
||||
|
||||
const threadIdOfSeat = (seat: BusinessChain["seats"][number]): string | null => {
|
||||
const byId = threadIds?.[seat.agent_id];
|
||||
if (byId) return byId;
|
||||
const byName = Object.entries(threadIds ?? {}).find(
|
||||
([name]) => name.toLowerCase() === seat.agent_id.toLowerCase() || name === seat.name,
|
||||
)?.[1];
|
||||
return byName ?? null;
|
||||
};
|
||||
|
||||
for (const seat of seats) {
|
||||
if (coordinator && (seat.name === coordinator || seat.agent_id === coordinator)) continue;
|
||||
const seatThreadId = threadIdOfSeat(seat);
|
||||
const agentName = seat.name?.trim() || seat.agent_id;
|
||||
const seatHits = [...merged.values()].filter((hit) => {
|
||||
if (seatThreadId && hit.threadId === seatThreadId) return true;
|
||||
return hit.agentName === agentName || hit.agentName === seat.agent_id;
|
||||
});
|
||||
const latest = latestHit(seatHits) ?? latestHit(
|
||||
remoteFiles.filter((hit) => Boolean(seatThreadId) && hit.threadId === seatThreadId),
|
||||
);
|
||||
if (!latest) {
|
||||
items.push(pendingSeatCard(seat));
|
||||
continue;
|
||||
}
|
||||
const enriched = latest.content
|
||||
? latest
|
||||
: remoteFiles.find((hit) =>
|
||||
hit.threadId === latest.threadId &&
|
||||
(hit.path === latest.path || hit.name === latest.name),
|
||||
) ?? latest;
|
||||
items.push(hitToCard(enriched, agentName, `eightbf:seat:${seat.agent_id}:latest`));
|
||||
}
|
||||
|
||||
const deliveredSeatCount = items.filter((item) => item.delivered).length;
|
||||
if (summaryReport?.md.trim()) {
|
||||
items.push({
|
||||
id: "eightbf:summary",
|
||||
agentName: "方案总结智能体",
|
||||
productName: "方案总结报告.md",
|
||||
positionName: "方案总结",
|
||||
updatedAt: summaryReport.generatedAt,
|
||||
fallbackTime: "刚刚",
|
||||
delivered: true,
|
||||
artifactPath: SUMMARY_OUTPUT_PATH,
|
||||
artifactMimeType: "text/markdown",
|
||||
artifactThreadId: summaryReport.threadId ?? null,
|
||||
artifactContent: summaryReport.md,
|
||||
summary: summaryReport.closing ?? summaryReport.md,
|
||||
});
|
||||
} else if (deliveredSeatCount > 0) {
|
||||
items.push({
|
||||
id: "eightbf:summary:pending",
|
||||
agentName: "方案总结智能体",
|
||||
productName: "待生成:方案总结报告.md",
|
||||
positionName: "方案总结",
|
||||
delivered: false,
|
||||
fallbackTime: "待完成",
|
||||
unavailableMessage: "第三步方案总结尚未生成 Markdown 报告。完成总结后,这里会直接展示产物。",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useEightBfAnalysisResults(input: {
|
||||
chain: BusinessChain | null;
|
||||
dialogues: unknown[];
|
||||
threadIds: ThreadIdMap | null;
|
||||
coordinatorName: string | null;
|
||||
seatStubMessages?: Record<string, unknown[]>;
|
||||
summaryReport: DraftStep3SummarySnapshot | null;
|
||||
}): AnalysisResultCardItem[] {
|
||||
const [remoteFiles, setRemoteFiles] = useState<MdFileHit[]>([]);
|
||||
const threadEntries = useMemo(
|
||||
() => Object.entries(input.threadIds ?? {}).filter(([, threadId]) => Boolean(threadId)),
|
||||
[input.threadIds],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (threadEntries.length === 0) {
|
||||
setRemoteFiles([]);
|
||||
return;
|
||||
}
|
||||
const abort = new AbortController();
|
||||
let cancelled = false;
|
||||
void Promise.all(
|
||||
threadEntries.map(async ([agentName, threadId]) => {
|
||||
try {
|
||||
const res = await listSessionArtifacts(threadId, { signal: abort.signal });
|
||||
return res.files
|
||||
.filter((file) => isMarkdownPath(file.path) || isMarkdownPath(file.name))
|
||||
.map((file) => ({
|
||||
path: file.path,
|
||||
name: file.name || filenameOf(file.path),
|
||||
threadId,
|
||||
content: null,
|
||||
updatedAt: file.modifiedAt,
|
||||
agentName,
|
||||
}));
|
||||
} catch {
|
||||
return [] as MdFileHit[];
|
||||
}
|
||||
}),
|
||||
).then((groups) => {
|
||||
if (!cancelled) setRemoteFiles(groups.flat());
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
abort.abort();
|
||||
};
|
||||
}, [threadEntries]);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
collectEightBfAnalysisResults({
|
||||
...input,
|
||||
remoteFiles,
|
||||
}),
|
||||
[
|
||||
input.chain,
|
||||
input.coordinatorName,
|
||||
input.dialogues,
|
||||
input.seatStubMessages,
|
||||
input.summaryReport,
|
||||
input.threadIds,
|
||||
remoteFiles,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -266,6 +266,7 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa
|
||||
hideHeaderSettings
|
||||
newTaskLabel="新建会话"
|
||||
hideDefaultStep1RightSidebar
|
||||
step2HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 多智能体会商`}
|
||||
renderStep1={(props) => (
|
||||
<EightBfStep1Panel
|
||||
{...props}
|
||||
|
||||
@@ -17,6 +17,25 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .position-roundtable-intent-header-actions button:nth-child(2) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .position-roundtable-intent-header-actions .eightbf-roundtable__back-button {
|
||||
width: 86.04px;
|
||||
height: 28px;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
background: linear-gradient(109.46deg, rgba(0, 98, 217, 0.59) 3.83%, rgba(0, 98, 217, 0.3) 98.23%);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .position-roundtable-intent-header-actions .eightbf-roundtable__back-button img {
|
||||
width: 14px;
|
||||
height: 12px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .eightbf-roundtable__chain-status-label { color: #0f5f56; font-weight: 700; }
|
||||
.eightbf-roundtable .eightbf-roundtable__chain-status-value { color: #134e4a; font-weight: 600; }
|
||||
.eightbf-roundtable .eightbf-roundtable__configured-icon { color: var(--eightbf-accent); }
|
||||
@@ -178,17 +197,6 @@
|
||||
flex-direction: column;
|
||||
background: #032650;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-workspace > header {
|
||||
display: flex;
|
||||
min-height: 64px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid rgb(43 143 254 / 35%);
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-workspace h3 { margin: 0; color: #fff; font-size: 17px; font-weight: 500; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-workspace p { margin: 4px 0 0; color: #aaceff; font-size: 11px; line-height: 1.45; }
|
||||
.eightbf-roundtable .eightbf-roundtable__configure-chain-button {
|
||||
display: inline-flex;
|
||||
height: 28px;
|
||||
@@ -202,45 +210,170 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__configure-chain-button:hover { background: rgb(0 98 217 / 40%); }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-search {
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin: 12px;
|
||||
border: 1px solid rgb(43 143 254 / 36%);
|
||||
border-radius: 4px;
|
||||
background: rgb(0 12 28 / 20%);
|
||||
gap: 8px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-title b { font-weight: 500; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-title i {
|
||||
min-width: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
color: #aaceff;
|
||||
padding: 8px 9px;
|
||||
background: rgb(21 66 128 / 58%);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-search {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgb(43 143 254 / 40%);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
background: rgb(0 12 28 / 40%);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-search::placeholder { color: rgb(255 255 255 / 40%); }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-search:focus { border-color: #45c3f3; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-list {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: 40px 1fr;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgb(43 143 254 / 20%);
|
||||
border-radius: 8px;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
background: rgb(0 98 217 / 8%);
|
||||
cursor: pointer;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-card:hover {
|
||||
border-color: rgb(43 143 254 / 60%);
|
||||
background: rgb(0 98 217 / 22%);
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-card:hover .eightbf-roundtable__agent-selector-bar {
|
||||
background: #45c3f3;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-avatar {
|
||||
display: grid;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1675d1, #06447f);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-main strong {
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-main p {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #9ebfe2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-search span:last-child { margin-left: auto; border-radius: 999px; background: #0b386d; padding: 1px 6px; color: #83c1ff; font-size: 10px; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-scroll { min-height: 0; flex: 1; overflow-y: auto; padding: 0 12px 12px; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-loading,
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-empty { display: flex; min-height: 100px; align-items: center; justify-content: center; gap: 7px; color: #8aa0c4; font-size: 12px; text-align: center; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid rgb(43 143 254 / 21%);
|
||||
padding: 11px 2px;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
transition: background-color .16s ease, transform .16s ease;
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-bar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
width: 6px;
|
||||
height: 32px;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
transform: translateY(-50%);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-empty {
|
||||
display: flex;
|
||||
min-height: 200px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 32px 16px;
|
||||
color: rgb(255 255 255 / 60%);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-empty p { margin: 0; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-empty button {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid rgb(43 143 254 / 60%);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
background: rgb(0 98 217 / 18%);
|
||||
cursor: pointer;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-selector-empty button:hover {
|
||||
background: rgb(0 98 217 / 32%);
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-row:hover { background: rgb(0 98 217 / 13%); transform: translateX(2px); }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-avatar { display: inline-flex; width: 34px; height: 34px; flex: 0 0 auto; align-items: center; justify-content: center; border: 1px solid #007e82; border-radius: 7px; background: #00306a; color: #73c8ff; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-avatar svg { width: 18px; height: 18px; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-row strong,
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-row small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-row strong { color: #fff; font-size: 13px; font-weight: 600; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-row small { margin-top: 3px; color: #aaceff; font-size: 11px; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-list-index { color: #75b7f5; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-detail-topbar { display: flex; height: 48px; align-items: center; justify-content: space-between; border-bottom: 1px solid rgb(43 143 254 / 35%); padding: 0 12px; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-detail-topbar > button:first-child { display: inline-flex; align-items: center; gap: 2px; color: #aaceff; font-size: 12px; }
|
||||
.eightbf-roundtable .eightbf-roundtable__agent-detail-topbar > button:first-child:hover { color: #fff; }
|
||||
.eightbf-roundtable .eightbf-roundtable__intent-results-scroll { padding: 12px; }
|
||||
.eightbf-roundtable .position-roundtable-intent-config-pane-inner {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .eightbf-roundtable-intent-results-scroll {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
padding: 10px;
|
||||
}
|
||||
.eightbf-roundtable .eightbf-roundtable__intent-results-empty { margin: 0; color: #aaceff; font-size: 13px; line-height: 1.8; }
|
||||
.eightbf-roundtable .eightbf-roundtable__intent-results-list { display: grid; gap: 10px; }
|
||||
.eightbf-roundtable .eightbf-roundtable__intent-results-list section { border: 1px solid rgb(43 143 254 / 32%); border-radius: 4px; background: rgb(0 12 28 / 20%); padding: 12px; }
|
||||
|
||||
@@ -81,6 +81,46 @@
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.dark .eightbf-roundtable[data-roundtable-step="2"] > main {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.dark .eightbf-roundtable[data-roundtable-step="2"] #middle-viewport {
|
||||
border-color: rgb(43 143 254 / 50%);
|
||||
border-radius: 4px;
|
||||
background: #032650;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .eightbf-roundtable-step2-chat {
|
||||
min-height: 0;
|
||||
background: #032650;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .eightbf-roundtable-step2-log {
|
||||
background: #032650;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .eightbf-roundtable-step2-chat .position-roundtable-intent-header-actions .eightbf-roundtable__suspend-button {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.eightbf-roundtable .eightbf-roundtable-step2-chat .relative.flex-1 > .shrink-0 {
|
||||
background: #032650;
|
||||
border-color: rgb(43 143 254 / 28%);
|
||||
}
|
||||
|
||||
.eightbf-roundtable[data-roundtable-step="2"] .eightbf-roundtable-step2-log .text-slate-700,
|
||||
.eightbf-roundtable[data-roundtable-step="2"] .eightbf-roundtable-step2-log .text-slate-500 {
|
||||
color: #aaceff;
|
||||
}
|
||||
|
||||
.eightbf-roundtable[data-roundtable-step="2"] .eightbf-roundtable-step2-log .text-slate-400 {
|
||||
color: #8aa0c4;
|
||||
}
|
||||
|
||||
/* 第二步输入区直接复用第一步 PositionIntentInput;共识动作也使用其
|
||||
#enter-roundtable-btn 规则,保证和「确认意图并启动业务链」完全同款。 */
|
||||
.dark .eightbf-roundtable[data-roundtable-step="2"] .eightbf-roundtable-step2-input {
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "tdesign-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { AnalysisResultCardList, type AnalysisResultCardItem } from "@/components/analysis-results";
|
||||
import { agentColor } from "@/components/workspace/agent-icon-color";
|
||||
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
|
||||
import { useAgentModuleSidebarData } from "@/components/workspace/agents/agent-module-sidebar";
|
||||
@@ -91,14 +92,6 @@ import askQuestionIcon from "@/assets/position-roundtable/ask-question.svg";
|
||||
import agentConfigLogo from "@/assets/position-roundtable/agent-config-logo.svg";
|
||||
import doubleArrowDecoration from "@/assets/position-roundtable/double-arrow-decoration.svg";
|
||||
import collapsePanelIcon from "@/assets/position-roundtable/collapse-panel.svg";
|
||||
import analysisTag1 from "@/assets/position-roundtable/标签1.svg";
|
||||
import analysisTag2 from "@/assets/position-roundtable/标签2.svg";
|
||||
import analysisTag3 from "@/assets/position-roundtable/标签3.svg";
|
||||
import analysisTag4 from "@/assets/position-roundtable/标签4.svg";
|
||||
import analysisTag5 from "@/assets/position-roundtable/标签5.svg";
|
||||
import analysisTag6 from "@/assets/position-roundtable/标签6.svg";
|
||||
import responsibilityIcon from "@/assets/position-roundtable/责任岗位.svg";
|
||||
import resultTimeIcon from "@/assets/position-roundtable/时间.svg";
|
||||
import { deleteChain, listChains } from "@/roundtable-planning/api/chains";
|
||||
import { Step1Panel } from "@/roundtable-planning/components/Step1Panel";
|
||||
import { PositionIntentInput } from "@/position-roundtable/components/PositionIntentInput";
|
||||
@@ -5961,15 +5954,6 @@ interface ResultItem {
|
||||
artifactContent?: string | null;
|
||||
}
|
||||
|
||||
const ANALYSIS_RESULT_TAG_ICONS = [
|
||||
analysisTag1,
|
||||
analysisTag2,
|
||||
analysisTag3,
|
||||
analysisTag4,
|
||||
analysisTag5,
|
||||
analysisTag6,
|
||||
] as const;
|
||||
|
||||
function canOpenDeliverable(item: ResultItem): boolean {
|
||||
return Boolean(
|
||||
item.status === "done" &&
|
||||
@@ -6407,6 +6391,25 @@ function AnalysisResults({
|
||||
),
|
||||
[kindFilter, nodeFilter, positionFilter, resultItems],
|
||||
);
|
||||
const visibleCardItems = useMemo<AnalysisResultCardItem[]>(
|
||||
() =>
|
||||
visibleItems.map((item) => ({
|
||||
id: item.id,
|
||||
agentName: item.agentName,
|
||||
productName: item.productName,
|
||||
positionName: resultPositionName(item.positionId),
|
||||
updatedAt: item.updatedAt,
|
||||
fallbackTime: intentCompletedAt || "刚刚",
|
||||
delivered: canOpenDeliverable(item),
|
||||
artifactPath: item.artifactPath,
|
||||
artifactMimeType: item.artifactMimeType,
|
||||
artifactThreadId: item.artifactThreadId,
|
||||
artifactContent: item.artifactContent,
|
||||
summary: item.summary,
|
||||
unavailableMessage: deliverableUnavailableMessage(item),
|
||||
})),
|
||||
[intentCompletedAt, visibleItems],
|
||||
);
|
||||
|
||||
const openDetail = useCallback(
|
||||
(item: ResultItem) => {
|
||||
@@ -6462,13 +6465,12 @@ function AnalysisResults({
|
||||
|
||||
if (resultItems.length === 0) {
|
||||
return (
|
||||
<div className="position-roundtable-analysis-results-empty flex h-full min-h-[260px] flex-col items-center justify-center p-6 text-center">
|
||||
<FileOutput className="mb-3 size-8 text-muted-foreground/50" />
|
||||
<p className="text-sm font-semibold">暂无交付产物</p>
|
||||
<p className="mt-1 max-w-[240px] text-xs leading-5 text-muted-foreground">
|
||||
情报分析岗启动业务链后,各岗位待交付产物会在这里排队展示。
|
||||
</p>
|
||||
</div>
|
||||
<AnalysisResultCardList
|
||||
items={[]}
|
||||
emptyTitle="暂无交付产物"
|
||||
emptyDescription="情报分析岗启动业务链后,各岗位待交付产物会在这里排队展示。"
|
||||
onOpen={() => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6547,67 +6549,14 @@ function AnalysisResults({
|
||||
当前筛选条件下没有结果
|
||||
</div>
|
||||
) : (
|
||||
visibleItems.map((item, index) => {
|
||||
const displayedTime = item.updatedAt
|
||||
? new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
}).format(new Date(item.updatedAt))
|
||||
: intentCompletedAt || "刚刚";
|
||||
const hasDeliveredArtifact = Boolean(item.artifactPath || item.artifactContent?.trim());
|
||||
const displayedProductName =
|
||||
hasDeliveredArtifact || (item.kind !== "delivery" && item.status === "done")
|
||||
? item.productName
|
||||
: "未交付";
|
||||
const contents = (
|
||||
<div className="position-roundtable-analysis-result-card-body">
|
||||
<div className="position-roundtable-analysis-result-tag" aria-label={desensitize(item.agentName)}>
|
||||
<img
|
||||
src={ANALYSIS_RESULT_TAG_ICONS[index % ANALYSIS_RESULT_TAG_ICONS.length]}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{desensitize(item.agentName)}</span>
|
||||
</div>
|
||||
<p className="position-roundtable-analysis-result-product">
|
||||
{desensitize(displayedProductName)}
|
||||
</p>
|
||||
<footer className="position-roundtable-analysis-result-meta">
|
||||
<span>
|
||||
<img src={responsibilityIcon} alt="" aria-hidden="true" />
|
||||
{resultPositionName(item.positionId)}
|
||||
</span>
|
||||
<i aria-hidden="true" />
|
||||
<span>
|
||||
<img src={resultTimeIcon} alt="" aria-hidden="true" />
|
||||
{displayedTime}
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
return item.node ? (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => openDetail(item)}
|
||||
className="position-roundtable-analysis-result-card"
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => openDetail(item)}
|
||||
className="position-roundtable-analysis-result-card"
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
<AnalysisResultCardList
|
||||
items={visibleCardItems}
|
||||
padded={false}
|
||||
onOpen={(card) => {
|
||||
const original = resultItems.find((item) => item.id === card.id);
|
||||
if (original) openDetail(original);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1108,8 +1108,10 @@
|
||||
}
|
||||
|
||||
.position-roundtable-intent-analysis-results-scroll {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -1147,26 +1149,32 @@
|
||||
top: -40px;
|
||||
left: -12px;
|
||||
display: inline-flex;
|
||||
width: max-content;
|
||||
min-width: 95px;
|
||||
max-width: calc(100% - 12px);
|
||||
width: fit-content;
|
||||
max-width: calc(100% + 12px);
|
||||
height: 26px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
padding: 0 22px 0 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px 0 0 0;
|
||||
background: rgba(212, 120, 0, 0.5);
|
||||
color: #ffffff;
|
||||
font-family: "Noto Sans S Chinese", "Noto Sans SC", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
clip-path: polygon(0 0, 100% 0, calc(100% - 14px) 100%, 0 100%);
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-tag--1 { background: rgba(212, 120, 0, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--2 { background: rgba(207, 19, 34, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--3 { background: rgba(29, 171, 123, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--4 { background: rgba(132, 85, 230, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--5 { background: rgba(201, 181, 0, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--6 { background: rgba(69, 195, 243, 0.5); }
|
||||
|
||||
.position-roundtable-analysis-result-tag img {
|
||||
position: absolute;
|
||||
inset: 0 auto auto 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-tag span {
|
||||
@@ -1176,7 +1184,7 @@
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0 12px;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,10 @@ interface ModelOption {
|
||||
export interface Step2PanelProps {
|
||||
/** 独立业务页需要与第一步完全相同的输入组件(8BF 专用,默认不改变原会商)。 */
|
||||
useStep1InputAppearance?: boolean;
|
||||
/** 8BF 第二步对话区标题,与第一步「业务链 · 意图识别」同一套顶栏。 */
|
||||
headerTitle?: string;
|
||||
/** 8BF 第二步顶栏右侧附加操作(如返回)。 */
|
||||
headerRightSlot?: ReactNode;
|
||||
// ─── State ────────────────────────────────────────────────────
|
||||
step2RoundtableDialogues: Step2Dialogue[];
|
||||
isInitializing: boolean;
|
||||
@@ -165,6 +169,8 @@ export interface Step2PanelProps {
|
||||
export function Step2Panel(props: Step2PanelProps) {
|
||||
const {
|
||||
useStep1InputAppearance = false,
|
||||
headerTitle,
|
||||
headerRightSlot,
|
||||
step2RoundtableDialogues,
|
||||
isInitializing,
|
||||
isOrchestrating,
|
||||
@@ -412,42 +418,64 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
const chatBody = (
|
||||
<>
|
||||
{/* Headline */}
|
||||
<div ref={headerDzRef} className="shrink-0 px-6 py-4.5 border-b border-slate-100 flex items-center justify-between bg-white shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-primary" strokeWidth={2.5} />
|
||||
<div>
|
||||
<h3 className="text-sm font-extrabold text-slate-900">多智能体圆桌会商中心</h3>
|
||||
<p className="text-[calc(10px*var(--ui-font-scale,1))] text-slate-400 font-medium">
|
||||
分布式领域专家实时动态博弈与纳什协商收敛中
|
||||
</p>
|
||||
<div
|
||||
ref={headerDzRef}
|
||||
className={
|
||||
useStep1InputAppearance
|
||||
? "shrink-0 flex items-center justify-between gap-3"
|
||||
: "shrink-0 px-6 py-4.5 border-b border-slate-100 flex items-center justify-between bg-white shadow-sm"
|
||||
}
|
||||
>
|
||||
{useStep1InputAppearance ? (
|
||||
<div className="position-roundtable-intent-header-title">
|
||||
<span aria-hidden="true" />
|
||||
<h2>{headerTitle || "多智能体会商"}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 shrink-0">
|
||||
<select
|
||||
id="step2-global-model-select"
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
disabled={modelsLoading || availableModels.length === 0}
|
||||
title={
|
||||
modelsLoading
|
||||
? "正在加载模型列表…"
|
||||
: availableModels.length === 0
|
||||
? "未在 config.yaml 中配置任何模型"
|
||||
: "切换全局推理模型"
|
||||
}
|
||||
className="max-w-[220px] bg-slate-50/50 border border-slate-200 rounded-xl px-3 py-1.5 text-xs font-bold text-slate-700 cursor-pointer focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-secondary dark:text-foreground dark:border-border"
|
||||
>
|
||||
{availableModels.length === 0 && (
|
||||
<option value="">{modelsLoading ? "加载中…" : "未配置模型"}</option>
|
||||
)}
|
||||
{availableModels.map((m) => (
|
||||
<option key={m.name} value={m.name}>
|
||||
{m.display_name && m.display_name !== m.name
|
||||
? `${m.display_name} (${m.name})`
|
||||
: m.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-primary" strokeWidth={2.5} />
|
||||
<div>
|
||||
<h3 className="text-sm font-extrabold text-slate-900">多智能体圆桌会商中心</h3>
|
||||
<p className="text-[calc(10px*var(--ui-font-scale,1))] text-slate-400 font-medium">
|
||||
分布式领域专家实时动态博弈与纳什协商收敛中
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
useStep1InputAppearance
|
||||
? "position-roundtable-intent-header-actions"
|
||||
: "flex items-center gap-2.5 shrink-0"
|
||||
}
|
||||
>
|
||||
{!useStep1InputAppearance && (
|
||||
<select
|
||||
id="step2-global-model-select"
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
disabled={modelsLoading || availableModels.length === 0}
|
||||
title={
|
||||
modelsLoading
|
||||
? "正在加载模型列表…"
|
||||
: availableModels.length === 0
|
||||
? "未在 config.yaml 中配置任何模型"
|
||||
: "切换全局推理模型"
|
||||
}
|
||||
className="max-w-[220px] bg-slate-50/50 border border-slate-200 rounded-xl px-3 py-1.5 text-xs font-bold text-slate-700 cursor-pointer focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-secondary dark:text-foreground dark:border-border"
|
||||
>
|
||||
{availableModels.length === 0 && (
|
||||
<option value="">{modelsLoading ? "加载中…" : "未配置模型"}</option>
|
||||
)}
|
||||
{availableModels.map((m) => (
|
||||
<option key={m.name} value={m.name}>
|
||||
{m.display_name && m.display_name !== m.name
|
||||
? `${m.display_name} (${m.name})`
|
||||
: m.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{/* 后台挂起:查看历史记录时隐藏(isViewingHistory),
|
||||
重新从第一步进入第二步研讨时(isViewingHistory 为假)正常显示。 */}
|
||||
{SHOW_BACKGROUND_SUSPEND && !isViewingHistory && (
|
||||
@@ -455,12 +483,17 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
type="button"
|
||||
onClick={onBackgroundSuspend}
|
||||
title="把当前研讨转入后台自动跑到结果绘制完成;可离开页面,进度在历史记录里查看"
|
||||
className="bg-slate-100 hover:bg-slate-200 text-slate-700 text-[calc(10px*var(--ui-font-scale,1))] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-slate-200/60 shadow-sm dark:bg-secondary dark:text-foreground dark:border-border"
|
||||
className={
|
||||
useStep1InputAppearance
|
||||
? "eightbf-roundtable__suspend-button"
|
||||
: "bg-slate-100 hover:bg-slate-200 text-slate-700 text-[calc(10px*var(--ui-font-scale,1))] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-slate-200/60 shadow-sm dark:bg-secondary dark:text-foreground dark:border-border"
|
||||
}
|
||||
>
|
||||
<Network className="w-3.5 h-3.5" />
|
||||
{!useStep1InputAppearance && <Network className="w-3.5 h-3.5" />}
|
||||
后台挂起
|
||||
</button>
|
||||
)}
|
||||
{headerRightSlot}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -530,7 +563,12 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
// 另一轴会被计算成 auto,任何子元素溢出一两像素都会让整列横向可滚 → 鼠标移到右侧
|
||||
// 竖滚条滑块时极易误触发横向拖动、抓不住滑块。宽内容(代码块/表格)在各自气泡内
|
||||
// 已有 max-w-full + overflow-x-auto 自带横滚,故此处裁掉横向不影响查看。
|
||||
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-6 space-y-4 bg-white border-t border-slate-150 border-b border-slate-150 shadow-inner"
|
||||
className={cn(
|
||||
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-6 space-y-4",
|
||||
useStep1InputAppearance
|
||||
? "eightbf-roundtable-step2-log"
|
||||
: "bg-white border-t border-slate-150 border-b border-slate-150 shadow-inner",
|
||||
)}
|
||||
>
|
||||
{isInitializing && step2RoundtableDialogues.length === 0 && (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 py-12 text-slate-500">
|
||||
@@ -791,7 +829,16 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
seatMessages={activeSeatMessages}
|
||||
taskArtifactSubmission={taskArtifactSubmission}
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-col bg-slate-50/20">{chatBody}</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full min-h-0 flex-col",
|
||||
useStep1InputAppearance
|
||||
? "position-roundtable-intent-panel eightbf-roundtable-intent-panel eightbf-roundtable-step2-chat"
|
||||
: "bg-slate-50/20",
|
||||
)}
|
||||
>
|
||||
{chatBody}
|
||||
</div>
|
||||
</Step2SandboxLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,9 @@ import type {
|
||||
DraftStep2RunSnapshot,
|
||||
DraftStep2Snapshot,
|
||||
DraftStep3Snapshot,
|
||||
DraftStep3SummarySnapshot,
|
||||
} from "@/roundtable-planning/api/drafts";
|
||||
import type { ThreadIdMap } from "@/roundtable-planning/api/multi-agent";
|
||||
import {
|
||||
useStep1Intent,
|
||||
type InteractiveMessage,
|
||||
@@ -316,6 +318,8 @@ export default function RoundtablePlanningPage({
|
||||
renderStep2LeftSidebarHeader,
|
||||
/** 第一阶段使用内嵌右栏时,隐藏通用的模型/摘要/澄清记录栏。 */
|
||||
hideDefaultStep1RightSidebar = false,
|
||||
step2HeaderTitle,
|
||||
step2HeaderRightSlot,
|
||||
/** 独立业务页的工作区品牌区,取代通用步骤导航。 */
|
||||
headerLeftSlot,
|
||||
/** 放在历史记录后面的独立业务操作或状态。 */
|
||||
@@ -343,6 +347,11 @@ export default function RoundtablePlanningPage({
|
||||
selectedModel: string;
|
||||
modelOptions: Array<{ label: string; value: string }>;
|
||||
onSelectedModelChange: (model: string) => void;
|
||||
step2Dialogues: unknown[];
|
||||
step2ThreadIds: ThreadIdMap | null;
|
||||
step2CoordinatorName: string | null;
|
||||
step2SeatStubMessages: Record<string, unknown[]>;
|
||||
step3Summary: DraftStep3SummarySnapshot | null;
|
||||
}) => ReactNode;
|
||||
renderStep1LeftSidebar?: (context: { intentReady: object | null }) => ReactNode;
|
||||
renderStep2LeftSidebarHeader?: (context: {
|
||||
@@ -350,6 +359,10 @@ export default function RoundtablePlanningPage({
|
||||
onCollapse: () => void;
|
||||
}) => ReactNode;
|
||||
hideDefaultStep1RightSidebar?: boolean;
|
||||
/** 8BF 第二步对话区标题,与第一步顶栏同一套样式。 */
|
||||
step2HeaderTitle?: string;
|
||||
/** 8BF 第二步顶栏右侧附加操作(如返回)。 */
|
||||
step2HeaderRightSlot?: ReactNode;
|
||||
headerLeftSlot?: ReactNode;
|
||||
headerActionAfterHistory?: ReactNode;
|
||||
hideHeaderSaveDraft?: boolean;
|
||||
@@ -2401,6 +2414,11 @@ export default function RoundtablePlanningPage({
|
||||
selectedModel,
|
||||
modelOptions: availableModels.map((model) => ({ label: model.name, value: model.name })),
|
||||
onSelectedModelChange: setSelectedModel,
|
||||
step2Dialogues: step2.step2RoundtableDialogues,
|
||||
step2ThreadIds: step2.threadIds,
|
||||
step2CoordinatorName: step2.coordinatorName,
|
||||
step2SeatStubMessages: step2.seatStubMessages,
|
||||
step3Summary: step3Report?.summaryReport ?? null,
|
||||
}) : <Step1Panel
|
||||
{...step1PanelProps}
|
||||
headerLeftSlot={
|
||||
@@ -2421,6 +2439,8 @@ export default function RoundtablePlanningPage({
|
||||
{currentStep === 2 && (
|
||||
<Step2Panel
|
||||
useStep1InputAppearance={rootClassName?.includes("eightbf-roundtable") ?? false}
|
||||
headerTitle={step2HeaderTitle}
|
||||
headerRightSlot={step2HeaderRightSlot}
|
||||
step2RoundtableDialogues={step2.step2RoundtableDialogues}
|
||||
isInitializing={step2.isInitializing}
|
||||
isOrchestrating={step2.isOrchestrating}
|
||||
|
||||
Reference in new issue
Block a user