From 80dcda0bf1dc52bfb957d5dcd249fb7fdfa5742d Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sat, 21 Feb 2026 15:22:23 +0800 Subject: [PATCH] feat(electron): enhance Electron app integration and CI/CD workflows - Add new commands for Electron development, compilation, and building processes in CLAUDE.md. - Update README.md to reflect the availability of the application as both a web and desktop app. - Introduce a FixedChecklist component in the AI chat panel for better user interaction with generated tasks. - Implement CI/CD workflows for automated testing and Electron builds in GitHub Actions. - Refactor design generator prompts to support element-by-element streaming for improved performance. --- .github/workflows/build-electron.yml | 88 +++++++++ .github/workflows/ci.yml | 55 ++++++ CLAUDE.md | 25 ++- README.md | 81 +++++--- src/components/panels/ai-chat-panel.tsx | 95 +++++++++- src/components/panels/chat-message.tsx | 155 ++++++--------- src/services/ai/ai-prompts.ts | 95 +++++----- src/services/ai/design-generator.ts | 242 ++++++++++++++++++------ src/stores/document-store.ts | 4 +- 9 files changed, 611 insertions(+), 229 deletions(-) create mode 100644 .github/workflows/build-electron.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/build-electron.yml b/.github/workflows/build-electron.yml new file mode 100644 index 000000000..929f01e2d --- /dev/null +++ b/.github/workflows/build-electron.yml @@ -0,0 +1,88 @@ +name: Build Electron + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build: + name: Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + platform: mac + - os: windows-latest + platform: win + - os: ubuntu-latest + platform: linux + + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build web (electron target) + run: bun --bun run build + env: + BUILD_TARGET: electron + + - name: Compile electron + run: bun run electron:compile + + - name: Build Electron app + run: npx electron-builder --config electron-builder.yml --${{ matrix.platform }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: electron-${{ matrix.platform }} + path: | + dist-electron/*.dmg + dist-electron/*.zip + dist-electron/*.exe + dist-electron/*.AppImage + dist-electron/*.deb + retention-days: 30 + + release: + name: Create Release + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + merge-multiple: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + draft: true + generate_release_notes: true + files: artifacts/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..e12bbf5fc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: [main, v0.0.1] + pull_request: + branches: [main, v0.0.1] + +jobs: + lint-and-test: + name: Lint & Test + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Type check + run: npx tsc --noEmit + + - name: Run tests + run: bun --bun run test + + build-web: + name: Build Web + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: lint-and-test + + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build + run: bun --bun run build + + - name: Upload web build artifact + uses: actions/upload-artifact@v4 + with: + name: web-build + path: .output/ + retention-days: 7 diff --git a/CLAUDE.md b/CLAUDE.md index e627099a7..67147827a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,16 +11,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Run a single test:** `bun --bun vitest run path/to/test.ts` - **Type check:** `npx tsc --noEmit` - **Install dependencies:** `bun install` +- **Electron dev:** `bun run electron:dev` (starts Vite + Electron together) +- **Electron compile:** `bun run electron:compile` (esbuild electron/ to electron-dist/) +- **Electron build:** `bun run electron:build` (full web build + compile + electron-builder package) ## Architecture -OpenPencil is an open-source vector design tool (alternative to Pencil.dev) with a Design-as-Code philosophy. Built as a **TanStack Start** full-stack React application with Bun runtime. Server API powered by **Nitro**. +OpenPencil is an open-source vector design tool (alternative to Pencil.dev) with a Design-as-Code philosophy. Built as a **TanStack Start** full-stack React application with Bun runtime. Server API powered by **Nitro**. Also ships as an **Electron** desktop app for macOS, Windows, and Linux. -**Key technologies:** React 19, Fabric.js v7 (canvas engine), Zustand v5 (state management), TanStack Router (file-based routing), Tailwind CSS v4, shadcn/ui (UI primitives), Vite 7, Nitro (server), TypeScript (strict mode). +**Key technologies:** React 19, Fabric.js v7 (canvas engine), Zustand v5 (state management), TanStack Router (file-based routing), Tailwind CSS v4, shadcn/ui (UI primitives), Vite 7, Nitro (server), Electron 35 (desktop), TypeScript (strict mode). ### Data Flow -``` +```text React Components (Toolbar, LayerPanel, PropertyPanel) │ Zustand hooks ▼ @@ -43,7 +46,7 @@ React Components (Toolbar, LayerPanel, PropertyPanel) ### Design Variables Architecture -``` +```text PenDocument (source of truth) ├── variables: Record ($color-1, $spacing-md, ...) ├── themes: Record ({Theme-1: ["Default","Dark"]}) @@ -155,6 +158,20 @@ File-based routing via TanStack Router. Routes in `src/routes/`, auto-generated Tailwind CSS v4 imported via `src/styles.css`. UI primitives from shadcn/ui (`src/components/ui/`). Icons from `lucide-react`. shadcn/ui config in `components.json`. +### Electron Desktop App + +- **`electron/main.ts`** — Main process: window creation, Nitro server fork, IPC for native file dialogs, macOS traffic-light padding +- **`electron/preload.ts`** — Context bridge for renderer ↔ main IPC +- **`electron-builder.yml`** — Packaging config: macOS (dmg/zip), Windows (nsis/portable), Linux (AppImage/deb) +- **`scripts/electron-dev.ts`** — Dev workflow: starts Vite → waits for port 3000 → compiles electron/ with esbuild → launches Electron +- Build flow: `BUILD_TARGET=electron bun run build` → `bun run electron:compile` → `npx electron-builder` +- In production, Nitro server is forked as a child process on a random port; Electron loads `http://127.0.0.1:{port}/editor` + +### CI / CD + +- **`.github/workflows/ci.yml`** — Push/PR: type check (`tsc --noEmit`), tests (`vitest`), web build +- **`.github/workflows/build-electron.yml`** — Tag push (`v*`) or manual: builds Electron for macOS, Windows, Linux in parallel, creates draft GitHub Release with all artifacts + ## Code Style - 单个文件不要超过 800 行。超出时应拆分为更小的模块。 diff --git a/README.md b/README.md index 9ec8b9251..7c9b1bdc1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Open-source vector design tool with a Design-as-Code philosophy. An alternative to [Pencil.dev](https://pencil.dev). +Available as a **web app** and **desktop app** (macOS / Windows / Linux via Electron). + ## Features ### Canvas @@ -132,12 +134,16 @@ Open-source vector design tool with a Design-as-Code philosophy. An alternative - **Styling:** [Tailwind CSS](https://tailwindcss.com/) v4 - **Icons:** [Lucide React](https://lucide.dev/) - **Server:** [Nitro](https://nitro.build/) (API routes) +- **Desktop:** [Electron](https://www.electronjs.org/) 35 + [electron-builder](https://www.electron.build/) - **AI:** [Anthropic SDK](https://docs.anthropic.com/) + [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk) - **Runtime:** [Bun](https://bun.sh/) - **Build:** [Vite](https://vite.dev/) 7 +- **CI/CD:** GitHub Actions ## Getting Started +### Web (Development) + ```bash bun install bun --bun run dev @@ -145,6 +151,16 @@ bun --bun run dev Open http://localhost:3000 and click "New Design" to enter the editor. +### Electron (Desktop) + +```bash +# Development: starts Vite dev server + Electron +bun run electron:dev + +# Production build (current platform) +bun run electron:build +``` + ### AI Configuration The AI assistant works in two modes: @@ -156,40 +172,50 @@ The AI assistant works in two modes: | Command | Description | |---|---| -| `bun --bun run dev` | Start dev server on port 3000 | -| `bun --bun run build` | Production build | +| `bun --bun run dev` | Start web dev server on port 3000 | +| `bun --bun run build` | Production web build | | `bun --bun run preview` | Preview production build | | `bun --bun run test` | Run tests (Vitest) | | `npx tsc --noEmit` | Type check | +| `bun run electron:dev` | Start Vite + Electron for desktop dev | +| `bun run electron:compile` | Compile electron/ with esbuild | +| `bun run electron:build` | Full Electron package (web build + compile + electron-builder) | + +## CI / CD + +### CI (`ci.yml`) + +Runs on every push and PR to `main` / `v0.0.1`: + +1. **Lint & Test** — type check (`tsc --noEmit`) + unit tests (`vitest`) +2. **Build Web** — production web build, uploads `.output/` as artifact + +### Build Electron (`build-electron.yml`) + +Triggered by version tags (`v*`) or manual dispatch: + +1. **Build** — parallel matrix across macOS, Windows, Linux + - macOS: `.dmg` + `.zip` + - Windows: `.exe` (NSIS installer + portable) + - Linux: `.AppImage` + `.deb` +2. **Release** — creates a draft GitHub Release with all platform artifacts + +To create a release: + +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` ## Project Structure -``` +```text src/ - canvas/ # Fabric.js canvas engine (16 files) - fabric-canvas.tsx Canvas component initialization - canvas-object-factory Creates Fabric objects from PenNodes - canvas-object-sync Syncs object properties Fabric ↔ store - canvas-sync-lock Prevents circular sync loops - canvas-controls Custom rotation controls and cursors - canvas-constants Default colors, zoom limits - use-canvas-events Drawing events, tool management - use-canvas-sync Bidirectional PenDocument ↔ Fabric sync + variable resolution - use-canvas-viewport Zoom, pan, tool cursor switching - use-canvas-selection Selection sync Fabric ↔ store - use-canvas-guides Smart alignment guides - guide-utils Guide calculation and rendering - pen-tool Bezier pen tool with anchors/handles - parent-child-transform Parent transform propagation to children - use-dimension-label Size/position labels during manipulation - use-frame-labels Frame name/boundary rendering + canvas/ # Fabric.js canvas engine variables/ # Design variables/tokens system - resolve-variables Core $variable resolution for canvas rendering - replace-refs Replace/resolve $refs on rename/delete components/ editor/ # Editor layout, toolbar, tool buttons, status bar - panels/ # Layer panel, property panel (17 files), AI chat, code panel, - # variables panel, variable row + panels/ # Layer panel, property panel, AI chat, code panel, variables panel shared/ # ColorPicker, NumberInput, VariablePicker, ExportDialog, etc. icons/ # Provider logos (Claude, OpenAI) ui/ # shadcn/ui primitives (Button, Select, Slider, Switch, etc.) @@ -202,8 +228,15 @@ src/ types/ # PenDocument/PenNode types, style types, variables, agent settings utils/ # File operations, export, node clone, SVG parser, syntax highlight routes/ # TanStack Router pages (/, /editor) +electron/ + main.ts # Electron main process (window, Nitro server, IPC) + preload.ts # Context bridge for renderer ↔ main IPC server/ api/ai/ # Nitro API: streaming chat, generation, agent connection, models +.github/ + workflows/ + ci.yml # CI: type check, test, web build + build-electron.yml # Electron build for macOS/Windows/Linux + GitHub Release ``` ## Roadmap diff --git a/src/components/panels/ai-chat-panel.tsx b/src/components/panels/ai-chat-panel.tsx index b711d28c4..341b47301 100644 --- a/src/components/panels/ai-chat-panel.tsx +++ b/src/components/panels/ai-chat-panel.tsx @@ -1,5 +1,5 @@ -import { useState, useRef, useEffect, useCallback } from 'react' -import { Send, Plus, ChevronDown, ChevronUp, Check, MessageSquare, Loader2 } from 'lucide-react' +import { useState, useRef, useEffect, useCallback, useMemo } from 'react' +import { Send, Plus, ChevronDown, ChevronUp, Check, MessageSquare, Loader2, Pencil } from 'lucide-react' import { nanoid } from 'nanoid' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' @@ -24,7 +24,11 @@ import type { AIProviderType } from '@/types/agent-settings' import ClaudeLogo from '@/components/icons/claude-logo' import OpenAILogo from '@/components/icons/openai-logo' import OpenCodeLogo from '@/components/icons/opencode-logo' -import ChatMessage from './chat-message' +import ChatMessage, { + parseStepBlocks, + countDesignJsonBlocks, + buildPipelineProgress, +} from './chat-message' const PROVIDER_ICON: Record = { anthropic: ClaudeLogo, @@ -274,6 +278,88 @@ function useChatHandlers() { return { input, setInput, handleSend, isStreaming } } +/** Fixed collapsible checklist pinned between messages and input */ +function FixedChecklist({ messages, isStreaming }: { messages: ChatMessageType[]; isStreaming: boolean }) { + const [collapsed, setCollapsed] = useState(false) + + // Find the last assistant message to extract checklist data + const lastAssistant = useMemo(() => { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === 'assistant') return messages[i] + } + return null + }, [messages]) + + const items = useMemo(() => { + if (!lastAssistant) return [] + const content = lastAssistant.content + const steps = parseStepBlocks(content, isStreaming) + const planSteps = steps.filter((s) => s.title !== 'Thinking') + if (planSteps.length === 0) return [] + const jsonCount = countDesignJsonBlocks(content) + const isApplied = content.includes('\u2705') || content.includes('') + const hasError = /\*\*Error:\*\*/i.test(content) + return buildPipelineProgress(planSteps, jsonCount, isStreaming, isApplied, hasError) + }, [lastAssistant, isStreaming]) + + if (items.length === 0) return null + + const completed = items.filter((item) => item.done).length + + return ( +
+ + {!collapsed && ( +
+ {items.map((item, index) => ( +
+ + {item.done ? ( + + ) : ( + + )} + + {item.label} +
+ ))} +
+ )} +
+ ) +} + /** * Minimized AI bar — a compact clickable pill. * Parent is responsible for placing it in the layout. @@ -678,6 +764,9 @@ export default function AIChatPanel() {
+ {/* --- Fixed Checklist --- */} + + {/* --- Input area --- */}