From 3955996fe876b2e4343ab6089f6f395e58fa91ea Mon Sep 17 00:00:00 2001 From: huangmin <2927933426@qq.com> Date: Thu, 4 Jun 2026 16:43:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E6=AC=A2?= =?UTF-8?q?=E8=BF=8E=E5=BC=B9=E7=AA=97=E7=9B=B8=E5=85=B3=E7=9A=84=E6=89=80?= =?UTF-8?q?=E6=9C=89=E4=BB=A3=E7=A0=81=E5=92=8C=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 彻底删除了 Quickstart 欢迎弹窗功能,包括对应的 store 状态、国际化文案、测试用例和组件代码,简化了项目代码结构。 --- src/app/canvas/page.tsx | 5 +- src/components/Header.tsx | 11 +- src/components/WorkflowCanvas.tsx | 41 +-- .../__tests__/WelcomeModal.test.tsx | 299 ------------------ .../__tests__/WorkflowCanvas.test.tsx | 62 ---- src/components/quickstart/WelcomeModal.tsx | 94 ------ src/components/quickstart/index.ts | 1 - src/i18n/index.tsx | 4 - src/store/workflowStore.ts | 9 - 9 files changed, 5 insertions(+), 521 deletions(-) delete mode 100644 src/components/__tests__/WelcomeModal.test.tsx delete mode 100644 src/components/quickstart/WelcomeModal.tsx diff --git a/src/app/canvas/page.tsx b/src/app/canvas/page.tsx index 7c4624b0..60d94ac6 100644 --- a/src/app/canvas/page.tsx +++ b/src/app/canvas/page.tsx @@ -22,7 +22,6 @@ function CanvasPageContent() { (state) => state.initializeAutoSave ); const cleanupAutoSave = useWorkflowStore((state) => state.cleanupAutoSave); - const setShowQuickstart = useWorkflowStore((state) => state.setShowQuickstart); const initializePopiModels = useModelStore((state) => state.initializePopiModels); const [showFTUX, setShowFTUX] = useState(false); @@ -57,7 +56,6 @@ function CanvasPageContent() { const isPopiaiEmbed = new URLSearchParams(window.location.search).get("embed") === "popiai"; if (isPopiaiEmbed) { setShowFTUX(false); - setShowQuickstart(false); useFTUXStore.getState().resetTutorial(); return; } @@ -65,7 +63,7 @@ function CanvasPageContent() { if (!getFTUXCompleted()) { setShowFTUX(true); } - }, [setShowQuickstart]); + }, []); const handleFTUXComplete = () => { setShowFTUX(false); @@ -75,7 +73,6 @@ function CanvasPageContent() { const handleStartTutorial = () => { setShowFTUX(false); setFTUXCompleted(true); - setShowQuickstart(false); // Close WelcomeModal if open useFTUXStore.getState().startTutorial(); }; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index b5c803f4..5a595121 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useMemo, useCallback, useEffect, type ReactNode } from "react"; +import Link from "next/link"; import { Button } from "antd"; import { useWorkflowStore } from "@/store/workflowStore"; import { useCanvasWorkflowStore } from "@/store/canvasWorkflowStore"; @@ -106,7 +107,6 @@ export function Header() { revertToSnapshot, shortcutsDialogOpen, setShortcutsDialogOpen, - setShowQuickstart, } = useWorkflowStore(useShallow((state) => ({ workflowName: state.workflowName, workflowId: state.workflowId, @@ -120,7 +120,6 @@ export function Header() { revertToSnapshot: state.revertToSnapshot, shortcutsDialogOpen: state.shortcutsDialogOpen, setShortcutsDialogOpen: state.setShortcutsDialogOpen, - setShowQuickstart: state.setShowQuickstart, }))); const [showProjectModal, setShowProjectModal] = useState(false); @@ -288,16 +287,12 @@ export function Header() { )}
- +
{isProjectConfigured ? ( diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 931ed313..53a8a2cd 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -88,8 +88,6 @@ async function readImageDimensions(src: string): Promise<{ width: number; height img.src = src; }); } -import { WelcomeModal } from "./quickstart"; -import { ProjectSetupModal } from "./ProjectSetupModal"; import { ChatPanel, type ChatRunAction } from "./ChatPanel"; import { GenerationComposer } from "./composer/GenerationComposer"; import { EditOperation } from "@/lib/chat/editOperations"; @@ -459,13 +457,12 @@ export const isDraggingNodeRef = { current: false }; export function WorkflowCanvas() { const { t } = useI18n(); const popiProviderMode = isPopiProviderMode(); - const { nodes, edges, groups, isModalOpen, showQuickstart, navigationTarget, canvasNavigationSettings, dimmedNodeIds, skippedNodeIds, hoveredNodeId, providerSettings, providerEnvStatus, isRunning } = + const { nodes, edges, groups, isModalOpen, navigationTarget, canvasNavigationSettings, dimmedNodeIds, skippedNodeIds, hoveredNodeId, providerSettings, providerEnvStatus, isRunning } = useWorkflowStore(useShallow((state) => ({ nodes: state.nodes, edges: state.edges, groups: state.groups, isModalOpen: state.isModalOpen, - showQuickstart: state.showQuickstart, navigationTarget: state.navigationTarget, canvasNavigationSettings: state.canvasNavigationSettings, dimmedNodeIds: state.dimmedNodeIds, @@ -486,14 +483,11 @@ export function WorkflowCanvas() { const setNodeGroupId = useWorkflowStore((state) => state.setNodeGroupId); const executeWorkflow = useWorkflowStore((state) => state.executeWorkflow); const executeSelectedNodes = useWorkflowStore((state) => state.executeSelectedNodes); - const setShowQuickstart = useWorkflowStore((state) => state.setShowQuickstart); const setNavigationTarget = useWorkflowStore((state) => state.setNavigationTarget); const captureSnapshot = useWorkflowStore((state) => state.captureSnapshot); const applyEditOperations = useWorkflowStore((state) => state.applyEditOperations); - const setWorkflowMetadata = useWorkflowStore((state) => state.setWorkflowMetadata); const setShortcutsDialogOpen = useWorkflowStore((state) => state.setShortcutsDialogOpen); const regenerateNode = useWorkflowStore((state) => state.regenerateNode); - const clearWorkflow = useWorkflowStore((state) => state.clearWorkflow); const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); const setHoveredNodeId = useWorkflowStore((state) => state.setHoveredNodeId); const updateCanvasNavigationSettings = useWorkflowStore((state) => state.updateCanvasNavigationSettings); @@ -507,7 +501,6 @@ export function WorkflowCanvas() { const [multiAngleDialog, setMultiAngleDialog] = useState(null); const [isSplitting, setIsSplitting] = useState(false); const [isBuildingWorkflow, setIsBuildingWorkflow] = useState(false); - const [showNewProjectSetup, setShowNewProjectSetup] = useState(false); const [isMacOSClient, setIsMacOSClient] = useState(false); useEffect(() => { @@ -2582,38 +2575,6 @@ export function WorkflowCanvas() {
)} - {/* Welcome Modal */} - {showQuickstart && ( - { - await loadWorkflow(workflow, directoryPath); - setShowQuickstart(false); - }} - onClose={() => setShowQuickstart(false)} - onNewProject={() => { - clearWorkflow(); - setShowQuickstart(false); - setShowNewProjectSetup(true); - }} - /> - )} - - {/* New Project Setup Modal */} - {showNewProjectSetup && ( - { - setWorkflowMetadata(id, name, directoryPath); - setShowNewProjectSetup(false); - }} - onClose={() => { - setShowNewProjectSetup(false); - setShowQuickstart(true); - }} - /> - )} - ({ - WorkflowBrowserView: ({ - onBack, - onWorkflowLoaded, - onClose, - }: { - onBack: () => void; - onWorkflowLoaded: (w: WorkflowFile, p: string) => void; - onClose: () => void; - }) => ( -
- - - -
- ), -})); - -// Mock templates -vi.mock("@/lib/quickstart/templates", () => { - const template = { - id: "product-shot", - name: "Product Shot", - description: "Place product in a new scene or environment", - icon: "M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4", - category: "product", - tags: ["Gemini"], - workflow: { - name: "Product Shot", - nodes: [{ id: "1", type: "imageInput", position: { x: 0, y: 0 }, data: {} }], - edges: [], - }, - }; - return { - getAllPresets: () => [template], - PRESET_TEMPLATES: [template], - getPresetTemplate: (id: string) => (id === "product-shot" ? { ...template, id: `workflow-${Date.now()}` } : null), - getTemplateContent: () => ({ prompts: {}, images: {} }), - }; -}); - -describe("WelcomeModal", () => { - const mockOnWorkflowGenerated = vi.fn(); - const mockOnClose = vi.fn(); - const mockOnNewProject = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - // Setup default fetch mock for community workflows - mockFetch.mockImplementation((url: string) => { - if (url === "/api/community-workflows") { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ success: true, workflows: [] }), - }); - } - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ success: true }), - }); - }); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("Basic Rendering", () => { - it("should render welcome modal with initial view by default", () => { - render( - - ); - - expect(screen.getByText("Popi.TV")).toBeInTheDocument(); - expect(screen.getByText("New project")).toBeInTheDocument(); - expect(screen.getByText("Templates")).toBeInTheDocument(); - expect(screen.getByText("Prompt a workflow")).toBeInTheDocument(); - }); - - it("should render modal overlay with backdrop", () => { - const { container } = render( - - ); - - const backdrop = container.querySelector(".bg-black\\/60"); - expect(backdrop).toBeInTheDocument(); - }); - }); - - describe("Initial View Navigation", () => { - it("should call onNewProject when 'New project' is clicked", () => { - render( - - ); - - fireEvent.click(screen.getByText("New project")); - - expect(mockOnNewProject).toHaveBeenCalled(); - }); - - it("should navigate to templates view when 'Templates' is clicked", async () => { - render( - - ); - - await act(async () => { - fireEvent.click(screen.getByText("Templates")); - }); - - await waitFor(() => { - expect(screen.getByText("Template Explorer")).toBeInTheDocument(); - expect(screen.getByText("Quick Start")).toBeInTheDocument(); - }); - }); - - it("should navigate to vibe view when 'Prompt a workflow' is clicked", () => { - render( - - ); - - fireEvent.click(screen.getByText("Prompt a workflow")); - - expect(screen.getByText("Prompt a Workflow")).toBeInTheDocument(); - expect(screen.getByText("Describe your workflow")).toBeInTheDocument(); - }); - }); - - describe("View Transitions", () => { - it("should navigate back to initial view from templates view", async () => { - render( - - ); - - // Navigate to templates - await act(async () => { - fireEvent.click(screen.getByText("Templates")); - }); - - await waitFor(() => { - expect(screen.getByText("Template Explorer")).toBeInTheDocument(); - }); - - // Click back - await act(async () => { - fireEvent.click(screen.getByText("Back")); - }); - - expect(screen.getByText("Popi.TV")).toBeInTheDocument(); - expect(screen.getByText("New project")).toBeInTheDocument(); - }); - - it("should navigate back to initial view from prompt view", () => { - render( - - ); - - // Navigate to prompt view - fireEvent.click(screen.getByText("Prompt a workflow")); - expect(screen.getByText("Prompt a Workflow")).toBeInTheDocument(); - - // Click back - fireEvent.click(screen.getByText("Back")); - - expect(screen.getByText("Popi.TV")).toBeInTheDocument(); - }); - }); - - describe("Load Workflow via Browser View", () => { - it("should show WorkflowBrowserView when 'Load workflow' is clicked", () => { - render( - - ); - - fireEvent.click(screen.getByText("Load workflow")); - - expect(screen.getByTestId("workflow-browser-view")).toBeInTheDocument(); - }); - - it("should navigate back to initial view from browse view", () => { - render( - - ); - - fireEvent.click(screen.getByText("Load workflow")); - expect(screen.getByTestId("workflow-browser-view")).toBeInTheDocument(); - - fireEvent.click(screen.getByText("Back")); - expect(screen.getByText("Popi.TV")).toBeInTheDocument(); - }); - - it("should call onWorkflowGenerated when a workflow is loaded from browser", () => { - render( - - ); - - fireEvent.click(screen.getByText("Load workflow")); - fireEvent.click(screen.getByTestId("load-workflow-btn")); - - expect(mockOnWorkflowGenerated).toHaveBeenCalledWith( - expect.objectContaining({ version: 1, nodes: [], edges: [] }), - "/test/dir" - ); - }); - }); - - describe("Workflow Selection from Child Views", () => { - it("should call onWorkflowGenerated when workflow is generated from templates view", async () => { - render( - - ); - - // Navigate to templates - await act(async () => { - fireEvent.click(screen.getByText("Templates")); - }); - - await waitFor(() => { - expect(screen.getByText("Template Explorer")).toBeInTheDocument(); - }); - - // Verify templates view is showing - the actual workflow selection is tested in QuickstartTemplatesView tests - expect(screen.getByText("Quick Start")).toBeInTheDocument(); - }); - - it("should show prompt view when navigating to vibe", () => { - render( - - ); - - // Navigate to vibe/prompt view - fireEvent.click(screen.getByText("Prompt a workflow")); - - expect(screen.getByText("Prompt a Workflow")).toBeInTheDocument(); - expect(screen.getByText("Generate Workflow")).toBeInTheDocument(); - }); - }); -}); diff --git a/src/components/__tests__/WorkflowCanvas.test.tsx b/src/components/__tests__/WorkflowCanvas.test.tsx index c2a5b8c8..6c3fb6c6 100644 --- a/src/components/__tests__/WorkflowCanvas.test.tsx +++ b/src/components/__tests__/WorkflowCanvas.test.tsx @@ -17,7 +17,6 @@ const mockExecuteWorkflow = vi.fn(); const mockCopySelectedNodes = vi.fn(); const mockPasteNodes = vi.fn(); const mockClearClipboard = vi.fn(); -const mockSetShowQuickstart = vi.fn(); const mockSetHoveredNodeId = vi.fn(); const mockRegenerateNode = vi.fn(); const mockExecuteSelectedNodes = vi.fn(); @@ -109,10 +108,6 @@ vi.mock("@/components/GroupsOverlay", () => ({ GroupControlsOverlay: () => null, })); -vi.mock("@/components/quickstart", () => ({ - WelcomeModal: () =>
, -})); - vi.mock("@/utils/gridSplitter", () => ({ detectAndSplitGrid: vi.fn(), })); @@ -164,8 +159,6 @@ const createDefaultState = (overrides = {}) => ({ executeSelectedNodes: mockExecuteSelectedNodes, isRunning: false, isModalOpen: false, - showQuickstart: false, - setShowQuickstart: mockSetShowQuickstart, copySelectedNodes: mockCopySelectedNodes, pasteNodes: mockPasteNodes, clearClipboard: mockClearClipboard, @@ -555,61 +548,6 @@ describe("WorkflowCanvas", () => { }); }); - describe("Welcome Modal", () => { - it("should show welcome modal when canvas is empty and showQuickstart is true", () => { - mockUseWorkflowStore.mockImplementation((selector) => { - return selector(createDefaultState({ - nodes: [], - showQuickstart: true, - })); - }); - - render( - - - - ); - - expect(screen.getByTestId("welcome-modal")).toBeInTheDocument(); - }); - - it("should not show welcome modal when canvas has nodes", () => { - // When a workflow with nodes is loaded, the store sets showQuickstart to false. - // The component renders the modal based solely on showQuickstart state. - mockUseWorkflowStore.mockImplementation((selector) => { - return selector(createDefaultState({ - nodes: [createMockNode("node-1", "prompt")], - showQuickstart: false, - })); - }); - - render( - - - - ); - - expect(screen.queryByTestId("welcome-modal")).not.toBeInTheDocument(); - }); - - it("should not show welcome modal when showQuickstart is false", () => { - mockUseWorkflowStore.mockImplementation((selector) => { - return selector(createDefaultState({ - nodes: [], - showQuickstart: false, - })); - }); - - render( - - - - ); - - expect(screen.queryByTestId("welcome-modal")).not.toBeInTheDocument(); - }); - }); - describe("Node Types Registration", () => { it("should register all required node types for the canvas", () => { // We verify the canvas renders with the node types object defined diff --git a/src/components/quickstart/WelcomeModal.tsx b/src/components/quickstart/WelcomeModal.tsx deleted file mode 100644 index 02f1e8a1..00000000 --- a/src/components/quickstart/WelcomeModal.tsx +++ /dev/null @@ -1,94 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { WorkflowFile } from "@/store/workflowStore"; -import { QuickstartView } from "@/types/quickstart"; -import { QuickstartInitialView } from "./QuickstartInitialView"; -import { TemplateExplorerView } from "./TemplateExplorerView"; -import { PromptWorkflowView } from "./PromptWorkflowView"; -import { WorkflowBrowserView } from "./WorkflowBrowserView"; - -interface WelcomeModalProps { - onWorkflowGenerated: (workflow: WorkflowFile, directoryPath?: string) => void; - onClose: () => void; - onNewProject: () => void; -} - -export function WelcomeModal({ - onWorkflowGenerated, - onClose, - onNewProject, -}: WelcomeModalProps) { - const [currentView, setCurrentView] = useState("initial"); - - const handleNewProject = useCallback(() => { - onNewProject(); - }, [onNewProject]); - - const handleSelectTemplates = useCallback(() => { - setCurrentView("templates"); - }, []); - - const handleSelectVibe = useCallback(() => { - setCurrentView("vibe"); - }, []); - - const handleSelectLoad = useCallback(() => { - setCurrentView("browse"); - }, []); - - const handleBack = useCallback(() => { - setCurrentView("initial"); - }, []); - - const handleWorkflowSelected = useCallback( - (workflow: WorkflowFile) => { - onWorkflowGenerated(workflow); - }, - [onWorkflowGenerated] - ); - - // Template explorer needs more width for two-column layout - const dialogWidth = currentView === "templates" ? "max-w-6xl" : "max-w-2xl"; - const dialogHeight = currentView === "templates" || currentView === "browse" ? "max-h-[85vh]" : "max-h-[80vh]"; - - return ( -
e.stopPropagation()} - onClick={onClose} - > -
e.stopPropagation()}> - {currentView === "initial" && ( - - )} - {currentView === "templates" && ( - - )} - {currentView === "vibe" && ( - - )} - {currentView === "browse" && ( - - onWorkflowGenerated(workflow, dirPath) - } - onClose={onClose} - /> - )} -
-
- ); -} diff --git a/src/components/quickstart/index.ts b/src/components/quickstart/index.ts index f5b29296..cba9b27f 100644 --- a/src/components/quickstart/index.ts +++ b/src/components/quickstart/index.ts @@ -1,4 +1,3 @@ -export { WelcomeModal } from "./WelcomeModal"; export { QuickstartBackButton } from "./QuickstartBackButton"; export { QuickstartInitialView } from "./QuickstartInitialView"; export { QuickstartTemplatesView } from "./QuickstartTemplatesView"; diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index dd60f9a6..11f40f11 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -108,7 +108,6 @@ const en = { "error.providerOverloaded": "{model}: provider is temporarily overloaded. Please retry in a moment or switch models.", "node.optionalTitle": "This input is optional - empty inputs will be skipped", "node.markOptionalTitle": "Mark as optional - empty inputs will skip this branch", - "header.openWelcome": "Open welcome screen", "header.untitled": "Untitled", "header.saving": "Saving...", "header.savedAt": "Saved {time}", @@ -1071,7 +1070,6 @@ const zhCN: Dictionary = { "error.providerOverloaded": "{model}:服务商暂时过载。请稍后重试,或切换到其他模型。", "node.optionalTitle": "此输入为可选,空输入会被跳过", "node.markOptionalTitle": "标记为可选,空输入会跳过这个分支", - "header.openWelcome": "打开欢迎页", "header.untitled": "未命名", "header.saving": "保存中...", "header.savedAt": "已保存 {time}", @@ -1879,7 +1877,6 @@ const zhTW: Dictionary = { "error.providerOverloaded": "{model}:服務商暫時過載。請稍後重試,或切換到其他模型。", "node.optionalTitle": "此輸入為可選,空輸入會被略過", "node.markOptionalTitle": "標記為可選,空輸入會略過這個分支", - "header.openWelcome": "開啟歡迎頁", "header.untitled": "未命名", "header.saving": "儲存中...", "header.savedAt": "已儲存 {time}", @@ -2488,7 +2485,6 @@ const ja: Dictionary = { "error.providerOverloaded": "{model}: プロバイダーが一時的に混雑しています。少し待って再試行するか、別のモデルに切り替えてください。", "node.optionalTitle": "この入力は任意です。空の入力はスキップされます", "node.markOptionalTitle": "任意に設定します。空の入力はこの分岐をスキップします", - "header.openWelcome": "ウェルカム画面を開く", "header.untitled": "無題", "header.saving": "保存中...", "header.savedAt": "{time} に保存済み", diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index b22a8813..76c29b08 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -293,11 +293,9 @@ interface WorkflowStore { // UI State openModalCount: number; isModalOpen: boolean; - showQuickstart: boolean; hoveredNodeId: string | null; incrementModalCount: () => void; decrementModalCount: () => void; - setShowQuickstart: (show: boolean) => void; setHoveredNodeId: (id: string | null) => void; // Execution @@ -754,7 +752,6 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ groups: {}, openModalCount: 0, isModalOpen: false, - showQuickstart: false, hoveredNodeId: null, isRunning: false, currentNodeIds: [], // Changed from currentNodeId for parallel execution @@ -883,10 +880,6 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ }); }, - setShowQuickstart: (show: boolean) => { - set({ showQuickstart: show }); - }, - setHoveredNodeId: (id: string | null) => { if (hoverRafId !== null) cancelAnimationFrame(hoverRafId); hoverRafId = requestAnimationFrame(() => { @@ -2634,8 +2627,6 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ useExternalImageStorage: savedConfig?.useExternalImageStorage ?? true, // Reset viewed comments when loading new workflow viewedCommentNodeIds: new Set(), - // Dismiss welcome modal after loading a workflow - showQuickstart: false, hasUnsavedChanges: promotedModels.changed || restoredExecutionState.changed, });