diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 261bc42..f33ec2f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -129,7 +129,15 @@ "Bash(git -C /Users/USER/Desktop/originmain status --short)", "Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/api/workspace/\\\\[id\\\\]/tokens)", "Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/api/workspace/\\\\[id\\\\]/projects/\\\\[pid\\\\])", - "Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/api/workspace/\\\\[id\\\\]/invite)" + "Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/api/workspace/\\\\[id\\\\]/invite)", + "Bash(grep anthropic *)", + "Bash(npx --yes npm-check-updates --packageFile /Users/USER/Desktop/originmain/packages/ai-layer/package.json --filter \"@anthropic-ai/sdk\")", + "Bash(pnpm --filter @originmain/app tsc --noEmit)", + "Bash(pnpm --filter @originmain/ai-layer add @anthropic-ai/sdk@latest)", + "Bash(pnpm --filter @originmain/origin-graph test)", + "Bash(grep -E \"\\\\.d\\\\.ts$\")", + "Bash(pnpm -w ls @anthropic-ai/sdk)", + "Bash(grep -v \"\\\\.map$\")" ] } } diff --git a/HANDOFF.md b/HANDOFF.md index f1ba643..5a3578d 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -114,7 +114,7 @@ - **`src/agent-qa.ts`** — `answerAgentQuestion()`: temp 0.7 ### SDK note -`@anthropic-ai/sdk` v0.56.0 is installed. Adaptive thinking (`thinking: {type: 'adaptive'}`) requires ≥0.58 — a TODO comment marks all 5 feature files for upgrade. All features remain fully functional at temperature level. +`@anthropic-ai/sdk` upgraded to **v0.91.1** in Session 4. All 5 feature files now use `thinking: {type: 'adaptive'}` via the gateway. `temperature` parameter removed from `GatewayRequest` interface and all feature call sites (Opus 4.7 with adaptive thinking rejects temperature with HTTP 400). --- @@ -162,9 +162,9 @@ Each connector validates untrusted webhook JSON at the boundary with Zod (`parse | `packages/diff-engine` | Complete | 49 tests, ~88% coverage | | `packages/ui` | Active | Theme + FluentProvider | | `packages/renderer` | Complete | Layer 2: postMessage protocol, fiber hook, MF config | -| `packages/origin-graph` | Complete | Layer 4: migrations, RLS, Zod types, query helpers | +| `packages/origin-graph` | Complete | Layer 4: migrations, RLS, Zod types, query helpers; 45 tests, 100% types.ts coverage | | `packages/design-language` | Complete | Layer 5: DLF schema, validator, token pipeline | -| `packages/ai-layer` | Complete | Layer 6: gateway, 5 features, prompt library | +| `packages/ai-layer` | Complete | Layer 6: gateway, 5 features, prompt library; SDK v0.91.1, adaptive thinking | | `packages/agent-bridge` | Complete | Layer 7: MCP tools, auth, rate limiter, adapters | | `packages/integrations` | Complete | Layer 8: OriginIngester + 4 connectors | | `packages/multiplayer` | Foundation | Layer 9: room schema, MultiplayerAdapter, cursor palette | @@ -239,12 +239,11 @@ Actual `createClient` / `createRoomContext` calls go in `packages/app/src/lib/li ## Known Pending Items -1. **`@anthropic-ai/sdk` upgrade to ≥0.58** — unlocks `thinking: {type: 'adaptive'}` in all 5 AI feature files (marked with TODO comments) -2. **Redis-backed rate limiter** — `packages/agent-bridge/src/rate-limiter.ts` is in-process only; needs Redis for multi-instance MCP server deployment -3. **Layer 0** — Infrastructure (Vercel, Supabase, Render, GitHub Actions) deferred per plan -4. **Layer 9 Phase 3** — Wire `MultiplayerAdapter` into app components; add `createClient`/`createRoomContext` in `packages/app/src/lib/liveblocks.ts`; install `@liveblocks/client` + `@liveblocks/react` -5. **Layer 10 Phase 4** — Plugin sandbox runtime (iframe + postMessage bridge); SCIM webhook endpoint; SSO provider registration UI; audit log Supabase table + extension -6. **`packages/e2e`** — Playwright E2E tests not yet created +1. **Redis-backed rate limiter** — `packages/agent-bridge/src/rate-limiter.ts` is in-process only; needs Redis for multi-instance MCP server deployment +2. **Layer 0** — Infrastructure (Vercel, Supabase, Render, GitHub Actions) deferred per plan +3. **Layer 9 Phase 3** — Wire `MultiplayerAdapter` into app components; add `createClient`/`createRoomContext` in `packages/app/src/lib/liveblocks.ts`; install `@liveblocks/client` + `@liveblocks/react` +4. **Layer 10 Phase 4** — Plugin sandbox runtime (iframe + postMessage bridge); SCIM webhook endpoint; SSO provider registration UI; audit log Supabase table + extension +5. **`packages/e2e`** — Playwright E2E tests not yet created --- @@ -270,62 +269,44 @@ The following is a second-pass audit of all real product gaps, independent of la - `WorkspaceCard.tsx` + `ProjectCard.tsx` extracted as `'use client'` components → fixed server-side exception on breadcrumb click ### ✅ Completed — Session 2 (2026-04-26) -- **Server-side crash fixed**: `workspaces/page.tsx` and `workspace/[wid]/page.tsx` confirmed using `WorkspaceCard`/`ProjectCard` client components (no `onMouseEnter` in server components) -- **Inspector fully rewired**: removed `useDiff`/`ARTBOARD_SNAPSHOTS` hardcoded data; now uses `useDiffs` (real DB diffs) + `useHistory` (pending local changes) -- **Export Diff button**: Inspector Diff tab shows pending `PropChange[]` from history store with "Export diff →" button that calls `POST /api/diffs` -- **`renderUrl` inline editor**: Inspector Props tab has inline input for `renderUrl`; saves via `PATCH /api/artboards/[id]`; `patchArtboard()` added to `useArtboards.ts` -- **Inspector status bar**: now reads `liveArtboardIds` from canvas store — shows pulsing green dot when live render connected, grey + hint when not -- **Empty canvas state**: removed `DEMO_ARTBOARDS` fallback from `useArtboards.ts`; Canvas shows dashed hint "Press A to create an artboard" when empty -- **Zone tool drag preview**: Canvas draws live dashed rectangle + dimension label during zone drag; cleans up on mouse-up -- **Workspace settings page**: `GET+PATCH /api/workspace/[id]`, `/workspace/[wid]/settings` page + `WorkspaceSettingsForm` client component (rename, IDE token issuance with full config snippets, danger zone) -- **Settings link in workspace page**: gear icon button added next to "New project" +- **Server-side crash fixed**: `workspaces/page.tsx` and `workspace/[wid]/page.tsx` confirmed using `WorkspaceCard`/`ProjectCard` client components +- **Inspector fully rewired**: uses `useDiffs` (DB) + `useHistory` (pending); Export Diff button; `renderUrl` inline editor; live status bar reads `liveArtboardIds` +- **Empty canvas state**: DEMO_ARTBOARDS removed; Canvas shows hint when empty +- **Zone tool drag preview**: live dashed rectangle + dimension label +- **Workspace settings page**: `GET+PATCH /api/workspace/[id]`, rename, IDE token issuance, danger zone +- **Settings link** added to workspace page -### 🔧 Remaining — In Order of Priority +### ✅ Completed — Session 3 (2026-04-26) +- **Canvas store `artboardFiberRoots`**: new `setFiberRoot(artboardId, root)` — Artboard calls it on every fiber update; Inspector Graph tab reads from it +- **Inspector Graph tab**: shows real collapsible `FiberTreeView` from live artboard; shows node count + depth; placeholder when no live render +- **Inspector Props tab — component selection**: when a component is clicked via `SelectionOverlay`, fiber props appear in a `↳ ComponentName` section above artboard metadata +- **Artboard delete button**: ✕ icon appears in label when artboard is selected; confirm dialog → `DELETE /api/artboards/[id]` → invalidate query +- **Artboard drag + rename**: already implemented (drag label to move, double-click to rename inline) +- **Navigator delete/rename**: hover on artboard row reveals pencil (rename) and trash (delete) icon buttons; calls `patchArtboard` / `DELETE` API +- **Zone prompt popup**: after zone drag ends, `ZonePromptOverlay` floats at zone position; textarea + "Generate ⌘↵" → `POST /api/ai/completion-zone`; shows result; Escape/close to dismiss +- **Dead demo code removed**: `ArtboardContent`, `DashboardCard`, `UserProfile`, `NavSidebar`, `DataTable` all deleted from `Artboard.tsx` -**CRITICAL** — all done ✅ +### ✅ Completed — Session 4 (2026-04-26) +- **Viewport per-workspace persistence**: `viewport.ts` `restore()` action + `AppChrome.tsx` useEffect saves/restores `panX/panY/zoom` per workspace in localStorage under `originmain:viewport:{workspaceId}` +- **Webhook `?project=` param**: `/api/webhooks/[provider]/route.ts` reads `project` query param → `project_id` on created artboard +- **Navigator live render badge**: artboard rows show pulsing green dot when artboard ID is in `liveArtboardIds` +- **Navigator real graph stats**: `artboardFiberRoots` traversal gives true component counts; live artboard count from `liveArtboardIds.size` +- **Cross-Artboard Query UI**: `CrossArtboardQuery` component in Navigator, queries `POST /api/ai/query` +- **Drift Report UI**: "↻ Generate drift report" button at bottom of Inspector Props tab; calls `POST /api/ai/drift-report`; shows scrollable pre-formatted result panel +- **`@anthropic-ai/sdk` upgraded to v0.91.1**: adaptive thinking wired in gateway; `temperature` removed from interface + all 5 feature files +- **Team invitation UI**: `TeamInviteForm` added to `WorkspaceSettingsForm` — Clerk userId + role picker → `POST /api/workspace/[id]/invite`; conflict/error/success feedback +- **Project settings page**: `/workspace/[wid]/project/[pid]/settings` — rename, description, app URL, framework selector, type-to-confirm delete; gear icon in AppChrome breadcrumb +- **Vitest: diff-engine**: stray test moved to correct location; all 49 tests pass, 88% coverage +- **Vitest: origin-graph**: 45 new Zod schema tests in `__tests__/types.test.ts`; 100% `types.ts` coverage -**HIGH** -- [x] Replace `useDiff.ts` with real `useDiffs.ts` ✅ -- [x] `renderUrl` edit UI ✅ -- [x] Export Diff button ✅ -- [x] Zone tool canvas drag preview ✅ -- [x] Empty canvas state ✅ -- [x] Workspace settings page ✅ -- [ ] Artboard content fallback — Artboard.tsx still shows UUID as a placeholder title when no fiber tree is connected +### 🔧 Remaining — Lower Priority -**MEDIUM** -- [ ] `handleComponentSelected` → canvas `selectComponent()` → Inspector shows selected component's fiber props -- [ ] Graph tab wired to real fiber tree (currently shows hardcoded DashboardCard tree) -- [ ] Navigator files tree selectable + artboard delete/rename actions -- [ ] Artboard drag-to-move (label drag → `PATCH /api/artboards/[id]` with new x/y in metadata_jsonb) -- [ ] Artboard delete (trash icon → confirm → `DELETE /api/artboards/[id]`) -- [ ] Artboard rename (double-click label → inline edit → PATCH name) -- [ ] Viewport persistence to localStorage -- [ ] Webhook `project_id` from `?project=` query param -- [ ] `@anthropic-ai/sdk` upgrade to ≥0.58 for `thinking: {type: 'adaptive'}` +- [ ] **Multiplayer**: Wire `MultiplayerAdapter` into app (install `@liveblocks/client` + `@liveblocks/react`; create `packages/app/src/lib/liveblocks.ts`) +- [ ] **Plugin system stub page**: UI route at `/workspace/[wid]/plugins` listing installed plugins +- [ ] **`packages/e2e`**: Playwright E2E tests not yet created +- [ ] **Redis rate limiter**: Replace in-process rate limiter in agent-bridge for multi-instance support -**LOW** -- [ ] Project settings page -- [ ] Team invitation UI (in workspace settings — `POST /api/workspace/[id]/invite` exists) -- [ ] Drift Report UI (in Inspector AI section) -- [ ] Cross-Artboard Query UI (in navigator) -- [ ] Vitest test files (diff-engine + origin-graph) -- [ ] Multiplayer basic Liveblocks wiring -- [ ] Plugin system stub page -- [ ] Zone tool: after drag, show prompt input → `POST /api/ai/completion-zone` with bounds +**Webhook project_id:** +`/api/webhooks/[provider]/route.ts` currently sets `project_id: null`. Read the `?project=` query param from the request URL (`new URL(req.url).searchParams.get('project')`) and pass it to the ingestion result. -### Key Technical Notes for Next Agent - -**Artboard.tsx and component selection:** -The `handleComponentSelected` callback inside `Artboard.tsx` receives the clicked `FiberNode` (from `LiveArtboard`). It needs to call `useCanvas.getState().selectComponent(node.id, node)`. The Inspector's PropsTab should check `selectedComponentId`/`selectedComponentData` and show the fiber node's props when a component is selected (vs. showing artboard metadata when nothing is selected). - -**Artboard drag-to-move:** -The `Artboard` component label area needs `onMouseDown` → tracks mouse delta → `PATCH /api/artboards/[id]` with `{ metadata_jsonb: { ...meta, x: newX, y: newY } }`. Use `patchArtboard(id, { metadata_jsonb: ... })` from `useArtboards.ts` then `queryClient.invalidateQueries`. - -**Zone tool completion:** -After the zone drag ends (mouse-up), show a floating prompt input at the zone position. On submit → `POST /api/ai/completion-zone` with `{ artboard_id, bounds: {x,y,w,h}, prompt }`. Show result inline in the canvas. - -**`@anthropic-ai/sdk` upgrade path:** -All 5 AI feature files in `packages/ai-layer/src/` have `// TODO: upgrade to adaptive thinking when SDK ≥0.58` comments. After `pnpm add @anthropic-ai/sdk@latest` in `packages/ai-layer`, replace the temperature-based calls with `thinking: {type: 'adaptive'}` on the `gateway.complete()` calls. - -*Last updated: 2026-04-26 — Session 2 complete* +*Last updated: 2026-04-26 — Session 3 complete* diff --git a/packages/ai-layer/package.json b/packages/ai-layer/package.json index 0b23954..3da80d4 100644 --- a/packages/ai-layer/package.json +++ b/packages/ai-layer/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@anthropic-ai/sdk": "^0.56.0", + "@anthropic-ai/sdk": "^0.91.1", "@originmain/design-language": "workspace:*", "@originmain/diff-engine": "workspace:*", "zod": "^3.0.0" diff --git a/packages/ai-layer/src/features/agent-qa.ts b/packages/ai-layer/src/features/agent-qa.ts index 73cb0b0..1109458 100644 --- a/packages/ai-layer/src/features/agent-qa.ts +++ b/packages/ai-layer/src/features/agent-qa.ts @@ -36,7 +36,6 @@ export async function answerAgentQuestion( }, ], maxTokens: 1024, - temperature: 0.7, }); let parsed: unknown; diff --git a/packages/ai-layer/src/features/artboard-query.ts b/packages/ai-layer/src/features/artboard-query.ts index 3f656ce..16aaa4c 100644 --- a/packages/ai-layer/src/features/artboard-query.ts +++ b/packages/ai-layer/src/features/artboard-query.ts @@ -34,7 +34,6 @@ export async function queryCrossArtboard( }, ], maxTokens: 1024, - temperature: 0.3, }); // Returning empty results on parse failure is indistinguishable from "no match". @@ -49,5 +48,19 @@ export async function queryCrossArtboard( ); } + // Runtime guard: ensure the parsed value has the expected shape before casting. + // Without this, a malformed AI response (e.g. { results: null }) would let + // callers hit a TypeError on `.results.map()` instead of a clear error message. + if ( + typeof parsed !== 'object' || + parsed === null || + !Array.isArray((parsed as Record)['results']) + ) { + throw new Error( + `Artboard query response missing "results" array. ` + + `Raw response (first 300 chars): ${response.text.slice(0, 300)}` + ); + } + return parsed as ArtboardQueryOutput; } diff --git a/packages/ai-layer/src/features/completion-zone.ts b/packages/ai-layer/src/features/completion-zone.ts index 53d6f5f..1d99bce 100644 --- a/packages/ai-layer/src/features/completion-zone.ts +++ b/packages/ai-layer/src/features/completion-zone.ts @@ -57,7 +57,6 @@ export async function fillCompletionZone( system, messages: [{ role: 'user', content: userContent }], maxTokens: 4096, - temperature: 0.3, }); try { diff --git a/packages/ai-layer/src/features/diff-summary.ts b/packages/ai-layer/src/features/diff-summary.ts index 7f10cfb..32e3fa9 100644 --- a/packages/ai-layer/src/features/diff-summary.ts +++ b/packages/ai-layer/src/features/diff-summary.ts @@ -27,7 +27,6 @@ export async function generateDiffSummary( }, ], maxTokens: 128, - temperature: 0.3, }); return { summary: response.text.trim() }; diff --git a/packages/ai-layer/src/features/drift-report.ts b/packages/ai-layer/src/features/drift-report.ts index 302ae1e..3005309 100644 --- a/packages/ai-layer/src/features/drift-report.ts +++ b/packages/ai-layer/src/features/drift-report.ts @@ -2,10 +2,12 @@ import type { AIGateway } from '../gateway.js'; import { buildSystemPrompt } from '../prompts/system.js'; export interface DriftReportInput { - /** Screenshot of the live app as base64 data URL */ - screenshotBase64: string; - /** Active Design Language File as JSON string */ - dlfJson: string; + /** Screenshot of the live app as base64 data URL (optional — text-only analysis if absent) */ + screenshotBase64?: string; + /** Active Design Language File as JSON string (optional — generic advice if absent) */ + dlfJson?: string; + /** Human-readable artboard metadata to give the model context (name, size, origin, etc.) */ + artboardContext?: string; } export interface DriftViolation { @@ -29,32 +31,39 @@ export async function generateDriftReport( ): Promise { const system = buildSystemPrompt({ role: 'a design system compliance auditor', - dlfJson: input.dlfJson, + ...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}), + }); + + type ContentBlock = + | { type: 'image'; source: { type: 'base64'; media_type: 'image/png'; data: string } } + | { type: 'text'; text: string }; + + const userContent: ContentBlock[] = []; + + if (input.screenshotBase64) { + userContent.push({ + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: input.screenshotBase64.replace(/^data:image\/\w+;base64,/, ''), + }, + }); + } + + const analysisInstructions = input.screenshotBase64 + ? 'Compare this screenshot against the design language file provided in your system context.' + : `Analyse the following artboard for design system drift:\n\n${input.artboardContext ?? '(no artboard context provided)'}`; + + userContent.push({ + type: 'text', + text: `${analysisInstructions} Identify all design system drift violations.\n\nReturn a JSON object:\n{\n "violations": [{"component":"...", "property":"...", "currentValue":"...", "expectedValue":"...", "severity":"critical|warning", "description":"..."}],\n "summary": "...",\n "violationCount": N\n}\n\nRespond ONLY with valid JSON.`, }); const response = await gateway.complete({ system, - messages: [ - { - role: 'user', - content: [ - { - type: 'image', - source: { - type: 'base64', - media_type: 'image/png', - data: input.screenshotBase64.replace(/^data:image\/\w+;base64,/, ''), - }, - }, - { - type: 'text', - text: 'Compare this screenshot against the design language file provided in your system context. Identify all design system drift violations.\n\nReturn a JSON object:\n{\n "violations": [{"component":"...", "property":"...", "currentValue":"...", "expectedValue":"...", "severity":"critical|warning", "description":"..."}],\n "summary": "...",\n "violationCount": N\n}\n\nRespond ONLY with valid JSON.', - }, - ], - }, - ], + messages: [{ role: 'user', content: userContent }], maxTokens: 4096, - temperature: 0.3, }); // Returning empty violations on parse failure would be a false-negative in a diff --git a/packages/ai-layer/src/gateway.ts b/packages/ai-layer/src/gateway.ts index 86602b3..ded6c17 100644 --- a/packages/ai-layer/src/gateway.ts +++ b/packages/ai-layer/src/gateway.ts @@ -73,10 +73,8 @@ export interface GatewayRequest { messages: Anthropic.Messages.MessageParam[]; system?: Anthropic.Messages.TextBlockParam[]; maxTokens?: number; - // NOTE: adaptive thinking (`thinking: { type: 'adaptive' }`) requires SDK >=0.58. - // Upgrade @anthropic-ai/sdk and uncomment when available. - /** Temperature: 0.3 for deterministic, 0.7 for generative */ - temperature?: number; + // NOTE: temperature is intentionally omitted — Opus 4.7 with adaptive thinking + // rejects temperature, top_p, and top_k with a 400 error. } export interface GatewayResponse { @@ -106,7 +104,7 @@ export class AIGateway { const response = await this.client.messages.create({ model: MODEL, max_tokens: req.maxTokens ?? 4096, - ...(req.temperature !== undefined ? { temperature: req.temperature } : {}), + thinking: { type: 'adaptive' }, ...(req.system !== undefined ? { system: req.system } : {}), messages: req.messages, }); diff --git a/packages/app/src/app/api/ai/drift-report/route.ts b/packages/app/src/app/api/ai/drift-report/route.ts index b7dc51d..84f7701 100644 --- a/packages/app/src/app/api/ai/drift-report/route.ts +++ b/packages/app/src/app/api/ai/drift-report/route.ts @@ -1,20 +1,85 @@ // POST /api/ai/drift-report -// Analyzes a screenshot against the active Design Language File for drift violations. +// Accepts { artboard_id } — fetches artboard metadata + workspace DLF from DB, +// then calls generateDriftReport. Screenshots are optional; text-only analysis runs +// when the artboard has no renderUrl or the screenshot is not provided by the client. import { auth } from '@clerk/nextjs/server'; import { NextRequest, NextResponse } from 'next/server'; import { AIGateway, generateDriftReport } from '@originmain/ai-layer'; -import type { DriftReportInput } from '@originmain/ai-layer'; +import { serverClient } from '@/lib/supabase'; export async function POST(req: NextRequest) { const { userId } = await auth(); if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const body = (await req.json()) as DriftReportInput; + const body = (await req.json().catch(() => ({}))) as { + artboard_id?: string; + /** Optional — client may pass a base64 screenshot captured from the live iframe */ + screenshot_base64?: string; + }; + + if (!body.artboard_id) { + return NextResponse.json({ error: 'artboard_id is required' }, { status: 400 }); + } + + const db = serverClient(); + + // Fetch artboard + verify the caller is a workspace member + const { data: artboard } = await db + .from('artboards') + .select('id, name, workspace_id, project_id, metadata_jsonb') + .eq('id', body.artboard_id) + .single() as unknown as { + data: { + id: string; + name: string; + workspace_id: string; + project_id: string | null; + metadata_jsonb: Record; + } | null; + }; + + if (!artboard) { + return NextResponse.json({ error: 'Artboard not found' }, { status: 404 }); + } + + // Auth gate: require workspace membership + const { data: member } = await db + .from('team_members') + .select('id') + .eq('workspace_id', artboard.workspace_id) + .eq('user_id', userId) + .limit(1) + .single(); + + if (!member) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + // Fetch the most recently updated Design Language File for this workspace (optional) + const { data: dlf } = await db + .from('design_language_files') + .select('schema_jsonb, name') + .eq('workspace_id', artboard.workspace_id) + .order('updated_at', { ascending: false }) + .limit(1) + .single() as unknown as { data: { schema_jsonb: unknown; name: string } | null }; + + const meta = artboard.metadata_jsonb; + const artboardContext = [ + `Artboard: ${artboard.name}`, + `Size: ${meta['width'] ?? '?'} × ${meta['height'] ?? '?'}`, + ...(meta['renderUrl'] ? [`Render URL: ${String(meta['renderUrl'])}`] : []), + ...(artboard.project_id ? [`Project ID: ${artboard.project_id}`] : []), + ].join('\n'); try { const gateway = new AIGateway(); - const result = await generateDriftReport(gateway, body); + const result = await generateDriftReport(gateway, { + artboardContext, + ...(dlf ? { dlfJson: JSON.stringify(dlf.schema_jsonb) } : {}), + ...(body.screenshot_base64 ? { screenshotBase64: body.screenshot_base64 } : {}), + }); return NextResponse.json(result); } catch (err) { const message = err instanceof Error ? err.message : 'AI error'; diff --git a/packages/app/src/app/api/webhooks/[provider]/route.ts b/packages/app/src/app/api/webhooks/[provider]/route.ts index afffadb..ca20cf9 100644 --- a/packages/app/src/app/api/webhooks/[provider]/route.ts +++ b/packages/app/src/app/api/webhooks/[provider]/route.ts @@ -98,6 +98,8 @@ export async function POST( if (!workspaceId) { return NextResponse.json({ error: 'workspace query param is required' }, { status: 400 }); } + // Optional: ?project= scopes the created artboard to a specific project + const projectId = req.nextUrl.searchParams.get('project'); const rawBody = Buffer.from(await req.arrayBuffer()); @@ -147,7 +149,7 @@ export async function POST( const artboard = await createArtboard(db, { workspace_id: workspaceId, - project_id: null, + project_id: projectId, name: result.artboardTitle, origin_id: origin.id, parent_artboard_id: null, diff --git a/packages/app/src/app/workspace/[wid]/project/[pid]/settings/page.tsx b/packages/app/src/app/workspace/[wid]/project/[pid]/settings/page.tsx new file mode 100644 index 0000000..67fd238 --- /dev/null +++ b/packages/app/src/app/workspace/[wid]/project/[pid]/settings/page.tsx @@ -0,0 +1,103 @@ +import { auth } from '@clerk/nextjs/server'; +import { redirect } from 'next/navigation'; +import Link from 'next/link'; +import { serverClient } from '@/lib/supabase'; +import { ProjectSettingsForm } from '@/components/shell/ProjectSettingsForm'; + +export async function generateMetadata({ params }: { params: Promise<{ wid: string; pid: string }> }) { + const { pid } = await params; + const db = serverClient(); + const { data } = await db.from('projects').select('name').eq('id', pid).single() as unknown as { + data: { name: string } | null; + }; + return { title: `${data?.name ?? 'Project'} Settings — Originmain` }; +} + +export default async function ProjectSettingsPage({ + params, +}: { + params: Promise<{ wid: string; pid: string }>; +}) { + const { wid, pid } = await params; + const { userId } = await auth(); + if (!userId) redirect('/sign-in'); + + const db = serverClient(); + + // Verify membership and role + const { data: member } = await db + .from('team_members') + .select('role') + .eq('workspace_id', wid) + .eq('user_id', userId) + .limit(1) + .single() as unknown as { data: { role: string } | null }; + + if (!member) redirect('/workspaces'); + + // Fetch workspace name + project + type WsRow = { data: { name: string } | null }; + type ProjRow = { data: { id: string; name: string; description: string | null; app_url: string | null; framework: string | null } | null }; + + const [wsResult, projResult] = await Promise.all([ + db.from('workspaces').select('name').eq('id', wid).single() as unknown as Promise, + db.from('projects').select('id, name, description, app_url, framework').eq('id', pid).eq('workspace_id', wid).single() as unknown as Promise, + ]); + + if (!projResult.data) redirect(`/workspace/${wid}`); + + const project = projResult.data; + + return ( +
+ {/* Header */} +
+ + Originmain + + / + + {wsResult.data?.name ?? 'Workspace'} + + / + + {project.name} + + / + Settings +
+ + {/* Content */} +
+

