diff --git a/src/components/audit/CreateTaskDialog.tsx b/src/components/audit/CreateTaskDialog.tsx index 65c7d1e..018b43e 100644 --- a/src/components/audit/CreateTaskDialog.tsx +++ b/src/components/audit/CreateTaskDialog.tsx @@ -22,6 +22,7 @@ import { import { api } from "@/shared/config/database"; import type { Project, CreateAuditTaskForm } from "@/shared/types"; import { toast } from "sonner"; +import TerminalProgressDialog from "./TerminalProgressDialog"; interface CreateTaskDialogProps { open: boolean; @@ -36,6 +37,8 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr const [loading, setLoading] = useState(false); const [creating, setCreating] = useState(false); const [searchTerm, setSearchTerm] = useState(""); + const [showTerminalDialog, setShowTerminalDialog] = useState(false); + const [currentTaskId, setCurrentTaskId] = useState(null); const [taskForm, setTaskForm] = useState({ project_id: "", @@ -98,23 +101,21 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr try { setCreating(true); - await api.createAuditTask({ + const task = await api.createAuditTask({ ...taskForm, created_by: null // 无登录场景下设置为null } as any); - // 显示详细的提示信息 - toast.success("审计任务创建成功", { - description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...', - duration: 5000 - }); + const taskId = (task as any).id; + // 关闭创建对话框 onOpenChange(false); resetForm(); onTaskCreated(); - // 跳转到项目详情页面 - navigate(`/projects/${taskForm.project_id}`); + // 显示终端进度窗口 + setCurrentTaskId(taskId); + setShowTerminalDialog(true); } catch (error) { console.error('Failed to create task:', error); toast.error("创建任务失败"); @@ -547,6 +548,14 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr + + {/* 终端进度对话框 */} + ); } \ No newline at end of file diff --git a/src/components/audit/TerminalProgressDialog.tsx b/src/components/audit/TerminalProgressDialog.tsx new file mode 100644 index 0000000..380189a --- /dev/null +++ b/src/components/audit/TerminalProgressDialog.tsx @@ -0,0 +1,461 @@ +import { useEffect, useRef, useState } from "react"; +import { Dialog, DialogOverlay, DialogPortal } from "@/components/ui/dialog"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { Terminal, CheckCircle, XCircle, Loader2 } from "lucide-react"; +import { cn } from "@/shared/utils/utils"; +import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; + +interface TerminalProgressDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + taskId: string | null; + taskType: "repository" | "zip"; +} + +interface LogEntry { + timestamp: string; + message: string; + type: "info" | "success" | "error" | "warning"; +} + +export default function TerminalProgressDialog({ + open, + onOpenChange, + taskId, + taskType +}: TerminalProgressDialogProps) { + const [logs, setLogs] = useState([]); + const [isCompleted, setIsCompleted] = useState(false); + const [isFailed, setIsFailed] = useState(false); + const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" })); + const logsEndRef = useRef(null); + const pollIntervalRef = useRef(null); + const hasInitializedLogsRef = useRef(false); + + // 添加日志条目 + const addLog = (message: string, type: LogEntry["type"] = "info") => { + const timestamp = new Date().toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }); + setLogs(prev => [...prev, { timestamp, message, type }]); + }; + + // 自动滚动到底部 + useEffect(() => { + logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [logs]); + + // 实时更新光标处的时间 + useEffect(() => { + if (!open || isCompleted || isFailed) { + return; + } + + const timeInterval = setInterval(() => { + setCurrentTime(new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" })); + }, 1000); + + return () => { + clearInterval(timeInterval); + }; + }, [open, isCompleted, isFailed]); + + // 轮询任务状态 + useEffect(() => { + if (!open || !taskId) { + // 清理状态 + setLogs([]); + setIsCompleted(false); + setIsFailed(false); + hasInitializedLogsRef.current = false; + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + return; + } + + // 只初始化日志一次(防止React严格模式重复) + if (!hasInitializedLogsRef.current) { + hasInitializedLogsRef.current = true; + + // 初始化日志 + addLog("🚀 审计任务已启动", "info"); + addLog(`� 任务任ID: ${taskId}`, "info"); + addLog(`� 任务类D型: ${taskType === "repository" ? "仓库审计" : "ZIP文件审计"}`, "info"); + addLog("⏳ 正在初始化审计环境...", "info"); + } + + let lastScannedFiles = 0; + let lastIssuesCount = 0; + let lastTotalLines = 0; + let lastStatus = ""; + let pollCount = 0; + let hasDataChange = false; + let isFirstPoll = true; + + // 开始轮询 + const pollTask = async () => { + // 如果任务已完成或失败,停止轮询 + if (isCompleted || isFailed) { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + return; + } + + try { + pollCount++; + hasDataChange = false; + + const requestStartTime = Date.now(); + + // 使用 api.getAuditTaskById 获取任务状态 + const { api } = await import("@/shared/config/database"); + const task = await api.getAuditTaskById(taskId); + + const requestDuration = Date.now() - requestStartTime; + + if (!task) { + addLog(`❌ 任务不存在 (${requestDuration}ms)`, "error"); + throw new Error("任务不存在"); + } + + // 检查是否有数据变化 + const statusChanged = task.status !== lastStatus; + const filesChanged = task.scanned_files !== lastScannedFiles; + const issuesChanged = task.issues_count !== lastIssuesCount; + const linesChanged = task.total_lines !== lastTotalLines; + + hasDataChange = statusChanged || filesChanged || issuesChanged || linesChanged; + + // 标记首次轮询已完成 + if (isFirstPoll) { + isFirstPoll = false; + } + + // 只在有变化时显示请求/响应信息 + if (hasDataChange) { + addLog(`🔄 正在获取任务状态...`, "info"); + addLog( + `✓ 状态: ${task.status} | 文件: ${task.scanned_files}/${task.total_files} | 问题: ${task.issues_count} (${requestDuration}ms)`, + "success" + ); + } + + // 更新上次状态 + if (statusChanged) { + lastStatus = task.status; + } + + // 检查任务状态 + if (task.status === "pending") { + // 任务待处理(只在状态变化时显示) + if (statusChanged && logs.filter(l => l.message.includes("等待开始执行")).length === 0) { + addLog("⏳ 任务已创建,等待开始执行...", "info"); + } + } else if (task.status === "running") { + // 首次进入运行状态 + if (statusChanged && logs.filter(l => l.message.includes("开始扫描")).length === 0) { + addLog("🔍 开始扫描代码文件...", "info"); + if (task.project) { + addLog(`📁 项目: ${task.project.name}`, "info"); + if (task.branch_name) { + addLog(`🌿 分支: ${task.branch_name}`, "info"); + } + } + } + + // 显示进度更新(仅在有变化时) + if (filesChanged && task.scanned_files > lastScannedFiles) { + const progress = task.total_files > 0 + ? Math.round((task.scanned_files / task.total_files) * 100) + : 0; + const filesProcessed = task.scanned_files - lastScannedFiles; + addLog( + `📊 扫描进度: ${task.scanned_files}/${task.total_files} 文件 (${progress}%) [+${filesProcessed}]`, + "info" + ); + lastScannedFiles = task.scanned_files; + } + + // 显示问题发现(仅在有变化时) + if (issuesChanged && task.issues_count > lastIssuesCount) { + const newIssues = task.issues_count - lastIssuesCount; + addLog(`⚠️ 发现 ${newIssues} 个新问题 (总计: ${task.issues_count})`, "warning"); + lastIssuesCount = task.issues_count; + } + + // 显示代码行数(仅在有变化时) + if (linesChanged && task.total_lines > lastTotalLines) { + const newLines = task.total_lines - lastTotalLines; + addLog(`📝 已分析 ${task.total_lines.toLocaleString()} 行代码 [+${newLines.toLocaleString()}]`, "info"); + lastTotalLines = task.total_lines; + } + } else if (task.status === "completed") { + // 任务完成 + if (!isCompleted) { + addLog("", "info"); // 空行分隔 + addLog("✅ 代码扫描完成", "success"); + addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "info"); + addLog(`📊 总计扫描: ${task.total_files} 个文件`, "success"); + addLog(`📝 总计分析: ${task.total_lines.toLocaleString()} 行代码`, "success"); + addLog(`⚠️ 发现问题: ${task.issues_count} 个`, task.issues_count > 0 ? "warning" : "success"); + + // 解析问题类型分布 + if (task.issues_count > 0) { + try { + const { api: apiImport } = await import("@/shared/config/database"); + const issues = await apiImport.getAuditIssues(taskId); + + const severityCounts = { + critical: issues.filter(i => i.severity === 'critical').length, + high: issues.filter(i => i.severity === 'high').length, + medium: issues.filter(i => i.severity === 'medium').length, + low: issues.filter(i => i.severity === 'low').length + }; + + if (severityCounts.critical > 0) { + addLog(` 🔴 严重: ${severityCounts.critical} 个`, "error"); + } + if (severityCounts.high > 0) { + addLog(` 🟠 高: ${severityCounts.high} 个`, "warning"); + } + if (severityCounts.medium > 0) { + addLog(` 🟡 中等: ${severityCounts.medium} 个`, "warning"); + } + if (severityCounts.low > 0) { + addLog(` 🟢 低: ${severityCounts.low} 个`, "info"); + } + } catch (e) { + // 静默处理错误 + } + } + + addLog(`⭐ 质量评分: ${task.quality_score.toFixed(1)}/100`, "success"); + addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "info"); + addLog("🎉 审计任务已完成!", "success"); + + if (task.completed_at) { + const startTime = new Date(task.created_at).getTime(); + const endTime = new Date(task.completed_at).getTime(); + const duration = Math.round((endTime - startTime) / 1000); + addLog(`⏱️ 总耗时: ${duration} 秒`, "info"); + } + + setIsCompleted(true); + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + } + } else if (task.status === "failed") { + // 任务失败 + if (!isFailed) { + addLog("", "info"); // 空行分隔 + addLog("❌ 审计任务执行失败", "error"); + addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); + addLog("可能的原因:", "error"); + addLog(" • 网络连接问题", "error"); + addLog(" • 仓库访问权限不足", "error"); + addLog(" • GitHub API 限流", "error"); + addLog(" • 代码文件格式错误", "error"); + addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); + addLog("💡 建议: 检查网络连接和仓库配置后重试", "warning"); + + setIsFailed(true); + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + } + } + } catch (error: any) { + addLog(`❌ ${error.message || "未知错误"}`, "error"); + // 不中断轮询,继续尝试 + } + }; + + // 立即执行一次 + pollTask(); + + // 设置定时轮询(每2秒) + pollIntervalRef.current = window.setInterval(pollTask, 2000); + + // 清理函数 + return () => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + }; + }, [open, taskId, taskType]); + + // 获取日志颜色 - 使用优雅的深红色主题 + const getLogColor = (type: LogEntry["type"]) => { + switch (type) { + case "success": + return "text-emerald-400"; + case "error": + return "text-rose-400"; + case "warning": + return "text-amber-400"; + default: + return "text-gray-200"; + } + }; + + // 获取状态图标 + const getStatusIcon = () => { + if (isFailed) { + return ; + } + if (isCompleted) { + return ; + } + return ; + }; + + return ( + + + + e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + > + {/* 无障碍访问标题 */} + + 审计进度监控 + + 实时显示代码审计任务的执行进度和详细信息 + + + + {/* 终端头部 */} +
+
+ + 审计进度监控 + {getStatusIcon()} +
+
+
+
+
+
+ + {/* 终端内容 */} +
+
+ {logs.map((log, index) => ( +
+ + [{log.timestamp}] + + + {log.message} + +
+ ))} + + {/* 光标旋转闪烁效果 */} + {!isCompleted && !isFailed && ( +
+ [{currentTime}] + +
+ )} + + {/* 添加自定义动画 */} + + +
+
+
+ + {/* 底部提示 */} +
+
+ + {isCompleted ? "✅ 任务已完成,可以关闭此窗口" : + isFailed ? "❌ 任务失败,请检查配置后重试" : + "⏳ 审计进行中,请勿关闭窗口,过程可能较慢,请耐心等待......"} + + {(isCompleted || isFailed) && ( + + )} +
+
+ + +
+ ); +} diff --git a/src/features/projects/services/repoZipScan.ts b/src/features/projects/services/repoZipScan.ts index 9c02cc4..6a37875 100644 --- a/src/features/projects/services/repoZipScan.ts +++ b/src/features/projects/services/repoZipScan.ts @@ -124,137 +124,149 @@ export async function scanZipFile(params: { const taskId = (task as any).id; - try { - // 更新任务状态为运行中 - await api.updateAuditTask(taskId, { - status: "running", - started_at: new Date().toISOString() - } as any); + // 启动后台扫描任务,不阻塞返回 + (async () => { + try { + // 更新任务状态为运行中 + await api.updateAuditTask(taskId, { + status: "running", + started_at: new Date().toISOString() + } as any); - // 读取ZIP文件 - const arrayBuffer = await zipFile.arrayBuffer(); - const uint8Array = new Uint8Array(arrayBuffer); + // 读取ZIP文件 + const arrayBuffer = await zipFile.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); - return new Promise((resolve, reject) => { - unzip(uint8Array, async (err, unzipped) => { - if (err) { - await api.updateAuditTask(taskId, { status: "failed" } as any); - reject(new Error(`ZIP文件解压失败: ${err.message}`)); - return; - } - - try { - // 筛选需要分析的文件 - const filesToAnalyze: Array<{ path: string; content: string }> = []; - - for (const [path, data] of Object.entries(unzipped)) { - // 跳过目录 - if (path.endsWith('/')) continue; - - // 检查文件类型和排除模式 - if (!isTextFile(path) || shouldExclude(path, excludePatterns)) continue; - - // 检查文件大小 - if (data.length > MAX_FILE_SIZE_BYTES) continue; - - try { - const content = new TextDecoder('utf-8').decode(data); - filesToAnalyze.push({ path, content }); - } catch (decodeError) { - // 跳过无法解码的文件 - continue; - } + await new Promise((resolve, reject) => { + unzip(uint8Array, async (err, unzipped) => { + if (err) { + await api.updateAuditTask(taskId, { status: "failed" } as any); + reject(new Error(`ZIP文件解压失败: ${err.message}`)); + return; } - // 限制分析文件数量 - const limitedFiles = filesToAnalyze - .sort((a, b) => a.path.length - b.path.length) // 优先分析路径较短的文件 - .slice(0, MAX_ANALYZE_FILES); - - let totalFiles = limitedFiles.length; - let scannedFiles = 0; - let totalLines = 0; - let totalIssues = 0; - let qualityScores: number[] = []; - - // 分析每个文件 - for (const file of limitedFiles) { - try { - const language = getLanguageFromPath(file.path); - const lines = file.content.split(/\r?\n/).length; - totalLines += lines; - - // 使用AI分析代码 - const analysis = await CodeAnalysisEngine.analyzeCode(file.content, language); - qualityScores.push(analysis.quality_score); - - // 保存发现的问题 - for (const issue of analysis.issues) { - await api.createAuditIssue({ - task_id: taskId, - file_path: file.path, - line_number: issue.line || null, - column_number: issue.column || null, - issue_type: issue.type || "maintainability", - severity: issue.severity || "low", - title: issue.title || "Issue", - description: issue.description || null, - suggestion: issue.suggestion || null, - code_snippet: issue.code_snippet || null, - ai_explanation: issue.ai_explanation || null, - status: "open" - } as any); - - totalIssues++; + try { + // 筛选需要分析的文件 + const filesToAnalyze: Array<{ path: string; content: string }> = []; + + for (const [path, data] of Object.entries(unzipped)) { + // 跳过目录 + if (path.endsWith('/')) continue; + + // 检查文件类型和排除模式 + if (!isTextFile(path) || shouldExclude(path, excludePatterns)) continue; + + // 检查文件大小 + if (data.length > MAX_FILE_SIZE_BYTES) continue; + + try { + const content = new TextDecoder('utf-8').decode(data); + filesToAnalyze.push({ path, content }); + } catch (decodeError) { + // 跳过无法解码的文件 + continue; } + } - scannedFiles++; + // 限制分析文件数量 + const limitedFiles = filesToAnalyze + .sort((a, b) => a.path.length - b.path.length) // 优先分析路径较短的文件 + .slice(0, MAX_ANALYZE_FILES); - // 每分析10个文件更新一次进度 - if (scannedFiles % 10 === 0) { + let totalFiles = limitedFiles.length; + let scannedFiles = 0; + let totalLines = 0; + let totalIssues = 0; + let qualityScores: number[] = []; + + // 更新总文件数 + await api.updateAuditTask(taskId, { + total_files: totalFiles, + scanned_files: 0, + total_lines: 0, + issues_count: 0 + } as any); + + // 分析每个文件 + for (const file of limitedFiles) { + try { + const language = getLanguageFromPath(file.path); + const lines = file.content.split(/\r?\n/).length; + totalLines += lines; + + // 使用AI分析代码 + const analysis = await CodeAnalysisEngine.analyzeCode(file.content, language); + qualityScores.push(analysis.quality_score); + + // 保存发现的问题 + for (const issue of analysis.issues) { + await api.createAuditIssue({ + task_id: taskId, + file_path: file.path, + line_number: issue.line || null, + column_number: issue.column || null, + issue_type: issue.type || "maintainability", + severity: issue.severity || "low", + title: issue.title || "Issue", + description: issue.description || null, + suggestion: issue.suggestion || null, + code_snippet: issue.code_snippet || null, + ai_explanation: issue.ai_explanation || null, + status: "open" + } as any); + + totalIssues++; + } + + scannedFiles++; + + // 每分析一个文件更新一次进度 await api.updateAuditTask(taskId, { total_files: totalFiles, scanned_files: scannedFiles, total_lines: totalLines, issues_count: totalIssues } as any); + + // 添加延迟避免API限制 + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (analysisError) { + console.error(`分析文件 ${file.path} 失败:`, analysisError); + // 继续分析其他文件 } - - // 添加延迟避免API限制 - await new Promise(resolve => setTimeout(resolve, 500)); - } catch (analysisError) { - console.error(`分析文件 ${file.path} 失败:`, analysisError); - // 继续分析其他文件 } + + // 计算平均质量分 + const avgQualityScore = qualityScores.length > 0 + ? qualityScores.reduce((sum, score) => sum + score, 0) / qualityScores.length + : 0; + + // 更新任务完成状态 + await api.updateAuditTask(taskId, { + status: "completed", + total_files: totalFiles, + scanned_files: scannedFiles, + total_lines: totalLines, + issues_count: totalIssues, + quality_score: avgQualityScore, + completed_at: new Date().toISOString() + } as any); + + resolve(); + } catch (processingError) { + await api.updateAuditTask(taskId, { status: "failed" } as any); + reject(processingError); } - - // 计算平均质量分 - const avgQualityScore = qualityScores.length > 0 - ? qualityScores.reduce((sum, score) => sum + score, 0) / qualityScores.length - : 0; - - // 更新任务完成状态 - await api.updateAuditTask(taskId, { - status: "completed", - total_files: totalFiles, - scanned_files: scannedFiles, - total_lines: totalLines, - issues_count: totalIssues, - quality_score: avgQualityScore, - completed_at: new Date().toISOString() - } as any); - - resolve(taskId); - } catch (processingError) { - await api.updateAuditTask(taskId, { status: "failed" } as any); - reject(processingError); - } + }); }); - }); - } catch (error) { - await api.updateAuditTask(taskId, { status: "failed" } as any); - throw error; - } + } catch (error) { + console.error('ZIP扫描任务执行失败:', error); + await api.updateAuditTask(taskId, { status: "failed" } as any); + } + })(); + + // 立即返回任务ID,让用户可以看到进度 + return taskId; } export function validateZipFile(file: File): { valid: boolean; error?: string } { diff --git a/src/pages/ProjectDetail.tsx b/src/pages/ProjectDetail.tsx index 72792f6..f346fb8 100644 --- a/src/pages/ProjectDetail.tsx +++ b/src/pages/ProjectDetail.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { useParams, Link, useNavigate } from "react-router-dom"; +import { useParams, Link } from "react-router-dom"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -23,15 +23,17 @@ import { runRepositoryAudit } from "@/features/projects/services"; import type { Project, AuditTask } from "@/shared/types"; import { toast } from "sonner"; import CreateTaskDialog from "@/components/audit/CreateTaskDialog"; +import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog"; export default function ProjectDetail() { const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); const [project, setProject] = useState(null); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [scanning, setScanning] = useState(false); const [showCreateTaskDialog, setShowCreateTaskDialog] = useState(false); + const [showTerminalDialog, setShowTerminalDialog] = useState(false); + const [currentTaskId, setCurrentTaskId] = useState(null); useEffect(() => { if (id) { @@ -78,15 +80,12 @@ export default function ProjectDetail() { console.log('审计任务创建成功,taskId:', taskId); - // 显示详细的提示信息 - toast.success('审计任务已启动', { - description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...', - duration: 5000 - }); + // 显示终端进度窗口 + setCurrentTaskId(taskId); + setShowTerminalDialog(true); - // 跳转到任务详情页面 - console.log('准备跳转到:', `/tasks/${taskId}`); - navigate(`/tasks/${taskId}`); + // 重新加载项目数据 + loadProjectData(); } catch (e: any) { console.error('启动审计失败:', e); toast.error(e?.message || '启动审计失败'); @@ -485,6 +484,14 @@ export default function ProjectDetail() { onTaskCreated={handleTaskCreated} preselectedProjectId={id} /> + + {/* 终端进度对话框 */} + ); } \ No newline at end of file diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 8bd4b7a..97e4e04 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -30,6 +30,7 @@ import type { Project, CreateProjectForm } from "@/shared/types"; import { Link } from "react-router-dom"; import { toast } from "sonner"; import CreateTaskDialog from "@/components/audit/CreateTaskDialog"; +import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog"; export default function Projects() { const [projects, setProjects] = useState([]); @@ -41,6 +42,8 @@ export default function Projects() { const [uploadProgress, setUploadProgress] = useState(0); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); + const [showTerminalDialog, setShowTerminalDialog] = useState(false); + const [currentTaskId, setCurrentTaskId] = useState(null); const [createForm, setCreateForm] = useState({ name: "", description: "", @@ -153,15 +156,14 @@ export default function Projects() { clearInterval(progressInterval); setUploadProgress(100); - toast.success("项目创建并开始分析"); + // 关闭创建对话框 setShowCreateDialog(false); resetCreateForm(); loadProjects(); - // 跳转到任务详情页 - setTimeout(() => { - window.open(`/tasks/${taskId}`, '_blank'); - }, 1000); + // 显示终端进度窗口 + setCurrentTaskId(taskId); + setShowTerminalDialog(true); } catch (error: any) { console.error('Upload failed:', error); @@ -666,6 +668,14 @@ export default function Projects() { onTaskCreated={handleTaskCreated} preselectedProjectId={selectedProjectForTask} /> + + {/* 终端进度对话框 */} + ); } \ No newline at end of file