Merge branch 'master-nj' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-dev

This commit is contained in:
董宏凯 committed 2026-08-03 12:13:59 +08:00
commit d93cc62806
28 files changed
+2653 -360

No files matched your search

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;}</style></defs><rect class="a" width="16" height="16"/><path class="b" d="M24.036,10l-.012,9.333" transform="translate(-16.016 -6.667)"/><path class="b" d="M10,24h9.333" transform="translate(-6.667 -16)"/></svg>

After

Width:  |  Height:  |  Size: 401 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="36" height="36" viewBox="0 0 36 36"><defs><style>.a,.d{fill:#fff;}.a{opacity:0;}.b{fill:#da2331;}.c{fill:#0a54cf;}.e{fill:#1677ff;}.f{fill:#1e2433;}.g{fill:#10d6ff;}.h{fill:#ff8181;}.i{fill:none;stroke:#10d6ff;stroke-linecap:round;stroke-linejoin:round;}.j{filter:url(#d);}.k{filter:url(#a);}</style><filter id="a"><feOffset dx="4" input="SourceAlpha"/><feGaussianBlur stdDeviation="1" result="b"/><feFlood flood-opacity="0.161" result="c"/><feComposite operator="out" in="SourceGraphic" in2="b"/><feComposite operator="in" in="c"/><feComposite operator="in" in2="SourceGraphic"/></filter><filter id="d"><feOffset dx="-4" input="SourceAlpha"/><feGaussianBlur stdDeviation="1" result="e"/><feFlood flood-opacity="0.161" result="f"/><feComposite operator="out" in="SourceGraphic" in2="e"/><feComposite operator="in" in="f"/><feComposite operator="in" in2="SourceGraphic"/></filter></defs><g transform="translate(0 -1)"><rect class="a" width="36" height="36" transform="translate(0 1)"/><path class="b" d="M23.5,14.75a1.5,1.5,0,0,1-1.5-1.5V9.5a1.5,1.5,0,1,1,3,0v3.75A1.5,1.5,0,0,1,23.5,14.75Z" transform="translate(-5.5 -1)"/><g data-type="innerShadowGroup"><path class="c" d="M-2-2H1C3.206-2,5-.532,5,1.273V3.727C5,5.532,3.206,7,1,7H-2Z" transform="translate(28.66 19.541)"/><g class="k" transform="matrix(1, 0, 0, 1, 0, 1)"><path class="d" d="M-2-2H1C3.206-2,5-.532,5,1.273V3.727C5,5.532,3.206,7,1,7H-2Z" transform="translate(28.66 18.54)"/></g></g><g data-type="innerShadowGroup"><path class="c" d="M5-2H2C-.206-2-2-.532-2,1.273V3.727C-2,5.532-.206,7,2,7H5Z" transform="translate(4.34 19.541)"/><g class="j" transform="matrix(1, 0, 0, 1, 0, 1)"><path class="d" d="M5-2H2C-.206-2-2-.532-2,1.273V3.727C-2,5.532-.206,7,2,7H5Z" transform="translate(4.34 18.54)"/></g></g><path class="e" d="M5.5-2H16a7.508,7.508,0,0,1,7.5,7.5V13A7.508,7.508,0,0,1,16,20.5H5.5A7.508,7.508,0,0,1-2,13V5.5A7.508,7.508,0,0,1,5.5-2Z" transform="translate(7.25 13.5)"/><path class="f" d="M5.25,0h9A5.25,5.25,0,0,1,19.5,5.25V7.5a5.25,5.25,0,0,1-5.25,5.25h-9A5.25,5.25,0,0,1,0,7.5V5.25A5.25,5.25,0,0,1,5.25,0Z" transform="translate(8.25 15.25)"/><circle class="g" cx="1.5" cy="1.5" r="1.5" transform="translate(21 19)"/><circle class="g" cx="1.5" cy="1.5" r="1.5" transform="translate(12 19)"/><path class="d" d="M24,10v3.75" transform="translate(-6 -1.5)"/><circle class="h" cx="3" cy="3" r="3" transform="translate(15 4)"/><path class="i" d="M44,28.667a3.367,3.367,0,0,0,1.922.412,2.841,2.841,0,0,0,1.828-.412" transform="translate(-27.875 -5.017)"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96"><defs><style>.a{fill:#fff;opacity:0;}.b,.c{fill:none;stroke:#006efe;stroke-width:6px;}.b{stroke-linecap:round;stroke-linejoin:round;}</style></defs><g transform="translate(0 1)"><rect class="a" width="96" height="96" transform="translate(0 -1)"/><path class="b" d="M47,67H38L18,77V67H4V7H84V29" transform="translate(4 8)"/><circle class="c" cx="12" cy="12" r="12" transform="translate(56 45)"/><path class="b" d="M39,32l10,8" transform="translate(39 33)"/><path class="b" d="M12,15H24" transform="translate(12 16)"/><path class="b" d="M12,21H36" transform="translate(12 22)"/></g></svg>

After

Width:  |  Height:  |  Size: 670 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13" height="11" viewBox="0 0 13 11"><defs><style>.a{stroke:rgba(0,0,0,0);stroke-miterlimit:10;fill:url(#a);}</style><linearGradient id="a" x1="0.277" y1="0.03" x2="0.725" y2="1.083" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><path class="a" d="M1,10A1,1,0,1,1,1,8H6a1,1,0,0,1,0,2Zm9.8-.482L7.848,7.361A.445.445,0,0,1,7.661,7a.46.46,0,0,1,.187-.366L10.8,4.481a.45.45,0,0,1,.716.365V9.152a.44.44,0,0,1-.131.317.445.445,0,0,1-.322.136A.454.454,0,0,1,10.8,9.518ZM1,6A1,1,0,1,1,1,4H6A1,1,0,0,1,6,6ZM1,2A1,1,0,1,1,1,0H11a1,1,0,1,1,0,2Z" transform="translate(0.5 0.5)"/></svg>

After

Width:  |  Height:  |  Size: 753 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13" height="11" viewBox="0 0 13 11"><defs><style>.a{fill:url(#a);}.b{fill:url(#b);}</style><linearGradient id="a" x1="0.023" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff" stop-opacity="0.302"/><stop offset="1" stop-color="#00c7ff" stop-opacity="0.302"/></linearGradient><linearGradient id="b" x1="0.023" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><path class="a" d="M5.5,0,11,8H0Z" transform="translate(13) rotate(90)"/><path class="b" d="M5.5,0,11,8H0Z" transform="translate(8) rotate(90)"/></svg>

After

Width:  |  Height:  |  Size: 735 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:rgba(255,255,255,0.8);}</style></defs><rect class="a" width="16" height="16"/><path class="b" d="M10,1.5A1.5,1.5,0,1,1,11.5,3,1.5,1.5,0,0,1,10,1.5Zm-5,0A1.5,1.5,0,1,1,6.5,3,1.5,1.5,0,0,1,5,1.5Zm-5,0A1.5,1.5,0,1,1,1.5,3,1.5,1.5,0,0,1,0,1.5Z" transform="translate(1.498 6.5)"/></svg>

After

