From a9d497a9cf30ab274d92312ad375e1537b87170d Mon Sep 17 00:00:00 2001 From: Eric johnson Date: Tue, 17 Feb 2026 11:19:06 -0600 Subject: [PATCH 1/7] add home page details for all usage examples. Ecommerce and logistics with detailed usage demo examples --- ...scenario_workflow_dialogs_20260216.plan.md | 89 + ...e_anyclick_implementation_69a4d5c4.plan.md | 158 ++ .../swiftui-anyclick-macos_ff4c2c5e.plan.md | 83 + .cursor/worktrees.json | 3 + .myh/whiteboard.sqlite | Bin 0 -> 266240 bytes .../src/app/api/feedback/github/route.d.ts | 1 + apps/web/src/app/api/feedback/github/route.js | 80 + .../src/app/api/feedback/github/route.js.map | 1 + apps/web/src/app/api/feedback/github/route.ts | 111 + apps/web/src/app/page.tsx | 48 +- apps/web/src/components/AnimatedWord.tsx | 83 + .../cards/EcommerceCard.tsx | 352 ++- .../cards/SoftwareEditorCard.tsx | 72 +- .../sections/DefaultWorkstreamSection.tsx | 14 +- .../sections/EcommerceSection.tsx | 143 + .../sections/HealthcareSection.tsx | 14 +- .../sections/SoftwareDevelopmentSection.tsx | 22 +- .../sections/WorkstreamSection.tsx | 16 + .../immersive-workstream/softwareCode.ts | 14 +- .../immersive-workstream/themes.tsx | 25 +- .../components/immersive-workstream/types.ts | 2 - .../workflows/WorkflowContextSummary.tsx | 131 + .../workflows/WorkflowDrawer.tsx | 243 ++ .../workflows/WorkflowFlowRenderer.tsx | 2292 +++++++++++++++++ .../workflows/contextCapture.ts | 73 + .../workflows/registry.ts | 538 ++++ .../workflows/titleHeuristics.ts | 149 ++ .../immersive-workstream/workflows/types.ts | 75 + .../workflows/useWorkflowLauncher.tsx | 84 + apps/web/src/data/releases.json | 2 + .../src/lib/hooks/useFirstTimeInteraction.ts | 35 + .../anyclick-pointer/src/PointerProvider.tsx | 38 +- packages/anyclick-react/src/ContextMenu.tsx | 270 +- scripts/update-releases-json.mjs | 2 + 34 files changed, 4961 insertions(+), 302 deletions(-) create mode 100644 .cursor/plans/homepage_scenario_workflow_dialogs_20260216.plan.md create mode 100644 .cursor/plans/swift_native_anyclick_implementation_69a4d5c4.plan.md create mode 100644 .cursor/plans/swiftui-anyclick-macos_ff4c2c5e.plan.md create mode 100644 .cursor/worktrees.json create mode 100644 .myh/whiteboard.sqlite create mode 100644 apps/web/src/app/api/feedback/github/route.d.ts create mode 100644 apps/web/src/app/api/feedback/github/route.js create mode 100644 apps/web/src/app/api/feedback/github/route.js.map create mode 100644 apps/web/src/app/api/feedback/github/route.ts create mode 100644 apps/web/src/components/AnimatedWord.tsx create mode 100644 apps/web/src/components/immersive-workstream/sections/EcommerceSection.tsx create mode 100644 apps/web/src/components/immersive-workstream/workflows/WorkflowContextSummary.tsx create mode 100644 apps/web/src/components/immersive-workstream/workflows/WorkflowDrawer.tsx create mode 100644 apps/web/src/components/immersive-workstream/workflows/WorkflowFlowRenderer.tsx create mode 100644 apps/web/src/components/immersive-workstream/workflows/contextCapture.ts create mode 100644 apps/web/src/components/immersive-workstream/workflows/registry.ts create mode 100644 apps/web/src/components/immersive-workstream/workflows/titleHeuristics.ts create mode 100644 apps/web/src/components/immersive-workstream/workflows/types.ts create mode 100644 apps/web/src/components/immersive-workstream/workflows/useWorkflowLauncher.tsx create mode 100644 apps/web/src/lib/hooks/useFirstTimeInteraction.ts diff --git a/.cursor/plans/homepage_scenario_workflow_dialogs_20260216.plan.md b/.cursor/plans/homepage_scenario_workflow_dialogs_20260216.plan.md new file mode 100644 index 0000000..cd16f0c --- /dev/null +++ b/.cursor/plans/homepage_scenario_workflow_dialogs_20260216.plan.md @@ -0,0 +1,89 @@ +# Homepage Scenario Workflow Dialogs + +## Summary +Implement a shared workflow-drawer system on the homepage immersive workstreams so each scenario has exactly 3 right-click actions, and each action opens a relevant multi-step workflow dialog. + +Locked choices: +- GitHub submission is real only for software "Report this bug" GitHub path. +- All other workflow submissions are demo-only. +- Keep software code editor visual, but keep menu actions fixed. +- Use heuristic AI-style title generation (no model call). + +## Assumptions and Defaults +1. Use right-side panel UI for all workflows. +2. Every workflow shows Anyclick context: target selector, container selector, container screenshot, page URL/title. +3. Screenshot failures are non-blocking and show fallback. +4. GitHub failures/non-configured state offer "Complete as demo" fallback. +5. Changes remain in `apps/web` only (no `packages/*` API changes). + +## Public API / Interface / Type Changes +1. Add `POST /api/feedback/github` at `apps/web/src/app/api/feedback/github/route.ts`. +2. Endpoint body is `AnyclickPayload`; supports: + - `metadata.github.title` as preferred GitHub issue title. + - `metadata.routing.adapter = "github"`. +3. Internal immersive changes: + - Remove menu definitions from theme ownership. + - Add workflow types under `apps/web/src/components/immersive-workstream/workflows/types.ts`. + +## Workflow Catalog +- Software Development + - Report this bug: Ticket system -> Issue details -> Review & submit (GitHub real, Jira demo) + - Send to Cursor: Fix scope -> Prompt & constraints -> Review (demo) + - Copy selector: Selector format -> Copy bundle (demo) +- E-commerce & Logistics + - Item missing: Incident details -> Resolution path -> Confirm (demo) + - Shipping issue: Shipment details -> Customer impact -> Confirm (demo) + - Escalate order: Escalation target -> Priority/SLA -> Confirm (demo) +- Healthcare + - Check-in issue: Issue type -> Patient context -> Confirm (demo) + - Vital alert: Vital entry -> Clinical action -> Confirm (demo) + - Flag urgent: Urgency classification -> Notification route -> Confirm (demo) +- Social Media + - Save asset: Destination -> Metadata/tags -> Confirm (demo) + - Flag content: Policy category -> Enforcement action -> Confirm (demo) + - Quick reply: Draft reply -> Tone/approval -> Confirm (demo) + +## Architecture Decisions +1. Use `ContextMenuItem.onClick` for all 12 actions; bypass default submit flow. +2. Build shared workflow launcher + drawer; action definitions live in registry. +3. Keep software editor visible and add fixed-workflow indicator. +4. Capture context with Anyclick core utilities: + - `buildElementContext` + - `captureScreenshot(..., "container")` +5. Isolate GitHub submit via dedicated `/api/feedback/github` endpoint. + +## Implementation Steps +1. Add workflow foundation files: + - `workflows/types.ts` + - `workflows/registry.ts` + - `workflows/titleHeuristics.ts` + - `workflows/contextCapture.ts` +2. Add shared launcher + UI: + - `workflows/WorkflowDrawer.tsx` + - `workflows/useWorkflowLauncher.tsx` + - `workflows/WorkflowFlowRenderer.tsx` +3. Integrate sections: + - `sections/SoftwareDevelopmentSection.tsx` + - `sections/DefaultWorkstreamSection.tsx` + - `sections/HealthcareSection.tsx` +4. Lock software editor behavior and copy: + - `cards/SoftwareEditorCard.tsx` + - `softwareCode.ts` +5. Move menu ownership out of theme model: + - `types.ts` + - `themes.tsx` +6. Add GitHub-only endpoint: + - `app/api/feedback/github/route.ts` +7. Wire software GitHub submit path in workflow renderer. + +## Validation +1. Each scenario shows exactly 3 right-click menu items. +2. All 12 actions open relevant dialogs with multi-step forms. +3. Context panel shows selector/container selector/screenshot or fallback. +4. Software bug flow first step has Jira + GitHub choices. +5. GitHub title prefilled heuristically; editable/regeneratable. +6. GitHub submit succeeds when configured. +7. GitHub failure offers demo completion. +8. All non-software flows complete without backend dependency. +9. Escape/close behavior works and body scroll restores. +10. Run `yarn --cwd /Users/ericjohnson/Desktop/projects/anyclick/apps/web lint`. diff --git a/.cursor/plans/swift_native_anyclick_implementation_69a4d5c4.plan.md b/.cursor/plans/swift_native_anyclick_implementation_69a4d5c4.plan.md new file mode 100644 index 0000000..d9309a2 --- /dev/null +++ b/.cursor/plans/swift_native_anyclick_implementation_69a4d5c4.plan.md @@ -0,0 +1,158 @@ +--- +name: Swift Native AnyClick Implementation +overview: Create a native macOS application using Swift that intercepts right-clicks, resolves context (including deep browser context via an extension bridge), and displays a dynamic, graph-based native menu. +todos: + - id: scaffold-swift-project + content: Scaffold Swift Project in `apps/anyclick-native` + status: pending + - id: implement-input-monitor + content: Implement InputMonitor and MenuRenderer (Proof of Concept) + status: pending + dependencies: + - scaffold-swift-project + - id: implement-graph-engine + content: Design and Implement Graph Data Structure in Swift + status: pending + dependencies: + - scaffold-swift-project + - id: update-extension-manifest + content: Update Extension Manifest for Native Messaging + status: pending + - id: implement-browser-bridge + content: Implement Browser Bridge in Swift and Extension + status: pending + dependencies: + - update-extension-manifest + - scaffold-swift-project +isProject: false +--- + +# AnyClick Native macOS Implementation Plan + +This plan outlines the creation of a high-performance native macOS application that replaces/augments the web-based AnyClick flows. It uses system-level hooks for event tracking and a bridge to the browser for DOM interactions. + +## Architecture + +```mermaid +graph TD + User((User)) -->|Right Click| InputMonitor[Input Monitor (CGEventTap)] + InputMonitor -- Suppress OS Menu --> ContextEngine[Context Engine] + + subgraph Native App [Swift macOS App] + InputMonitor + ContextEngine + GraphEngine[Graph Engine] + MenuRenderer[Menu Renderer] + ActionExecutor[Action Executor] + end + + subgraph Browser [Chrome/Arc/Safari] + Extension[AnyClick Extension] + DOM[DOM Context] + end + + ContextEngine -->|Get Active App| OS[macOS Accessibility API] + ContextEngine -.->|Native Messaging| Extension + + Extension -->|DOM Data| ContextEngine + + ContextEngine -->|Context + AppID| GraphEngine + GraphEngine -->|Menu Items| MenuRenderer + MenuRenderer -->|Show| User + + User -->|Select Action| ActionExecutor + ActionExecutor -->|Perform| OS + ActionExecutor -->|Execute Script| Extension +``` + + + +## Core Components + +### 1. Native Application Structure (`apps/anyclick-native`) + +- **Technology**: Swift 6, AppKit (for low-level events/menus), SwiftUI (for Settings UI). +- **Permissions**: + - `Accessibility`: To read selected text and UI elements. + - `Input Monitoring`: To intercept global mouse clicks. + - `Native Messaging`: To communicate with the browser extension. + +### 2. The Graph Engine + +A JSON-based configuration engine that resolves menu items based on `(Application, Context)`. + +**Schema Definition:** + +```json +{ + "rules": [ + { + "appId": "com.microsoft.VSCode", + "context": { "hasSelection": true }, + "actions": ["copy", "save-to-context", "ask-ai"] + }, + { + "appId": "com.google.Chrome", + "context": { "domType": "image" }, + "actions": ["save-image", "upload-thing"] + } + ] +} +``` + +### 3. Context & Event Loop + +1. **Hook**: `CGEvent.tapCreate` listens for right-mouse-down. +2. **Filter**: If a modifier key (e.g., `Cmd`) is held (optional config), passthrough to OS. Otherwise, suppress. +3. **Resolve**: + - Get `NSWorkspace.shared.frontmostApplication`. + - Use `AXUIElement` to get selected text. + - If Browser, send "Ping" to Extension via Native Messaging to get DOM element under cursor. + +### 4. Extension Bridge + +- **Update** `packages/anyclick-extension` to support Native Messaging. +- **Protocol**: + - `Request: { type: "GET_CONTEXT", x: 100, y: 200 }` + - `Response: { element: "img", src: "...", id: "..." }` + - `Command: { type: "EXECUTE", action: "remove-element", targetId: "..." }` + +## Implementation Steps + +### Phase 1: Native Foundation + +- Initialize Xcode project in `apps/anyclick-native`. +- Implement `InputMonitor` class using `CoreGraphics`. +- Create `PermissionManager` to request/check Accessibility/Input permissions. +- Implement basic `MenuRenderer` using `NSMenu`. + +### Phase 2: Graph & Context + +- Define `GraphConfig` structs and load default rules. +- Implement `ContextResolver` (getting frontmost app bundle ID and selected text). +- Connect `InputMonitor` -> `ContextResolver` -> `Graph` -> `Menu`. + +### Phase 3: Browser Integration + +- Add `nativeMessaging` permission to `packages/anyclick-extension/manifest.json`. +- Implement Native Messaging Host registration script. +- Create `BrowserBridge` in Swift to handle stdio communication with Chrome. +- Update Extension content script to answer "Context Requests". + +## File Structure + +- `apps/anyclick-native/` + - `Sources/` + - `Core/` (Graph, EventLoop) + - `Mac/` (Accessibility, InputTap) + - `Bridge/` (NativeMessaging) + - `UI/` (Settings, Menu) +- `packages/anyclick-extension/src/native/` (New bridge logic) + +## Key Challenges & Mitigations + +- **Speed**: Waiting for Chrome extension response might delay menu. + - *Mitigation*: Show menu immediately with "Loading..." or generic actions, update when response arrives (or strict 50ms timeout). +- **Sandboxing**: App Store distribution is hard with Accessibility/NativeMessaging. + - *Mitigation*: Distribute as signed Developer ID app (outside App Store). + diff --git a/.cursor/plans/swiftui-anyclick-macos_ff4c2c5e.plan.md b/.cursor/plans/swiftui-anyclick-macos_ff4c2c5e.plan.md new file mode 100644 index 0000000..5b7a91a --- /dev/null +++ b/.cursor/plans/swiftui-anyclick-macos_ff4c2c5e.plan.md @@ -0,0 +1,83 @@ +--- +name: swiftui-anyclick-macos +overview: Assess feasibility and outline a staged implementation of a SwiftUI macOS Anyclick menu that is driven by a local graph, integrated app‑specifically with Chrome and Cursor, and reuses existing Anyclick types and context capture patterns. +todos: + - id: define-graph-model + content: Define menu graph schema + local persistence + JSON import/export + status: pending + - id: native-menu + content: Build SwiftUI/NSMenu renderer + action dispatch + status: pending + - id: chrome-bridge + content: Send Chrome right-click context to Mac app + status: pending + - id: cursor-bridge + content: Send Cursor right-click context to Mac app + status: pending + - id: action-adapters + content: Implement Anyclick/Cursor/3rd-party action adapters + status: pending +isProject: false +--- + +# SwiftUI Anyclick Menu Plan + +## Feasibility Notes + +- This is feasible with app‑specific integrations. The repo already has menu modeling (`ContextMenuItem`) and extension‑side right‑click interception that can be reused as the semantic model and context capture pattern for a native SwiftUI menu. +- A global system‑wide menu would require Accessibility/event taps; with your app‑specific choice we can avoid those and integrate per app (Chrome extension + Cursor plugin) while still presenting a native macOS menu. + +## Key Existing References (reuse/adapt) + +- Menu item modeling: `ContextMenuItem` in `packages/anyclick-react/src/types.ts` and `ExtensionMenuItem` in `packages/anyclick-extension/src/contextMenu.ts`. +- Right‑click interception in Chrome extension: `packages/anyclick-extension/src/content.ts`. +- Intent/protocol semantics to map actions: `packages/anyclick-protocol/src/spec.ts`. + +## Plan + +1. **Define the local menu graph & action schema** + - Create a Swift model for a graph that maps `(AppContext, SelectionContext, PageContext)` → ordered menu items + actions. + - Use a local‑only store (SwiftData/CoreData or SQLite) plus JSON import/export for easy editing and future sync. + - Add action “targets” for: Anyclick web chat/context, Cursor agent, and third‑party APIs. +2. **Build the SwiftUI macOS app shell + native menu renderer** + - Implement a background/status‑bar app that can: + - Receive a context event, resolve the menu graph, and render a native `NSMenu` at the cursor location. + - Execute action handlers (copy to clipboard, send to Anyclick web endpoints, invoke Cursor local adapter, call 3rd‑party API). + - Add a simple UI to edit graph rules and reorder menu items. +3. **Chrome integration (app‑specific)** + - Extend `packages/anyclick-extension` to send a right‑click context payload (selection text, URL, element metadata) to the Mac app. + - Use either Native Messaging or a local HTTP bridge to deliver events to the Swift app. + - The extension defers menu rendering to the Swift app (native menu), but still uses existing capture logic for context consistency. +4. **Cursor integration (app‑specific)** + - Create a small Cursor extension/plugin that captures selection + editor context on right‑click and forwards it to the Swift app. + - Reuse `@ewjdev/anyclick-cursor` / `@ewjdev/anyclick-cursor-local` conventions for action execution where appropriate. +5. **Action execution + telemetry (local‑only MVP)** + - Implement action adapters in the macOS app: + - **Anyclick web chat/context**: call configured endpoints with selection + page context. + - **Cursor agent**: use local adapter protocol (or CLI) for “add to chat” actions. + - **Third‑party**: generic HTTP webhook action. + - Capture local logs for debugging; no cloud sync in MVP. + +```mermaid +flowchart LR + ChromeExt --> LocalBridge + CursorExt --> LocalBridge + LocalBridge --> MacApp + MacApp --> MenuGraph + MenuGraph --> NativeMenu + NativeMenu --> ActionAdapters + ActionAdapters --> AnyclickWeb + ActionAdapters --> CursorAgent + ActionAdapters --> ThirdPartyAPI +``` + + + +## Initial Implementation Todos + +- `define-graph-model`: Swift models + local persistence + JSON import/export. +- `native-menu`: SwiftUI/NSMenu rendering and action dispatch. +- `chrome-bridge`: Extension event payload + local bridge to Mac app. +- `cursor-bridge`: Cursor extension + local bridge to Mac app. +- `action-adapters`: Anyclick web, Cursor agent, third‑party webhooks. + diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json new file mode 100644 index 0000000..50c4373 --- /dev/null +++ b/.cursor/worktrees.json @@ -0,0 +1,3 @@ +{ + "setup-worktree": ["yarn install"] +} diff --git a/.myh/whiteboard.sqlite b/.myh/whiteboard.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..584b8861c3d2149355a45bbe5fe5436f98a8335d GIT binary patch literal 266240 zcmeI)TWlQHdBE{qin}9T$FgEgWJlI$8JZePvb3aVnO4+#MXsE+$fZm!5vxVI810V8 zf%oRwAtkoR!>&~X=}XbqpifPT0zuINDGH-OpVEgQMjnC!ZC-i-0u*S9!VgUlG#3;- zGrO}pvp2;qkpjcN40A4LF6aEtch1b5BWnBYb<;M~hgGYr+v-x}nMgDmc|%nrk%%n* zPl^A%KNrQyxVIseQQ!KoRXOrX z6hB`Zm&DH-<8krx)$y44xja5DeqJ9xA$~56kBOg)<5BT5IVo~KD}UAb{{NQzP5B%0 zpU7X8e=Pr}{BQEV$UhL{0N<6r{p%PjS}snvmdUBs^4etl#Hr}P%7nXk?WDW7A-RiJzEQEfB*srAbfB*srAbq+ zP0dVwXX59RA5L67`OnhNq^k7#Y-)Boc6r^Qd{@cyXpIv)kM9zZxxJewP<9#1G1Yr zHM_IEu5P98q_@)9)%14fY87!{!o8TLZf4cB^mf zW7|F~8U1JKmQ{T;B&}c>x@{Cw!f8!ZTQ0K^xaxkbI5b&%^SQY4+so02+hx_?E19;D z*Z1wJyPa>>yi-^CvSI7uLV7+qXz!WE8=sPt<>hE|&L3ETGY6gb+`v!^oaV%wv!fGM zzPS4&k=5q9eb}R{H5X5~ZR5$M-Xi#NK0Y+VBihE}LA5RJ-yZ^xhQ-F7VealZHQX09 zzngI$HwiV9&86Q=Z}|htKCBIhrT;^`h^AN5Eu&(4m7y)2iYsqk7#cuU(azUx5q~0V zI_u9iq-P}M!i8w#eoMNO(mm!>k8|gc5zJ0_Ce+*Oo45T4BjdlJ%aWpL(dL)@b$(!2 zb+cM&S9^QwnVuZAy3Tf@mn*Ll{4tu^$!%t`;_QudHYWrhRjqez%jgEQuM4ovvLU?6 zwZUzm)k+KMqr%&HIS-f-UN%r}4Uh zd049Iz0V%!VKKr~YivwO%GImUX2vawQ#TJw)kk@2zmj)y(6?hcShT)$uZX>m$WPK$ ziA;D6%^%U;J+q=ddK|RWbX?iFI;=fwF&)Xil+*}y_WA=qZ&-9xw1>)q9O z4?{QQC>?C$@qjnJ!hkm_Z8;uSGIPU%-IsGw)^27c`M)DC4u26}Iz7s7*Q)N9Xs)R$y~10Me7FUNnp1J;Qux8dmFb!+QkyzdN*BV{zr}OT!|d=bBE91#)mg zM8QcX5}d#P|6FV;d?@|V*tf@if9&(ozmNPN^2d=cM!q;U?p%jI2q1s}0#6wW)pyUed;d&!E&Yxv zMwPn5r3q(5DB%sBiq9A^gqqN+?hToC8~ezQbKa-&O0e?%NyA>p zL>1M=Eyat>Q$oYZPW#AFv0+sQ4fGYN6N^_+T5J6w>I z74d@mLZ^M&M!8mUMz}gTY&Rb-;pP4&#wI7c0N?SJI*!L^X}K``;S`BieRPmoSq0;^rjBia7Io0yR)KE zFIZ-6@K9#A(SPh29nqmVTC3H4=ON;a4|`D~Qk{4jo@(#ymmgGgQ_OccR4WL9?b$^L!k8|TkSN-`O3n67jG5&e5!3~KiuFro>5f@uJL z|DP`w1Q0*~0R#|0009ILKmY**!Y{z@|A)VhDIo#~Ab8t$CMBO1Q0*~0R#|0009ILKmY;0|A_$v5I_I{1Q0*~0R#|0009KTFTlV5 z7yde?ga{yj00IagfB*srAbzEQEfB*srAb{b$dOnCISa0)QbtTm{4=+cXH~z zt;|Mh>%Mw7eP7*5-$`$!v#aTCHBrzj2YTJTaKb%YQ#Z5fT6#S#uDY7qUQMl~FRO{N zVeeJFYh^ccYIbLRUF_etO6?o?cJ3K^(Xi_M2@k5p!`5DLYDur#TSmQBt<;UT>eWgo z;k|vsI=rV_db#%!B42k>JL@@h;rG6@ATHrt@976>jfq>5lF_1#D`ur=JT{Au^OjMl zT1B(6>z2H`nJETSQf=Q%b}jvmDmX`-nsDe6!3zk~&W=nmp*3?iB}LPsPYQ0$cGBE@ z1h;-OShvB%qpaM{l|=?y(c-Jjie5JQ_dYaB#y#EM>rW}}Te@vlD;uKnGTB`E&GeS) zyPR=gRP5F2e#IV`T(@=0-We2BtxOullsIusgnTZuk?veryZO1evZ+NQZUxuhE19;D z*Z1wJyWOqcR&6Ic1$MXJn79);=`_3Y>yolO7j4L$FsT>zjIy2=&1zLNzf7^c?KH#R z=#Pxvqg(OdJ4XaV=TZr6c_pr7=7#0jcUnH_+xT2#`!z|Kn~OHfZUp&~e5*HuvExfQ z%E0LsrY~$-ckH(7PF0C!J0PUAl^bzoV__IZpkK=;1Dmgln)`*@YaYKJ_Ohhh6dl{C zPQ(`W>vpxAFI3BAy;96O)WKb^^~Y`S0Qa0$=mrx+^tv^t-Ml`ku?0`cF9x@7++6w? zCntj^zuJ6$(d|9ouez}xObu==2U|ceaabc59GvP-HuLt5^B61|^@3&Aoc^u9=hF+$ z{`y(A%ZD+In_S)Hhl#$dePB55)Q5p0%#f#d&*I?VUO9S49YL;FAb00IagfB*srAb; diff --git a/apps/web/src/app/api/feedback/github/route.js b/apps/web/src/app/api/feedback/github/route.js new file mode 100644 index 0000000..c9f555e --- /dev/null +++ b/apps/web/src/app/api/feedback/github/route.js @@ -0,0 +1,80 @@ +import { createGitHubAdapter, defaultFormatTitle, } from "@ewjdev/anyclick-github/server"; +function isRecord(value) { + return typeof value === "object" && value !== null; +} +function parseGitHubRepo(repoValue) { + const [owner, repo] = repoValue.split("/"); + if (!owner || !repo) { + throw new Error(`GITHUB_REPO is not in valid format. Expected \"owner/repo\", got \"${repoValue}\"`); + } + return { owner, repo }; +} +function readPreferredTitle(metadata) { + if (!isRecord(metadata)) + return undefined; + const github = metadata.github; + if (!isRecord(github)) + return undefined; + const preferredTitle = github.title; + if (typeof preferredTitle !== "string") + return undefined; + const trimmed = preferredTitle.trim(); + return trimmed || undefined; +} +function withRoutingMetadata(metadata) { + const baseMetadata = isRecord(metadata) ? metadata : {}; + const routing = isRecord(baseMetadata.routing) ? baseMetadata.routing : {}; + return { + ...baseMetadata, + routing: { + ...routing, + adapter: "github", + }, + }; +} +export async function POST(req) { + let payload; + try { + payload = (await req.json()); + } + catch { + return Response.json({ + success: false, + error: "Invalid JSON payload", + }, { status: 400 }); + } + const token = process.env.GITHUB_TOKEN; + const repoValue = process.env.GITHUB_REPO ?? "ewjdev/anyclick"; + if (!token) { + return Response.json({ + success: false, + error: "GITHUB_TOKEN is not configured. Set GITHUB_TOKEN in your environment.", + }, { status: 500 }); + } + try { + const { owner, repo } = parseGitHubRepo(repoValue); + const githubAdapter = createGitHubAdapter({ + owner, + repo, + token, + formatTitle: (issuePayload) => readPreferredTitle(issuePayload.metadata) || defaultFormatTitle(issuePayload), + }); + const normalizedPayload = { + ...payload, + metadata: withRoutingMetadata(payload.metadata), + }; + const issue = await githubAdapter.createIssue(normalizedPayload); + return Response.json({ + success: true, + issue, + }); + } + catch (error) { + const errorMessage = error instanceof Error ? error.message : "Failed to create GitHub issue"; + return Response.json({ + success: false, + error: errorMessage, + }, { status: 500 }); + } +} +//# sourceMappingURL=route.js.map \ No newline at end of file diff --git a/apps/web/src/app/api/feedback/github/route.js.map b/apps/web/src/app/api/feedback/github/route.js.map new file mode 100644 index 0000000..eaf6a5c --- /dev/null +++ b/apps/web/src/app/api/feedback/github/route.js.map @@ -0,0 +1 @@ +{"version":3,"file":"route.js","sourceRoot":"","sources":["route.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/apps/web/src/app/api/feedback/github/route.ts b/apps/web/src/app/api/feedback/github/route.ts new file mode 100644 index 0000000..c9b4032 --- /dev/null +++ b/apps/web/src/app/api/feedback/github/route.ts @@ -0,0 +1,111 @@ +import type { AnyclickPayload } from "@ewjdev/anyclick-core"; +import { + createGitHubAdapter, + defaultFormatTitle, +} from "@ewjdev/anyclick-github/server"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function parseGitHubRepo(repoValue: string): { owner: string; repo: string } { + const [owner, repo] = repoValue.split("/"); + + if (!owner || !repo) { + throw new Error( + `GITHUB_REPO is not in valid format. Expected \"owner/repo\", got \"${repoValue}\"`, + ); + } + + return { owner, repo }; +} + +function readPreferredTitle(metadata: unknown): string | undefined { + if (!isRecord(metadata)) return undefined; + const github = metadata.github; + if (!isRecord(github)) return undefined; + + const preferredTitle = github.title; + if (typeof preferredTitle !== "string") return undefined; + + const trimmed = preferredTitle.trim(); + return trimmed || undefined; +} + +function withRoutingMetadata(metadata: unknown): Record { + const baseMetadata = isRecord(metadata) ? metadata : {}; + const routing = isRecord(baseMetadata.routing) ? baseMetadata.routing : {}; + + return { + ...baseMetadata, + routing: { + ...routing, + adapter: "github", + }, + }; +} + +export async function POST(req: Request) { + let payload: AnyclickPayload; + + try { + payload = (await req.json()) as AnyclickPayload; + } catch { + return Response.json( + { + success: false, + error: "Invalid JSON payload", + }, + { status: 400 }, + ); + } + + const token = process.env.GITHUB_TOKEN; + const repoValue = process.env.GITHUB_REPO ?? "ewjdev/anyclick"; + + if (!token) { + return Response.json( + { + success: false, + error: + "GITHUB_TOKEN is not configured. Set GITHUB_TOKEN in your environment.", + }, + { status: 500 }, + ); + } + + try { + const { owner, repo } = parseGitHubRepo(repoValue); + + const githubAdapter = createGitHubAdapter({ + owner, + repo, + token, + formatTitle: (issuePayload) => + readPreferredTitle(issuePayload.metadata) || defaultFormatTitle(issuePayload), + }); + + const normalizedPayload: AnyclickPayload = { + ...payload, + metadata: withRoutingMetadata(payload.metadata), + }; + + const issue = await githubAdapter.createIssue(normalizedPayload); + + return Response.json({ + success: true, + issue, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Failed to create GitHub issue"; + + return Response.json( + { + success: false, + error: errorMessage, + }, + { status: 500 }, + ); + } +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 1e5ed92..c40885c 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,14 +1,34 @@ + "use client"; + import { AnyclickLogo } from "@/components/AnyclickLogo"; +import { AnimatedWord } from "@/components/AnimatedWord"; import { HeroCodeBlock } from "@/components/CodePreview"; import FeaturesSection from "@/components/FeaturesSection"; import { ImmersiveWorkstreamShowcase } from "@/components/ImmersiveWorkstreamShowcase"; import PackagesSection from "@/components/PackagesSection"; import QuickStartSection from "@/components/QuickStartSection"; import RoadmapSummary from "@/components/RoadmapSummary"; +import { useFirstTimeInteraction } from "@/lib/hooks/useFirstTimeInteraction"; import { ArrowRight } from "lucide-react"; +import { motion } from "motion/react"; import Link from "next/link"; +import { useEffect, useState } from "react"; export default function Home() { + const { hasInteracted, markComplete } = useFirstTimeInteraction(); + const [showButton, setShowButton] = useState(hasInteracted); + + useEffect(() => { + if (hasInteracted) { + setShowButton(true); + } + }, [hasInteracted]); + + const handleCycleComplete = () => { + markComplete(); + setShowButton(true); + }; + return (
{/* Animated gradient background */} @@ -79,9 +99,19 @@ export default function Home() { UX done right
- - Why not click on everything? - + + Why not click on + + ? + {/* Subheadline */} @@ -92,7 +122,15 @@ export default function Home() {

{/* CTA Buttons */} -
+ -
+
{/* Code Preview */} diff --git a/apps/web/src/components/AnimatedWord.tsx b/apps/web/src/components/AnimatedWord.tsx new file mode 100644 index 0000000..4ac3ff1 --- /dev/null +++ b/apps/web/src/components/AnimatedWord.tsx @@ -0,0 +1,83 @@ +"use client"; + +/** + * AnimatedWord - Component that cycles through words on click with smooth layout animations. + * + * @module components/AnimatedWord + */ +import { useState } from "react"; +import { AnimatePresence, motion } from "motion/react"; + +interface AnimatedWordProps { + words: string[]; + onCycleComplete?: () => void; + className?: string; +} + +/** + * AnimatedWord component that cycles through an array of words on click. + * When cycling back to the first word (index 0), calls onCycleComplete callback. + * + * @param words - Array of words to cycle through + * @param onCycleComplete - Callback fired when cycling back to the first word + * @param className - Additional CSS classes + */ +export function AnimatedWord({ + words, + onCycleComplete, + className = "", +}: AnimatedWordProps) { + const [currentIndex, setCurrentIndex] = useState(0); + const [hasStarted, setHasStarted] = useState(false); + + const handleClick = () => { + if (!hasStarted) { + // First click: animate out "everything" and show first word + setHasStarted(true); + setCurrentIndex(0); + } else { + // Subsequent clicks: cycle through words + const nextIndex = (currentIndex + 1) % words.length; + setCurrentIndex(nextIndex); + + // If we've cycled back to index 0, trigger completion callback + if (nextIndex === 0 && onCycleComplete) { + onCycleComplete(); + } + } + }; + + const currentWord = hasStarted ? words[currentIndex] : "everything"; + + return ( + { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleClick(); + } + }} + aria-label="Click to cycle through words" + > + + + {currentWord} + + + + ); +} diff --git a/apps/web/src/components/immersive-workstream/cards/EcommerceCard.tsx b/apps/web/src/components/immersive-workstream/cards/EcommerceCard.tsx index 1f5df40..ad56a5b 100644 --- a/apps/web/src/components/immersive-workstream/cards/EcommerceCard.tsx +++ b/apps/web/src/components/immersive-workstream/cards/EcommerceCard.tsx @@ -1,72 +1,320 @@ -export function EcommerceCard() { +import type { CSSProperties } from "react"; +import { PointerProvider } from "@ewjdev/anyclick-pointer"; +import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react"; +import { adapter } from "../adapter"; + +interface EcommerceCardProps { + menuStyle: CSSProperties; + orderMenuItems: ContextMenuItem[]; + pointerCircleColor: string; + pointerColor: string; + pointerIcon: React.ReactNode; + pricingMenuItems: ContextMenuItem[]; + primaryColor: string; + productMenuItems: ContextMenuItem[]; + stockMenuItems: ContextMenuItem[]; +} + +interface ExampleOrder { + customer: string; + discount: string; + fulfillment: string; + id: string; + originalPrice: string; + price: string; + productDescription: string; + productName: string; + shippingStatus: string; + stockLabel: string; +} + +const EXAMPLE_ORDERS: ExampleOrder[] = [ + { + id: "ORD-20481", + customer: "Alex P.", + productName: "Trail Running Jacket", + productDescription: "Weatherproof shell, matte black, size M", + price: "$49.99", + originalPrice: "$59.99", + discount: "17% off", + stockLabel: "In Stock", + fulfillment: "Packed in WH-West", + shippingStatus: "Label printed", + }, + { + id: "ORD-20482", + customer: "Jordan R.", + productName: "Performance Cargo Pack", + productDescription: "34L day pack, ember orange", + price: "$89.00", + originalPrice: "$89.00", + discount: "No active discount", + stockLabel: "Low Stock", + fulfillment: "Pick in progress", + shippingStatus: "Carrier pending", + }, + { + id: "ORD-20483", + customer: "Casey L.", + productName: "Hydro Grip Bottle", + productDescription: "24oz steel bottle, glacier blue", + price: "$28.50", + originalPrice: "$35.00", + discount: "Promo bundle", + stockLabel: "Backorder", + fulfillment: "Supplier transfer", + shippingStatus: "ETA Feb 23", + }, +]; + +function stockToneClass(stockLabel: string): string { + if (stockLabel === "In Stock") { + return "border-green-400/40 bg-green-500/15 text-green-300"; + } + + if (stockLabel === "Low Stock") { + return "border-amber-400/40 bg-amber-500/15 text-amber-200"; + } + + return "border-rose-400/40 bg-rose-500/15 text-rose-200"; +} + +export function EcommerceCard({ + menuStyle, + orderMenuItems, + pointerCircleColor, + pointerColor, + pointerIcon, + pricingMenuItems, + primaryColor, + productMenuItems, + stockMenuItems, +}: EcommerceCardProps) { + const providerTheme = { + menuStyle, + highlightConfig: { + enabled: true, + colors: { + targetColor: primaryColor, + containerColor: `${primaryColor}40`, + }, + }, + }; + return ( -
+
-
-
-
-
-
-
- - $49.99 - - - In Stock - -
+
+
+

+ Live Order Board +

+

+ Product, price, and stock workflows +

+
+
+ 3 active orders
-
-
-
+
+ {EXAMPLE_ORDERS.map((order) => (
-
- - - - - + key={order.id} + className="rounded-xl border border-amber-200/15 bg-black/25 p-3" + data-order-id={order.id} + > +
+ } + scoped + > + +
+
+
+

+ {order.productName} +

+

+ {order.productDescription} +

+

+ Right-click to edit details or upload media +

+
+
+ + + +
+ } + scoped + > + +
+

+ {order.price} +

+

+ {order.originalPrice} +

+

+ {order.discount} +

+
+
+
+ + } + scoped + > + +
+ + {order.stockLabel} + +
+
+
+
+
+ + } + scoped + > + +
+
+
+ Order +

{order.id}

+
+
+ Customer +

{order.customer}

+
+
+ Status +

{order.fulfillment}

+
+
+ +
+
+
+ +

+ {order.shippingStatus} +

+
+ + +
+ ))}
diff --git a/apps/web/src/components/immersive-workstream/cards/SoftwareEditorCard.tsx b/apps/web/src/components/immersive-workstream/cards/SoftwareEditorCard.tsx index 07e8b13..bd3f0c3 100644 --- a/apps/web/src/components/immersive-workstream/cards/SoftwareEditorCard.tsx +++ b/apps/web/src/components/immersive-workstream/cards/SoftwareEditorCard.tsx @@ -1,48 +1,16 @@ "use client"; -import { cn } from "@/lib/utils"; -import { useCallback, useEffect, useState } from "react"; +import { useState } from "react"; import { javascript } from "@codemirror/lang-javascript"; -import type { ContextMenuItem } from "@ewjdev/anyclick-react"; import CodeMirror from "@uiw/react-codemirror"; -import { parseMenuItems } from "../parseMenuItems"; import { DEFAULT_SOFTWARE_CODE } from "../softwareCode"; import { matrixHighlightStyle, matrixTheme } from "../themes/matrixTheme"; -interface SoftwareEditorCardProps { - onMenuItemsChange: (items: ContextMenuItem[]) => void; -} - -export function SoftwareEditorCard({ - onMenuItemsChange, -}: SoftwareEditorCardProps) { - const [code, setCode] = useState(DEFAULT_SOFTWARE_CODE); - const [parseError, setParseError] = useState(false); - - const handleCodeChange = useCallback( - (value: string) => { - setCode(value); - const parsed = parseMenuItems(value); - if (parsed) { - setParseError(false); - onMenuItemsChange(parsed); - } else { - setParseError(true); - } - }, - [onMenuItemsChange], - ); - - useEffect(() => { - const parsed = parseMenuItems(DEFAULT_SOFTWARE_CODE); - if (parsed) { - onMenuItemsChange(parsed); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); +export function SoftwareEditorCard() { + const [code] = useState(DEFAULT_SOFTWARE_CODE); return ( -
+
- - {parseError ? "Syntax error" : "Config valid"} + + Workflow actions fixed for this demo
- Live Preview Active + Right-click to run prototype flows
diff --git a/apps/web/src/components/immersive-workstream/sections/DefaultWorkstreamSection.tsx b/apps/web/src/components/immersive-workstream/sections/DefaultWorkstreamSection.tsx index d0dc02d..f10a53c 100644 --- a/apps/web/src/components/immersive-workstream/sections/DefaultWorkstreamSection.tsx +++ b/apps/web/src/components/immersive-workstream/sections/DefaultWorkstreamSection.tsx @@ -5,6 +5,9 @@ import { type MotionValue, motion } from "motion/react"; import { FloatingIcon } from "../FloatingIcon"; import { adapter } from "../adapter"; import type { ImmersiveTheme } from "../types"; +import { WorkflowDrawer } from "../workflows/WorkflowDrawer"; +import { isWorkflowWorkstreamId } from "../workflows/registry"; +import { useWorkflowLauncher } from "../workflows/useWorkflowLauncher"; interface DefaultWorkstreamSectionProps { theme: ImmersiveTheme; @@ -19,6 +22,13 @@ export function DefaultWorkstreamSection({ isInView, bgY, }: DefaultWorkstreamSectionProps) { + const workflowWorkstreamId = isWorkflowWorkstreamId(theme.id) + ? theme.id + : "software"; + const { activeWorkflow, closeWorkflow, menuItems } = useWorkflowLauncher( + workflowWorkstreamId, + ); + return ( <> + + ); } diff --git a/apps/web/src/components/immersive-workstream/sections/EcommerceSection.tsx b/apps/web/src/components/immersive-workstream/sections/EcommerceSection.tsx new file mode 100644 index 0000000..19fd031 --- /dev/null +++ b/apps/web/src/components/immersive-workstream/sections/EcommerceSection.tsx @@ -0,0 +1,143 @@ +import { useMemo } from "react"; +import { MousePointer2 } from "lucide-react"; +import { type MotionValue, motion } from "motion/react"; +import { FloatingIcon } from "../FloatingIcon"; +import { EcommerceCard } from "../cards/EcommerceCard"; +import type { ImmersiveTheme } from "../types"; +import { WorkflowDrawer } from "../workflows/WorkflowDrawer"; +import { useWorkflowLauncher } from "../workflows/useWorkflowLauncher"; + +interface EcommerceSectionProps { + bgY: MotionValue; + isInView: boolean; + scrollYProgress: MotionValue; + theme: ImmersiveTheme; +} + +export function EcommerceSection({ + bgY, + isInView, + scrollYProgress, + theme, +}: EcommerceSectionProps) { + const { activeWorkflow, closeWorkflow, getMenuItemsForActionIds } = + useWorkflowLauncher("ecommerce"); + + const orderMenuItems = useMemo( + () => + getMenuItemsForActionIds([ + "ecommerce.item_missing", + "ecommerce.shipping_issue", + "ecommerce.escalate_order", + ]), + [getMenuItemsForActionIds], + ); + + const stockMenuItems = useMemo( + () => + getMenuItemsForActionIds([ + "ecommerce.view_stock_details", + "ecommerce.adjust_stock", + "ecommerce.set_restock_alert", + ]), + [getMenuItemsForActionIds], + ); + + const pricingMenuItems = useMemo( + () => + getMenuItemsForActionIds([ + "ecommerce.view_discounts", + "ecommerce.change_price", + "ecommerce.schedule_promotion", + ]), + [getMenuItemsForActionIds], + ); + + const productMenuItems = useMemo( + () => + getMenuItemsForActionIds([ + "ecommerce.edit_product", + "ecommerce.upload_product_media", + "ecommerce.update_product_copy", + ]), + [getMenuItemsForActionIds], + ); + + return ( + <> + + + {theme.gridPattern && ( +
+ )} + + {theme.floatingElements.map((element, index) => ( + + ))} + +
+ +

+ {theme.title} +

+

{theme.tagline}

+
+ +
+
+ + + + + Right-click stock, pricing, product details, or order rows + + +
+
+
+ +
+ + + + ); +} diff --git a/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx b/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx index b58903d..681c0f1 100644 --- a/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx +++ b/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx @@ -7,6 +7,9 @@ import { adapter } from "../adapter"; import { HealthcareMedicalBackground } from "../backgrounds/HealthcareMedicalBackground"; import { HealthcareCard } from "../cards/HealthcareCard"; import type { ImmersiveTheme } from "../types"; +import { WorkflowDrawer } from "../workflows/WorkflowDrawer"; +import { isWorkflowWorkstreamId } from "../workflows/registry"; +import { useWorkflowLauncher } from "../workflows/useWorkflowLauncher"; interface HealthcareSectionProps { theme: ImmersiveTheme; @@ -21,6 +24,13 @@ export function HealthcareSection({ isInView, bgY, }: HealthcareSectionProps) { + const workflowWorkstreamId = isWorkflowWorkstreamId(theme.id) + ? theme.id + : "healthcare"; + const { activeWorkflow, closeWorkflow, menuItems } = useWorkflowLauncher( + workflowWorkstreamId, + ); + return ( <> + + ); } diff --git a/apps/web/src/components/immersive-workstream/sections/SoftwareDevelopmentSection.tsx b/apps/web/src/components/immersive-workstream/sections/SoftwareDevelopmentSection.tsx index ac7df9b..74618be 100644 --- a/apps/web/src/components/immersive-workstream/sections/SoftwareDevelopmentSection.tsx +++ b/apps/web/src/components/immersive-workstream/sections/SoftwareDevelopmentSection.tsx @@ -1,13 +1,14 @@ -import { useCallback, useState } from "react"; import { PointerProvider } from "@ewjdev/anyclick-pointer"; import { AnyclickProvider } from "@ewjdev/anyclick-react"; -import type { ContextMenuItem } from "@ewjdev/anyclick-react"; import { MousePointer2 } from "lucide-react"; import { type MotionValue, motion } from "motion/react"; import { FloatingIcon } from "../FloatingIcon"; import { adapter } from "../adapter"; import { SoftwareEditorCard } from "../cards/SoftwareEditorCard"; import type { ImmersiveTheme } from "../types"; +import { WorkflowDrawer } from "../workflows/WorkflowDrawer"; +import { isWorkflowWorkstreamId } from "../workflows/registry"; +import { useWorkflowLauncher } from "../workflows/useWorkflowLauncher"; interface SoftwareDevelopmentSectionProps { theme: ImmersiveTheme; @@ -22,12 +23,11 @@ export function SoftwareDevelopmentSection({ isInView, bgY, }: SoftwareDevelopmentSectionProps) { - const [menuItems, setMenuItems] = useState( - theme.menuItems, - ); - const handleMenuItemsChange = useCallback( - (items: ContextMenuItem[]) => setMenuItems(items), - [], + const workflowWorkstreamId = isWorkflowWorkstreamId(theme.id) + ? theme.id + : "software"; + const { activeWorkflow, closeWorkflow, menuItems } = useWorkflowLauncher( + workflowWorkstreamId, ); return ( @@ -115,7 +115,7 @@ export function SoftwareDevelopmentSection({ }} >
- + - Right-click to see your custom menu + Right-click to launch workflow demos
@@ -139,6 +139,8 @@ export function SoftwareDevelopmentSection({ opacity: 0.3, }} /> + + ); } diff --git a/apps/web/src/components/immersive-workstream/sections/WorkstreamSection.tsx b/apps/web/src/components/immersive-workstream/sections/WorkstreamSection.tsx index 80b74fb..a888810 100644 --- a/apps/web/src/components/immersive-workstream/sections/WorkstreamSection.tsx +++ b/apps/web/src/components/immersive-workstream/sections/WorkstreamSection.tsx @@ -2,6 +2,7 @@ import { useRef } from "react"; import { useInView, useScroll, useTransform } from "motion/react"; import type { ImmersiveTheme } from "../types"; import { DefaultWorkstreamSection } from "./DefaultWorkstreamSection"; +import { EcommerceSection } from "./EcommerceSection"; import { HealthcareSection } from "./HealthcareSection"; import { SoftwareDevelopmentSection } from "./SoftwareDevelopmentSection"; @@ -50,6 +51,21 @@ export function WorkstreamSection({ theme }: WorkstreamSectionProps) { /> ); + case "ecommerce": + return ( +
+ +
+ ); default: return (
Issue Details -> Review" }, + { label: "Send to Cursor", flow: "Fix Scope -> Prompt & Constraints -> Review" }, + { label: "Copy selector", flow: "Selector Format -> Copy Bundle" }, ]; -// Right-click anywhere to see your menu in action! +// This editor remains visible to show how the workflow configuration might look. `; diff --git a/apps/web/src/components/immersive-workstream/themes.tsx b/apps/web/src/components/immersive-workstream/themes.tsx index 572bcd6..f45737f 100644 --- a/apps/web/src/components/immersive-workstream/themes.tsx +++ b/apps/web/src/components/immersive-workstream/themes.tsx @@ -13,7 +13,6 @@ import { Truck, Users, } from "lucide-react"; -import { EcommerceCard } from "./cards/EcommerceCard"; import { HealthcareCard } from "./cards/HealthcareCard"; import { SocialCard } from "./cards/SocialCard"; import type { ImmersiveTheme } from "./types"; @@ -22,7 +21,7 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [ { id: "software", title: "Software Development", - tagline: "Edit the code below to customize your context menu in real-time", + tagline: "Right-click to run full development workflow prototypes", primaryColor: "#00ff41", secondaryColor: "#22c55e", glowColor: "rgba(0, 255, 65, 0.4)", @@ -77,11 +76,6 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [ }, ], cardContent: null, - menuItems: [ - { label: "Report bug", type: "bug", showComment: true }, - { label: "Send to Cursor", type: "cursor", showComment: false }, - { label: "Copy selector", type: "selector", showComment: false }, - ], menuStyle: { "--anyclick-menu-bg": "#0a0a0a", "--anyclick-menu-border": "#00ff41", @@ -146,12 +140,7 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [ opacity: 0.4, }, ], - cardContent: , - menuItems: [ - { label: "Item missing", type: "missing", showComment: true }, - { label: "Shipping issue", type: "shipping", showComment: true }, - { label: "Escalate", type: "escalate", showComment: false }, - ], + cardContent: null, menuStyle: { "--anyclick-menu-bg": "#1c1917", "--anyclick-menu-border": "#f59e0b", @@ -209,11 +198,6 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [ }, ], cardContent: , - menuItems: [ - { label: "Check-in issue", type: "checkin", showComment: true }, - { label: "Vital alert", type: "vital", showComment: true }, - { label: "Flag urgent", type: "urgent", showComment: false }, - ], menuStyle: { "--anyclick-menu-bg": "#ffffff", "--anyclick-menu-border": "#10b981", @@ -280,11 +264,6 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [ }, ], cardContent: , - menuItems: [ - { label: "Save asset", type: "save", showComment: false }, - { label: "Flag content", type: "flag", showComment: true }, - { label: "Quick reply", type: "reply", showComment: true }, - ], menuStyle: { "--anyclick-menu-bg": "rgba(236, 72, 153, 0.1)", "--anyclick-menu-border": "rgba(236, 72, 153, 0.3)", diff --git a/apps/web/src/components/immersive-workstream/types.ts b/apps/web/src/components/immersive-workstream/types.ts index 0b37842..78ea10e 100644 --- a/apps/web/src/components/immersive-workstream/types.ts +++ b/apps/web/src/components/immersive-workstream/types.ts @@ -1,5 +1,4 @@ import type { CSSProperties, ReactNode } from "react"; -import type { ContextMenuItem } from "@ewjdev/anyclick-react"; export interface ImmersiveTheme { id: string; @@ -14,7 +13,6 @@ export interface ImmersiveTheme { backgroundImage?: string; floatingElements: FloatingElement[]; cardContent: ReactNode; - menuItems: ContextMenuItem[]; menuStyle: CSSProperties; pointerIcon: ReactNode; } diff --git a/apps/web/src/components/immersive-workstream/workflows/WorkflowContextSummary.tsx b/apps/web/src/components/immersive-workstream/workflows/WorkflowContextSummary.tsx new file mode 100644 index 0000000..78df4c0 --- /dev/null +++ b/apps/web/src/components/immersive-workstream/workflows/WorkflowContextSummary.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useState } from "react"; +import { Loader2, RefreshCw } from "lucide-react"; +import type { WorkflowCaptureState } from "./types"; + +interface WorkflowContextSummaryProps { + captureState: WorkflowCaptureState; + onRetryCapture: () => Promise; +} + +export function WorkflowContextSummary({ + captureState, + onRetryCapture, +}: WorkflowContextSummaryProps) { + const [isExpanded, setIsExpanded] = useState(false); + const data = captureState.data; + + return ( +
+
+

+ Captured Context +

+ + +
+ +

+ This workflow auto-captures selectors, page metadata, and screenshot + evidence so teams can act on precise context instead of manually + re-entering details and risking handoff mistakes. +

+ + {!isExpanded ? null : captureState.status === "capturing" ? ( +
+
+ + Capturing target and container context... +
+
+ ) : !data ? ( +
+
+ Context capture is unavailable. + +
+ {captureState.error && ( +

+ {captureState.error} +

+ )} +
+ ) : ( + <> +
+

+ + Target: + {" "} + + {data.targetSelector} + +

+

+ + Container: + {" "} + + {data.containerSelector} + +

+
+ +
+
+
+ Page +
+
{data.pageContext.title}
+
+ {data.pageContext.url} +
+
+
+ + {data.containerScreenshot ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Container screenshot +
+ ) : ( +
+ Screenshot unavailable. + {data.screenshotError ? ` ${data.screenshotError}` : ""} +
+ )} + + )} +
+ ); +} diff --git a/apps/web/src/components/immersive-workstream/workflows/WorkflowDrawer.tsx b/apps/web/src/components/immersive-workstream/workflows/WorkflowDrawer.tsx new file mode 100644 index 0000000..f631066 --- /dev/null +++ b/apps/web/src/components/immersive-workstream/workflows/WorkflowDrawer.tsx @@ -0,0 +1,243 @@ +"use client"; + +import type { CSSProperties } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { X } from "lucide-react"; +import { captureWorkflowContext } from "./contextCapture"; +import { WorkflowFlowRenderer } from "./WorkflowFlowRenderer"; +import type { + WorkflowCaptureState, + WorkflowLaunchState, + WorkflowWorkstreamId, +} from "./types"; + +interface WorkflowDrawerProps { + activeWorkflow: WorkflowLaunchState | null; + onClose: () => void; +} + +interface WorkflowPalette { + accent: string; + accentSoftBg: string; + accentSoftBorder: string; + accentStrongBg: string; + accentText: string; + accentTextMuted: string; + closeHoverBg: string; +} + +const WORKSTREAM_PALETTES: Record = { + software: { + accent: "#00ff41", + accentSoftBg: "rgba(0, 255, 65, 0.12)", + accentSoftBorder: "rgba(0, 255, 65, 0.35)", + accentStrongBg: "rgba(0, 255, 65, 0.95)", + accentText: "#c7ffd8", + accentTextMuted: "rgba(151, 255, 185, 0.8)", + closeHoverBg: "rgba(0, 255, 65, 0.16)", + }, + ecommerce: { + accent: "#f59e0b", + accentSoftBg: "rgba(245, 158, 11, 0.14)", + accentSoftBorder: "rgba(245, 158, 11, 0.38)", + accentStrongBg: "rgba(245, 158, 11, 0.96)", + accentText: "#ffe8b8", + accentTextMuted: "rgba(255, 212, 128, 0.82)", + closeHoverBg: "rgba(245, 158, 11, 0.18)", + }, + healthcare: { + accent: "#10b981", + accentSoftBg: "rgba(16, 185, 129, 0.13)", + accentSoftBorder: "rgba(16, 185, 129, 0.34)", + accentStrongBg: "rgba(16, 185, 129, 0.95)", + accentText: "#c6ffec", + accentTextMuted: "rgba(152, 255, 224, 0.82)", + closeHoverBg: "rgba(16, 185, 129, 0.16)", + }, + social: { + accent: "#ec4899", + accentSoftBg: "rgba(236, 72, 153, 0.14)", + accentSoftBorder: "rgba(236, 72, 153, 0.36)", + accentStrongBg: "rgba(236, 72, 153, 0.95)", + accentText: "#ffd7ea", + accentTextMuted: "rgba(255, 186, 223, 0.84)", + closeHoverBg: "rgba(236, 72, 153, 0.17)", + }, +}; + +function getDrawerStyle(workstream: WorkflowWorkstreamId): CSSProperties { + const palette = WORKSTREAM_PALETTES[workstream]; + + return { + "--workflow-accent": palette.accent, + "--workflow-accent-soft-bg": palette.accentSoftBg, + "--workflow-accent-soft-border": palette.accentSoftBorder, + "--workflow-accent-strong-bg": palette.accentStrongBg, + "--workflow-accent-text": palette.accentText, + "--workflow-accent-text-muted": palette.accentTextMuted, + "--workflow-close-hover-bg": palette.closeHoverBg, + } as CSSProperties; +} + +const INITIAL_CAPTURE_STATE: WorkflowCaptureState = { + data: null, + status: "idle", +}; + +export function WorkflowDrawer({ + activeWorkflow, + onClose, +}: WorkflowDrawerProps) { + const [captureState, setCaptureState] = + useState(INITIAL_CAPTURE_STATE); + + const drawerStyle = getDrawerStyle( + activeWorkflow?.action.workstream ?? "software", + ); + + const refreshCapture = useCallback(async () => { + if (!activeWorkflow) return; + + setCaptureState({ data: null, status: "capturing" }); + + try { + const data = await captureWorkflowContext( + activeWorkflow.targetElement, + activeWorkflow.containerElement, + ); + + setCaptureState({ + data, + status: "ready", + }); + } catch (error) { + setCaptureState({ + data: null, + error: + error instanceof Error + ? error.message + : "Failed to capture workflow context", + status: "ready", + }); + } + }, [activeWorkflow]); + + useEffect(() => { + if (!activeWorkflow) { + setCaptureState(INITIAL_CAPTURE_STATE); + return; + } + + let isActive = true; + + setCaptureState({ data: null, status: "capturing" }); + + captureWorkflowContext( + activeWorkflow.targetElement, + activeWorkflow.containerElement, + ) + .then((data) => { + if (!isActive) return; + setCaptureState({ data, status: "ready" }); + }) + .catch((error) => { + if (!isActive) return; + setCaptureState({ + data: null, + error: + error instanceof Error + ? error.message + : "Failed to capture workflow context", + status: "ready", + }); + }); + + return () => { + isActive = false; + }; + }, [activeWorkflow]); + + useEffect(() => { + if (!activeWorkflow) return; + + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + + return () => { + document.body.style.overflow = previousOverflow; + }; + }, [activeWorkflow]); + + useEffect(() => { + if (!activeWorkflow) return; + + const onEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + + window.addEventListener("keydown", onEscape); + return () => window.removeEventListener("keydown", onEscape); + }, [activeWorkflow, onClose]); + + if (!activeWorkflow) { + return null; + } + + return ( +
+ +
+ + + +
+ ); +} diff --git a/apps/web/src/components/immersive-workstream/workflows/WorkflowFlowRenderer.tsx b/apps/web/src/components/immersive-workstream/workflows/WorkflowFlowRenderer.tsx new file mode 100644 index 0000000..72ef932 --- /dev/null +++ b/apps/web/src/components/immersive-workstream/workflows/WorkflowFlowRenderer.tsx @@ -0,0 +1,2292 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + buildAnyclickPayload, + type AnyclickPayload, + type ScreenshotData, +} from "@ewjdev/anyclick-core"; +import { + AlertCircle, + CheckCircle2, + Clipboard, + ExternalLink, +} from "lucide-react"; +import { buildSelectorBundle, generateHeuristicIssueTitle } from "./titleHeuristics"; +import { WorkflowContextSummary } from "./WorkflowContextSummary"; +import type { + WorkflowActionId, + WorkflowCaptureData, + WorkflowCaptureState, + WorkflowLaunchState, +} from "./types"; + +type GenericActionId = Exclude< + WorkflowActionId, + "software.report_bug" | "software.copy_selector" +>; + +type FieldKind = "select" | "text" | "textarea"; + +interface FieldOption { + label: string; + value: string; +} + +interface FieldConfig { + key: string; + kind: FieldKind; + label: string; + options?: FieldOption[]; + placeholder?: string; + required?: boolean; +} + +interface GenericStepConfig { + description: string; + fields?: FieldConfig[]; + kind?: "form" | "review"; + title: string; +} + +interface GenericWorkflowConfig { + successMessage: string; + successTitle: string; + steps: GenericStepConfig[]; +} + +interface WorkflowFlowRendererProps { + captureState: WorkflowCaptureState; + launch: WorkflowLaunchState; + onClose: () => void; + onRetryCapture: () => Promise; +} + +interface WorkflowSuccessState { + demo: boolean; + issueUrl?: string; + message: string; + title: string; +} + +const GENERIC_WORKFLOWS: Record = { + "ecommerce.escalate_order": { + successTitle: "Escalation workflow simulated", + successMessage: + "Order escalation payload was assembled with context and ready for adapter routing.", + steps: [ + { + title: "Escalation Target", + description: "Route this incident to the right team.", + kind: "form", + fields: [ + { + key: "targetTeam", + kind: "select", + label: "Target Team", + required: true, + options: [ + { value: "fulfillment", label: "Fulfillment" }, + { value: "support", label: "Customer Support" }, + { value: "operations", label: "Operations" }, + ], + }, + { + key: "escalationReason", + kind: "textarea", + label: "Escalation Reason", + placeholder: "Describe why this order should be escalated.", + required: true, + }, + ], + }, + { + title: "Priority & SLA", + description: "Set urgency and expected response window.", + kind: "form", + fields: [ + { + key: "priority", + kind: "select", + label: "Priority", + required: true, + options: [ + { value: "high", label: "High" }, + { value: "medium", label: "Medium" }, + { value: "low", label: "Low" }, + ], + }, + { + key: "slaWindow", + kind: "select", + label: "Target SLA", + required: true, + options: [ + { value: "30m", label: "30 minutes" }, + { value: "2h", label: "2 hours" }, + { value: "24h", label: "24 hours" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review the escalation request.", + kind: "review", + }, + ], + }, + "ecommerce.item_missing": { + successTitle: "Missing item workflow simulated", + successMessage: + "Incident details are packaged with selectors and screenshot context for routing.", + steps: [ + { + title: "Incident Details", + description: "Capture missing item details.", + kind: "form", + fields: [ + { + key: "orderId", + kind: "text", + label: "Order ID", + placeholder: "ORD-102944", + required: true, + }, + { + key: "sku", + kind: "text", + label: "SKU", + placeholder: "SKU-3482", + required: true, + }, + { + key: "quantityMissing", + kind: "text", + label: "Quantity Missing", + placeholder: "1", + required: true, + }, + ], + }, + { + title: "Resolution Path", + description: "Define next action and owner.", + kind: "form", + fields: [ + { + key: "resolutionPath", + kind: "select", + label: "Resolution Path", + required: true, + options: [ + { value: "reship", label: "Reship item" }, + { value: "refund", label: "Refund line item" }, + { value: "investigate", label: "Investigate warehouse pick" }, + ], + }, + { + key: "owner", + kind: "select", + label: "Owner", + required: true, + options: [ + { value: "warehouse", label: "Warehouse Lead" }, + { value: "support", label: "Support Manager" }, + { value: "ops", label: "Operations Duty" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review details and complete.", + kind: "review", + }, + ], + }, + "ecommerce.shipping_issue": { + successTitle: "Shipping issue workflow simulated", + successMessage: + "Shipping disruption data was captured with page and DOM context.", + steps: [ + { + title: "Shipment Details", + description: "Capture shipment-level context.", + kind: "form", + fields: [ + { + key: "carrier", + kind: "select", + label: "Carrier", + required: true, + options: [ + { value: "ups", label: "UPS" }, + { value: "fedex", label: "FedEx" }, + { value: "usps", label: "USPS" }, + { value: "other", label: "Other" }, + ], + }, + { + key: "trackingId", + kind: "text", + label: "Tracking ID", + placeholder: "1Z999AA10123456784", + required: true, + }, + ], + }, + { + title: "Customer Impact", + description: "Capture impact and communication strategy.", + kind: "form", + fields: [ + { + key: "impactLevel", + kind: "select", + label: "Impact Level", + required: true, + options: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High" }, + ], + }, + { + key: "customerMessage", + kind: "textarea", + label: "Customer Message", + placeholder: "Draft a customer-facing update.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review details and complete.", + kind: "review", + }, + ], + }, + "ecommerce.view_stock_details": { + successTitle: "Stock details workflow simulated", + successMessage: + "Inventory and warehouse context were assembled for operational review.", + steps: [ + { + title: "Inventory Snapshot", + description: "Inspect the current stock position.", + kind: "form", + fields: [ + { + key: "sku", + kind: "text", + label: "SKU", + placeholder: "SKU-4821", + required: true, + }, + { + key: "availableUnits", + kind: "text", + label: "Available Units", + placeholder: "128", + required: true, + }, + { + key: "reservedUnits", + kind: "text", + label: "Reserved Units", + placeholder: "27", + required: true, + }, + ], + }, + { + title: "Source Location", + description: "Review warehouse and replenishment source.", + kind: "form", + fields: [ + { + key: "warehouse", + kind: "select", + label: "Primary Warehouse", + required: true, + options: [ + { value: "wh-west", label: "WH-West" }, + { value: "wh-central", label: "WH-Central" }, + { value: "wh-east", label: "WH-East" }, + ], + }, + { + key: "restockEta", + kind: "text", + label: "Restock ETA", + placeholder: "2026-02-22 14:00", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review stock context and complete.", + kind: "review", + }, + ], + }, + "ecommerce.adjust_stock": { + successTitle: "Stock adjustment workflow simulated", + successMessage: + "Inventory adjustment instructions were prepared with captured UI context.", + steps: [ + { + title: "Adjustment Type", + description: "Choose how inventory should be adjusted.", + kind: "form", + fields: [ + { + key: "adjustmentType", + kind: "select", + label: "Adjustment Type", + required: true, + options: [ + { value: "increase", label: "Increase stock" }, + { value: "decrease", label: "Decrease stock" }, + { value: "release", label: "Release reserved units" }, + ], + }, + { + key: "channel", + kind: "select", + label: "Inventory Channel", + required: true, + options: [ + { value: "online", label: "Online store" }, + { value: "marketplace", label: "Marketplace" }, + { value: "all", label: "All channels" }, + ], + }, + ], + }, + { + title: "Quantity & Reason", + description: "Specify quantity change and audit reason.", + kind: "form", + fields: [ + { + key: "delta", + kind: "text", + label: "Quantity Delta", + placeholder: "+24", + required: true, + }, + { + key: "adjustReason", + kind: "textarea", + label: "Reason", + placeholder: "Cycle count reconciliation after receiving batch.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review adjustment details and complete.", + kind: "review", + }, + ], + }, + "ecommerce.set_restock_alert": { + successTitle: "Restock alert workflow simulated", + successMessage: + "Threshold and notification routing were captured for alert automation.", + steps: [ + { + title: "Threshold", + description: "Define when low-stock alerts should fire.", + kind: "form", + fields: [ + { + key: "thresholdUnits", + kind: "text", + label: "Threshold Units", + placeholder: "20", + required: true, + }, + { + key: "lookbackWindow", + kind: "select", + label: "Sales Velocity Window", + required: true, + options: [ + { value: "7d", label: "Last 7 days" }, + { value: "14d", label: "Last 14 days" }, + { value: "30d", label: "Last 30 days" }, + ], + }, + ], + }, + { + title: "Notification Route", + description: "Select where alerts should be sent.", + kind: "form", + fields: [ + { + key: "notifyTarget", + kind: "select", + label: "Notify Target", + required: true, + options: [ + { value: "ops-slack", label: "Ops Slack" }, + { value: "inventory-email", label: "Inventory Email" }, + { value: "pager", label: "On-call Pager" }, + ], + }, + { + key: "escalationDelay", + kind: "select", + label: "Escalation Delay", + required: true, + options: [ + { value: "15m", label: "15 minutes" }, + { value: "60m", label: "1 hour" }, + { value: "4h", label: "4 hours" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review alert configuration and complete.", + kind: "review", + }, + ], + }, + "ecommerce.view_discounts": { + successTitle: "Discount review workflow simulated", + successMessage: + "Discount and margin context were prepared for pricing review.", + steps: [ + { + title: "Active Promotions", + description: "Inspect currently applied discount programs.", + kind: "form", + fields: [ + { + key: "promoSource", + kind: "select", + label: "Promotion Source", + required: true, + options: [ + { value: "catalog", label: "Catalog pricing rules" }, + { value: "coupon", label: "Coupon campaign" }, + { value: "bundle", label: "Bundle offer" }, + ], + }, + { + key: "activeDiscount", + kind: "text", + label: "Current Discount", + placeholder: "15%", + required: true, + }, + ], + }, + { + title: "Margin Impact", + description: "Evaluate margin effect before changes.", + kind: "form", + fields: [ + { + key: "grossMargin", + kind: "text", + label: "Gross Margin", + placeholder: "32%", + required: true, + }, + { + key: "impactNotes", + kind: "textarea", + label: "Impact Notes", + placeholder: "Describe expected revenue and conversion impact.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review pricing signals and complete.", + kind: "review", + }, + ], + }, + "ecommerce.change_price": { + successTitle: "Price change workflow simulated", + successMessage: + "Price update details were captured with guardrails and approval routing.", + steps: [ + { + title: "New Price", + description: "Set the new price and effective window.", + kind: "form", + fields: [ + { + key: "currentPrice", + kind: "text", + label: "Current Price", + placeholder: "49.99", + required: true, + }, + { + key: "newPrice", + kind: "text", + label: "New Price", + placeholder: "44.99", + required: true, + }, + { + key: "effectiveAt", + kind: "text", + label: "Effective At", + placeholder: "2026-02-20 09:00", + required: true, + }, + ], + }, + { + title: "Guardrails", + description: "Validate floor and approval requirements.", + kind: "form", + fields: [ + { + key: "floorPrice", + kind: "text", + label: "Floor Price", + placeholder: "39.99", + required: true, + }, + { + key: "approvalRoute", + kind: "select", + label: "Approval Route", + required: true, + options: [ + { value: "pricing-manager", label: "Pricing manager" }, + { value: "merch-lead", label: "Merchandising lead" }, + { value: "auto", label: "Auto-approved under threshold" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review the price update and complete.", + kind: "review", + }, + ], + }, + "ecommerce.schedule_promotion": { + successTitle: "Promotion scheduling workflow simulated", + successMessage: + "Promotion timing and audience parameters were prepared for launch.", + steps: [ + { + title: "Promotion Details", + description: "Define promotion mechanics.", + kind: "form", + fields: [ + { + key: "promotionType", + kind: "select", + label: "Promotion Type", + required: true, + options: [ + { value: "percent", label: "Percent off" }, + { value: "fixed", label: "Fixed amount off" }, + { value: "bogo", label: "BOGO" }, + ], + }, + { + key: "promotionValue", + kind: "text", + label: "Promotion Value", + placeholder: "20%", + required: true, + }, + ], + }, + { + title: "Timing & Audience", + description: "Configure activation window and target segment.", + kind: "form", + fields: [ + { + key: "window", + kind: "text", + label: "Active Window", + placeholder: "2026-02-21 to 2026-02-28", + required: true, + }, + { + key: "audience", + kind: "select", + label: "Audience", + required: true, + options: [ + { value: "all", label: "All customers" }, + { value: "new", label: "New customers" }, + { value: "vip", label: "VIP segment" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review the promotion setup and complete.", + kind: "review", + }, + ], + }, + "ecommerce.edit_product": { + successTitle: "Product edit workflow simulated", + successMessage: + "Product metadata and merchandising changes were staged with context.", + steps: [ + { + title: "Core Fields", + description: "Update product metadata.", + kind: "form", + fields: [ + { + key: "productName", + kind: "text", + label: "Product Name", + placeholder: "Trail Running Jacket", + required: true, + }, + { + key: "category", + kind: "select", + label: "Category", + required: true, + options: [ + { value: "outerwear", label: "Outerwear" }, + { value: "footwear", label: "Footwear" }, + { value: "accessories", label: "Accessories" }, + ], + }, + ], + }, + { + title: "Description & Highlights", + description: "Tune copy and merchandising bullets.", + kind: "form", + fields: [ + { + key: "shortDescription", + kind: "textarea", + label: "Short Description", + placeholder: "Lightweight shell with weatherproof finish.", + required: true, + }, + { + key: "highlightBullet", + kind: "text", + label: "Highlight Bullet", + placeholder: "Breathable 3-layer fabric", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review product edits and complete.", + kind: "review", + }, + ], + }, + "ecommerce.upload_product_media": { + successTitle: "Product media workflow simulated", + successMessage: + "Media upload details were captured for image pipeline handoff.", + steps: [ + { + title: "Media Source", + description: "Select media input details.", + kind: "form", + fields: [ + { + key: "mediaType", + kind: "select", + label: "Media Type", + required: true, + options: [ + { value: "image", label: "Image" }, + { value: "video", label: "Video" }, + { value: "360", label: "360 view" }, + ], + }, + { + key: "sourcePath", + kind: "text", + label: "Source URL / Path", + placeholder: "https://cdn.example.com/product/sku-4821/front.jpg", + required: true, + }, + ], + }, + { + title: "Variants & Alt Text", + description: "Map to variants and accessibility copy.", + kind: "form", + fields: [ + { + key: "variantMap", + kind: "text", + label: "Variant Mapping", + placeholder: "color:black,size:m", + required: true, + }, + { + key: "altText", + kind: "textarea", + label: "Alt Text", + placeholder: "Front view of black trail running jacket on white background.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review media updates and complete.", + kind: "review", + }, + ], + }, + "ecommerce.update_product_copy": { + successTitle: "Product copy workflow simulated", + successMessage: + "Content edits were captured with SEO and tone guidance.", + steps: [ + { + title: "Copy Focus", + description: "Choose which copy section to revise.", + kind: "form", + fields: [ + { + key: "copySection", + kind: "select", + label: "Copy Section", + required: true, + options: [ + { value: "headline", label: "Headline" }, + { value: "description", label: "Description" }, + { value: "bullets", label: "Feature bullets" }, + ], + }, + { + key: "draftCopy", + kind: "textarea", + label: "Draft Copy", + placeholder: "Write the updated product copy.", + required: true, + }, + ], + }, + { + title: "Tone & SEO", + description: "Set tone and search target phrase.", + kind: "form", + fields: [ + { + key: "copyTone", + kind: "select", + label: "Tone", + required: true, + options: [ + { value: "premium", label: "Premium" }, + { value: "practical", label: "Practical" }, + { value: "energetic", label: "Energetic" }, + ], + }, + { + key: "seoPhrase", + kind: "text", + label: "SEO Phrase", + placeholder: "lightweight waterproof running jacket", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review content changes and complete.", + kind: "review", + }, + ], + }, + "healthcare.check_in_issue": { + successTitle: "Check-in issue workflow simulated", + successMessage: + "Non-PHI intake issue details are prepared for front-desk routing.", + steps: [ + { + title: "Issue Type", + description: "Classify the check-in issue.", + kind: "form", + fields: [ + { + key: "checkinIssueType", + kind: "select", + label: "Issue Type", + required: true, + options: [ + { value: "insurance", label: "Insurance mismatch" }, + { value: "identity", label: "Identity verification" }, + { value: "appointment", label: "Appointment mismatch" }, + ], + }, + { + key: "deskQueue", + kind: "select", + label: "Front Desk Queue", + required: true, + options: [ + { value: "intake", label: "Intake Queue" }, + { value: "billing", label: "Billing Queue" }, + { value: "triage", label: "Triage Queue" }, + ], + }, + ], + }, + { + title: "Patient Context", + description: "Capture non-PHI context fields.", + kind: "form", + fields: [ + { + key: "patientToken", + kind: "text", + label: "Patient Token", + placeholder: "PT-80321", + required: true, + }, + { + key: "contextNotes", + kind: "textarea", + label: "Context Notes", + placeholder: "No names or DOB. Include only operational context.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review details and complete.", + kind: "review", + }, + ], + }, + "healthcare.flag_urgent": { + successTitle: "Urgent flag workflow simulated", + successMessage: + "Urgency classification and notifications are prepared for routing.", + steps: [ + { + title: "Urgency Classification", + description: "Define urgency and risk posture.", + kind: "form", + fields: [ + { + key: "urgencyLevel", + kind: "select", + label: "Urgency Level", + required: true, + options: [ + { value: "stat", label: "STAT" }, + { value: "high", label: "High" }, + { value: "moderate", label: "Moderate" }, + ], + }, + { + key: "riskCategory", + kind: "select", + label: "Risk Category", + required: true, + options: [ + { value: "safety", label: "Patient safety" }, + { value: "compliance", label: "Compliance" }, + { value: "operational", label: "Operational" }, + ], + }, + ], + }, + { + title: "Notification Route", + description: "Choose whom to notify.", + kind: "form", + fields: [ + { + key: "notifyGroup", + kind: "select", + label: "Notify Group", + required: true, + options: [ + { value: "oncall", label: "On-call clinician" }, + { value: "charge", label: "Charge nurse" }, + { value: "ops", label: "Clinical operations" }, + ], + }, + { + key: "responseWindow", + kind: "select", + label: "Response Window", + required: true, + options: [ + { value: "5m", label: "5 minutes" }, + { value: "15m", label: "15 minutes" }, + { value: "30m", label: "30 minutes" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review details and complete.", + kind: "review", + }, + ], + }, + "healthcare.vital_alert": { + successTitle: "Vital alert workflow simulated", + successMessage: + "Vital threshold and action details were captured for triage.", + steps: [ + { + title: "Vital Entry", + description: "Document observed vital values.", + kind: "form", + fields: [ + { + key: "vitalType", + kind: "select", + label: "Vital Type", + required: true, + options: [ + { value: "hr", label: "Heart rate" }, + { value: "bp", label: "Blood pressure" }, + { value: "spo2", label: "SpO2" }, + { value: "temp", label: "Temperature" }, + ], + }, + { + key: "vitalValue", + kind: "text", + label: "Observed Value", + placeholder: "e.g. 88/54", + required: true, + }, + ], + }, + { + title: "Clinical Action", + description: "Define immediate clinical response.", + kind: "form", + fields: [ + { + key: "clinicalAction", + kind: "select", + label: "Clinical Action", + required: true, + options: [ + { value: "notify", label: "Notify provider" }, + { value: "repeat", label: "Repeat measurement" }, + { value: "rapid", label: "Call rapid response" }, + ], + }, + { + key: "actionNotes", + kind: "textarea", + label: "Action Notes", + placeholder: "Document immediate next steps.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review details and complete.", + kind: "review", + }, + ], + }, + "social.flag_content": { + successTitle: "Content flag workflow simulated", + successMessage: + "Moderation policy and enforcement intent were captured with context.", + steps: [ + { + title: "Policy Category", + description: "Choose applicable policy category.", + kind: "form", + fields: [ + { + key: "policyCategory", + kind: "select", + label: "Policy Category", + required: true, + options: [ + { value: "harassment", label: "Harassment" }, + { value: "misinfo", label: "Misinformation" }, + { value: "spam", label: "Spam" }, + { value: "other", label: "Other" }, + ], + }, + { + key: "flagNotes", + kind: "textarea", + label: "Flag Notes", + placeholder: "Add reviewer context.", + required: true, + }, + ], + }, + { + title: "Enforcement Action", + description: "Choose moderation action.", + kind: "form", + fields: [ + { + key: "enforcementAction", + kind: "select", + label: "Action", + required: true, + options: [ + { value: "hide", label: "Hide content" }, + { value: "warn", label: "Warn account" }, + { value: "escalate", label: "Escalate for review" }, + ], + }, + { + key: "reviewQueue", + kind: "select", + label: "Review Queue", + required: true, + options: [ + { value: "safety", label: "Safety queue" }, + { value: "trust", label: "Trust queue" }, + { value: "legal", label: "Legal queue" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review details and complete.", + kind: "review", + }, + ], + }, + "social.quick_reply": { + successTitle: "Quick reply workflow simulated", + successMessage: + "Reply draft and moderation guardrails were captured.", + steps: [ + { + title: "Draft Reply", + description: "Compose contextual response.", + kind: "form", + fields: [ + { + key: "draftReply", + kind: "textarea", + label: "Draft Reply", + placeholder: "Write the reply that should be suggested.", + required: true, + }, + ], + }, + { + title: "Tone & Approval", + description: "Set tone and approval requirements.", + kind: "form", + fields: [ + { + key: "replyTone", + kind: "select", + label: "Tone", + required: true, + options: [ + { value: "friendly", label: "Friendly" }, + { value: "formal", label: "Formal" }, + { value: "empathetic", label: "Empathetic" }, + ], + }, + { + key: "approvalLevel", + kind: "select", + label: "Approval", + required: true, + options: [ + { value: "auto", label: "Auto-send" }, + { value: "manager", label: "Manager approval" }, + { value: "legal", label: "Legal approval" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review details and complete.", + kind: "review", + }, + ], + }, + "social.save_asset": { + successTitle: "Save asset workflow simulated", + successMessage: + "Asset destination and metadata tagging were captured for handoff.", + steps: [ + { + title: "Destination", + description: "Pick where the asset should be stored.", + kind: "form", + fields: [ + { + key: "assetDestination", + kind: "select", + label: "Destination", + required: true, + options: [ + { value: "campaign", label: "Campaign library" }, + { value: "brand", label: "Brand vault" }, + { value: "draft", label: "Draft workspace" }, + ], + }, + { + key: "assetName", + kind: "text", + label: "Asset Name", + placeholder: "holiday-launch-variant-b", + required: true, + }, + ], + }, + { + title: "Metadata & Tags", + description: "Attach categorization metadata.", + kind: "form", + fields: [ + { + key: "assetTags", + kind: "text", + label: "Tags", + placeholder: "holiday,retargeting,ugc", + required: true, + }, + { + key: "usageRights", + kind: "select", + label: "Usage Rights", + required: true, + options: [ + { value: "internal", label: "Internal" }, + { value: "paid", label: "Paid media" }, + { value: "global", label: "Global unrestricted" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review details and complete.", + kind: "review", + }, + ], + }, + "software.send_to_cursor": { + successTitle: "Cursor handoff workflow simulated", + successMessage: + "Fix scope, prompt, and constraints are staged for Cursor integration.", + steps: [ + { + title: "Fix Scope", + description: "Select scope and expected output.", + kind: "form", + fields: [ + { + key: "fixScope", + kind: "select", + label: "Fix Scope", + required: true, + options: [ + { value: "element", label: "Selected element only" }, + { value: "container", label: "Container component" }, + { value: "page", label: "Whole page" }, + ], + }, + { + key: "impact", + kind: "select", + label: "Impact", + required: true, + options: [ + { value: "blocker", label: "Blocker" }, + { value: "high", label: "High" }, + { value: "medium", label: "Medium" }, + ], + }, + ], + }, + { + title: "Prompt & Constraints", + description: "Specify agent prompt and boundaries.", + kind: "form", + fields: [ + { + key: "cursorPrompt", + kind: "textarea", + label: "Prompt", + placeholder: "Describe the fix that should be generated.", + required: true, + }, + { + key: "branchName", + kind: "text", + label: "Branch", + placeholder: "codex/fix-login-button", + required: true, + }, + { + key: "constraints", + kind: "textarea", + label: "Constraints", + placeholder: "Do not modify API contracts. Keep visual styles stable.", + required: false, + }, + ], + }, + { + title: "Review", + description: "Review payload and context before send.", + kind: "review", + }, + ], + }, +}; + +const INITIAL_FORM_VALUES: Partial>> = + { + "software.report_bug": { + ticketSystem: "github", + severity: "medium", + labels: "bug,homepage", + }, + "software.copy_selector": { + selectorFormat: "css", + }, + "software.send_to_cursor": { + branchName: "codex/fix-ui-feedback", + }, + }; + +const DEFAULT_BUG_SUMMARY_PLACEHOLDER = + "Observed UI behavior does not match expected interaction."; + +function isGenericAction(actionId: WorkflowActionId): actionId is GenericActionId { + return actionId !== "software.report_bug" && actionId !== "software.copy_selector"; +} + +function defaultSummaryFromCapture(capture: WorkflowCaptureData | null): string { + if (!capture) { + return "Observed UI behavior does not match expected interaction."; + } + + const text = capture.targetContext?.innerText?.trim(); + if (text) { + return `Observed unexpected behavior on \"${text.slice(0, 80)}\" when interacting in ${capture.pageContext.title}.`; + } + + return `Observed unexpected behavior on ${capture.targetSelector} in ${capture.pageContext.title}.`; +} + +function formatFormValue(value: string): string { + if (!value) return "Not provided"; + return value; +} + +function parseLabels(csvValue: string): string[] { + return csvValue + .split(",") + .map((label) => label.trim()) + .filter(Boolean); +} + +async function writeClipboard(text: string): Promise { + if (!navigator.clipboard) { + throw new Error("Clipboard API not available"); + } + await navigator.clipboard.writeText(text); +} + +function buildMetadata( + launch: WorkflowLaunchState, + formState: Record, + capture: WorkflowCaptureData | null, +) { + return { + routing: { + adapter: "github", + }, + workflow: { + actionId: launch.action.id, + actionLabel: launch.action.menuLabel, + source: "homepage-immersive-workstream", + ticketSystem: formState.ticketSystem, + workstream: launch.action.workstream, + severity: formState.severity, + labels: parseLabels(formState.labels || ""), + }, + context: { + containerSelector: capture?.containerSelector || "unknown", + targetSelector: capture?.targetSelector || "unknown", + pageTitle: capture?.pageContext.title || "", + }, + github: { + title: formState.bugTitle, + }, + }; +} + +function buildScreenshots(capture: WorkflowCaptureData | null): ScreenshotData | undefined { + if (!capture) return undefined; + + const screenshots: ScreenshotData = { + capturedAt: capture.capturedAt, + }; + + if (capture.containerScreenshot) { + screenshots.container = capture.containerScreenshot; + } + + if (capture.screenshotError) { + screenshots.errors = { + container: { + message: capture.screenshotError, + name: "SCREENSHOT_CAPTURE_ERROR", + }, + }; + } + + if (!screenshots.container && !screenshots.errors) { + return undefined; + } + + return screenshots; +} + +function FieldControl({ + field, + value, + onChange, +}: { + field: FieldConfig; + onChange: (value: string) => void; + value: string; +}) { + return ( +