fix: resolve eslint warnings in agent chat

This commit is contained in:
coso
2026-01-12 02:13:00 +08:00
parent ae30af743f
commit 862ce44172
3 changed files with 723 additions and 95 deletions
@@ -2,7 +2,68 @@ import React from "react";
import { InputbarCore } from "./components/InputbarCore";
import { toast } from "sonner";
import { useState, useCallback, useRef } from "react";
import styled from "styled-components";
import type { MessageImage } from "../../types";
import { TaskFileList, type TaskFile } from "../TaskFiles";
import { FolderOpen, ChevronUp } from "lucide-react";
// 任务文件触发器区域(在输入框上方,与输入框对齐)
const TaskFilesArea = styled.div`
display: flex;
justify-content: flex-end;
padding: 0 18px 8px 18px;
width: 100%;
max-width: 900px;
margin: 0 auto;
`;
// 按钮和面板的包装容器
const TaskFilesWrapper = styled.div`
position: relative;
`;
// 任务文件按钮
const TaskFilesButton = styled.button<{
$expanded?: boolean;
$hasFiles?: boolean;
}>`
display: ${(props) => (props.$hasFiles ? "flex" : "none")};
align-items: center;
gap: 6px;
padding: 6px 12px;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 8px;
font-size: 13px;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: hsl(var(--primary) / 0.5);
color: hsl(var(--foreground));
}
${(props) =>
props.$expanded &&
`
border-color: hsl(var(--primary));
color: hsl(var(--foreground));
background: hsl(var(--primary) / 0.05);
`}
`;
const FileCount = styled.span`
font-weight: 500;
`;
const ChevronIcon = styled.span<{ $expanded?: boolean }>`
display: flex;
align-items: center;
transform: ${(props) =>
props.$expanded ? "rotate(0deg)" : "rotate(180deg)"};
transition: transform 0.2s;
`;
interface InputbarProps {
input: string;
@@ -21,6 +82,16 @@ interface InputbarProps {
onToggleCanvas?: () => void;
/** 画布是否打开 */
isCanvasOpen?: boolean;
/** 任务文件列表 */
taskFiles?: TaskFile[];
/** 选中的文件 ID */
selectedFileId?: string;
/** 任务文件面板是否展开 */
taskFilesExpanded?: boolean;
/** 切换任务文件面板 */
onToggleTaskFiles?: () => void;
/** 文件点击回调 */
onTaskFileClick?: (file: TaskFile) => void;
}
export const Inputbar: React.FC<InputbarProps> = ({
@@ -33,6 +104,11 @@ export const Inputbar: React.FC<InputbarProps> = ({
onClearMessages,
onToggleCanvas: _onToggleCanvas,
isCanvasOpen: _isCanvasOpen,
taskFiles = [],
selectedFileId,
taskFilesExpanded = false,
onToggleTaskFiles,
onTaskFileClick,
}) => {
const [activeTools, setActiveTools] = useState<Record<string, boolean>>({});
const [pendingImages, setPendingImages] = useState<MessageImage[]>([]);
@@ -193,6 +269,10 @@ export const Inputbar: React.FC<InputbarProps> = ({
setPendingImages([]);
}, [input, pendingImages, onSend, activeTools]);
const handleToggleTaskFiles = useCallback(() => {
onToggleTaskFiles?.();
}, [onToggleTaskFiles]);
return (
<div
onDragOver={handleDragOver}
@@ -201,6 +281,40 @@ export const Inputbar: React.FC<InputbarProps> = ({
isFullscreen ? "fixed inset-0 z-50 bg-background p-4 flex flex-col" : ""
}
>
{/* 任务文件区域 - 在输入框上方 */}
{taskFiles.length > 0 && (
<TaskFilesArea>
{/* 按钮和面板的包装容器 */}
<TaskFilesWrapper>
{/* 任务文件面板 */}
<TaskFileList
files={taskFiles}
selectedFileId={selectedFileId}
onFileClick={onTaskFileClick}
expanded={taskFilesExpanded}
onExpandedChange={(expanded) => {
if (expanded !== taskFilesExpanded) {
onToggleTaskFiles?.();
}
}}
/>
{/* 任务文件按钮 */}
<TaskFilesButton
$hasFiles={taskFiles.length > 0}
$expanded={taskFilesExpanded}
onClick={handleToggleTaskFiles}
data-task-files-trigger
>
<FolderOpen size={14} />
任务文件
<FileCount>({taskFiles.length})</FileCount>
<ChevronIcon $expanded={taskFilesExpanded}>
<ChevronUp size={14} />
</ChevronIcon>
</TaskFilesButton>
</TaskFilesWrapper>
</TaskFilesArea>
)}
<input
ref={fileInputRef}
type="file"
@@ -1,12 +1,23 @@
/**
* @file 任务文件列表组件
* @description 展示内容创作过程中生成的文件列表
* 完全按照 AnyGen 的文件面板样式实现
*/
import React, { memo } from "react";
import React, { memo, useRef, useEffect, useState } from "react";
import styled from "styled-components";
import { FileText, ChevronDown, ChevronUp } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import {
FileText,
FolderOpen,
List,
Copy,
Image,
Code,
MoreHorizontal,
ChevronRight,
Folder,
} from "lucide-react";
/** 任务文件类型 */
export interface TaskFile {
@@ -33,131 +44,488 @@ interface TaskFileListProps {
onExpandedChange?: (expanded: boolean) => void;
}
// 外层容器
const Container = styled.div`
padding: 8px 16px;
border-top: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
position: relative;
`;
const Header = styled.div`
// 展开的文件面板
const FilePanel = styled.div`
position: absolute;
bottom: calc(100% + 8px);
right: 0;
width: 320px;
max-height: 400px;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
overflow: hidden;
z-index: 100;
display: flex;
flex-direction: column;
`;
// 面板头部
const PanelHeader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding: 12px 14px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--background));
flex-shrink: 0;
`;
const Title = styled.div`
// 面包屑导航
const Breadcrumb = styled.div`
display: flex;
align-items: center;
gap: 8px;
gap: 4px;
font-size: 13px;
font-weight: 500;
color: hsl(var(--muted-foreground));
`;
const FileCount = styled.span`
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
`;
const FileList = styled.div`
display: flex;
flex-wrap: wrap;
gap: 8px;
`;
const FileItem = styled.div<{ $selected?: boolean }>`
const BreadcrumbItem = styled.button`
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: ${(props) =>
props.$selected ? "hsl(var(--primary) / 0.1)" : "hsl(var(--background))"};
border: 1px solid
${(props) =>
props.$selected ? "hsl(var(--primary))" : "hsl(var(--border))"};
border-radius: 8px;
gap: 4px;
padding: 2px 6px;
background: transparent;
border: none;
border-radius: 4px;
font-size: 13px;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.2s;
transition: all 0.15s;
&:hover {
border-color: hsl(var(--primary));
background: hsl(var(--primary) / 0.05);
background: hsl(var(--muted) / 0.5);
color: hsl(var(--foreground));
}
`;
const FileIcon = styled.div`
color: hsl(var(--primary));
const BreadcrumbSeparator = styled.span`
color: hsl(var(--muted-foreground) / 0.5);
display: flex;
align-items: center;
`;
const FileName = styled.span`
font-size: 13px;
color: hsl(var(--foreground));
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
// 工具栏
const Toolbar = styled.div`
display: flex;
align-items: center;
gap: 2px;
`;
const FileVersion = styled.span`
font-size: 11px;
const ToolbarButton = styled.button<{ $active?: boolean }>`
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: ${(props) =>
props.$active ? "hsl(var(--primary) / 0.1)" : "transparent"};
border-radius: 6px;
color: ${(props) =>
props.$active ? "hsl(var(--primary))" : "hsl(var(--muted-foreground))"};
cursor: pointer;
transition: all 0.15s;
&:hover {
background: hsl(var(--muted) / 0.5);
color: hsl(var(--foreground));
}
`;
// 文件列表区域
const FileListContainer = styled.div`
flex: 1;
overflow-y: auto;
padding: 8px;
min-height: 100px;
max-height: 320px;
/* 自定义滚动条 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.3);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.5);
}
`;
// 空状态
const EmptyState = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: hsl(var(--muted-foreground));
`;
const EmptyIcon = styled.div`
margin-bottom: 12px;
opacity: 0.5;
`;
const EmptyText = styled.div`
font-size: 13px;
`;
// 文件项
const FileItem = styled.div<{ $selected?: boolean }>`
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
background: ${(props) =>
props.$selected ? "hsl(var(--primary) / 0.08)" : "transparent"};
&:hover {
background: ${(props) =>
props.$selected
? "hsl(var(--primary) / 0.12)"
: "hsl(var(--muted) / 0.4)"};
}
`;
// 文件图标容器
const FileIconWrapper = styled.div<{ $color?: string }>`
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
background: ${(props) => props.$color || "#f59e0b"}20;
color: ${(props) => props.$color || "#f59e0b"};
flex-shrink: 0;
`;
const FileInfo = styled.div`
flex: 1;
min-width: 0;
`;
const FileName = styled.div`
font-size: 13px;
font-weight: 450;
color: hsl(var(--foreground));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const FileMeta = styled.div`
font-size: 11px;
color: hsl(var(--muted-foreground));
margin-top: 2px;
`;
const MoreButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
background: transparent;
border-radius: 4px;
color: hsl(var(--muted-foreground));
cursor: pointer;
opacity: 0;
transition: all 0.15s;
flex-shrink: 0;
${FileItem}:hover & {
opacity: 1;
}
&:hover {
background: hsl(var(--muted));
color: hsl(var(--foreground));
}
`;
// 底部触发按钮(放在 Inputbar 中)
export const TaskFilesTrigger = styled.button<{
$expanded?: boolean;
$hasFiles?: boolean;
}>`
display: ${(props) => (props.$hasFiles ? "flex" : "none")};
align-items: center;
gap: 4px;
padding: 4px 10px;
background: transparent;
border: 1px solid hsl(var(--border));
border-radius: 6px;
font-size: 12px;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
margin-right: 8px;
&:hover {
border-color: hsl(var(--primary) / 0.5);
color: hsl(var(--foreground));
}
${(props) =>
props.$expanded &&
`
border-color: hsl(var(--primary));
color: hsl(var(--foreground));
background: hsl(var(--primary) / 0.05);
`}
`;
/**
* 格式化相对时间
*/
function formatRelativeTime(timestamp: number): string {
const now = Date.now();
const diff = now - timestamp;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return "刚刚";
if (minutes < 60) return `${minutes} 分钟前`;
if (hours < 24) return `${hours} 小时前`;
if (days === 1) return "1 天前";
if (days < 7) return `${days} 天前`;
return new Date(timestamp).toLocaleDateString("zh-CN");
}
/**
* 根据文件类型获取图标颜色
*/
function getFileColor(type: string, name: string): string {
// 根据文件扩展名判断
if (name.endsWith(".md") || name.endsWith(".txt")) {
return "#3b82f6"; // 蓝色 - 文档
}
if (
name.endsWith(".pptx") ||
name.endsWith(".ppt") ||
name.endsWith(".key")
) {
return "#f59e0b"; // 橙色 - 演示文稿
}
if (name.endsWith(".doc") || name.endsWith(".docx")) {
return "#2563eb"; // 深蓝 - Word文档
}
if (name.endsWith(".xls") || name.endsWith(".xlsx")) {
return "#10b981"; // 绿色 - Excel
}
if (name.endsWith(".pdf")) {
return "#ef4444"; // 红色 - PDF
}
if (type === "image") {
return "#8b5cf6"; // 紫色 - 图片
}
if (type === "audio") {
return "#ec4899"; // 粉色 - 音频
}
return "#f59e0b"; // 默认橙色
}
/**
* 任务文件列表组件
*
* 展示内容创作过程中生成的文件列表,支持点击查看文件内容
* 展示内容创作过程中生成的文件列表
* 采用 AnyGen 风格的浮动面板设计
*/
export const TaskFileList: React.FC<TaskFileListProps> = memo(
({
files,
selectedFileId,
onFileClick,
expanded = true,
expanded = false,
onExpandedChange,
}) => {
const panelRef = useRef<HTMLDivElement>(null);
const [currentFolder, setCurrentFolder] = useState<string | null>(null);
const [filterType, setFilterType] = useState<"all" | "image" | "code">(
"all",
);
const [viewMode, setViewMode] = useState<"list" | "grid">("list");
// 点击外部关闭面板
useEffect(() => {
if (!expanded) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
// 检查点击是否在面板外部
if (panelRef.current && !panelRef.current.contains(target)) {
// 检查是否点击了触发按钮(在 Inputbar 中)
const triggerButton = document.querySelector(
"[data-task-files-trigger]",
);
if (triggerButton && triggerButton.contains(target)) {
return;
}
onExpandedChange?.(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}, [expanded, onExpandedChange]);
// 复制所有文件内容
const handleCopyAll = () => {
const allContent = files
.filter((f) => f.content)
.map((f) => `=== ${f.name} ===\n${f.content}`)
.join("\n\n");
if (allContent) {
navigator.clipboard.writeText(allContent);
toast.success("已复制所有文件内容");
} else {
toast.info("没有可复制的内容");
}
};
// 切换筛选类型
const handleFilterImage = () => {
setFilterType((prev) => (prev === "image" ? "all" : "image"));
};
const handleFilterCode = () => {
setFilterType((prev) => (prev === "code" ? "all" : "code"));
};
// 切换视图模式
const handleToggleView = () => {
setViewMode((prev) => (prev === "list" ? "grid" : "list"));
};
if (files.length === 0) {
return null;
}
// 过滤文件
let displayFiles = currentFolder
? files.filter((f) => f.name.startsWith(currentFolder + "/"))
: files;
// 根据类型筛选
if (filterType === "image") {
displayFiles = displayFiles.filter(
(f) =>
f.type === "image" || /\.(png|jpg|jpeg|gif|svg|webp)$/i.test(f.name),
);
} else if (filterType === "code") {
displayFiles = displayFiles.filter((f) =>
/\.(js|ts|jsx|tsx|py|go|rs|java|cpp|c|h|css|html|json|md)$/i.test(
f.name,
),
);
}
return (
<Container>
<Header>
<Title>
<FileText size={14} />
生成的文件
<FileCount>{files.length}</FileCount>
</Title>
<Button
variant="ghost"
size="sm"
onClick={() => onExpandedChange?.(!expanded)}
>
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</Button>
</Header>
{expanded && (
<FileList>
{files.map((file) => (
<FileItem
key={file.id}
$selected={selectedFileId === file.id}
onClick={() => onFileClick?.(file)}
>
<FileIcon>
<FileText size={16} />
</FileIcon>
<FileName title={file.name}>{file.name}</FileName>
{file.version > 1 && <FileVersion>v{file.version}</FileVersion>}
</FileItem>
))}
</FileList>
<FilePanel ref={panelRef}>
<PanelHeader>
<Breadcrumb>
<BreadcrumbItem onClick={() => setCurrentFolder(null)}>
<FolderOpen size={14} />
所有文件
</BreadcrumbItem>
{currentFolder && (
<>
<BreadcrumbSeparator>
<ChevronRight size={14} />
</BreadcrumbSeparator>
<BreadcrumbItem>{currentFolder}</BreadcrumbItem>
</>
)}
</Breadcrumb>
<Toolbar>
<ToolbarButton
title={viewMode === "list" ? "网格视图" : "列表视图"}
onClick={handleToggleView}
$active={viewMode === "grid"}
>
<List size={16} />
</ToolbarButton>
<ToolbarButton title="复制全部" onClick={handleCopyAll}>
<Copy size={16} />
</ToolbarButton>
<ToolbarButton
title="筛选图片"
onClick={handleFilterImage}
$active={filterType === "image"}
>
<Image size={16} />
</ToolbarButton>
<ToolbarButton
title="筛选代码"
onClick={handleFilterCode}
$active={filterType === "code"}
>
<Code size={16} />
</ToolbarButton>
</Toolbar>
</PanelHeader>
<FileListContainer>
{displayFiles.length === 0 ? (
<EmptyState>
<EmptyIcon>
<FileText size={40} strokeWidth={1} />
</EmptyIcon>
<EmptyText>未找到任何项目</EmptyText>
</EmptyState>
) : (
displayFiles.map((file) => {
const color = getFileColor(file.type, file.name);
return (
<FileItem
key={file.id}
$selected={selectedFileId === file.id}
onClick={() => onFileClick?.(file)}
>
<FileIconWrapper $color={color}>
<Folder size={18} />
</FileIconWrapper>
<FileInfo>
<FileName title={file.name}>{file.name}</FileName>
<FileMeta>
{formatRelativeTime(file.updatedAt)}
</FileMeta>
</FileInfo>
<MoreButton
onClick={(e) => {
e.stopPropagation();
// TODO: 显示更多操作菜单
}}
>
<MoreHorizontal size={16} />
</MoreButton>
</FileItem>
);
})
)}
</FileListContainer>
</FilePanel>
)}
</Container>
);
+160 -14
View File
@@ -15,7 +15,7 @@ import { ChatSettings } from "./components/ChatSettings";
import { MessageList } from "./components/MessageList";
import { Inputbar } from "./components/Inputbar";
import { EmptyState, CreationMode } from "./components/EmptyState";
import { TaskFileList, type TaskFile } from "./components/TaskFiles";
import { type TaskFile } from "./components/TaskFiles";
import { LayoutTransition } from "@/components/content-creator/core/LayoutTransition/LayoutTransition";
import { StepProgress } from "@/components/content-creator/core/StepGuide/StepProgress";
import { useWorkflow } from "@/components/content-creator/hooks/useWorkflow";
@@ -166,13 +166,121 @@ export function AgentChatPage({
});
// 会话文件持久化 hook
const { saveFile: saveSessionFile } = useSessionFiles({
const {
saveFile: saveSessionFile,
files: sessionFiles,
readFile: readSessionFile,
meta: sessionMeta,
} = useSessionFiles({
sessionId,
theme: mappedTheme,
creationMode,
autoInit: true,
});
// 追踪已恢复元数据和文件的会话 ID
const restoredMetaSessionId = useRef<string | null>(null);
const restoredFilesSessionId = useRef<string | null>(null);
// 当 sessionMeta 加载完成时,恢复主题和创建模式
useEffect(() => {
if (!sessionId || !sessionMeta) {
return;
}
// 检查 sessionMeta 是否属于当前 sessionId
if (sessionMeta.sessionId !== sessionId) {
return;
}
// 避免重复恢复
if (restoredMetaSessionId.current === sessionId) {
return;
}
console.log("[AgentChatPage] 恢复会话元数据:", sessionId, sessionMeta);
// 从会话元数据恢复主题
if (sessionMeta.theme) {
const themeEntry = Object.entries(THEME_MAP).find(
([_, v]) => v === sessionMeta.theme,
);
if (themeEntry) {
console.log("[AgentChatPage] 恢复主题:", themeEntry[0]);
setActiveTheme(themeEntry[0]);
}
}
// 从会话元数据恢复创建模式
if (sessionMeta.creationMode) {
console.log("[AgentChatPage] 恢复创建模式:", sessionMeta.creationMode);
setCreationMode(sessionMeta.creationMode as CreationMode);
}
restoredMetaSessionId.current = sessionId;
}, [sessionId, sessionMeta]);
// 当 sessionFiles 加载完成时,恢复文件到 taskFiles
useEffect(() => {
if (!sessionId || sessionFiles.length === 0) {
return;
}
// 避免重复恢复
if (restoredFilesSessionId.current === sessionId) {
return;
}
// 如果当前已有 taskFiles,说明是本次会话新生成的文件,不需要从持久化恢复
if (taskFiles.length > 0) {
restoredFilesSessionId.current = sessionId;
return;
}
console.log(
"[AgentChatPage] 开始恢复文件:",
sessionId,
sessionFiles.length,
"个文件",
);
// 恢复文件到 taskFiles
const restoreFiles = async () => {
const restoredFiles: TaskFile[] = [];
for (const file of sessionFiles) {
try {
const content = await readSessionFile(file.name);
if (content) {
restoredFiles.push({
id: crypto.randomUUID(),
name: file.name,
type: file.fileType === "document" ? "document" : "document",
content,
version: 1,
createdAt: file.createdAt,
updatedAt: file.updatedAt,
});
}
} catch (err) {
console.error("[AgentChatPage] 恢复文件失败:", file.name, err);
}
}
if (restoredFiles.length > 0) {
console.log(
"[AgentChatPage] 从持久化存储恢复",
restoredFiles.length,
"个文件",
);
setTaskFiles(restoredFiles);
}
restoredFilesSessionId.current = sessionId;
};
restoreFiles();
}, [sessionId, sessionFiles, readSessionFile, taskFiles.length]);
// 包装 switchTopic,在切换话题时重置相关状态
const switchTopic = useCallback(
async (topicId: string) => {
@@ -182,6 +290,9 @@ export function AgentChatPage({
setTaskFiles([]);
setSelectedFileId(undefined);
processedMessageIds.current.clear();
// 清空已恢复的会话 ID,以便新话题能触发恢复
restoredMetaSessionId.current = null;
restoredFilesSessionId.current = null;
// 然后调用原始的 switchTopic
await originalSwitchTopic(topicId);
@@ -308,6 +419,38 @@ export function AgentChatPage({
}
}, [hasMessages]);
// 当有文件时默认在画布中显示最后一个文件
useEffect(() => {
if (taskFiles.length > 0) {
const lastFile = taskFiles[taskFiles.length - 1];
// 设置选中的文件
setSelectedFileId(lastFile.id);
// 如果文件有内容,在画布中显示
if (lastFile.content) {
setCanvasState((prev) => {
if (mappedTheme === "music") {
const sections = parseLyrics(lastFile.content!);
if (!prev || prev.type !== "music") {
const musicState = createInitialMusicState();
musicState.sections = sections;
const titleMatch = lastFile.content!.match(/^#\s*(.+)$/m);
if (titleMatch) {
musicState.spec.title = titleMatch[1].trim();
}
return musicState;
}
return { ...prev, sections };
}
if (!prev || prev.type !== "document") {
return createInitialDocumentState(lastFile.content!);
}
return { ...prev, content: lastFile.content! };
});
setLayoutMode("chat-canvas");
}
}
}, [taskFiles, mappedTheme]);
const handleToggleSidebar = () => {
setShowSidebar(!showSidebar);
};
@@ -637,16 +780,6 @@ export function AgentChatPage({
{hasMessages && (
<>
{/* 任务文件列表 - 显示在输入框上方 */}
{taskFiles.length > 0 && (
<TaskFileList
files={taskFiles}
selectedFileId={selectedFileId}
onFileClick={handleTaskFileClick}
expanded={taskFilesExpanded}
onExpandedChange={setTaskFilesExpanded}
/>
)}
<Inputbar
input={input}
setInput={setInput}
@@ -657,6 +790,11 @@ export function AgentChatPage({
onClearMessages={handleClearMessages}
onToggleCanvas={handleToggleCanvas}
isCanvasOpen={layoutMode === "chat-canvas"}
taskFiles={taskFiles}
selectedFileId={selectedFileId}
taskFilesExpanded={taskFilesExpanded}
onToggleTaskFiles={() => setTaskFilesExpanded(!taskFilesExpanded)}
onTaskFileClick={handleTaskFileClick}
/>
</>
)}
@@ -671,10 +809,18 @@ export function AgentChatPage({
isSupported: isCanvasSupported(mappedTheme),
layoutMode,
});
if (canvasState && isCanvasSupported(mappedTheme)) {
// 只要有 canvasState 就显示画布(支持显示任意主题的文件)
if (canvasState) {
// 根据 canvasState.type 确定使用的主题
const effectiveTheme: ThemeType =
canvasState.type === "music"
? "music"
: canvasState.type === "poster"
? "poster"
: "document";
return (
<CanvasFactory
theme={mappedTheme}
theme={effectiveTheme}
state={canvasState}
onStateChange={setCanvasState}
onClose={handleCloseCanvas}