Width:  |  Height:  |  Size: 410 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:rgba(85,166,254,0.6);}</style></defs><rect class="a" width="16" height="16"/><path class="b" d="M12.715,13.421,9.588,10.294a5.841,5.841,0,1,1,.707-.707l3.128,3.128a.5.5,0,0,1-.708.706ZM1,5.833A4.833,4.833,0,1,0,5.834,1,4.838,4.838,0,0,0,1,5.833Z" transform="translate(1.334 1.216)"/></svg>

After

Width:  |  Height:  |  Size: 418 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;}</style></defs><path class="a" d="M14.488.053.824,4.256a1.167,1.167,0,0,0-.285,2.1L4.576,8.936a1.167,1.167,0,0,0,1.382-.092L11,4.593A.292.292,0,0,1,11.408,5L7.156,10.043a1.167,1.167,0,0,0-.091,1.382l2.58,4.036a1.168,1.168,0,0,0,2.1-.286l4.2-13.663A1.167,1.167,0,0,0,14.488.053Z" transform="translate(0)"/></svg>

After

Width:  |  Height:  |  Size: 422 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16.131" height="16" viewBox="0 0 16.131 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:none;stroke:#97edff;stroke-linecap:round;stroke-linejoin:round;}</style></defs><rect class="a" width="16" height="16"/><path class="b" d="M6,30V34.12H18.361V30" transform="translate(-2.73 -19.994)"/><path class="b" d="M21.181,9.09,18.09,6,15,9.09" transform="translate(-8.64 -4.235)"/><path class="b" d="M23.992,14.241V6" transform="translate(-14.544 -4.235)"/></svg>

After