+ Project settings +

+

+ Configure your project name, URL, and framework. +

+ + +
+
+ ); +} diff --git a/packages/app/src/components/canvas/Artboard.tsx b/packages/app/src/components/canvas/Artboard.tsx index a71015d..1561c68 100644 --- a/packages/app/src/components/canvas/Artboard.tsx +++ b/packages/app/src/components/canvas/Artboard.tsx @@ -19,24 +19,24 @@ interface ArtboardProps { } export function Artboard({ id, label, x, y, width, height, renderUrl }: ArtboardProps) { - const { selectedArtboardId, selectArtboard, workspaceId, projectId, setArtboardLive, selectComponent } = useCanvas(); + const { selectedArtboardId, selectArtboard, workspaceId, projectId, setArtboardLive, setFiberRoot, selectComponent } = useCanvas(); const selected = selectedArtboardId === id; const queryClient = useQueryClient(); // ── Fiber tree (from LiveArtboard) ───────────────────────────────────────── - const [fiberRoot, setFiberRoot] = useState(undefined); + const [localFiberRoot, setLocalFiberRoot] = useState(undefined); const handleFiberUpdate = useCallback((root: FiberNode) => { - setFiberRoot(root); + setFiberRoot(id, root); setArtboardLive(id, true); - }, [id, setArtboardLive]); + setLocalFiberRoot(root); + }, [id, setFiberRoot, setArtboardLive]); const handleComponentSelected = useCallback((nodeId: string) => { - if (!fiberRoot) return; - // Walk fiber tree to find the selected node - const node = findFiberNode(fiberRoot, nodeId); + if (!localFiberRoot) return; + const node = findFiberNode(localFiberRoot, nodeId); selectComponent(nodeId, node ?? null); - }, [fiberRoot, selectComponent]); + }, [localFiberRoot, selectComponent]); // ── Drag to reposition ───────────────────────────────────────────────────── const isDragging = useRef(false); @@ -121,6 +121,17 @@ export function Artboard({ id, label, x, y, width, height, renderUrl }: Artboard }).catch(console.error); }, [id, renameValue, label, workspaceId, projectId, queryClient]); + // ── Delete artboard ──────────────────────────────────────────────────────── + const deleteArtboard = useCallback(() => { + if (!window.confirm(`Delete "${label}"? This cannot be undone.`)) return; + fetch(`/api/artboards/${id}`, { method: 'DELETE' }) + .then(() => { + selectArtboard(null); + queryClient.invalidateQueries({ queryKey: ['artboards', workspaceId, projectId ?? undefined] }); + }) + .catch(console.error); + }, [id, label, selectArtboard, workspaceId, projectId, queryClient]); + const effectiveX = x + (isDragging.current ? dragOffset.dx : 0); const effectiveY = y + (isDragging.current ? dragOffset.dy : 0); @@ -171,19 +182,41 @@ export function Artboard({ id, label, x, y, width, height, renderUrl }: Artboard }} /> ) : ( - - {label} - + <> + + {label} + + {/* Delete button — only visible when selected */} + {selected && ( + + )} + )} @@ -226,7 +259,7 @@ export function Artboard({ id, label, x, y, width, height, renderUrl }: Artboard /> @@ -350,18 +383,6 @@ function EmptyArtboardContent({ ); } -// ── Demo content (only for hardcoded demo IDs) ──────────────────────────────── - -function ArtboardContent({ id }: { id: string }) { - switch (id) { - case 'dashboard-card': return ; - case 'user-profile': return ; - case 'nav-sidebar': return ; - case 'data-table': return ; - default: return null; // shouldn't reach here for real artboards - } -} - // ── Helpers ─────────────────────────────────────────────────────────────────── function findFiberNode(root: FiberNode, nodeId: string): FiberNode | null { @@ -384,115 +405,3 @@ function Handle({ pos }: { pos: React.CSSProperties }) { ); } -// ── Demo card components ────────────────────────────────────────────────────── - -function DashboardCard() { - return ( -
-
- Revenue Overview - Live -
-
$12,450
-
- +2.4% vs last month -
-
-
-
-
- {['Q4 2024', 'MRR', 'SaaS'].map(t => ( - {t} - ))} -
-
- ); -} - -function UserProfile() { - return ( -
-
-
Sarah Chen
-
Design Engineer
-
- {[['Team', 'Acme Inc'], ['Role', 'Admin'], ['Plan', 'Team']].map(([k, v]) => ( -
- {k} - {v} -
- ))} -
-
- ); -} - -function NavSidebar() { - const items = [ - { icon: '⊞', label: 'Dashboard', active: true }, - { icon: '◉', label: 'Origin Graph', active: false }, - { icon: '⬜', label: 'Artboards', active: false }, - { icon: '△', label: 'Diffs', active: false }, - { icon: '🔗', label: 'Integrations',active: false }, - ]; - return ( -
-
- Originmain -
-
- {items.map(({ icon, label, active }) => ( -
- {icon}{label} -
- ))} -
- ); -} - -function DataTable() { - const rows = [ - { name: 'DashboardCard', status: 'Live', nodes: 12, tokens: 8 }, - { name: 'UserProfile', status: 'Draft', nodes: 7, tokens: 3 }, - { name: 'NavSidebar', status: 'Live', nodes: 19, tokens: 11 }, - { name: 'DataTable', status: 'Review', nodes: 24, tokens: 14 }, - ]; - return ( -
-
- Component Inventory -
-
- {['Name', 'Status', 'Nodes', 'Tokens'].map(h => ( - {h} - ))} -
- {rows.map((row, i) => ( -
- {row.name} - {row.status} - {row.nodes} - {row.tokens} -
- ))} -
- ); -} - -// Suppress unused warning — kept for demo IDs in ArtboardContent -void ArtboardContent; diff --git a/packages/app/src/components/canvas/Canvas.tsx b/packages/app/src/components/canvas/Canvas.tsx index 4ca3600..997ac1c 100644 --- a/packages/app/src/components/canvas/Canvas.tsx +++ b/packages/app/src/components/canvas/Canvas.tsx @@ -12,7 +12,7 @@ export function Canvas() { const panX = useViewport((s) => s.panX); const panY = useViewport((s) => s.panY); const zoom = useViewport((s) => s.zoom); - const { activeTool, setActiveTool, selectArtboard, workspaceId, projectId } = useCanvas(); + const { activeTool, setActiveTool, selectArtboard, selectedArtboardId, workspaceId, projectId } = useCanvas(); const { artboards } = useArtboards(workspaceId ?? undefined, projectId ?? undefined); const queryClient = useQueryClient(); @@ -23,6 +23,7 @@ export function Canvas() { // Zone tool: drag to draw a completion zone const zoneStart = useRef<{ x: number; y: number } | null>(null); const [zonePreview, setZonePreview] = useState<{ x: number; y: number; w: number; h: number } | null>(null); + const [zoneDone, setZoneDone] = useState<{ x: number; y: number; w: number; h: number } | null>(null); // Wheel: pan or pinch-zoom useEffect(() => { @@ -125,13 +126,15 @@ export function Canvas() { const onMouseUp = useCallback(() => { isPanning.current = false; - // Zone tool: finalise zone on mouse-up (clear preview; zone result is handled elsewhere) - if (activeTool === 'zone' && zoneStart.current) { + // Zone tool: finalise zone — keep the bounds and show prompt popup + if (activeTool === 'zone' && zoneStart.current && zonePreview) { + const bounds = { ...zonePreview }; zoneStart.current = null; setZonePreview(null); setActiveTool('select'); + if (bounds.w > 8 && bounds.h > 8) setZoneDone(bounds); } - }, [activeTool, setActiveTool]); + }, [activeTool, setActiveTool, zonePreview]); // Dot grid that shifts with pan and scales with zoom const gridSpacing = Math.max(6, 20 * zoom); @@ -215,6 +218,16 @@ export function Canvas() { )}
+ {/* Zone prompt overlay — shown after a zone drag completes */} + {zoneDone && ( + setZoneDone(null)} + /> + )} + {/* Empty canvas hint — shown only when workspace has no artboards yet */} {artboards.length === 0 && (
); } + +/* ── Zone prompt overlay ──────────────────────────────────── */ +function ZonePromptOverlay({ + bounds, artboardId, panX, panY, zoom, onClose, +}: { + bounds: { x: number; y: number; w: number; h: number }; + artboardId: string | null; + panX: number; panY: number; zoom: number; + onClose: () => void; +}) { + const [prompt, setPrompt] = useState(''); + const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle'); + const [result, setResult] = useState(''); + + // Convert canvas → screen coordinates (relative to canvas container) + const screenX = bounds.x * zoom + panX; + const screenY = (bounds.y + bounds.h) * zoom + panY + 10; // 10px below zone + + const submit = useCallback(async () => { + if (!prompt.trim() || !artboardId) return; + setStatus('loading'); + try { + const res = await fetch('/api/ai/completion-zone', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + artboard_id: artboardId, + bounds: { x: bounds.x, y: bounds.y, width: bounds.w, height: bounds.h }, + prompt: prompt.trim(), + }), + }); + if (!res.ok) throw new Error(`${res.status}`); + const data = await res.json() as { completion?: string; result?: string }; + setResult(data.completion ?? data.result ?? 'Done'); + setStatus('done'); + } catch (e) { + console.error('[ZonePrompt]', e); + setStatus('error'); + } + }, [prompt, artboardId, bounds]); + + return ( +
e.stopPropagation()} + > + {/* Header */} +
+ + ⚡ Completion zone · {bounds.w}×{bounds.h} + + +
+ + {status === 'done' ? ( +
+ {result} +
+ ) : ( + <> +