feat: refine position roundtable workflow

This commit is contained in:
董宏凯 committed 2026-07-22 22:22:38 +08:00
1 parent 359bd28f2c
commit 13bfa719ee
12 files changed
+1838 -458

No files matched your search

+11 -9
View File
@@ -71,15 +71,17 @@ const DialogOverlay = React.forwardRef<
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}
>(({ className, children, showCloseButton = true, ...props }, ref) => {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
/** Use when this dialog is opened from a higher-layer surface such as a Drawer. */
overlayClassName?: string;
}
>(({ className, children, showCloseButton = true, overlayClassName, ...props }, ref) => {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay className={overlayClassName} />
<DialogPrimitive.Content
ref={ref}
data-slot="dialog-content"
@@ -121,13 +121,14 @@ export function ArtifactFileDetail({
className,
filepath: filepathFromProps,
threadId,
onClose,
allowVirtualMarkdownEdit = false,
disableDesensitize = false,
}: {
className?: string;
filepath: string;
threadId: string;
onClose,
allowVirtualMarkdownEdit = false,
disableDesensitize = false,
hideFileName = false,
}: {
className?: string;
filepath: string;
threadId: string;
/** 覆盖默认关闭行为(仅 setOpen(false));圆桌 Step3 文件预览用于退出沙箱模式。 */
onClose?: () => void;
/**
@@ -140,9 +141,10 @@ export function ArtifactFileDetail({
* 关闭对预览内容的脱敏。多智能体会商页的「消息列表沙箱」按需求**不脱敏**沙箱内的
* 产物内容(用户要拿原始文件),故 Step2SandboxLayout 传 true;主聊天等其它调用方
* 默认 false,沿用全局脱敏。
*/
disableDesensitize?: boolean;
}) {
*/
disableDesensitize?: boolean;
hideFileName?: boolean;
}) {
const { t } = useI18n();
const { artifacts, setOpen, select } = useArtifacts();
const { isMock, thread, publicShareCode } = useThread();
@@ -216,10 +218,11 @@ export function ArtifactFileDetail({
!isMock;
const [draftContent, setDraftContent] = useState("");
const [hasLocalEdits, setHasLocalEdits] = useState(false);
const updateArtifact = useUpdateArtifactContent();
const displayContent = canEditArtifact ? draftContent : (content ?? "");
const isHtmlStreaming = language === "html" && thread.isLoading;
const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1;
const updateArtifact = useUpdateArtifactContent();
const displayContent = canEditArtifact ? draftContent : (content ?? "");
const isAutoPreviewLanguage = language === "html" || language === "markdown";
const isPreviewStreaming = isAutoPreviewLanguage && thread.isLoading;
const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1;
const fileTypeLabel = useMemo(() => {
if (isImageFile(filepath)) return "IMG";
if (isSpreadsheetFile) return "XLSX";
@@ -258,28 +261,31 @@ export function ArtifactFileDetail({
setDraftContent(content ?? "");
}, [canEditArtifact, content, hasLocalEdits]);
useEffect(() => {
setAllowAutoViewSwitch(true);
setAutoScrollEnabled(true);
setShowResumeAutoScroll(false);
if (language === "html") {
setViewMode(thread.isLoading ? "code" : "preview");
return;
}
useEffect(() => {
setAllowAutoViewSwitch(true);
setAutoScrollEnabled(true);
setShowResumeAutoScroll(false);
setShowPreviewSwitchPulse(false);
}, [filepathFromProps]);
useEffect(() => {
if (isPreviewStreaming) {
setViewMode("code");
return;
}
if (isSupportPreview) {
setViewMode("preview");
return;
}
setViewMode("code");
}, [filepathFromProps, isSupportPreview, language, thread.isLoading]);
useEffect(() => {
if (!allowAutoViewSwitch || language !== "html") {
return;
}
setViewMode("code");
}, [isPreviewStreaming, isSupportPreview]);
useEffect(() => {
if (!allowAutoViewSwitch || !isAutoPreviewLanguage) {
return;
}
if (thread.isLoading) {
setViewMode("code");
@@ -291,9 +297,9 @@ export function ArtifactFileDetail({
const timeoutId = window.setTimeout(() => {
setShowPreviewSwitchPulse(false);
}, 1400);
return () => window.clearTimeout(timeoutId);
}, [allowAutoViewSwitch, language, thread.isLoading]);
return () => window.clearTimeout(timeoutId);
}, [allowAutoViewSwitch, isAutoPreviewLanguage, isSupportPreview, thread.isLoading]);
const scrollArtifactToBottom = useCallback(
(behavior: ScrollBehavior = "auto") => {
@@ -576,28 +582,30 @@ export function ArtifactFileDetail({
<span className="bg-background text-muted-foreground inline-flex h-7 shrink-0 items-center rounded-full border border-black/6 px-2.5 text-[10px] font-semibold tracking-[0.16em] dark:border-white/8">
{fileTypeLabel}
</span>
<ArtifactTitle className="min-w-0">
{useWriteFileStreaming ? (
<div className="truncate px-3">{getFileName(filepath)}</div>
) : hasMultipleArtifacts ? (
<Select value={filepath} onValueChange={select}>
<SelectTrigger className="text-foreground h-8 min-w-0 max-w-[16rem] border-none bg-transparent! px-3 shadow-none select-none focus-visible:ring-0 focus:outline-0 active:outline-0">
<SelectValue placeholder="Select a file" />
</SelectTrigger>
<SelectContent className="select-none">
<SelectGroup>
{(artifacts ?? []).map((artifactPath) => (
<SelectItem key={artifactPath} value={artifactPath}>
{getFileName(artifactPath)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
) : (
<div className="truncate px-3">{getFileName(filepath)}</div>
)}
</ArtifactTitle>
{!hideFileName && (
<ArtifactTitle className="min-w-0">
{useWriteFileStreaming ? (
<div className="truncate px-3">{getFileName(filepath)}</div>
) : hasMultipleArtifacts ? (
<Select value={filepath} onValueChange={select}>
<SelectTrigger className="text-foreground h-8 min-w-0 max-w-[16rem] border-none bg-transparent! px-3 shadow-none select-none focus-visible:ring-0 focus:outline-0 active:outline-0">
<SelectValue placeholder="Select a file" />
</SelectTrigger>
<SelectContent className="select-none">
<SelectGroup>
{(artifacts ?? []).map((artifactPath) => (
<SelectItem key={artifactPath} value={artifactPath}>
{getFileName(artifactPath)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
) : (
<div className="truncate px-3">{getFileName(filepath)}</div>
)}
</ArtifactTitle>
)}
</div>
</div>
<div className="flex w-full items-center justify-center md:w-auto md:flex-1">
@@ -634,10 +642,10 @@ export function ArtifactFileDetail({
<EyeIcon />
</ToggleGroupItem>
</ToggleGroup>
{isHtmlStreaming && (
<div className="animate-[sandbox-preview-badge_1.4s_ease-in-out_infinite] text-muted-foreground bg-muted/80 inline-flex items-center rounded-full border px-2.5 py-1 text-[11px]">
<span className="bg-primary mr-2 size-1.5 animate-pulse rounded-full" />
正在生成,完成后自动预览
{isPreviewStreaming && (
<div className="animate-[sandbox-preview-badge_1.4s_ease-in-out_infinite] text-muted-foreground bg-muted/80 inline-flex items-center rounded-full border px-2.5 py-1 text-[11px]">
<span className="bg-primary mr-2 size-1.5 animate-pulse rounded-full" />
正在生成,完成后自动预览
</div>
)}
</div>
@@ -23,28 +23,34 @@ import { useThread } from "../messages/context";
import { useArtifacts } from "./context";
import { SkillInstallDialog } from "./skill-install-dialog";
export function ArtifactFileList({
className,
files,
threadId,
}: {
className?: string;
files: string[];
threadId: string;
}) {
export function ArtifactFileList({
className,
files,
threadId,
onOpenArtifact,
}: {
className?: string;
files: string[];
threadId: string;
onOpenArtifact?: (filepath: string) => void;
}) {
const { t } = useI18n();
const { isMock, publicShareCode } = useThread();
const { select: selectArtifact, setOpen } = useArtifacts();
const [installFile, setInstallFile] = useState<string | null>(null);
const [downloadingFile, setDownloadingFile] = useState<string | null>(null);
const handleClick = useCallback(
(filepath: string) => {
selectArtifact(filepath);
setOpen(true);
},
[selectArtifact, setOpen],
);
const handleClick = useCallback(
(filepath: string) => {
if (onOpenArtifact) {
onOpenArtifact(filepath);
return;
}
selectArtifact(filepath);
setOpen(true);
},
[onOpenArtifact, selectArtifact, setOpen],
);
const handleInstallSkill = useCallback(
(e: React.MouseEvent, filepath: string) => {
@@ -101,10 +101,11 @@ export function MessageList({
onEditResubmit,
onSetupAgentApprove,
onSetupAgentReject,
onSetupWritingApprove,
onSetupWritingReject,
onSetupWritingStart,
}: {
onSetupWritingApprove,
onSetupWritingReject,
onSetupWritingStart,
onOpenArtifact,
}: {
className?: string;
contentClassName?: string;
fitMessageRowsToContent?: boolean;
@@ -119,11 +120,12 @@ export function MessageList({
onEditResubmit?: (text: string) => void;
onSetupAgentApprove?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
onSetupAgentReject?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
onSetupWritingApprove?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
onSetupWritingReject?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
onSetupWritingApprove?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
onSetupWritingReject?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
/** 审批卡「直接开始写作」回调:应用配置(含已确认大纲)并立即启动写作流程(仅写作页提供,模板编辑等场景缺省隐藏该按钮)。 */
onSetupWritingStart?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
}) {
onSetupWritingStart?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
onOpenArtifact?: (filepath: string) => void;
}) {
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
const rehypePlugins = useMemo(
() => (extraRehypePlugins?.length ? [...baseRehypePlugins, ...extraRehypePlugins] : baseRehypePlugins),
@@ -488,7 +490,11 @@ export function MessageList({
className="mb-4"
/>
)}
<ArtifactFileList files={files} threadId={threadId} />
<ArtifactFileList
files={files}
threadId={threadId}
onOpenArtifact={onOpenArtifact}
/>
<MessageTokenUsageList
enabled={tokenUsageEnabled}
isLoading={thread.isLoading}