Width:  |  Height:  |  Size: 504 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b,.c{fill:none;stroke:#346fb1;stroke-linejoin:round;}.c{stroke-linecap:round;}</style></defs><rect class="a" width="16" height="16"/><path class="b" d="M10.667,17.333A6.667,6.667,0,1,0,4,10.667,6.667,6.667,0,0,0,10.667,17.333Z" transform="translate(-2.667 -2.667)"/><path class="c" d="M24.008,12v4l2.826,2.826" transform="translate(-16.005 -8)"/></svg>

After

Width:  |  Height:  |  Size: 473 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="95.001" height="26" viewBox="0 0 95.001 26"><defs><style>.a{fill:rgba(212,120,0,0.5);}</style></defs><path class="a" d="M250.146,407.92h91l-11.4,18.235c-2.679,4.26-5.243,7.5-11.211,7.765H246.146v-22A4,4,0,0,1,250.146,407.92Z" transform="translate(-246.146 -407.92)"/></svg>

After

Width:  |  Height:  |  Size: 320 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="95.001" height="26" viewBox="0 0 95.001 26"><defs><style>.a{fill:rgba(207,19,34,0.5);}</style></defs><path class="a" d="M250.146,407.92h91l-11.4,18.235c-2.679,4.26-5.243,7.5-11.211,7.765H246.146v-22A4,4,0,0,1,250.146,407.92Z" transform="translate(-246.146 -407.92)"/></svg>

After

Width:  |  Height:  |  Size: 320 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="95.001" height="26" viewBox="0 0 95.001 26"><defs><style>.a{fill:rgba(29,171,123,0.5);}</style></defs><path class="a" d="M250.146,407.92h91l-11.4,18.235c-2.679,4.26-5.243,7.5-11.211,7.765H246.146v-22A4,4,0,0,1,250.146,407.92Z" transform="translate(-246.146 -407.92)"/></svg>

After

Width:  |  Height:  |  Size: 321 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="95.001" height="26" viewBox="0 0 95.001 26"><defs><style>.a{fill:rgba(132,85,230,0.5);}</style></defs><path class="a" d="M250.146,407.92h91l-11.4,18.235c-2.679,4.26-5.243,7.5-11.211,7.765H246.146v-22A4,4,0,0,1,250.146,407.92Z" transform="translate(-246.146 -407.92)"/></svg>

After

Width:  |  Height:  |  Size: 321 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="95.001" height="26" viewBox="0 0 95.001 26"><defs><style>.a{fill:rgba(201,181,0,0.5);}</style></defs><path class="a" d="M250.146,407.92h91l-11.4,18.235c-2.679,4.26-5.243,7.5-11.211,7.765H246.146v-22A4,4,0,0,1,250.146,407.92Z" transform="translate(-246.146 -407.92)"/></svg>

After

Width:  |  Height:  |  Size: 320 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="95.001" height="26" viewBox="0 0 95.001 26"><defs><style>.a{fill:rgba(69,195,243,0.5);}</style></defs><path class="a" d="M250.146,407.92h91l-11.4,18.235c-2.679,4.26-5.243,7.5-11.211,7.765H246.146v-22A4,4,0,0,1,250.146,407.92Z" transform="translate(-246.146 -407.92)"/></svg>

After

Width:  |  Height:  |  Size: 321 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:none;stroke:#346fb1;stroke-linecap:round;stroke-linejoin:round;}</style></defs><g transform="translate(0 -2)"><rect class="a" width="16" height="16" transform="translate(0 2)"/><path class="b" d="M17.333,8H4V18H9l1.667,1.667L12.333,18h5Z" transform="translate(-2.667 -3.667)"/><circle class="b" cx="1.667" cy="1.667" r="1.667" transform="translate(6.333 6.333)"/><path class="b" d="M21,26.667A2.848,2.848,0,0,0,18,24a2.848,2.848,0,0,0-3,2.667" transform="translate(-10 -14.333)"/></g></svg>

After

Width:  |  Height:  |  Size: 618 B

@@ -100,15 +100,17 @@ function Section({
icon, icon,
title, title,
extra, extra,
className,
children, children,
}: { }: {
icon?: ReactNode; icon?: ReactNode;
title: string; title: string;
extra?: ReactNode; extra?: ReactNode;
className?: string;
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<section className="bg-card/60 rounded-xl border p-4"> <section className={cn("bg-card/60 rounded-xl border p-4", className)}>
<div className="mb-3 flex items-center justify-between gap-2"> <div className="mb-3 flex items-center justify-between gap-2">
<h4 className="text-foreground flex items-center gap-1.5 text-sm font-semibold"> <h4 className="text-foreground flex items-center gap-1.5 text-sm font-semibold">
{icon} {icon}
@@ -139,6 +141,15 @@ export function AgentDetailPanel({
defaultCollapsed, defaultCollapsed,
showRecentChats, showRecentChats,
compactAppearanceActions, compactAppearanceActions,
avatarSrc,
positionName,
skillsIcon,
skillsTitle,
showCommonQuestions = false,
panelTitle,
embedded,
hideStatusBadges = false,
editIcon,
}: { }: {
agent: Agent | null | undefined; agent: Agent | null | undefined;
agentName: string; agentName: string;
@@ -150,6 +161,19 @@ export function AgentDetailPanel({
showRecentChats?: boolean; showRecentChats?: boolean;
/** 简洁模式下挂在右上角收起/展开按钮左侧的外观控件。 */ /** 简洁模式下挂在右上角收起/展开按钮左侧的外观控件。 */
compactAppearanceActions?: ReactNode; compactAppearanceActions?: ReactNode;
avatarSrc?: string;
positionName?: string;
skillsIcon?: ReactNode;
skillsTitle?: string;
showCommonQuestions?: boolean;
/** Overrides the standard panel heading for an embedded consumer. */
panelTitle?: string;
/** Renders one fixed panel instead of the responsive collapsible rail. */
embedded?: boolean;
/** Keeps deployment/status badges out of an embedded product surface. */
hideStatusBadges?: boolean;
/** Optional icon used by the compact embedded edit action. */
editIcon?: ReactNode;
}) { }) {
const router = useRouter(); const router = useRouter();
const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false); const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false);
@@ -472,11 +496,25 @@ export function AgentDetailPanel({
</div> </div>
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button size="sm" variant="outline" onClick={beginEdit}> {embedded ? (
<SquarePenIcon className="size-3.5" /> <Tooltip content="编辑智能体">
编辑 <Button
</Button> type="button"
{canTogglePublish && ( size="icon-sm"
variant="ghost"
onClick={beginEdit}
aria-label="编辑智能体"
>
{editIcon ?? <SquarePenIcon className="size-3.5" />}
</Button>
</Tooltip>
) : (
<Button size="sm" variant="outline" onClick={beginEdit}>
<SquarePenIcon className="size-3.5" />
编辑
</Button>
)}
{!embedded && canTogglePublish && (
<Tooltip content={agent?.published ? "下架" : "发布"}> <Tooltip content={agent?.published ? "下架" : "发布"}>
<Button <Button
size="icon-sm" size="icon-sm"
@@ -493,17 +531,19 @@ export function AgentDetailPanel({
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
<Tooltip content="删除智能体"> {!embedded && (
<Button <Tooltip content="删除智能体">
size="icon-sm" <Button
variant="ghost" size="icon-sm"
className="text-destructive hover:text-destructive" variant="ghost"
onClick={() => setDeleteOpen(true)} className="text-destructive hover:text-destructive"
aria-label="删除智能体" onClick={() => setDeleteOpen(true)}
> aria-label="删除智能体"
<Trash2Icon className="size-3.5" /> >
</Button> <Trash2Icon className="size-3.5" />
</Tooltip> </Button>
</Tooltip>
)}
</div> </div>
) )
) : null; ) : null;
@@ -521,7 +561,11 @@ export function AgentDetailPanel({
<section className="bg-card/60 rounded-xl border p-4"> <section className="bg-card/60 rounded-xl border p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="agent-avatar bg-primary/10 ring-primary/15 flex size-12 shrink-0 items-center justify-center rounded-2xl ring-1"> <div className="agent-avatar bg-primary/10 ring-primary/15 flex size-12 shrink-0 items-center justify-center rounded-2xl ring-1">
<BotIcon className="text-primary size-6" /> {avatarSrc ? (
<img src={avatarSrc} alt="" aria-hidden="true" className="size-9" />
) : (
<BotIcon className="text-primary size-6" />
)}
</div> </div>
<div className="min-w-0 flex-1 space-y-1.5"> <div className="min-w-0 flex-1 space-y-1.5">
{editing && draft ? ( {editing && draft ? (
@@ -541,9 +585,16 @@ export function AgentDetailPanel({
{displayName} {displayName}
</div> </div>
)} )}
<div className="flex flex-wrap items-center gap-1"> {!editing && positionName && (
{statusBadges} <div className="position-agent-role text-sm">
</div> 岗位:{desensitize(positionName)}
</div>
)}
{!hideStatusBadges && (
<div className="flex flex-wrap items-center gap-1">
{statusBadges}
</div>
)}
{!editing && agent.owner_username && !agent.builtin && ( {!editing && agent.owner_username && !agent.builtin && (
<div className="text-muted-foreground flex items-center gap-1 text-xs"> <div className="text-muted-foreground flex items-center gap-1 text-xs">
<UserRoundIcon className="size-3 shrink-0" /> <UserRoundIcon className="size-3 shrink-0" />
@@ -646,7 +697,7 @@ export function AgentDetailPanel({
{/* ── 常用问题 ───────────────────────────────────────────────────── */} {/* ── 常用问题 ───────────────────────────────────────────────────── */}
{(editing || questions.length > 0 || canEdit) && ( {(showCommonQuestions || editing || questions.length > 0 || canEdit) && (
<Section <Section
icon={<MessageCircleQuestionIcon className="size-3.5" />} icon={<MessageCircleQuestionIcon className="size-3.5" />}
title="常用问题" title="常用问题"
@@ -787,8 +838,9 @@ export function AgentDetailPanel({
{/* ── 技能管理 ───────────────────────────────────────────────────── */} {/* ── 技能管理 ───────────────────────────────────────────────────── */}
<Section <Section
icon={<SparklesIcon className="size-3.5" />} icon={skillsIcon ?? <SparklesIcon className="size-3.5" />}
title={`已启用技能 ${skillNamesShown.length} 个`} title={skillsTitle ?? `已启用技能 ${skillNamesShown.length} 个`}
className="position-roundtable-skills-section"
extra={ extra={
editing ? ( editing ? (
<Button <Button
@@ -830,7 +882,9 @@ export function AgentDetailPanel({
const panelChrome = (collapseButton: ReactNode) => ( const panelChrome = (collapseButton: ReactNode) => (
<> <>
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4"> <div className="flex h-12 shrink-0 items-center justify-between border-b px-4">
<span className="shrink-0 whitespace-nowrap text-sm font-semibold">智能体信息</span> <span className="shrink-0 whitespace-nowrap text-sm font-semibold">
{panelTitle ?? "智能体信息"}
</span>
<div className="flex min-w-0 items-center gap-1"> <div className="flex min-w-0 items-center gap-1">
{headerActions} {headerActions}
{compactAppearanceActions} {compactAppearanceActions}
@@ -844,7 +898,11 @@ export function AgentDetailPanel({
return ( return (
<> <>
{/* ── Desktop (lg+): inline aside, collapsible to a rail ───────────── */} {/* ── Desktop (lg+): inline aside, collapsible to a rail ───────────── */}
{collapsed ? ( {embedded ? (
<aside className="bg-card/40 flex h-full w-full min-w-0 shrink-0 flex-col border-l">
{panelChrome(null)}
</aside>
) : collapsed ? (
<div className="hidden shrink-0 items-start border-l px-2 pt-3 lg:flex"> <div className="hidden shrink-0 items-start border-l px-2 pt-3 lg:flex">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{compactAppearanceActions} {compactAppearanceActions}
@@ -877,30 +935,34 @@ export function AgentDetailPanel({
</aside> </aside>
)} )}
{/* ── Narrow screens (<lg): rail trigger + drawer with the same body ── */} {!embedded && (
<div className="flex shrink-0 flex-col items-center border-l pt-3 lg:hidden"> <>
<Tooltip content="智能体信息"> {/* ── Narrow screens (<lg): rail trigger + drawer with the same body ── */}
<Button <div className="flex shrink-0 flex-col items-center border-l pt-3 lg:hidden">
variant="ghost" <Tooltip content="智能体信息">
size="icon-sm" <Button
onClick={() => setMobileOpen(true)} variant="ghost"
aria-label="打开智能体信息" size="icon-sm"
> onClick={() => setMobileOpen(true)}
<InfoIcon className="size-4" /> aria-label="打开智能体信息"
</Button> >
</Tooltip> <InfoIcon className="size-4" />
</div> </Button>
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}> </Tooltip>
<SheetContent side="right" className="flex w-full flex-col gap-0 p-0 sm:max-w-md"> </div>
<SheetHeader className="border-b px-4 pb-3 pt-4"> <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTitle className="flex items-center justify-between text-sm"> <SheetContent side="right" className="flex w-full flex-col gap-0 p-0 sm:max-w-md">
智能体信息 <SheetHeader className="border-b px-4 pb-3 pt-4">
<span className="mr-6">{headerActions}</span> <SheetTitle className="flex items-center justify-between text-sm">
</SheetTitle> 智能体信息
</SheetHeader> <span className="mr-6">{headerActions}</span>
<div className="min-h-0 flex-1 overflow-y-auto">{body}</div> </SheetTitle>
</SheetContent> </SheetHeader>
</Sheet> <div className="min-h-0 flex-1 overflow-y-auto">{body}</div>
</SheetContent>
</Sheet>
</>
)}
{/* ── Secondary surfaces ───────────────────────────────────────────── */} {/* ── Secondary surfaces ───────────────────────────────────────────── */}
<SkillPickerSheet <SkillPickerSheet
@@ -1,10 +1,30 @@
import { useEffect, useRef, type ComponentProps } from "react"; import { useEffect, useMemo, useRef, type ComponentProps } from "react";
import { InputBox } from "@/components/workspace/input-box"; import {
import { usePromptInputController } from "@/components/ai-elements/prompt-input"; usePromptInputController,
import { cn } from "@/lib/utils"; type PromptInputMessage,
} from "@/components/ai-elements/prompt-input";
import type { InputBox } from "@/components/workspace/input-box";
import { useModels } from "@/core/models/hooks";
import type { AgentQuestion } from "@/core/agents";
import { PositionIntentInput } from "@/position-roundtable/components/PositionIntentInput";
type PositionAgentInputProps = ComponentProps<typeof InputBox> & { type InputBoxProps = ComponentProps<typeof InputBox>;
type PositionAgentInputProps = Pick<
InputBoxProps,
| "className"
| "assistantId"
| "status"
| "disabled"
| "context"
| "onContextChange"
| "onSubmit"
| "onStop"
| "isNewThread"
| "threadId"
| "hideAdminRecommendedQuestions"
> & {
commonQuestions?: (string | AgentQuestion)[] | null;
/** Stable node/builtin key used to restore the matching SQL-backed draft. */ /** Stable node/builtin key used to restore the matching SQL-backed draft. */
draftKey?: string; draftKey?: string;
draftValue?: string; draftValue?: string;
@@ -18,18 +38,36 @@ type PositionAgentInputProps = ComponentProps<typeof InputBox> & {
* 只改这个组件即可,不会影响普通对话、工作区或其他业务页面。 * 只改这个组件即可,不会影响普通对话、工作区或其他业务页面。
*/ */
export function PositionAgentInput({ export function PositionAgentInput({
className, context,
disabled = false,
status = "ready",
onContextChange,
onSubmit,
onStop,
commonQuestions,
draftKey = "", draftKey = "",
draftValue = "", draftValue = "",
onDraftChange, onDraftChange,
...props
}: PositionAgentInputProps) { }: PositionAgentInputProps) {
const { textInput } = usePromptInputController(); const { textInput, attachments } = usePromptInputController();
const { models } = useModels();
const activeDraftKeyRef = useRef(""); const activeDraftKeyRef = useRef("");
const restoringValueRef = useRef<string | null>(null); const restoringValueRef = useRef<string | null>(null);
const textInputRef = useRef(textInput); const textInputRef = useRef(textInput);
textInputRef.current = textInput; textInputRef.current = textInput;
const modelOptions = useMemo(
() => models.map((model) => ({ label: model.display_name ?? model.name, value: model.name })),
[models],
);
const attachedFiles = useMemo(
() =>
attachments.files
.map((attachment) => attachment.file)
.filter((file): file is File => file instanceof File),
[attachments.files],
);
useEffect(() => { useEffect(() => {
activeDraftKeyRef.current = draftKey; activeDraftKeyRef.current = draftKey;
if (textInputRef.current.value !== draftValue) { if (textInputRef.current.value !== draftValue) {
@@ -51,13 +89,60 @@ export function PositionAgentInput({
onDraftChange?.(textInput.value); onDraftChange?.(textInput.value);
}, [draftKey, onDraftChange, textInput.value]); }, [draftKey, onDraftChange, textInput.value]);
// Keep the page-specific class on the wrapper rather than adding a variant const handleSend = () => {
// to InputBox. Node chat and built-in chat can evolve together while every if (status === "streaming") {
// other workspace keeps the global composer untouched. onStop?.();
return;
}
const text = textInput.value.trim();
if (!text && attachedFiles.length === 0) return;
const message: PromptInputMessage = {
text: textInput.value,
files: attachments.files,
};
void onSubmit?.(message);
textInput.clear();
attachments.clear();
};
const handleModelChange = (modelName: string) => {
const model = models.find((candidate) => candidate.name === modelName);
if (!model) return;
onContextChange?.({ ...context, model_name: model.name });
};
const selectedModel =
typeof context.model_name === "string"
? context.model_name
: modelOptions[0]?.value;
return ( return (
<InputBox <PositionIntentInput
{...props} value={textInput.value}
className={cn("position-roundtable-agent-input w-full", className)} onValueChange={textInput.setInput}
attachedFiles={attachedFiles}
onAddFiles={attachments.add}
onRemoveFile={(index) => {
const attachment = attachments.files[index];
if (attachment) attachments.remove(attachment.id);
}}
charactersLeft={textInput.value.length}
isSending={status === "streaming"}
canSend={!disabled && (Boolean(textInput.value.trim()) || attachedFiles.length > 0)}
pendingClarification={false}
onSend={handleSend}
onStop={() => onStop?.()}
onFollowUp={() => undefined}
onSkip={() => undefined}
onEnterRoundtable={() => undefined}
enterButtonLabel=""
intentReady={null}
hideEnterRoundtable
onManualSave={() => undefined}
onTriggerToast={() => undefined}
modelOptions={modelOptions}
selectedModel={selectedModel}
onSelectedModelChange={handleModelChange}
commonQuestions={commonQuestions}
/> />
); );
} }
@@ -716,7 +716,7 @@ export function PositionBuiltinAgentChat({
</div> </div>
)} )}
<div className="position-roundtable-agent-composer-bar shrink-0 border-t border-slate-100 px-4 py-3 dark:border-[#2b8ffe]/20"> <div className="position-roundtable-agent-composer-bar position-roundtable-node-composer shrink-0">
<PositionAgentInput <PositionAgentInput
className="w-full" className="w-full"
isNewThread={!boundThreadId} isNewThread={!boundThreadId}
@@ -1,12 +1,9 @@
import { useRef } from "react"; import { useRef, useState } from "react";
import { import {
FileText, FileText,
Loader2,
Paperclip,
Pause, Pause,
Repeat, Repeat,
Save, Save,
Send,
Smile, Smile,
Sparkles, Sparkles,
X, X,
@@ -14,7 +11,11 @@ import {
import { Select as TSelect } from "tdesign-react"; import { Select as TSelect } from "tdesign-react";
import { AutoGrowTextarea } from "@/components/ui/auto-grow-textarea"; import { AutoGrowTextarea } from "@/components/ui/auto-grow-textarea";
import { normalizeAgentQuestions, type AgentQuestion } from "@/core/agents";
import type { Step1InputAreaProps } from "@/roundtable-planning/components/Step1Panel"; import type { Step1InputAreaProps } from "@/roundtable-planning/components/Step1Panel";
import moreTaskIcon from "@/assets/position-roundtable/more-task.svg";
import uploadFileIcon from "@/assets/position-roundtable/upload-file.svg";
import sendMessageIcon from "@/assets/position-roundtable/send-message.svg";
interface PositionIntentModelOption { interface PositionIntentModelOption {
label: string; label: string;
@@ -25,6 +26,7 @@ interface PositionIntentInputProps extends Step1InputAreaProps {
modelOptions?: PositionIntentModelOption[]; modelOptions?: PositionIntentModelOption[];
selectedModel?: string; selectedModel?: string;
onSelectedModelChange?: (model: string) => void; onSelectedModelChange?: (model: string) => void;
commonQuestions?: (string | AgentQuestion)[] | null;
} }
/** Keep attachment metadata presentation local to this position-only composer. */ /** Keep attachment metadata presentation local to this position-only composer. */
@@ -64,40 +66,25 @@ export function PositionIntentInput({
modelOptions = [], modelOptions = [],
selectedModel, selectedModel,
onSelectedModelChange, onSelectedModelChange,
commonQuestions,
}: PositionIntentInputProps) { }: PositionIntentInputProps) {
// Files are owned by Step1Panel. This ref only opens the native picker and // Files are owned by Step1Panel. This ref only opens the native picker and
// forwards its selection through onAddFiles, so replacement layouts do not // forwards its selection through onAddFiles, so replacement layouts do not
// accidentally create a second attachment state. // accidentally create a second attachment state.
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [showMoreQuestions, setShowMoreQuestions] = useState(false);
const normalizedCommonQuestions = normalizeAgentQuestions(commonQuestions);
const visibleCommonQuestions = normalizedCommonQuestions.slice(0, 2);
const extraCommonQuestions = normalizedCommonQuestions.slice(2);
const pickCommonQuestion = (question: AgentQuestion) => {
onValueChange(question.prompt);
setShowMoreQuestions(false);
};
return ( return (
<div className="position-roundtable-intent-input position-roundtable-intent-composer shrink-0 border-t border-slate-200 bg-[#f7faff] px-4 py-3 dark:border-[#2b8ffe]/35 dark:bg-[#071a38]"> <div className="position-roundtable-intent-input position-roundtable-intent-composer shrink-0 border-t border-slate-200 bg-[#f7faff] px-4 py-3 dark:border-[#2b8ffe]/35 dark:bg-[#071a38]">
<div className="position-roundtable-intent-composer-frame relative overflow-hidden rounded-[4px] border border-slate-300 bg-white shadow-[0_2px_10px_rgba(15,23,42,0.08)] transition-all focus-within:border-primary focus-within:ring-2 focus-within:ring-[var(--brand-200)]/45 dark:border-[#2b8ffe]/55 dark:bg-[#0d2855] dark:shadow-none dark:focus-within:border-[#56baff] dark:focus-within:ring-[#2b8ffe]/25"> <div className="position-roundtable-intent-composer-frame relative overflow-hidden rounded-[4px] border border-slate-300 bg-white shadow-[0_2px_10px_rgba(15,23,42,0.08)] transition-all focus-within:border-primary focus-within:ring-2 focus-within:ring-[var(--brand-200)]/45 dark:border-[#2b8ffe]/55 dark:bg-[#0d2855] dark:shadow-none dark:focus-within:border-[#56baff] dark:focus-within:ring-[#2b8ffe]/25">
{attachedFiles.length > 0 && (
<div className="position-roundtable-intent-attachments mx-3 mb-2 flex flex-wrap gap-1.5 border-b border-slate-100 pb-2 pt-2.5 dark:border-[#2b8ffe]/25">
{attachedFiles.map((file, index) => (
<span
key={`${file.name}-${file.size}-${index}`}
className="inline-flex max-w-[200px] items-center gap-1.5 rounded-[4px] border border-slate-200 bg-slate-50 py-1 pl-2 pr-1 text-[11px] text-slate-600 dark:border-[#2b8ffe]/30 dark:bg-[#102f63] dark:text-[#c4dcff]"
>
<FileText className="size-3 shrink-0 text-primary" />
<span className="truncate font-medium" title={file.name}>
{file.name}
</span>
<span className="shrink-0 text-slate-400">{formatFileSize(file.size)}</span>
<button
type="button"
onClick={() => onRemoveFile(index)}
className="shrink-0 rounded-[3px] p-0.5 text-slate-400 transition hover:bg-slate-200 hover:text-slate-600 dark:text-[#8aaee0] dark:hover:bg-[#21447c] dark:hover:text-white"
title="移除"
>
<X className="size-3" />
</button>
</span>
))}
</div>
)}
<AutoGrowTextarea <AutoGrowTextarea
id="input-text-area" id="input-text-area"
value={value} value={value}
@@ -144,7 +131,8 @@ export function PositionIntentInput({
title="上传参考文件(智能体将读取文件内容辅助理解任务)" title="上传参考文件(智能体将读取文件内容辅助理解任务)"
className="rounded-[3px] p-1.5 transition hover:bg-white hover:text-primary dark:hover:bg-[#16386e] dark:hover:text-[#73c8ff]" className="rounded-[3px] p-1.5 transition hover:bg-white hover:text-primary dark:hover:bg-[#16386e] dark:hover:text-[#73c8ff]"
> >
<Paperclip className="size-4" /> <img src={uploadFileIcon} alt="" aria-hidden="true" />
<span>上传附件</span>
</button> </button>
</div> </div>
<span className="font-mono text-[10px] text-slate-400">{charactersLeft}/500</span> <span className="font-mono text-[10px] text-slate-400">{charactersLeft}/500</span>
@@ -154,8 +142,79 @@ export function PositionIntentInput({
Parent pages can still hide optional actions by their stable ids Parent pages can still hide optional actions by their stable ids
without changing this component's layout or global input styles. without changing this component's layout or global input styles.
*/} */}
<div className="position-roundtable-intent-toolbar flex flex-wrap items-center gap-3 border-t border-slate-100 bg-white px-3 py-2.5 dark:border-[#2b8ffe]/25 dark:bg-[#0b234c]"> <div className="position-roundtable-intent-toolbar flex flex-wrap items-center gap-3 border-t border-slate-100 px-3 py-2.5 dark:border-[#2b8ffe]/25">
<div className="flex shrink-0 items-center"> <div className="position-roundtable-intent-common-slot">
{visibleCommonQuestions.length > 0 && (
<div className="position-roundtable-intent-common-questions">
{visibleCommonQuestions.map((question, index) => (
<button
key={`${question.prompt}-${index}`}
type="button"
className="position-roundtable-intent-common-question"
title={question.prompt}
onClick={() => pickCommonQuestion(question)}
>
<span aria-hidden="true" />
<span>{question.title}</span>
</button>
))}
{extraCommonQuestions.length > 0 && (
<div className="position-roundtable-intent-common-more">
<button
type="button"
className="position-roundtable-intent-common-more-button"
aria-label="更多常用问题"
aria-expanded={showMoreQuestions}
title="更多常用问题"
onClick={() => setShowMoreQuestions((visible) => !visible)}
>
<img src={moreTaskIcon} alt="" aria-hidden="true" />
</button>
{showMoreQuestions && (
<div className="position-roundtable-intent-common-more-menu">
{extraCommonQuestions.map((question, index) => (
<button
key={`${question.prompt}-${index}`}
type="button"
title={question.prompt}
onClick={() => pickCommonQuestion(question)}
>
{question.title}
</button>
))}
</div>
)}
</div>
)}
</div>
)}
</div>
{attachedFiles.length > 0 && (
<div className="position-roundtable-intent-file-cards flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto">
{attachedFiles.map((file, index) => (
<span
key={`${file.name}-${file.size}-${index}`}
className="inline-flex max-w-[220px] shrink-0 items-center gap-1.5 rounded-[4px] border border-[#2b8ffe]/45 bg-[#0b3168] py-1 pl-2 pr-1 text-[11px] text-[#c4dcff]"
>
<FileText className="size-3 shrink-0 text-[#73c8ff]" />
<span className="truncate font-medium" title={file.name}>
{file.name}
</span>
<span className="shrink-0 text-[#8aaee0]">{formatFileSize(file.size)}</span>
<button
type="button"
onClick={() => onRemoveFile(index)}
className="shrink-0 cursor-pointer rounded-[3px] p-0.5 text-[#8aaee0] transition hover:bg-[#21447c] hover:text-white"
title="移除"
>
<X className="size-3" />
</button>
</span>
))}
</div>
)}
<div className="position-roundtable-intent-actions ml-auto flex min-w-0 flex-wrap items-center justify-end gap-1.5">
<div className="position-roundtable-intent-upload-wrap flex shrink-0 items-center">
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
@@ -169,28 +228,28 @@ export function PositionIntentInput({
<button <button
type="button" type="button"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
title="上传参考文件" title="上传附件"
aria-label="上传参考文件" aria-label="上传附件"
className="position-roundtable-intent-upload-btn flex size-10 items-center justify-center rounded-[4px] border border-slate-200 bg-white text-slate-500 transition hover:border-primary/45 hover:text-primary dark:border-[#2b8ffe]/35 dark:bg-[#102f63] dark:text-[#b9d3f7] dark:hover:bg-[#1a417b] dark:hover:text-[#73c8ff]" className="position-roundtable-intent-upload-btn flex items-center justify-center rounded-[24px] border-0"
> >
<Paperclip className="size-4" /> <img src={uploadFileIcon} alt="" aria-hidden="true" />
<span>上传附件</span>
</button> </button>
</div> </div>
<div className="position-roundtable-intent-actions ml-auto flex min-w-0 flex-wrap items-center justify-end gap-1.5">
{modelOptions.length > 0 && ( {modelOptions.length > 0 && (
<div <div
className="position-roundtable-intent-model-switch flex h-10 shrink-0 items-center gap-1.5 rounded-[4px] border border-slate-200 bg-white pl-2 text-xs font-bold text-slate-700 shadow-sm dark:border-[#2b8ffe]/35 dark:bg-[#102f63] dark:text-[#b9d3f7]" className="position-roundtable-intent-model-switch flex h-8 shrink-0 items-center gap-1.5 rounded-[20px] border border-[#2b8ffe]/55 bg-[#0b3168] px-2.5 text-xs font-bold text-[#dceaff]"
title="切换意图理解模型" title="鍒囨崲鎰忓浘鐞嗚В妯″瀷"
> >
<Sparkles className="size-3.5 shrink-0 text-primary dark:text-[#73c8ff]" /> <Sparkles className="size-3.5 shrink-0 text-[#73c8ff]" />
<TSelect <TSelect
size="small" size="small"
aria-label="切换意图理解模型" aria-label="鍒囨崲鎰忓浘鐞嗚В妯″瀷"
value={selectedModel} value={selectedModel}
options={modelOptions} options={modelOptions}
disabled={isSending} disabled={isSending}
onChange={(value) => onSelectedModelChange?.(String(value))} onChange={(value) => onSelectedModelChange?.(String(value))}
className="w-[150px] shrink-0 [&_.t-input]:!h-8 [&_.t-input]:!border-0 [&_.t-input]:!bg-transparent [&_.t-input]:!px-1 [&_.t-input]:!text-xs [&_.t-input]:!font-bold [&_.t-input]:!shadow-none dark:[&_.t-input]:!text-[#dceaff]" className="w-[150px] shrink-0 [&_.t-input]:!h-7 [&_.t-input]:!rounded-[4px] [&_.t-input]:!border-0 [&_.t-input]:!bg-transparent [&_.t-input]:!px-1 [&_.t-input]:!text-xs [&_.t-input]:!font-bold [&_.t-input]:!text-[#dceaff] [&_.t-input]:!shadow-none"
popupProps={{ overlayClassName: "z-[80]" }} popupProps={{ overlayClassName: "z-[80]" }}
/> />
</div> </div>
@@ -198,26 +257,20 @@ export function PositionIntentInput({
<button <button
id="send-message-btn" id="send-message-btn"
type="button" type="button"
onClick={onSend} onClick={isSending ? onStop : onSend}
disabled={!canSend} disabled={isSending ? false : !canSend}
className="position-roundtable-intent-send-btn flex h-10 shrink-0 items-center gap-1.5 rounded-[4px] bg-primary px-3 text-xs font-bold text-white shadow-[0_2px_8px_rgba(245,34,45,0.15)] transition-all active:scale-[0.98] hover:bg-[var(--brand-900)] disabled:cursor-not-allowed disabled:bg-slate-300 dark:bg-[#1478e8] dark:hover:bg-[#2995ff] dark:disabled:bg-[#28476f]" aria-label={isSending ? "暂停生成" : "发送"}
title={isSending ? "暂停生成" : "发送"}
className={`position-roundtable-intent-send-btn flex h-10 shrink-0 items-center gap-1.5 rounded-[4px] bg-primary px-3 text-xs font-bold text-white shadow-[0_2px_8px_rgba(245,34,45,0.15)] transition-all active:scale-[0.98] hover:bg-[var(--brand-900)] disabled:cursor-not-allowed disabled:bg-slate-300 dark:bg-[#1478e8] dark:hover:bg-[#2995ff] dark:disabled:bg-[#28476f]${isSending ? " position-roundtable-intent-send-btn--stopping" : ""}`}
> >
{isSending ? <Loader2 className="size-3 animate-spin" /> : <Send className="size-3" />} {isSending ? (
{isSending ? "发送中" : "发送"} <Pause className="size-4" aria-hidden="true" />
) : (
<img src={sendMessageIcon} alt="" aria-hidden="true" />
)}
<span className="sr-only">{isSending ? "暂停生成" : "发送"}</span>
</button> </button>
{isSending && (
<button
id="pause-intent-btn"
type="button"
onClick={onStop}
className="flex h-10 shrink-0 items-center gap-1.5 rounded-[4px] border border-amber-200/40 bg-amber-100 px-3 text-xs font-bold text-amber-800 shadow-sm transition active:scale-[0.98] hover:bg-amber-200 dark:border-amber-300/30 dark:bg-amber-400/15 dark:text-amber-200 dark:hover:bg-amber-400/25"
>
<Pause className="size-3" />
暂停
</button>
)}
<button <button
id="suggest-followup-btn" id="suggest-followup-btn"
type="button" type="button"
@@ -811,7 +811,7 @@ export function PositionNodeChat({
</div> </div>
)} )}
<div className="position-roundtable-agent-composer-bar shrink-0 border-t border-slate-100 px-4 py-3 dark:border-[#2b8ffe]/20"> <div className="position-roundtable-agent-composer-bar position-roundtable-node-composer shrink-0">
<PositionAgentInput <PositionAgentInput
className="w-full" className="w-full"
isNewThread={!boundThreadId} isNewThread={!boundThreadId}
@@ -825,6 +825,7 @@ export function PositionNodeChat({
onDraftChange={setInputDraft} onDraftChange={setInputDraft}
hideAdminRecommendedQuestions hideAdminRecommendedQuestions
onContextChange={(context) => setSettings("context", context)} onContextChange={(context) => setSettings("context", context)}
commonQuestions={agent?.common_questions}
onSubmit={(message) => void handleSubmit(message)} onSubmit={(message) => void handleSubmit(message)}
onStop={() => void thread.stop()} onStop={() => void thread.stop()}
/> />
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -7,6 +7,9 @@ import { resolveAllowMultiple } from "@/roundtable-planning/lib/clarification";
export type { ClarificationPayload }; export type { ClarificationPayload };
/** Stable built-in agent used by the multi-agent roundtable Step 1 flow. */
export const INTENT_AGENT_ID = "roundtable-intent";
/** /**
* Task-intent agent (Step 1) API. Mirror of multi-agent.ts but with a single * Task-intent agent (Step 1) API. Mirror of multi-agent.ts but with a single
* built-in agent and no per-call coordinator: one /init creates a thread * built-in agent and no per-call coordinator: one /init creates a thread
@@ -103,6 +103,12 @@ export interface Step1PanelProps {
*/ */
headerLeftSlot?: React.ReactNode; headerLeftSlot?: React.ReactNode;
headerRightSlot?: React.ReactNode; headerRightSlot?: React.ReactNode;
/** Optional side content rendered below the shared header, beside the dialogue area. */
rightSidebar?: React.ReactNode;
/** Hide the shared question-count badge when a workspace supplies its own header actions. */
hideQuestionCount?: boolean;
/** Optional empty state rendered in the centre of an otherwise blank dialogue list. */
emptyState?: React.ReactNode;
interactiveMessages: InteractiveMessage[]; interactiveMessages: InteractiveMessage[];
intentThreadId: string | null; intentThreadId: string | null;
intentReady: object | null; intentReady: object | null;
@@ -146,6 +152,9 @@ export function Step1Panel(props: Step1PanelProps) {
const { const {
headerLeftSlot, headerLeftSlot,
headerRightSlot, headerRightSlot,
rightSidebar,
hideQuestionCount = false,
emptyState,
rootClassName, rootClassName,
interactiveMessages, interactiveMessages,
intentThreadId, intentThreadId,
@@ -230,6 +239,7 @@ export function Step1Panel(props: Step1PanelProps) {
// 顶部标题栏整片脱敏(消息列表正文在下方,不在此容器内,保持原文)。 // 顶部标题栏整片脱敏(消息列表正文在下方,不在此容器内,保持原文)。
const headerDzRef = useDesensitizeSubtree<HTMLDivElement>(); const headerDzRef = useDesensitizeSubtree<HTMLDivElement>();
const showEmptyState = interactiveMessages.length === 0 && Boolean(emptyState);
return ( return (
<div className={`flex-1 flex flex-col min-h-0 ${rootClassName ?? ""}`}> <div className={`flex-1 flex flex-col min-h-0 ${rootClassName ?? ""}`}>
@@ -244,34 +254,45 @@ export function Step1Panel(props: Step1PanelProps) {
</div> </div>
)} )}
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-2">
<span className="shrink-0 text-[11px] font-bold text-slate-400 bg-white border px-2 py-0.5 rounded-md"> {!hideQuestionCount && (
问答轮次:{interactiveMessages.filter((m) => m.sender === "user").length} 轮 <span className="shrink-0 text-[11px] font-bold text-slate-400 bg-white border px-2 py-0.5 rounded-md">
</span> 问答轮次:{interactiveMessages.filter((m) => m.sender === "user").length} 轮
</span>
)}
{headerRightSlot} {headerRightSlot}
</div> </div>
</div> </div>
<div className={rightSidebar ? "flex min-h-0 flex-1" : "contents"}>
<div className={rightSidebar ? "flex min-w-0 flex-1 flex-col" : "contents"}>
{/* Chat list */} {/* Chat list */}
<div className="relative flex-1 min-h-0 flex flex-col font-normal"> <div className="relative flex-1 min-h-0 flex flex-col font-normal">
<div <div
ref={step1ScrollRef} ref={step1ScrollRef}
// overflow-x-hidden:消息列表只竖向滚(见 Step2Panel 同处注释),避免误触横向拖动抢滑块。 // overflow-x-hidden:消息列表只竖向滚(见 Step2Panel 同处注释),避免误触横向拖动抢滑块。
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden bg-white" className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden bg-white"
> >
<div ref={step1ContentRef} className="space-y-4 p-6"> <div
{interactiveMessages.map((msg) => ( ref={step1ContentRef}
<Step1Message className={showEmptyState ? "flex min-h-full flex-1 p-6" : "space-y-4 p-6"}
key={msg.id} >
msg={msg} {showEmptyState ? (
metric={speedMeterEnabled ? messageMetrics.get(msg.id) : undefined} <div className="flex min-h-full w-full flex-1 items-center justify-center">{emptyState}</div>
intentReady={intentReady} ) : (
pendingIntentClarificationMsgId={pendingIntentClarificationMsgId} interactiveMessages.map((msg) => (
intentClarificationSelections={intentClarificationSelections} <Step1Message
setIntentClarificationSelections={setIntentClarificationSelections} key={msg.id}
isIntentSending={isIntentSending} msg={msg}
submitIntentClarifications={submitIntentClarifications} metric={speedMeterEnabled ? messageMetrics.get(msg.id) : undefined}
/> intentReady={intentReady}
))} pendingIntentClarificationMsgId={pendingIntentClarificationMsgId}
intentClarificationSelections={intentClarificationSelections}
setIntentClarificationSelections={setIntentClarificationSelections}
isIntentSending={isIntentSending}
submitIntentClarifications={submitIntentClarifications}
/>
))
)}
</div> </div>
</div> </div>
{!step1AtBottom && <ScrollToBottomButton onClick={scrollStep1ToBottom} />} {!step1AtBottom && <ScrollToBottomButton onClick={scrollStep1ToBottom} />}
@@ -453,6 +474,9 @@ export function Step1Panel(props: Step1PanelProps) {
</div> </div>
</div> </div>
)} )}
</div>
{rightSidebar}
</div>
</div> </div>
); );
} }
@@ -94,6 +94,8 @@ export interface UseStep1IntentOptions {
taskContext?: Record<string, unknown> | null; taskContext?: Record<string, unknown> | null;
/** Optional task-aware welcome bubble text before the first user turn. */ /** Optional task-aware welcome bubble text before the first user turn. */
initialWelcomeText?: string; initialWelcomeText?: string;
/** Keep the dialogue visually empty while its intent thread is initialising. */
showInitialWelcome?: boolean;
/** 主页面统一的 toast 入口。 */ /** 主页面统一的 toast 入口。 */
triggerToast: (msg: string) => void; triggerToast: (msg: string) => void;
} }
@@ -104,6 +106,7 @@ export interface UseStep1IntentReturn {
intentReady: IntentSummary | null; intentReady: IntentSummary | null;
intentInitError: string | null; intentInitError: string | null;
intentInitErrorDetail: IntentErrorDetail | null; intentInitErrorDetail: IntentErrorDetail | null;
isIntentInitializing: boolean;
isIntentSending: boolean; isIntentSending: boolean;
pendingIntentClarificationMsgId: number | null; pendingIntentClarificationMsgId: number | null;
/** Per-question selections, keyed by `clarificationKey(msgId, clarId, index)`. */ /** Per-question selections, keyed by `clarificationKey(msgId, clarId, index)`. */
@@ -172,13 +175,21 @@ function intentPhaseLabel(phase: IntentStreamPhase, toolName?: string): string {
} }
export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentReturn { export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentReturn {
const { currentStep, selectedModel, taskContext, initialWelcomeText, triggerToast } = options; const {
currentStep,
selectedModel,
taskContext,
initialWelcomeText,
showInitialWelcome = true,
triggerToast,
} = options;
const [intentThreadId, setIntentThreadId] = useState<string | null>(null); const [intentThreadId, setIntentThreadId] = useState<string | null>(null);
const [intentReady, setIntentReady] = useState<IntentSummary | null>(null); const [intentReady, setIntentReady] = useState<IntentSummary | null>(null);
const [intentInitError, setIntentInitError] = useState<string | null>(null); const [intentInitError, setIntentInitError] = useState<string | null>(null);
const [intentInitErrorDetail, setIntentInitErrorDetail] = const [intentInitErrorDetail, setIntentInitErrorDetail] =
useState<IntentErrorDetail | null>(null); useState<IntentErrorDetail | null>(null);
const [isIntentInitializing, setIsIntentInitializing] = useState<boolean>(false);
const [isIntentSending, setIsIntentSending] = useState<boolean>(false); const [isIntentSending, setIsIntentSending] = useState<boolean>(false);
const [pendingIntentClarificationMsgId, setPendingIntentClarificationMsgId] = const [pendingIntentClarificationMsgId, setPendingIntentClarificationMsgId] =
useState<number | null>(null); useState<number | null>(null);
@@ -230,18 +241,23 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
setIntentInitError(null); setIntentInitError(null);
setIntentInitErrorDetail(null); setIntentInitErrorDetail(null);
setIntentThreadId(null); setIntentThreadId(null);
setIsIntentInitializing(true);
setPendingIntentClarificationMsgId(null); setPendingIntentClarificationMsgId(null);
setIntentClarificationSelections(new Map()); setIntentClarificationSelections(new Map());
setTimelineEntries([]); setTimelineEntries([]);
setInteractiveMessages([ setInteractiveMessages(
{ showInitialWelcome
id: 1, ? [
sender: "agent", {
senderName: "任务理解智能体", id: 1,
content: initialWelcomeTextRef.current || INTENT_WELCOME_TEXT, sender: "agent",
time: nowTime(), senderName: "任务理解智能体",
}, content: initialWelcomeTextRef.current || INTENT_WELCOME_TEXT,
]); time: nowTime(),
},
]
: [],
);
(async () => { (async () => {
try { try {
// initIntent 当前不在 thread 上落盘 model,只为占位与兼容;空时由后端 // initIntent 当前不在 thread 上落盘 model,只为占位与兼容;空时由后端
@@ -252,6 +268,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
); );
if (step1InitGenRef.current !== myGen) return; if (step1InitGenRef.current !== myGen) return;
setIntentThreadId(res.thread_id); setIntentThreadId(res.thread_id);
setIsIntentInitializing(false);
setTimelineEntries([ setTimelineEntries([
{ {
id: 1, id: 1,
@@ -262,6 +279,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
]); ]);
} catch (err) { } catch (err) {
if (step1InitGenRef.current !== myGen) return; if (step1InitGenRef.current !== myGen) return;
setIsIntentInitializing(false);
console.error("init intent failed:", err); console.error("init intent failed:", err);
if (err instanceof IntentInitError) { if (err instanceof IntentInitError) {
setIntentInitError(err.detail?.message || err.message); setIntentInitError(err.detail?.message || err.message);
@@ -272,13 +290,14 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
} }
} }
})(); })();
}, []); }, [showInitialWelcome]);
const resetIntent = useCallback(() => { const resetIntent = useCallback(() => {
stopIntentStream(); stopIntentStream();
step1InitRef.current = false; step1InitRef.current = false;
step1InitGenRef.current += 1; step1InitGenRef.current += 1;
setIntentThreadId(null); setIntentThreadId(null);
setIsIntentInitializing(false);
setIntentReady(null); setIntentReady(null);
setIntentInitError(null); setIntentInitError(null);
setIntentInitErrorDetail(null); setIntentInitErrorDetail(null);
@@ -654,6 +673,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
// 或继续显示旧会话的澄清选择。 // 或继续显示旧会话的澄清选择。
stopIntentStream(); stopIntentStream();
setIsIntentSending(false); setIsIntentSending(false);
setIsIntentInitializing(false);
setPendingIntentClarificationMsgId(null); setPendingIntentClarificationMsgId(null);
setIntentClarificationSelections(new Map()); setIntentClarificationSelections(new Map());
setIntentThreadId(snapshot.intentThreadId); setIntentThreadId(snapshot.intentThreadId);
@@ -690,6 +710,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
intentReady, intentReady,
intentInitError, intentInitError,
intentInitErrorDetail, intentInitErrorDetail,
isIntentInitializing,
isIntentSending, isIntentSending,
pendingIntentClarificationMsgId, pendingIntentClarificationMsgId,
intentClarificationSelections, intentClarificationSelections,
+1 -1
View File
@@ -746,7 +746,7 @@
/* 岗位会商左侧总面板:仅收窄滚动条宽度,沿用全局既有的滑块颜色、圆角和轨道样式。 */ /* 岗位会商左侧总面板:仅收窄滚动条宽度,沿用全局既有的滑块颜色、圆角和轨道样式。 */
.position-roundtable-sidebar-scrollbar::-webkit-scrollbar { .position-roundtable-sidebar-scrollbar::-webkit-scrollbar {
width: 8px; width: 5px;
} }
.workspace-shell { .workspace-shell {