mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: bump version to 0.40.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"private": true,
|
||||
"version": "0.39.0",
|
||||
"version": "0.40.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxycast"
|
||||
version = "0.39.0"
|
||||
version = "0.40.0"
|
||||
description = "AI API Proxy Desktop App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ProxyCast",
|
||||
"version": "0.39.0",
|
||||
"version": "0.40.0",
|
||||
"identifier": "com.proxycast.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@@ -20,6 +20,7 @@
|
||||
"minHeight": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"maximized": true,
|
||||
"center": true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
* AI Agent 聊天页面
|
||||
*
|
||||
* 包含聊天区域和侧边栏(话题/技能列表)
|
||||
* 支持内容创作模式下的布局过渡和步骤引导
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useAgentChat } from "./hooks/useAgentChat";
|
||||
import { ChatNavbar } from "./components/ChatNavbar";
|
||||
@@ -12,8 +13,33 @@ import { ChatSidebar } from "./components/ChatSidebar";
|
||||
import { ChatSettings } from "./components/ChatSettings";
|
||||
import { MessageList } from "./components/MessageList";
|
||||
import { Inputbar } from "./components/Inputbar";
|
||||
import { EmptyState } from "./components/EmptyState";
|
||||
import { EmptyState, CreationMode } from "./components/EmptyState";
|
||||
import { TaskFileList, 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";
|
||||
import {
|
||||
DocumentCanvas,
|
||||
createInitialDocumentState,
|
||||
} from "@/components/content-creator/canvas/document";
|
||||
import {
|
||||
generateContentCreationPrompt,
|
||||
isContentCreationTheme,
|
||||
} from "@/components/content-creator/utils/systemPrompt";
|
||||
import type { DocumentCanvasState } from "@/components/content-creator/canvas/document/types";
|
||||
import type { MessageImage } from "./types";
|
||||
import type { ThemeType, LayoutMode } from "@/components/content-creator/types";
|
||||
import type { A2UIFormData } from "@/components/content-creator/a2ui/types";
|
||||
|
||||
// 文件名到步骤索引的映射(静态常量,移到组件外部避免重复创建)
|
||||
const FILE_TO_STEP_MAP: Record<string, number> = {
|
||||
"brief.md": 0, // 明确需求
|
||||
"specification.md": 1, // 调研收集
|
||||
"research.md": 1, // 调研收集(备选文件名)
|
||||
"outline.md": 2, // 生成大纲
|
||||
"draft.md": 3, // 撰写内容
|
||||
"article.md": 4, // 润色优化
|
||||
};
|
||||
|
||||
const PageContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -27,6 +53,8 @@ const MainArea = styled.div`
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const ChatContainer = styled.div`
|
||||
@@ -34,6 +62,7 @@ const ChatContainer = styled.div`
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ChatContent = styled.div`
|
||||
@@ -42,13 +71,70 @@ const ChatContent = styled.div`
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 16px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
// 主题到 ThemeType 的映射
|
||||
const THEME_MAP: Record<string, ThemeType> = {
|
||||
general: "general",
|
||||
knowledge: "knowledge",
|
||||
planning: "planning",
|
||||
social: "social-media",
|
||||
image: "poster",
|
||||
office: "document",
|
||||
video: "video",
|
||||
};
|
||||
|
||||
export function AgentChatPage({
|
||||
onNavigate: _onNavigate,
|
||||
}: {
|
||||
onNavigate?: (page: string) => void;
|
||||
}) {
|
||||
const [showSidebar, setShowSidebar] = useState(true);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
// 内容创作相关状态
|
||||
const [activeTheme, setActiveTheme] = useState<string>("general");
|
||||
const [creationMode, setCreationMode] = useState<CreationMode>("guided");
|
||||
const [layoutMode, setLayoutMode] = useState<LayoutMode>("chat");
|
||||
|
||||
// 文档画布状态
|
||||
const [documentState, setDocumentState] =
|
||||
useState<DocumentCanvasState | null>(null);
|
||||
|
||||
// 任务文件状态
|
||||
const [taskFiles, setTaskFiles] = useState<TaskFile[]>([]);
|
||||
const [taskFilesExpanded, setTaskFilesExpanded] = useState(false);
|
||||
const [selectedFileId, setSelectedFileId] = useState<string | undefined>();
|
||||
|
||||
// 用于追踪已处理的消息 ID,避免重复处理
|
||||
const processedMessageIds = useRef<Set<string>>(new Set());
|
||||
|
||||
// 文件写入回调 ref(用于传递给 useAgentChat)
|
||||
const handleWriteFileRef =
|
||||
useRef<(content: string, fileName: string) => void>();
|
||||
|
||||
// 工作流状态(仅在内容创作模式下使用)
|
||||
const mappedTheme = THEME_MAP[activeTheme] || "general";
|
||||
const { steps, currentStepIndex, goToStep, completeStep } = useWorkflow(
|
||||
mappedTheme,
|
||||
creationMode,
|
||||
);
|
||||
|
||||
// 判断是否为内容创作模式
|
||||
const isContentCreationMode = isContentCreationTheme(activeTheme);
|
||||
|
||||
// 生成系统提示词(仅在内容创作模式下)
|
||||
const systemPrompt = useMemo(() => {
|
||||
if (isContentCreationMode) {
|
||||
return generateContentCreationPrompt(mappedTheme);
|
||||
}
|
||||
return undefined;
|
||||
}, [isContentCreationMode, mappedTheme]);
|
||||
|
||||
// 使用 Agent Chat Hook(传递系统提示词)
|
||||
const {
|
||||
processStatus,
|
||||
providerType,
|
||||
@@ -58,18 +144,115 @@ export function AgentChatPage({
|
||||
messages,
|
||||
isSending,
|
||||
sendMessage,
|
||||
stopSending,
|
||||
clearMessages,
|
||||
deleteMessage,
|
||||
editMessage,
|
||||
topics,
|
||||
sessionId,
|
||||
switchTopic,
|
||||
switchTopic: originalSwitchTopic,
|
||||
deleteTopic,
|
||||
} = useAgentChat();
|
||||
} = useAgentChat({
|
||||
systemPrompt,
|
||||
onWriteFile: (content, fileName) => {
|
||||
// 使用 ref 调用最新的 handleWriteFile
|
||||
handleWriteFileRef.current?.(content, fileName);
|
||||
},
|
||||
});
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState(true);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
// 包装 switchTopic,在切换话题时重置相关状态
|
||||
const switchTopic = useCallback(
|
||||
async (topicId: string) => {
|
||||
// 先重置本地状态
|
||||
setLayoutMode("chat");
|
||||
setDocumentState(null);
|
||||
setTaskFiles([]);
|
||||
setSelectedFileId(undefined);
|
||||
processedMessageIds.current.clear();
|
||||
|
||||
// 然后调用原始的 switchTopic
|
||||
await originalSwitchTopic(topicId);
|
||||
},
|
||||
[originalSwitchTopic],
|
||||
);
|
||||
|
||||
/**
|
||||
* 从 AI 响应中提取文档内容
|
||||
* 支持多种格式:
|
||||
* 1. <document>...</document> 标签
|
||||
* 2. ```markdown ... ``` 代码块
|
||||
* 3. 以 # 开头的 Markdown 内容(整个响应)
|
||||
*/
|
||||
const extractDocumentContent = useCallback(
|
||||
(content: string): string | null => {
|
||||
// 1. 检查 <document> 标签
|
||||
const documentMatch = content.match(/<document>([\s\S]*?)<\/document>/);
|
||||
if (documentMatch) {
|
||||
return documentMatch[1].trim();
|
||||
}
|
||||
|
||||
// 2. 检查 markdown 代码块
|
||||
const markdownMatch = content.match(/```(?:markdown|md)\n([\s\S]*?)```/);
|
||||
if (markdownMatch) {
|
||||
return markdownMatch[1].trim();
|
||||
}
|
||||
|
||||
// 3. 如果整个内容以 # 开头且长度超过 200 字符,认为是文档
|
||||
if (content.trim().startsWith("#") && content.length > 200) {
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 监听 AI 消息变化,自动提取文档内容
|
||||
useEffect(() => {
|
||||
if (!isContentCreationMode) return;
|
||||
|
||||
// 找到最新的 assistant 消息
|
||||
const lastAssistantMsg = [...messages]
|
||||
.reverse()
|
||||
.find(
|
||||
(msg) => msg.role === "assistant" && !msg.isThinking && msg.content,
|
||||
);
|
||||
|
||||
if (!lastAssistantMsg) return;
|
||||
|
||||
// 检查是否已处理过
|
||||
if (processedMessageIds.current.has(lastAssistantMsg.id)) return;
|
||||
|
||||
// 提取文档内容
|
||||
const docContent = extractDocumentContent(lastAssistantMsg.content);
|
||||
if (docContent) {
|
||||
// 标记为已处理
|
||||
processedMessageIds.current.add(lastAssistantMsg.id);
|
||||
|
||||
// 更新画布内容
|
||||
setDocumentState((prev) => {
|
||||
if (!prev) {
|
||||
return createInitialDocumentState(docContent);
|
||||
}
|
||||
// 添加新版本
|
||||
const newVersion = {
|
||||
id: crypto.randomUUID(),
|
||||
content: docContent,
|
||||
createdAt: Date.now(),
|
||||
description: `AI 生成 - 版本 ${prev.versions.length + 1}`,
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
content: docContent,
|
||||
versions: [...prev.versions, newVersion],
|
||||
currentVersionId: newVersion.id,
|
||||
};
|
||||
});
|
||||
|
||||
// 自动打开画布
|
||||
setLayoutMode("chat-canvas");
|
||||
}
|
||||
}, [messages, isContentCreationMode, extractDocumentContent]);
|
||||
|
||||
const handleSend = useCallback(
|
||||
async (
|
||||
@@ -88,14 +271,293 @@ export function AgentChatPage({
|
||||
const handleClearMessages = useCallback(() => {
|
||||
clearMessages();
|
||||
setInput("");
|
||||
// 重置布局模式
|
||||
setLayoutMode("chat");
|
||||
}, [clearMessages]);
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
setShowSidebar(!showSidebar);
|
||||
};
|
||||
|
||||
// 切换画布显示
|
||||
const handleToggleCanvas = useCallback(() => {
|
||||
setLayoutMode((prev) => {
|
||||
if (prev === "chat") {
|
||||
// 打开画布时,如果没有文档状态则创建初始状态
|
||||
if (!documentState) {
|
||||
setDocumentState(
|
||||
createInitialDocumentState("# 新文档\n\n在这里开始编写内容..."),
|
||||
);
|
||||
}
|
||||
return "chat-canvas";
|
||||
}
|
||||
return "chat";
|
||||
});
|
||||
}, [documentState]);
|
||||
|
||||
// 关闭画布
|
||||
const handleCloseCanvas = useCallback(() => {
|
||||
setLayoutMode("chat");
|
||||
}, []);
|
||||
|
||||
// 处理文件写入 - 同名文件更新内容,不同名文件独立保存
|
||||
const handleWriteFile = useCallback(
|
||||
(content: string, fileName: string) => {
|
||||
console.log(
|
||||
"[AgentChatPage] 收到文件写入:",
|
||||
fileName,
|
||||
content.length,
|
||||
"字符",
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// 根据文件名推进工作流步骤
|
||||
const stepIndex = FILE_TO_STEP_MAP[fileName];
|
||||
if (
|
||||
stepIndex !== undefined &&
|
||||
stepIndex === currentStepIndex &&
|
||||
isContentCreationMode
|
||||
) {
|
||||
console.log(
|
||||
"[AgentChatPage] 推进工作流步骤:",
|
||||
stepIndex,
|
||||
"->",
|
||||
stepIndex + 1,
|
||||
);
|
||||
completeStep({
|
||||
aiOutput: { fileName, preview: content.slice(0, 100) },
|
||||
});
|
||||
}
|
||||
|
||||
// 更新或创建文件
|
||||
setTaskFiles((prev) => {
|
||||
// 查找同名文件
|
||||
const existingIndex = prev.findIndex((f) => f.name === fileName);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// 同名文件存在 - 直接更新内容(不创建新版本)
|
||||
const existing = prev[existingIndex];
|
||||
|
||||
// 如果内容完全相同,跳过
|
||||
if (existing.content === content) {
|
||||
console.log("[AgentChatPage] 文件内容相同,跳过:", fileName);
|
||||
setSelectedFileId(existing.id);
|
||||
return prev;
|
||||
}
|
||||
|
||||
// 更新文件内容
|
||||
console.log("[AgentChatPage] 更新文件:", fileName);
|
||||
const updated = [...prev];
|
||||
updated[existingIndex] = {
|
||||
...existing,
|
||||
content,
|
||||
updatedAt: now,
|
||||
};
|
||||
setSelectedFileId(existing.id);
|
||||
return updated;
|
||||
}
|
||||
|
||||
// 新文件 - 添加到列表
|
||||
console.log("[AgentChatPage] 创建新文件:", fileName);
|
||||
const newFile: TaskFile = {
|
||||
id: crypto.randomUUID(),
|
||||
name: fileName,
|
||||
type: "document",
|
||||
content,
|
||||
version: 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
setSelectedFileId(newFile.id);
|
||||
return [...prev, newFile];
|
||||
});
|
||||
|
||||
// 更新画布内容
|
||||
setDocumentState((prev) => {
|
||||
if (!prev) {
|
||||
return createInitialDocumentState(content);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
// 自动打开画布显示流式内容
|
||||
setLayoutMode("chat-canvas");
|
||||
},
|
||||
[currentStepIndex, isContentCreationMode, completeStep],
|
||||
);
|
||||
|
||||
// 更新 ref,供 useAgentChat 使用
|
||||
useEffect(() => {
|
||||
handleWriteFileRef.current = handleWriteFile;
|
||||
}, [handleWriteFile]);
|
||||
|
||||
// 处理文件点击 - 在画布中显示文件内容
|
||||
const handleFileClick = useCallback((fileName: string, content: string) => {
|
||||
console.log("[AgentChatPage] 文件点击:", fileName);
|
||||
|
||||
// 查找或创建任务文件
|
||||
setTaskFiles((prev) => {
|
||||
const existingFile = prev.find((f) => f.name === fileName);
|
||||
if (existingFile) {
|
||||
setSelectedFileId(existingFile.id);
|
||||
return prev;
|
||||
}
|
||||
// 如果文件不存在,添加到列表
|
||||
const newFile: TaskFile = {
|
||||
id: crypto.randomUUID(),
|
||||
name: fileName,
|
||||
type: "document",
|
||||
content,
|
||||
version: 1,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
setSelectedFileId(newFile.id);
|
||||
return [...prev, newFile];
|
||||
});
|
||||
|
||||
// 更新画布内容
|
||||
setDocumentState((prev) => {
|
||||
if (!prev) {
|
||||
return createInitialDocumentState(content);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
// 打开画布
|
||||
setLayoutMode("chat-canvas");
|
||||
}, []);
|
||||
|
||||
// 处理任务文件点击 - 在画布中显示文件内容
|
||||
const handleTaskFileClick = useCallback((file: TaskFile) => {
|
||||
if (file.type === "document" && file.content) {
|
||||
setSelectedFileId(file.id);
|
||||
setDocumentState((prev) => {
|
||||
if (!prev) {
|
||||
return createInitialDocumentState(file.content!);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
content: file.content!,
|
||||
};
|
||||
});
|
||||
// 只打开画布,不关闭文件列表(让用户自己关闭)
|
||||
setLayoutMode("chat-canvas");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hasMessages = messages.length > 0;
|
||||
|
||||
// A2UI 表单提交处理
|
||||
const handleA2UISubmit = useCallback(
|
||||
async (formData: A2UIFormData, _messageId: string) => {
|
||||
console.log("[AgentChatPage] A2UI 表单提交:", formData);
|
||||
|
||||
// 将表单数据格式化为用户消息
|
||||
const formattedData = Object.entries(formData)
|
||||
.map(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return `- ${key}: ${value.join(", ")}`;
|
||||
}
|
||||
return `- ${key}: ${value}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const userMessage = `我的选择:\n${formattedData}`;
|
||||
|
||||
// 发送用户消息
|
||||
await sendMessage(userMessage, [], false, false);
|
||||
},
|
||||
[sendMessage],
|
||||
);
|
||||
|
||||
// 聊天区域内容
|
||||
const chatContent = (
|
||||
<ChatContainer>
|
||||
{/* 步骤进度条 - 仅在内容创作模式且有消息时显示 */}
|
||||
{isContentCreationMode && hasMessages && steps.length > 0 && (
|
||||
<StepProgress
|
||||
steps={steps}
|
||||
currentIndex={currentStepIndex}
|
||||
onStepClick={goToStep}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasMessages ? (
|
||||
<ChatContent>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
onDeleteMessage={deleteMessage}
|
||||
onEditMessage={editMessage}
|
||||
onA2UISubmit={handleA2UISubmit}
|
||||
onWriteFile={handleWriteFile}
|
||||
onFileClick={handleFileClick}
|
||||
/>
|
||||
</ChatContent>
|
||||
) : (
|
||||
<EmptyState
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
onSend={(text) => {
|
||||
setInput(text);
|
||||
setTimeout(() => handleSend([], false, false), 0);
|
||||
}}
|
||||
creationMode={creationMode}
|
||||
onCreationModeChange={setCreationMode}
|
||||
activeTheme={activeTheme}
|
||||
onThemeChange={setActiveTheme}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasMessages && (
|
||||
<>
|
||||
{/* 任务文件列表 - 显示在输入框上方 */}
|
||||
{taskFiles.length > 0 && (
|
||||
<TaskFileList
|
||||
files={taskFiles}
|
||||
selectedFileId={selectedFileId}
|
||||
onFileClick={handleTaskFileClick}
|
||||
expanded={taskFilesExpanded}
|
||||
onExpandedChange={setTaskFilesExpanded}
|
||||
/>
|
||||
)}
|
||||
<Inputbar
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
onSend={handleSend}
|
||||
onStop={stopSending}
|
||||
isLoading={isSending}
|
||||
disabled={!processStatus.running && false}
|
||||
onClearMessages={handleClearMessages}
|
||||
onToggleCanvas={handleToggleCanvas}
|
||||
isCanvasOpen={layoutMode === "chat-canvas"}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ChatContainer>
|
||||
);
|
||||
|
||||
// 画布区域内容
|
||||
const canvasContent = useMemo(() => {
|
||||
if (documentState) {
|
||||
return (
|
||||
<DocumentCanvas
|
||||
state={documentState}
|
||||
onStateChange={setDocumentState}
|
||||
onClose={handleCloseCanvas}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [documentState, handleCloseCanvas]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{showSidebar && (
|
||||
@@ -120,38 +582,12 @@ export function AgentChatPage({
|
||||
onToggleSettings={() => setShowSettings(!showSettings)}
|
||||
/>
|
||||
|
||||
<ChatContainer>
|
||||
{hasMessages ? (
|
||||
<ChatContent>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
onDeleteMessage={deleteMessage}
|
||||
onEditMessage={editMessage}
|
||||
/>
|
||||
</ChatContent>
|
||||
) : (
|
||||
<EmptyState
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
onSend={(text) => {
|
||||
setInput(text);
|
||||
// 使用 setTimeout 确保 state 更新后再发送
|
||||
setTimeout(() => handleSend([], false, false), 0);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasMessages && (
|
||||
<Inputbar
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
onSend={handleSend}
|
||||
isLoading={isSending}
|
||||
disabled={!processStatus.running && false}
|
||||
onClearMessages={handleClearMessages}
|
||||
/>
|
||||
)}
|
||||
</ChatContainer>
|
||||
{/* 使用布局过渡组件 */}
|
||||
<LayoutTransition
|
||||
mode={layoutMode}
|
||||
chatContent={chatContent}
|
||||
canvasContent={canvasContent}
|
||||
/>
|
||||
</MainArea>
|
||||
|
||||
{showSettings && <ChatSettings onClose={() => setShowSettings(false)} />}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* @file 步骤结果展示组件
|
||||
* @description 展示步骤完成后的结果
|
||||
* @module components/content-creator/core/StepGuide/StepResult
|
||||
*/
|
||||
|
||||
import React, { memo } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Check, Edit2, FileText, Sparkles } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StepResult as StepResultType, ContentFile } from "../../types";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
const ResultCard = styled.div`
|
||||
padding: 16px;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 12px;
|
||||
border: 1px solid hsl(var(--border));
|
||||
`;
|
||||
|
||||
const ResultHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
const ResultTitle = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
`;
|
||||
|
||||
const SuccessIcon = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: hsl(142 76% 36% / 0.1);
|
||||
color: hsl(142 76% 36%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const EditButton = styled(Button)`
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
const UserInputSection = styled.div`
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
const SectionLabel = styled.div`
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 6px;
|
||||
`;
|
||||
|
||||
const InputList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
const InputItem = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
const InputLabel = styled.span`
|
||||
color: hsl(var(--muted-foreground));
|
||||
min-width: 80px;
|
||||
`;
|
||||
|
||||
const InputValue = styled.span`
|
||||
color: hsl(var(--foreground));
|
||||
`;
|
||||
|
||||
const AIOutputSection = styled.div`
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
`;
|
||||
|
||||
const AIOutputContent = styled.div`
|
||||
font-size: 14px;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
const ArtifactsSection = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
`;
|
||||
|
||||
const ArtifactCard = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
`;
|
||||
|
||||
const ArtifactIcon = styled.div`
|
||||
color: hsl(var(--primary));
|
||||
`;
|
||||
|
||||
const ArtifactName = styled.span`
|
||||
font-size: 13px;
|
||||
color: hsl(var(--foreground));
|
||||
`;
|
||||
|
||||
interface StepResultProps {
|
||||
/** 步骤结果 */
|
||||
result: StepResultType;
|
||||
/** 步骤标题 */
|
||||
title?: string;
|
||||
/** 编辑回调 */
|
||||
onEdit?: () => void;
|
||||
/** 点击产物回调 */
|
||||
onArtifactClick?: (file: ContentFile) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤结果展示组件
|
||||
*
|
||||
* 展示用户输入、AI 输出和生成的产物
|
||||
*/
|
||||
export const StepResult: React.FC<StepResultProps> = memo(
|
||||
({ result, title = "已完成", onEdit, onArtifactClick }) => {
|
||||
const { userInput, aiOutput, artifacts } = result;
|
||||
|
||||
// 格式化用户输入值
|
||||
const formatValue = (value: unknown): string => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join("、");
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// 渲染用户输入部分
|
||||
const renderUserInput = (): React.JSX.Element | null => {
|
||||
if (!userInput || Object.keys(userInput).length === 0) {
|
||||
return null;
|
||||
}
|
||||
const entries = Object.entries(userInput) as [string, unknown][];
|
||||
return (
|
||||
<UserInputSection>
|
||||
<SectionLabel>你的输入</SectionLabel>
|
||||
<InputList>
|
||||
{entries.map(([key, val]) => (
|
||||
<InputItem key={key}>
|
||||
<InputLabel>{key}:</InputLabel>
|
||||
<InputValue>{formatValue(val)}</InputValue>
|
||||
</InputItem>
|
||||
))}
|
||||
</InputList>
|
||||
</UserInputSection>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ResultCard>
|
||||
<ResultHeader>
|
||||
<ResultTitle>
|
||||
<SuccessIcon>
|
||||
<Check size={14} />
|
||||
</SuccessIcon>
|
||||
{title}
|
||||
</ResultTitle>
|
||||
{onEdit && (
|
||||
<EditButton variant="ghost" size="sm" onClick={onEdit}>
|
||||
<Edit2 size={14} />
|
||||
编辑
|
||||
</EditButton>
|
||||
)}
|
||||
</ResultHeader>
|
||||
|
||||
{/* 用户输入 */}
|
||||
{renderUserInput() as React.ReactNode}
|
||||
|
||||
{/* AI 输出 */}
|
||||
{aiOutput ? (
|
||||
<AIOutputSection>
|
||||
<SectionLabel>
|
||||
<Sparkles
|
||||
size={12}
|
||||
style={{ display: "inline", marginRight: 4 }}
|
||||
/>
|
||||
AI 生成
|
||||
</SectionLabel>
|
||||
<AIOutputContent>
|
||||
{typeof aiOutput === "string"
|
||||
? aiOutput
|
||||
: JSON.stringify(aiOutput, null, 2)}
|
||||
</AIOutputContent>
|
||||
</AIOutputSection>
|
||||
) : null}
|
||||
|
||||
{/* 产物列表 */}
|
||||
{artifacts && artifacts.length > 0 && (
|
||||
<ArtifactsSection>
|
||||
<SectionLabel style={{ width: "100%" }}>生成的文件</SectionLabel>
|
||||
{artifacts.map((file) => (
|
||||
<ArtifactCard
|
||||
key={file.id}
|
||||
onClick={() => onArtifactClick?.(file)}
|
||||
>
|
||||
<ArtifactIcon>
|
||||
<FileText size={16} />
|
||||
</ArtifactIcon>
|
||||
<ArtifactName>{file.name}</ArtifactName>
|
||||
</ArtifactCard>
|
||||
))}
|
||||
</ArtifactsSection>
|
||||
)}
|
||||
</ResultCard>
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
StepResult.displayName = "StepResult";
|
||||
Reference in New Issue
Block a user