From 338654bfa61f8f855cb7b416a8ba23752ef04368 Mon Sep 17 00:00:00 2001 From: Devin-AXIS Date: Sat, 15 Aug 2026 18:34:36 +0800 Subject: [PATCH 1/5] feat: add reusable Design Studio Harness plugin --- .github/workflows/ci-dsh-design-studio.yml | 96 + apps/app/package.json | 1 + .../domains/session/chat/session-page.tsx | 2 +- .../design/design-ai-selection-store.ts | 2 +- .../domains/session/design/design-panel.tsx | 64 +- .../domains/session/design/public.ts | 1 + .../domains/session/panel/side-panel.tsx | 2 +- .../domains/session/sync/session-sync.ts | 2 +- .../domains/session/sync/usechat-adapter.ts | 2 +- .../domains/session/video/video-panel.tsx | 2 +- .../app/src/react-app/shell/session-prompt.ts | 2 +- .../app/src/react-app/shell/session-route.tsx | 2 +- .../tests/design-ai-message-stream.test.ts | 2 +- apps/app/tests/design-ai-selection.test.ts | 2 +- .../app/tests/design-ai-session-route.test.ts | 2 +- apps/app/tests/design-deck-navigation.test.ts | 2 +- apps/app/tests/design-studio-bridge.test.ts | 53 + packages/design-studio/package.json | 19 + .../design-studio/src/ai-selection.ts | 0 packages/design-studio/src/bridge.ts | 53 + packages/design-studio/src/host.ts | 74 + packages/design-studio/src/index.ts | 3 + packages/design-studio/tsconfig.json | 11 + plugins/README.md | 13 + .../deepseek-harness/design-studio/.gitignore | 3 + .../deepseek-harness/design-studio/.npmignore | 10 + .../deepseek-harness/design-studio/README.md | 32 + .../design-studio/cordis.patch.yml | 3 + .../design-studio/package.json | 89 + .../design-studio/pnpm-lock.yaml | 4197 +++++++++++++++++ .../design-studio/pnpm-workspace.yaml | 10 + .../design-studio/src/client.tsx | 146 + .../design-studio/src/index.ts | 448 ++ .../design-studio/studio/index.html | 14 + .../studio/src/deepseek-client.ts | 78 + .../design-studio/studio/src/env.d.ts | 1 + .../design-studio/studio/src/main.tsx | 63 + .../design-studio/studio/tsconfig.json | 29 + .../design-studio/studio/vite.config.ts | 30 + .../design-studio/tsconfig.json | 12 + .../design-studio/tsdown.config.ts | 46 + pnpm-lock.yaml | 10 + 42 files changed, 5606 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/ci-dsh-design-studio.yml create mode 100644 apps/app/src/react-app/domains/session/design/public.ts create mode 100644 apps/app/tests/design-studio-bridge.test.ts create mode 100644 packages/design-studio/package.json rename apps/app/src/react-app/domains/session/design/design-ai-selection.ts => packages/design-studio/src/ai-selection.ts (100%) create mode 100644 packages/design-studio/src/bridge.ts create mode 100644 packages/design-studio/src/host.ts create mode 100644 packages/design-studio/src/index.ts create mode 100644 packages/design-studio/tsconfig.json create mode 100644 plugins/README.md create mode 100644 plugins/deepseek-harness/design-studio/.gitignore create mode 100644 plugins/deepseek-harness/design-studio/.npmignore create mode 100644 plugins/deepseek-harness/design-studio/README.md create mode 100644 plugins/deepseek-harness/design-studio/cordis.patch.yml create mode 100644 plugins/deepseek-harness/design-studio/package.json create mode 100644 plugins/deepseek-harness/design-studio/pnpm-lock.yaml create mode 100644 plugins/deepseek-harness/design-studio/pnpm-workspace.yaml create mode 100644 plugins/deepseek-harness/design-studio/src/client.tsx create mode 100644 plugins/deepseek-harness/design-studio/src/index.ts create mode 100644 plugins/deepseek-harness/design-studio/studio/index.html create mode 100644 plugins/deepseek-harness/design-studio/studio/src/deepseek-client.ts create mode 100644 plugins/deepseek-harness/design-studio/studio/src/env.d.ts create mode 100644 plugins/deepseek-harness/design-studio/studio/src/main.tsx create mode 100644 plugins/deepseek-harness/design-studio/studio/tsconfig.json create mode 100644 plugins/deepseek-harness/design-studio/studio/vite.config.ts create mode 100644 plugins/deepseek-harness/design-studio/tsconfig.json create mode 100644 plugins/deepseek-harness/design-studio/tsdown.config.ts diff --git a/.github/workflows/ci-dsh-design-studio.yml b/.github/workflows/ci-dsh-design-studio.yml new file mode 100644 index 000000000..cc841696a --- /dev/null +++ b/.github/workflows/ci-dsh-design-studio.yml @@ -0,0 +1,96 @@ +name: DeepSeek Harness Design Studio plugin + +on: + push: + branches: [main, dev] + paths: + - "plugins/deepseek-harness/design-studio/**" + - "packages/design-studio/**" + - "apps/app/src/react-app/domains/session/design/**" + - ".github/workflows/ci-dsh-design-studio.yml" + tags: + - "ipollowork-dsh-design-studio-v*" + pull_request: + branches: [main, dev] + paths: + - "plugins/deepseek-harness/design-studio/**" + - "packages/design-studio/**" + - "apps/app/src/react-app/domains/session/design/**" + - ".github/workflows/ci-dsh-design-studio.yml" + +permissions: + contents: read + +defaults: + run: + working-directory: plugins/deepseek-harness/design-studio + +jobs: + check: + name: Build and inspect package + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.4.0 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + cache: pnpm + + - name: Install iPolloWork workspace + run: pnpm --dir ../../.. install --frozen-lockfile + + - name: Install plugin workspace + run: pnpm install --frozen-lockfile + + - name: Typecheck and build + run: pnpm run check && pnpm run build + + - name: Dry-run publish + run: npm publish --dry-run --access public + + publish: + name: Publish to npm + needs: check + if: startsWith(github.ref, 'refs/tags/ipollowork-dsh-design-studio-v') + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.4.0 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + cache: pnpm + + - name: Install iPolloWork workspace + run: pnpm --dir ../../.. install --frozen-lockfile + + - name: Install plugin workspace + run: pnpm install --frozen-lockfile + + - name: Publish + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/apps/app/package.json b/apps/app/package.json index 5bfe2624f..d809c81c8 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -46,6 +46,7 @@ "@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/ibm-plex-sans": "^5.2.8", "@heroicons/react": "^2.2.0", + "@ipollowork/design-studio": "workspace:*", "@ipollowork/types": "workspace:*", "@ipollowork/ui": "workspace:*", "@lexical/react": "^0.35.0", diff --git a/apps/app/src/react-app/domains/session/chat/session-page.tsx b/apps/app/src/react-app/domains/session/chat/session-page.tsx index 2f90ee329..394479d4f 100644 --- a/apps/app/src/react-app/domains/session/chat/session-page.tsx +++ b/apps/app/src/react-app/domains/session/chat/session-page.tsx @@ -89,7 +89,7 @@ import { isCollectibleArtifactTarget, isLocalhostBrowserTarget, isOpenableFileTa import type { OpenTargetOptions } from "@/lib/target-provider"; import { VoicePanel } from "../voice/voice-panel"; import { DesignPanel } from "../design/design-panel"; -import { designAiSelectionToken, type DesignAiSelectionContext } from "../design/design-ai-selection"; +import { designAiSelectionToken, type DesignAiSelectionContext } from "@ipollowork/design-studio"; import { useDesignAiSelectionStore } from "../design/design-ai-selection-store"; import { waitForTemplateEntrySurface } from "../templates/template-entry-route"; import { loadTemplateSession } from "../templates/template-session-probe"; diff --git a/apps/app/src/react-app/domains/session/design/design-ai-selection-store.ts b/apps/app/src/react-app/domains/session/design/design-ai-selection-store.ts index de380218c..5801fb1b5 100644 --- a/apps/app/src/react-app/domains/session/design/design-ai-selection-store.ts +++ b/apps/app/src/react-app/domains/session/design/design-ai-selection-store.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; -import type { DesignAiSelectionContext, DesignAiUndoCheckpoint } from "./design-ai-selection"; +import type { DesignAiSelectionContext, DesignAiUndoCheckpoint } from "@ipollowork/design-studio"; type DesignAiSelectionStatus = "pending" | "running" | "completing" | "completed" | "failed"; diff --git a/apps/app/src/react-app/domains/session/design/design-panel.tsx b/apps/app/src/react-app/domains/session/design/design-panel.tsx index 6aa1e7d72..0b0dbf433 100644 --- a/apps/app/src/react-app/domains/session/design/design-panel.tsx +++ b/apps/app/src/react-app/domains/session/design/design-panel.tsx @@ -3,7 +3,12 @@ import * as React from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Check, ChevronLeft, ChevronRight, Code2, Focus, Loader2, Minus, Monitor, MousePointer2, Palette, Plus, Save, Share2, SlidersHorizontal, Smartphone, Undo2 } from "lucide-react"; -import type { iPolloWorkServerClient } from "@/app/lib/ipollowork-server"; +import { + IPOLLOWORK_DESIGN_STUDIO_FEATURES, + type DesignAiSelectionContext, + type DesignStudioClient, + type DesignStudioFeatures, +} from "@ipollowork/design-studio"; import { pickLocalImageFile, readLocalImageAsDataUrl } from "@/app/lib/desktop"; import { downloadBlobAsFile } from "@/app/lib/download"; import { Button } from "@/components/ui/button"; @@ -22,7 +27,6 @@ import { toast } from "@/components/ui/sonner"; import { cn } from "@/lib/utils"; import { isPptxCompatibleTemplate } from "@ipollowork/types/templates"; import { ConfirmModal } from "@/react-app/design-system/modals/confirm-modal"; -import type { DesignAiSelectionContext } from "./design-ai-selection"; import { useDesignAiSelectionStore } from "./design-ai-selection-store"; import { buildDesignPreviewDocument, @@ -118,11 +122,12 @@ import { type DesignPanelProps = { sessionId: string; - client: iPolloWorkServerClient | null; + client: DesignStudioClient | null; workspaceId: string | null; isRemoteWorkspace?: boolean; initialPath?: string; expanded?: boolean; + features?: DesignStudioFeatures; onAskAi: (context: DesignAiSelectionContext) => void; onSaveAsTemplate?: () => void; }; @@ -221,7 +226,7 @@ function arrayBufferToPreviewDataUrl(data: ArrayBuffer, contentType: string | nu async function hydrateDesignPreviewAssets( source: string, - input: { client: iPolloWorkServerClient | null; workspaceId: string | null; activePagePath: string }, + input: { client: DesignStudioClient | null; workspaceId: string | null; activePagePath: string }, ): Promise { if (!input.client || !input.workspaceId || !input.activePagePath || typeof DOMParser === "undefined") { return { source, objectUrls: [] }; @@ -529,6 +534,7 @@ export function DesignPanel({ isRemoteWorkspace = false, initialPath, expanded = false, + features = IPOLLOWORK_DESIGN_STUDIO_FEATURES, onAskAi, onSaveAsTemplate, }: DesignPanelProps) { @@ -538,6 +544,7 @@ export function DesignPanel({ const previewViewportRef = React.useRef(null); const presentationPanRef = React.useRef(null); const imageInputRef = React.useRef(null); + const imageInputIntentRef = React.useRef<"replacement" | "element-background" | "design-background">("replacement"); const designTokenDraftRef = React.useRef(""); const designTokenSaveTimerRef = React.useRef(null); const templateQuery = useQuery({ @@ -1661,8 +1668,8 @@ export function DesignPanel({ const fontSize = Math.max(1, Math.round(Number.parseFloat(selection?.styles.fontSize || "16") || 16)); const setFontSize = (next: number, remember = false) => applyField("fontSize", `${Math.max(1, Math.min(240, next))}px`, remember); - const replaceImageFromFile = async (file: File | undefined) => { - if (!file || !selection || selection.tag !== "img") return; + const applyBrowserImage = async (file: File | undefined) => { + if (!file) return; if (!file.type.startsWith("image/")) { toast.error("Choose an image file to replace this image."); return; @@ -1673,9 +1680,25 @@ export function DesignPanel({ } try { const result = await imageFileToPortableDataUrl(file); - rememberHistory(); - applyField("src", result, false); - toast.success("Image replaced in the design."); + if (imageInputIntentRef.current === "replacement") { + if (!selection || selection.tag !== "img") return; + rememberHistory(); + applyField("src", result, false); + toast.success("Image replaced in the design."); + } else if (imageInputIntentRef.current === "element-background") { + if (!selection || selection.tag === "img") return; + applyStyleFields({ backgroundColor: "transparent", backgroundImage: `url(\"${result}\")` }); + toast.success("Image added as the fill."); + } else { + handleDesignTokenChange("--ipw-bg-image", `url(\"${result}\")`); + handleDesignTokenChange("--ipw-bg-gradient", "none"); + handleDesignTokenChange("--ipw-bg-overlay", "linear-gradient(rgba(28,27,26,.45), rgba(28,27,26,.45))"); + handleDesignTokenChange("--ipw-bg-overlay-opacity", "0.45"); + handleDesignTokenChange("--ipw-bg-mode", "image"); + handleDesignTokenChange("--ipw-bg-size", "cover"); + handleDesignTokenChange("--ipw-bg-position", "50% 50%"); + toast.success("Background image applied."); + } } catch { toast.error("Could not prepare that image. Try PNG, JPG, or WebP."); } @@ -1696,6 +1719,7 @@ export function DesignPanel({ return; } if (typeof window !== "undefined" && window.__IPOLLOWORK_ELECTRON__?.invokeDesktop) return; + imageInputIntentRef.current = "replacement"; imageInputRef.current?.click(); }; @@ -1787,7 +1811,12 @@ export function DesignPanel({ const chooseBackgroundImage = async () => { if (!selection || selection.tag === "img") return; const pickedPath = await pickLocalImageFile("选择填充图片"); - if (!pickedPath) return; + if (!pickedPath) { + if (typeof window !== "undefined" && window.__IPOLLOWORK_ELECTRON__?.invokeDesktop) return; + imageInputIntentRef.current = "element-background"; + imageInputRef.current?.click(); + return; + } const dataUrl = await readLocalImageAsDataUrl(pickedPath); if (!dataUrl) { toast.error("Could not prepare that image. Try PNG, JPG, or WebP."); @@ -1799,7 +1828,12 @@ export function DesignPanel({ const chooseDesignSystemBackgroundImage = async () => { const pickedPath = await pickLocalImageFile("选择全局背景图片"); - if (!pickedPath) return; + if (!pickedPath) { + if (typeof window !== "undefined" && window.__IPOLLOWORK_ELECTRON__?.invokeDesktop) return; + imageInputIntentRef.current = "design-background"; + imageInputRef.current?.click(); + return; + } const dataUrl = await readLocalImageAsDataUrl(pickedPath); if (!dataUrl) { toast.error("Could not prepare that image. Try PNG, JPG, or WebP."); @@ -1942,9 +1976,9 @@ export function DesignPanel({ type="file" accept={LOCAL_IMAGE_ACCEPT} className="sr-only" - aria-label="Choose replacement image" + aria-label="Choose design image" onChange={(event) => { - replaceImageFromFile(event.currentTarget.files?.[0]); + void applyBrowserImage(event.currentTarget.files?.[0]); event.currentTarget.value = ""; }} /> @@ -2086,7 +2120,7 @@ export function DesignPanel({ > {saveMutation.isPending ? : dirty ? : } - {!compactToolbar ? ( + {!compactToolbar && features.publish ? ( + +