diff --git a/.cursor/plans/decouple-ewjdev-anyclick-react-styling-and-ship-default-style-providers.md b/.cursor/plans/decouple-ewjdev-anyclick-react-styling-and-ship-default-style-providers.md new file mode 100644 index 0000000..c45eb83 --- /dev/null +++ b/.cursor/plans/decouple-ewjdev-anyclick-react-styling-and-ship-default-style-providers.md @@ -0,0 +1,399 @@ +# Decouple `@ewjdev/anyclick-react` Styling and Ship Default Style Providers + +## Summary + +Refactor `@ewjdev/anyclick-react` into a headless-by-default UI package that renders Anyclick behavior and structure without importing Tailwind or bundling a styling framework. Move styling into a formal adapter layer, then ship three companion packages for the default styling systems: + +- `@ewjdev/anyclick-react-tailwind` +- `@ewjdev/anyclick-react-shadcn` +- `@ewjdev/anyclick-react-mui` + +This will be a major-version change for `@ewjdev/anyclick-react`. The current implicit Tailwind import in [`packages/anyclick-react/src/index.ts`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/index.ts) is removed, and the current mixed styling model in [`packages/anyclick-react/src/styles.ts`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/styles.ts), [`packages/anyclick-react/src/ContextMenu.tsx`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/ContextMenu.tsx), [`packages/anyclick-react/src/ScreenshotPreview.tsx`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/ScreenshotPreview.tsx), and [`packages/anyclick-react/src/ui/button.tsx`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/ui/button.tsx) is replaced by a slot-based styling contract plus optional component overrides. + +## Context Summary + +Current coupling points: + +- `@ewjdev/anyclick-react` auto-imports Tailwind CSS from package entry. +- The package uses a hybrid of: + - inline style maps for menu/screenshot surfaces + - Tailwind-prefixed utility classes for buttons, inputs, icons, and some layout + - CSS variables from `tokens.css` +- Existing public styling hooks are too narrow: + - `menuClassName` + - `menuStyle` + - `AnyclickTheme.menuClassName` + - `AnyclickTheme.menuStyle` +- Styling inheritance today is merged through provider theme logic, not a reusable styling system. + +Target outcome: + +- Consumers can style Anyclick using the same mechanism as the host app. +- Tailwind, shadcn-style, and MUI defaults are first-class and shipped by us. +- Core runtime no longer depends on Tailwind or any other UI framework. + +## Architecture Decisions and Rationale + +- `@ewjdev/anyclick-react` becomes headless and framework-agnostic for styling. +Reason: this removes hard coupling, avoids shipping unwanted CSS, and gives a clean base for all style systems. + +- Styling defaults ship as companion packages, not subpath exports inside the core package. +Reason: MUI brings heavy peer dependencies, and companion packages keep dependency graphs clean and predictable. + +- The new styling contract is slot-based with optional component overrides. +Reason: Tailwind and shadcn work well with class-slot recipes, while MUI needs actual component wrappers. A contract that supports both avoids maintaining multiple incompatible rendering paths. + +- Shadcn support ships in two layers. +Reason: you chose “both.” + - Default shadcn-style class/token adapter for plain DOM rendering. + - Optional consumer-supplied component overrides for teams that want to inject local shadcn/ui components. + +- This is a major-version migration now, not a transitional minor. +Reason: you chose “major now,” so we should not preserve implicit Tailwind as the default path. + +- Existing `menuClassName` and `menuStyle` remain as deprecated compatibility shims for one major cycle. +Reason: even in a major, this materially reduces migration pain and lets us map old props onto the new slot model without blocking the cleaner architecture. + +## Public API and Interface Changes + +### Core package: `@ewjdev/anyclick-react` + +Add: + +- `AnyclickStyleAdapter` +- `AnyclickStyleProvider` +- `useAnyclickStyle` +- `AnyclickStyleTokens` +- `AnyclickStyleSlot` +- `AnyclickSlotState` +- `AnyclickSlotProps` +- `AnyclickComponentOverrides` +- `slotClassNames?: Partial>` on `AnyclickProviderProps` +- `slotStyles?: Partial>` on `AnyclickProviderProps` +- `components?: Partial` on `AnyclickProviderProps` +- `styleAdapter?: AnyclickStyleAdapter` on `AnyclickProviderProps` + +Deprecate but keep working in this major: + +- `menuClassName` +- `menuStyle` +- `AnyclickTheme.menuClassName` +- `AnyclickTheme.menuStyle` + +Remove from core package behavior: + +- implicit `import "./styles/tailwind.css"` from package entry +- Tailwind as a required styling dependency for rendering + +### New core styling contract + +Define a fixed slot taxonomy covering all user-visible surfaces shipped by `@ewjdev/anyclick-react`: + +- `menu.overlay` +- `menu.surface` +- `menu.header` +- `menu.headerAction` +- `menu.list` +- `menu.item` +- `menu.itemIcon` +- `menu.itemLabel` +- `menu.itemBadge` +- `menu.submenuIndicator` +- `menu.backButton` +- `menu.dragHandle` +- `comment.section` +- `comment.textarea` +- `comment.primaryAction` +- `comment.secondaryAction` +- `screenshot.surface` +- `screenshot.header` +- `screenshot.tab` +- `screenshot.tabActive` +- `screenshot.preview` +- `screenshot.empty` +- `screenshot.error` +- `screenshot.meta` +- `screenshot.action` +- `quickChat.surface` +- `quickChat.header` +- `quickChat.messageList` +- `quickChat.input` +- `quickChat.submit` +- `inspect.surface` +- `inspect.header` +- `inspect.content` +- `inspect.action` +- `shared.button` +- `shared.input` +- `shared.textarea` +- `shared.badge` + +Define slot resolver behavior as: + +- input: slot name + semantic state + resolved tokens + consumer overrides +- output: `{ className?, style?, attrs? }` + +Define `AnyclickComponentOverrides` for the few places where DOM replacement is required: + +- `Surface` +- `Button` +- `IconButton` +- `Input` +- `Textarea` +- `Tabs` +- `Tab` +- `Badge` + +Do not make icons part of v1 adapter scope. +Reason: style portability is the primary problem. Icon replacement can be added later if needed. + +### Companion packages + +#### `@ewjdev/anyclick-react-tailwind` + +Exports: + +- `createTailwindAnyclickStyleAdapter(options?)` +- `TailwindAnyclickStyleProvider` +- `tailwind.css` + +Behavior: + +- provides class-slot recipes using Tailwind utilities +- ships the current default visual look, adjusted to match today’s UX closely +- uses package-level CSS only inside the companion package, not core + +#### `@ewjdev/anyclick-react-shadcn` + +Exports: + +- `createShadcnAnyclickStyleAdapter(options?)` +- `ShadcnAnyclickStyleProvider` +- `createShadcnComponentOverrides(overrides)` +- optional `shadcn.css` token bridge + +Behavior: + +- default adapter renders plain DOM with class recipes aligned to shadcn-style tokens and utility conventions +- optional component override factory accepts consumer components for: + - `Button` + - `Input` + - `Textarea` + - `Tabs` + - `Tab` + - `Badge` + - `Surface`/`Card` +- no runtime dependency on a generated shadcn package + +#### `@ewjdev/anyclick-react-mui` + +Exports: + +- `createMuiAnyclickStyleAdapter(options?)` +- `MuiAnyclickStyleProvider` + +Peer dependencies: + +- `@mui/material` +- `@emotion/react` +- `@emotion/styled` + +Behavior: + +- uses MUI primitives for surfaces and controls +- maps semantic tokens to `sx` and MUI theme values +- default components: + - `Paper` for surfaces + - `Button`/`IconButton` + - `TextField` or multiline input wrapper + - `Tabs`/`Tab` + - `Chip` for badges + - `CircularProgress` for loading affordances + +## Step-by-Step Plan + +1. Inventory and freeze the render surface. +- Enumerate every rendered DOM/control surface in `ContextMenu`, `ScreenshotPreview`, `QuickChat`, `InspectSimple`, shared button/input primitives, and provider wrappers. +- Create a slot map document in the repo that ties each rendered node to one slot name and one semantic state model. +- Explicitly mark non-stylable internals out of scope for v1: event plumbing, highlight math, adapter submission flow. + +2. Introduce a dedicated styling context in core. +- Add `AnyclickStyleContext`. +- Add `AnyclickStyleProvider`. +- Add merge rules: + - base unstyled adapter + - inherited adapter from nearest style provider + - local `styleAdapter` on `AnyclickProvider` + - local `slotClassNames`, `slotStyles`, `components` + - deprecated `menuClassName` and `menuStyle` mapped last onto `menu.surface` +- Keep styling inheritance separate from existing behavioral `theme` inheritance. + +3. Define and implement the core slot resolver API. +- Add canonical slot names and state types. +- Add helper utilities: + - `mergeStyleAdapters` + - `resolveSlotProps` + - `composeClassNames` + - `composeInlineStyles` +- Normalize state shape so all adapters receive consistent state keys: + - `active` + - `disabled` + - `hovered` + - `pressed` + - `expanded` + - `selected` + - `error` + - `loading` + - `tone` + - `size` + +4. Refactor the current React components to consume slot props instead of hardcoded styles/classes. +- Replace direct reads from `menuStyles` for structural rendering with slot resolution. +- Replace Tailwind utility strings inside core components with slot-provided classes. +- Leave the current semantic markup and behavior intact unless required by MUI componentization. +- Keep the DOM structure stable where possible to avoid behavioral regressions. + +5. Convert `styles.ts` into semantic token defaults, not UI implementation. +- Retain only neutral semantic defaults needed for an unstyled fallback and behavior-safe layout. +- Move current appearance-specific rules into the Tailwind companion package. +- Remove Tailwind-specific CSS files from core package entry and build outputs. + +6. Build the minimal unstyled core fallback. +- Core should still render accessibly if no style provider is present. +- The fallback should be intentionally plain but usable: + - readable typography + - visible borders + - keyboard focus states + - accessible button/input affordances +- Emit a development warning when no non-fallback style adapter is configured. + +7. Build `@ewjdev/anyclick-react-tailwind`. +- Port the current look into slot recipes and CSS owned by the Tailwind adapter package. +- Export both a provider and a raw adapter factory. +- Ensure this package is the documented replacement for current out-of-the-box usage. + +8. Build `@ewjdev/anyclick-react-shadcn`. +- Implement class/token adapter first. +- Add optional consumer-component override factory second. +- Define the override interface precisely so a consumer can pass their own local shadcn components without wrapping every slot manually. +- Publish examples showing: + - class/token only + - component override mode + +9. Build `@ewjdev/anyclick-react-mui`. +- Implement MUI-specific component overrides rather than forcing class recipes. +- Require the consumer app’s existing MUI `ThemeProvider`. +- Expose optional adapter options for: + - density + - variant mapping + - elevation + - use of `TextField` vs custom multiline field +- Validate portals/z-index interactions against Anyclick’s overlay behavior. + +10. Update documentation, examples, and migration materials. +- Replace all docs that imply `@ewjdev/anyclick-react` self-styles via Tailwind. +- Add three first-class setup guides: + - Tailwind + - shadcn + - MUI +- Add a migration guide: + - old usage + - new provider/adaptor setup + - deprecated prop mapping + - common migration pitfalls + +11. Release strategy. +- Ship a major for `@ewjdev/anyclick-react`. +- Ship first stable versions of the companion style packages in the same release train. +- Keep deprecated prop shims for one major line, then remove them in the next major after adoption. + +## Testing and Validation + +### Unit and integration coverage + +- `AnyclickProvider` resolves style adapters from provider context, local props, and deprecated shims in the correct precedence order. +- `resolveSlotProps` merges classes and styles deterministically. +- Deprecated `menuClassName` and `menuStyle` still affect `menu.surface`. +- Rendering without a style provider falls back to accessible minimal styling. +- Tailwind adapter applies expected slot classes for default menu, comment form, screenshot preview, quick chat, and inspect surfaces. +- Shadcn class/token adapter renders without consumer component injection. +- Shadcn component override mode correctly uses consumer-supplied components. +- MUI adapter renders with MUI components and respects the host theme. + +### Behavioral regression coverage + +- Right-click menu open/close behavior unchanged. +- Touch press-and-hold behavior unchanged. +- Keyboard navigation and focus trapping unchanged. +- Screenshot preview flow unchanged. +- QuickChat flow unchanged. +- Nested provider theme inheritance still works for non-styling behavior fields. +- Scoped providers still isolate event handling correctly. + +### Visual validation + +- Storybook or equivalent component gallery for: + - unstyled fallback + - Tailwind default + - shadcn class/token + - shadcn component override + - MUI +- Visual snapshots for: + - default menu + - submenu + - comment form + - screenshot preview + - loading/error states + - quick chat + - tiny inspect dialog + +### Acceptance criteria + +- `@ewjdev/anyclick-react` has no Tailwind import and no Tailwind dependency. +- A consumer can style all shipped surfaces without forking core components. +- Tailwind, shadcn, and MUI setup each take one provider/adaptor import plus normal host-app theme setup. +- MUI package does not leak MUI dependencies into users who do not install it. +- Shadcn package works both without and with consumer component injection. + +## Risks and Mitigations + +- Risk: slot taxonomy misses a surface and forces ad hoc styling later. +Mitigation: complete inventory first and require every rendered node to map to a named slot before refactor starts. + +- Risk: MUI component overrides change DOM structure enough to break measurements, focus, or portal behavior. +Mitigation: keep overlay positioning and event logic in core; MUI adapter only swaps leaf/surface primitives, not behavior containers. + +- Risk: shadcn is not a true runtime dependency model. +Mitigation: treat shadcn default support as class/token recipes first, then optional consumer component injection. + +- Risk: too many override axes create an unmaintainable API. +Mitigation: separate concerns clearly: + - tokens for design semantics + - slots for class/style recipes + - components only for control/surface replacement + +- Risk: migration pain for current users. +Mitigation: keep deprecated `menuClassName` and `menuStyle` shims, provide Tailwind adapter that recreates the current experience, and publish a direct migration guide. + +## Rollout and Monitoring + +1. Land the core style-adapter refactor behind the new public API in the major branch. +2. Land the Tailwind adapter package and migrate first-party examples to it. +3. Land shadcn and MUI packages. +4. Update docs site and README examples to never show bare `AnyclickProvider` without a style provider unless intentionally demonstrating the unstyled fallback. +5. Monitor: +- issue volume for styling regressions +- install/download split between the three companion packages +- reports of missing slots or unstyleable surfaces +- DOM/focus regressions in MUI mode + +## Assumptions and Defaults Chosen + +- Packaging strategy: companion packages. +- Migration posture: major now. +- Shadcn model: both class/token adapter and optional consumer component overrides. +- Icons remain Lucide-based in v1 of the styling refactor. +- Core continues to own behavior, event handling, focus, and positioning. +- Style adapters own appearance and primitive/component selection. +- Deprecated `menuClassName` and `menuStyle` are retained for one major to ease migration, even though the package is otherwise headless by default. +- No additional styling systems beyond Tailwind, shadcn, and MUI are included in this release. 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/.gitignore b/.gitignore index 5224b6d..63cca7c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ yarn-error.log* # turbo .turbo .vercel + +.myh/ \ No newline at end of file diff --git a/README.md b/README.md index acd214e..f677b85 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Right-click any element in your app to capture feedback with full DOM context, s ### Installation ```bash -yarn install @ewjdev/anyclick-react @ewjdev/anyclick-github +yarn install @ewjdev/anyclick-core @ewjdev/anyclick-react @ewjdev/anyclick-github ``` ### Usage @@ -54,7 +54,7 @@ export async function POST(request: Request) { "use client"; import { FeedbackProvider } from "@ewjdev/anyclick-react"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; const adapter = createHttpAdapter({ endpoint: "/api/feedback" }); diff --git a/apps/web/package.json b/apps/web/package.json index 09887ed..4a58dbc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,6 +27,7 @@ "@ewjdev/anyclick-github": "*", "@ewjdev/anyclick-jira": "*", "@ewjdev/anyclick-react": "*", + "@ewjdev/anyclick-react-tailwind": "*", "@ewjdev/anyclick-uploadthing": "*", "@headlessui/react": "^2.2.8", "@heroicons/react": "^2.1.3", diff --git a/apps/web/src/app/api/feedback/github/route.d.ts b/apps/web/src/app/api/feedback/github/route.d.ts new file mode 100644 index 0000000..e7dfb1f --- /dev/null +++ b/apps/web/src/app/api/feedback/github/route.d.ts @@ -0,0 +1 @@ +export declare function POST(req: Request): Promise; 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":"AACA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,gCAAgC,CAAC;AAExC,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC;AAED,SAAS,eAAe,CAAC,SAAiB;IACxC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE3C,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,sEAAsE,SAAS,IAAI,CACpF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,SAAS,kBAAkB,CAAC,QAAiB;IAC3C,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAExC,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC;IACpC,IAAI,OAAO,cAAc,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAEzD,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,EAAE,CAAC;IACtC,OAAO,OAAO,IAAI,SAAS,CAAC;AAC9B,CAAC;AAED,SAAS,mBAAmB,CAAC,QAAiB;IAC5C,MAAM,YAAY,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACxD,MAAM,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAE3E,OAAO;QACL,GAAG,YAAY;QACf,OAAO,EAAE;YACP,GAAG,OAAO;YACV,OAAO,EAAE,QAAQ;SAClB;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,GAAY;IACrC,IAAI,OAAwB,CAAC;IAE7B,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAoB,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC,IAAI,CAClB;YACE,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,sBAAsB;SAC9B,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IACvC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,iBAAiB,CAAC;IAE/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,QAAQ,CAAC,IAAI,CAClB;YACE,OAAO,EAAE,KAAK;YACd,KAAK,EACH,uEAAuE;SAC1E,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;QAEnD,MAAM,aAAa,GAAG,mBAAmB,CAAC;YACxC,KAAK;YACL,IAAI;YACJ,KAAK;YACL,WAAW,EAAE,CAAC,YAAY,EAAE,EAAE,CAC5B,kBAAkB,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,kBAAkB,CAAC,YAAY,CAAC;SAChF,CAAC,CAAC;QAEH,MAAM,iBAAiB,GAAoB;YACzC,GAAG,OAAO;YACV,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC;SAChD,CAAC;QAEF,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,iBAAiB,CAAC,CAAC;QAEjE,OAAO,QAAQ,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,IAAI;YACb,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,YAAY,GAChB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;QAE3E,OAAO,QAAQ,CAAC,IAAI,CAClB;YACE,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,YAAY;SACpB,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;IACJ,CAAC;AACH,CAAC"} \ 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/docs/adapters/page.tsx b/apps/web/src/app/docs/adapters/page.tsx index 01086cf..d139e58 100644 --- a/apps/web/src/app/docs/adapters/page.tsx +++ b/apps/web/src/app/docs/adapters/page.tsx @@ -98,16 +98,19 @@ export default function AdaptersDocsPage() {

Installation

- {`npm install @ewjdev/anyclick-github`} + {`npm install @ewjdev/anyclick-core @ewjdev/anyclick-github`}

Browser-Side Setup

- Use the HTTP adapter to send feedback to your API endpoint: + Use the generic HTTP adapter from @ewjdev/anyclick-core{" "} + to send feedback to your API endpoint:

{`'use client'; -import { FeedbackProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { AnyclickProvider } from '@ewjdev/anyclick-react'; +import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind'; +import '@ewjdev/anyclick-react-tailwind/tailwind.css'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ endpoint: '/api/feedback', @@ -119,9 +122,11 @@ const adapter = createHttpAdapter({ export function Providers({ children }) { return ( - - {children} - + + + {children} + + ); }`} @@ -379,7 +384,7 @@ export async function POST(request: Request) { {`npm install @ewjdev/anyclick-cursor`}

Usage

- {`import { FeedbackProvider } from '@ewjdev/anyclick-react'; + {`import { AnyclickProvider } from '@ewjdev/anyclick-react'; import { createCursorAdapter } from '@ewjdev/anyclick-cursor'; const cursorAdapter = createCursorAdapter({ @@ -399,12 +404,12 @@ const menuItems = [ }, ]; - {children} -`} +`}

Format for Cursor Agent @@ -445,7 +450,7 @@ const agentPrompt = formatForCursorAgent(payload); npm run feedback-server`}

Browser-Side Setup

- {`import { FeedbackProvider } from '@ewjdev/anyclick-react'; + {`import { AnyclickProvider } from '@ewjdev/anyclick-react'; import { createLocalAdapter } from '@ewjdev/anyclick-cursor-local'; // Only use in development @@ -459,9 +464,9 @@ const adapter = process.env.NODE_ENV === 'development' ? localAdapter : productionAdapter; - + {children} -`} +`}

Server Configuration diff --git a/apps/web/src/app/docs/getting-started/page.tsx b/apps/web/src/app/docs/getting-started/page.tsx index ec539e9..6fa673b 100644 --- a/apps/web/src/app/docs/getting-started/page.tsx +++ b/apps/web/src/app/docs/getting-started/page.tsx @@ -46,7 +46,7 @@ export default function GettingStartedPage() { Install the core packages you need. For a typical React + GitHub setup:

- +

@@ -56,15 +56,21 @@ export default function GettingStartedPage() {
  • @ewjdev/anyclick-react – - React provider and UI components + Headless React provider and UI behavior
  • - @ewjdev/anyclick-github – - GitHub Issues adapter + + @ewjdev/anyclick-react-tailwind + {" "} + – Default Tailwind style adapter and provider
  • @ewjdev/anyclick-core – - Automatically included as a dependency + Core types and the browser HTTP adapter +
  • +
  • + @ewjdev/anyclick-github – + GitHub Issues adapter

@@ -140,15 +146,18 @@ export async function POST(request: Request) {

Step 4: Add the Provider

Wrap your application with the{" "} - FeedbackProvider: + AnyclickProvider and a style + provider:

- {children} - + + + {children} + + ); }`} /> @@ -213,7 +224,7 @@ export default function RootLayout({

Configuration Options

- The FeedbackProvider accepts + The AnyclickProvider accepts several configuration options:

diff --git a/apps/web/src/app/docs/react/page.tsx b/apps/web/src/app/docs/react/page.tsx index ecaf347..cea50f5 100644 --- a/apps/web/src/app/docs/react/page.tsx +++ b/apps/web/src/app/docs/react/page.tsx @@ -61,11 +61,14 @@ export default function ReactDocsPage() { {/* Installation */}

Installation

- {`npm install @ewjdev/anyclick-react`} + {`npm install @ewjdev/anyclick-react @ewjdev/anyclick-react-tailwind`}

Requires React 19+ and includes{" "} @ewjdev/anyclick-core as a - dependency. + dependency. The React package is headless by default, so pair it with + a style package such as{" "} + @ewjdev/anyclick-react-tailwind + .

@@ -75,7 +78,9 @@ export default function ReactDocsPage() { {`'use client'; import { AnyclickProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind'; +import '@ewjdev/anyclick-react-tailwind/tailwind.css'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ endpoint: '/api/feedback', @@ -83,14 +88,17 @@ const adapter = createHttpAdapter({ export function Providers({ children }: { children: React.ReactNode }) { return ( - - {children} - + + + {children} + + ); }`}

- Note:{" "} - FeedbackProvider is still + Note: use a companion style provider or your own{" "} + AnyclickStyleProvider. + AnyclickProvider is still exported for backward compatibility but is deprecated.

@@ -188,15 +196,35 @@ export function Providers({ children }: { children: React.ReactNode }) { type="(error: Error, payload: FeedbackPayload) => void" description="Callback fired after failed submission." /> + + + + - {children} - + + + {children} + + ); }`} /> diff --git a/apps/web/src/app/examples/cursor-local/page.tsx b/apps/web/src/app/examples/cursor-local/page.tsx index dda16dc..7a897e0 100644 --- a/apps/web/src/app/examples/cursor-local/page.tsx +++ b/apps/web/src/app/examples/cursor-local/page.tsx @@ -109,9 +109,9 @@ export default function CursorLocalPage() {

{`'use client'; -import { FeedbackProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react'; +import { AnyclickProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react'; import type { FeedbackMenuItem } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; import { createLocalAdapter } from '@ewjdev/anyclick-cursor-local'; import { Monitor, Cloud, Bug, Lightbulb } from 'lucide-react'; @@ -157,12 +157,12 @@ const menuItems: FeedbackMenuItem[] = [ export function Providers({ children }: { children: React.ReactNode }) { return ( - {children} - + ); }`} @@ -200,9 +200,9 @@ const adapter = createRoutingAdapter({ github: githubAdapter, }); - + {children} -`}
+`} {/* Server Configuration */} diff --git a/apps/web/src/app/examples/custom-menu/page.tsx b/apps/web/src/app/examples/custom-menu/page.tsx index 8d58ac7..a698c84 100644 --- a/apps/web/src/app/examples/custom-menu/page.tsx +++ b/apps/web/src/app/examples/custom-menu/page.tsx @@ -82,7 +82,7 @@ export default function CustomMenuExamplePage() { icon prop. Use any React component (Lucide, Heroicons, custom SVGs, etc.):

- {`import { FeedbackProvider } from '@ewjdev/anyclick-react'; + {`import { AnyclickProvider } from '@ewjdev/anyclick-react'; import { Bug, Lightbulb, Heart } from 'lucide-react'; const menuItems = [ @@ -106,9 +106,9 @@ const menuItems = [ }, ]; - + {children} -`} +`} {/* Submenus */} @@ -154,7 +154,7 @@ const menuItems = [ Show or hide menu items based on user roles using{" "} requiredRoles:

- {`import { FeedbackProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react'; + {`import { AnyclickProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react'; import type { FeedbackMenuItem, FeedbackUserContext } from '@ewjdev/anyclick-react'; // Define all menu items with role requirements @@ -194,13 +194,13 @@ function Providers({ children, currentUser }) { const menuItems = filterMenuItemsByRole(allMenuItems, userContext); return ( - {children} - + ); }`} @@ -211,7 +211,7 @@ function Providers({ children, currentUser }) {

Apply custom styles or classes to the context menu:

- {`{` {children} -`} +`}
{`/* Custom menu styling */ .my-custom-menu { @@ -247,7 +247,7 @@ function Providers({ children, currentUser }) {

Customize the colors used to highlight target and container elements:

- {`{` {children} -`} +`}
{/* Complete Example */} @@ -279,7 +279,7 @@ function Providers({ children, currentUser }) {

Complete Example

{`'use client'; -import { FeedbackProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react'; +import { AnyclickProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react'; import type { FeedbackMenuItem, FeedbackUserContext } from '@ewjdev/anyclick-react'; import { createHttpAdapter } from '@ewjdev/anyclick-'; import { Bug, Lightbulb, Heart, Code, Monitor, Cloud, Shield } from 'lucide-react'; @@ -347,7 +347,7 @@ export function Providers({ children }: { children: React.ReactNode }) { const menuItems = filterMenuItemsByRole(allMenuItems, userContext); return ( - {children} - + ); }`} diff --git a/apps/web/src/app/examples/custom-pointer/page.tsx b/apps/web/src/app/examples/custom-pointer/page.tsx index 2faa878..320513a 100644 --- a/apps/web/src/app/examples/custom-pointer/page.tsx +++ b/apps/web/src/app/examples/custom-pointer/page.tsx @@ -562,25 +562,25 @@ function ThemeSwitcher() { }`} - {/* Integration with FeedbackProvider */} + {/* Integration with AnyclickProvider */}

- Integration with FeedbackProvider + Integration with AnyclickProvider

Combine with the feedback system for a complete experience:

{`'use client'; -import { FeedbackProvider } from '@ewjdev/anyclick-react'; +import { AnyclickProvider } from '@ewjdev/anyclick-react'; import { PointerProvider } from '@ewjdev/anyclick-pointer'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ endpoint: '/api/feedback' }); export function Providers({ children }: { children: React.ReactNode }) { return ( - + {children} - + ); }`}
diff --git a/apps/web/src/app/examples/github-integration/page.tsx b/apps/web/src/app/examples/github-integration/page.tsx index da63c5b..1075682 100644 --- a/apps/web/src/app/examples/github-integration/page.tsx +++ b/apps/web/src/app/examples/github-integration/page.tsx @@ -98,7 +98,7 @@ GITHUB_REPO=your-username/your-repository-name`}

{`'use client'; -import { FeedbackProvider } from '@ewjdev/anyclick-react'; +import { AnyclickProvider } from '@ewjdev/anyclick-react'; import { createHttpAdapter } from '@ewjdev/anyclick-'; const adapter = createHttpAdapter({ @@ -107,7 +107,7 @@ const adapter = createHttpAdapter({ export function Providers({ children }: { children: React.ReactNode }) { return ( - {children} - + ); }`} diff --git a/apps/web/src/app/examples/jira-integration/page.tsx b/apps/web/src/app/examples/jira-integration/page.tsx index 5fd5474..a2ec148 100644 --- a/apps/web/src/app/examples/jira-integration/page.tsx +++ b/apps/web/src/app/examples/jira-integration/page.tsx @@ -2,7 +2,7 @@ import { CodeBlock } from "@/components/CodePreview"; import { useEffect, useState } from "react"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react"; import { AlertCircle, diff --git a/apps/web/src/app/examples/modes/page.tsx b/apps/web/src/app/examples/modes/page.tsx index 60d83a5..92d477d 100644 --- a/apps/web/src/app/examples/modes/page.tsx +++ b/apps/web/src/app/examples/modes/page.tsx @@ -3,7 +3,7 @@ import { CodeBlock } from "@/components/CodePreview"; import { useEffect, useMemo, useRef, useState } from "react"; import { createGameModeAdapter } from "@ewjdev/anyclick-adapters"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; import { usePointer } from "@ewjdev/anyclick-pointer"; import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react"; import { ArrowRight, Car, Keyboard, Sparkles } from "lucide-react"; @@ -238,7 +238,7 @@ export function Providers({ children }: { children: React.ReactNode }) { {`import { useEffect, useMemo, useRef, useState } from 'react'; import { AnyclickProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; import { createGameModeAdapter } from '@ewjdev/anyclick-adapters'; import { usePointer } from '@ewjdev/anyclick-pointer'; diff --git a/apps/web/src/app/examples/quick-chat/QuickChatProvider.tsx b/apps/web/src/app/examples/quick-chat/QuickChatProvider.tsx index 21a9de7..43feba9 100644 --- a/apps/web/src/app/examples/quick-chat/QuickChatProvider.tsx +++ b/apps/web/src/app/examples/quick-chat/QuickChatProvider.tsx @@ -1,5 +1,5 @@ "use client"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; import { AnyclickProvider, DEFAULT_QUICK_CHAT_CONFIG, diff --git a/apps/web/src/app/examples/quick-chat/page.tsx b/apps/web/src/app/examples/quick-chat/page.tsx index b6b2f3b..453b2f5 100644 --- a/apps/web/src/app/examples/quick-chat/page.tsx +++ b/apps/web/src/app/examples/quick-chat/page.tsx @@ -183,7 +183,7 @@ export default function QuickChatExamplePage() { code={`'use client'; import { AnyclickProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ endpoint: '/api/feedback', diff --git a/apps/web/src/app/examples/role-presets/RolePresetsClient.tsx b/apps/web/src/app/examples/role-presets/RolePresetsClient.tsx index 8dc7dc3..1aaa69d 100644 --- a/apps/web/src/app/examples/role-presets/RolePresetsClient.tsx +++ b/apps/web/src/app/examples/role-presets/RolePresetsClient.tsx @@ -23,7 +23,7 @@ import Link from "next/link"; const presetSnippet = `'use client'; import { AnyclickProvider, createPresetMenu } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ endpoint: '/api/feedback' }); diff --git a/apps/web/src/app/examples/scoped-providers/ScopedProvidersDemo.tsx b/apps/web/src/app/examples/scoped-providers/ScopedProvidersDemo.tsx index 6bca216..f632528 100644 --- a/apps/web/src/app/examples/scoped-providers/ScopedProvidersDemo.tsx +++ b/apps/web/src/app/examples/scoped-providers/ScopedProvidersDemo.tsx @@ -3,7 +3,7 @@ import type { CSSProperties } from "react"; import { AnyclickProvider } from "@ewjdev/anyclick-react"; import { PointerProvider } from "@ewjdev/anyclick-pointer"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; import { Layers, Palette, diff --git a/apps/web/src/app/examples/scoped-providers/page.tsx b/apps/web/src/app/examples/scoped-providers/page.tsx index c6bd5c0..a506e0d 100644 --- a/apps/web/src/app/examples/scoped-providers/page.tsx +++ b/apps/web/src/app/examples/scoped-providers/page.tsx @@ -79,16 +79,20 @@ export default function ScopedProvidersExamplePage() { {`'use client'; import { AnyclickProvider } from '@ewjdev/anyclick-react'; +import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind'; +import '@ewjdev/anyclick-react-tailwind/tailwind.css'; export function Dashboard({ children }) { return ( - - {children} - + + + {children} + + ); }`} @@ -218,7 +222,7 @@ export function PaymentForm({ children }) { {`'use client'; import { AnyclickProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; // Main app feedback goes to GitHub const githubAdapter = createHttpAdapter({ diff --git a/apps/web/src/app/examples/sensitive-masking/page.tsx b/apps/web/src/app/examples/sensitive-masking/page.tsx index 704a624..7697e4a 100644 --- a/apps/web/src/app/examples/sensitive-masking/page.tsx +++ b/apps/web/src/app/examples/sensitive-masking/page.tsx @@ -245,8 +245,8 @@ ${DEFAULT_SENSITIVE_SELECTORS.map((s) => `'${s}'`).join(",\n")}`}

{`'use client'; -import { FeedbackProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { AnyclickProvider } from '@ewjdev/anyclick-react'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; import { DEFAULT_SENSITIVE_SELECTORS } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ @@ -255,7 +255,7 @@ const adapter = createHttpAdapter({ export function Providers({ children }: { children: React.ReactNode }) { return ( - {children} - + ); }`} @@ -275,8 +275,8 @@ export function Providers({ children }: { children: React.ReactNode }) { {`'use client'; -import { FeedbackProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { AnyclickProvider } from '@ewjdev/anyclick-react'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; import { DEFAULT_SENSITIVE_SELECTORS } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ @@ -285,7 +285,7 @@ const adapter = createHttpAdapter({ export function Providers({ children }: { children: React.ReactNode }) { return ( - {children} - + ); }`} diff --git a/apps/web/src/app/examples/t3chat-integration/T3ChatProvider.tsx b/apps/web/src/app/examples/t3chat-integration/T3ChatProvider.tsx index 145fc4b..d8d4dfd 100644 --- a/apps/web/src/app/examples/t3chat-integration/T3ChatProvider.tsx +++ b/apps/web/src/app/examples/t3chat-integration/T3ChatProvider.tsx @@ -1,7 +1,7 @@ "use client"; import type { ReactNode } from "react"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; import { AnyclickProvider, createPresetMenu } from "@ewjdev/anyclick-react"; const chromePreset = createPresetMenu("chrome"); diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 89b83f0..a69723c 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -3,6 +3,7 @@ import { cn } from "@/lib/utils"; import type { ReactNode } from "react"; import type { Metadata, Viewport } from "next"; import { Inter, Lato, Montserrat } from "next/font/google"; +import "@ewjdev/anyclick-react-tailwind/tailwind.css"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 1e5ed92..adc55f0 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 */} @@ -109,7 +147,9 @@ export default function Home() { filename="app/layout.tsx" language="tsx" code={`import { AnyclickProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind'; +import '@ewjdev/anyclick-react-tailwind/tailwind.css'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ endpoint: '/api/feedback' @@ -117,9 +157,11 @@ const adapter = createHttpAdapter({ export default function RootLayout({ children }) { return ( - - {children} - + + + {children} + + ); }`} /> 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/AnyclickProviderWrapper.tsx b/apps/web/src/components/AnyclickProviderWrapper.tsx index 502acb8..f838d52 100644 --- a/apps/web/src/components/AnyclickProviderWrapper.tsx +++ b/apps/web/src/components/AnyclickProviderWrapper.tsx @@ -3,8 +3,9 @@ import type { ReactNode } from "react"; import { useEffect, useMemo } from "react"; import { InspectDialogManager } from "@ewjdev/anyclick-devtools"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; import { AnyclickProvider, createPresetMenu } from "@ewjdev/anyclick-react"; +import { TailwindAnyclickStyleProvider } from "@ewjdev/anyclick-react-tailwind"; const adapter = createHttpAdapter({ endpoint: "/api/feedback", @@ -14,8 +15,6 @@ export function AnyclickProviderWrapper({ children }: { children: ReactNode }) { // Use chrome preset for developer-focused menu with inspect, copy, etc. const chromePreset = useMemo(() => createPresetMenu("chrome"), []); - console.count("AnyclickProviderWrapper"); - // Clean up any stale cursor hiding styles when PointerProvider is not used // This ensures the default cursor is restored if PointerProvider was removed useEffect(() => { @@ -47,34 +46,35 @@ export function AnyclickProviderWrapper({ children }: { children: ReactNode }) { // useHideCursor(true); return ( - } - quickChatConfig={{ - endpoint: "/api/anyclick/chat", - model: "gpt-5-nano", - maxResponseLength: 500, - showRedactionUI: true, - showSuggestions: true, - placeholder: "Ask about this element...", - title: "Quick Ask", - }} - theme={{ - highlightConfig: { - enabled: false, - }, - }} - > - {children} - + } + quickChatConfig={{ + endpoint: "/api/anyclick/chat", + model: "gpt-5-nano", + maxResponseLength: 500, + showRedactionUI: true, + showSuggestions: true, + placeholder: "Ask about this element...", + title: "Quick Ask", }} - /> - {/* + {children} + + {/* - + */} - + + ); } diff --git a/apps/web/src/components/CodePreview.tsx b/apps/web/src/components/CodePreview.tsx index f3523fc..8150863 100644 --- a/apps/web/src/components/CodePreview.tsx +++ b/apps/web/src/components/CodePreview.tsx @@ -3,7 +3,7 @@ import { cn } from "@/lib/utils"; import * as React from "react"; import { CSSProperties, useMemo } from "react"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react"; import { Check, Copy, FileCode, Terminal } from "lucide-react"; diff --git a/apps/web/src/components/QuickStartSection.tsx b/apps/web/src/components/QuickStartSection.tsx index 95a1fde..9b2b855 100644 --- a/apps/web/src/components/QuickStartSection.tsx +++ b/apps/web/src/components/QuickStartSection.tsx @@ -19,7 +19,7 @@ const QuickStartSection = () => {

Install the packages

@@ -63,15 +63,19 @@ export async function POST(req) { filename="app/layout.tsx" language="tsx" code={`import { AnyclickProvider } from '@ewjdev/anyclick-react'; -import { createHttpAdapter } from '@ewjdev/anyclick-github'; +import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind'; +import '@ewjdev/anyclick-react-tailwind/tailwind.css'; +import { createHttpAdapter } from '@ewjdev/anyclick-core'; const adapter = createHttpAdapter({ endpoint: '/api/feedback' }); export default function Layout({ children }) { return ( - - {children} - + + + {children} + + ); }`} /> diff --git a/apps/web/src/components/WorkstreamShowcase.tsx b/apps/web/src/components/WorkstreamShowcase.tsx index dbb4a52..38928ba 100644 --- a/apps/web/src/components/WorkstreamShowcase.tsx +++ b/apps/web/src/components/WorkstreamShowcase.tsx @@ -3,7 +3,7 @@ import { cn } from "@/lib/utils"; import type { CSSProperties, ReactNode } from "react"; import { useRef } from "react"; -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; import { PointerProvider } from "@ewjdev/anyclick-pointer"; import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react"; import { diff --git a/apps/web/src/components/immersive-workstream/adapter.ts b/apps/web/src/components/immersive-workstream/adapter.ts index 4437180..c37a89c 100644 --- a/apps/web/src/components/immersive-workstream/adapter.ts +++ b/apps/web/src/components/immersive-workstream/adapter.ts @@ -1,3 +1,3 @@ -import { createHttpAdapter } from "@ewjdev/anyclick-github"; +import { createHttpAdapter } from "@ewjdev/anyclick-core"; export const adapter = createHttpAdapter({ endpoint: "/api/feedback" }); 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/HealthcareCard.tsx b/apps/web/src/components/immersive-workstream/cards/HealthcareCard.tsx index a902347..fd8fbe1 100644 --- a/apps/web/src/components/immersive-workstream/cards/HealthcareCard.tsx +++ b/apps/web/src/components/immersive-workstream/cards/HealthcareCard.tsx @@ -1,9 +1,76 @@ -import { cn } from "@/lib/utils"; +import type { CSSProperties, ReactNode } from "react"; +import { PointerProvider } from "@ewjdev/anyclick-pointer"; +import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react"; import { HeartPulse } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { adapter } from "../adapter"; + +interface HealthcareCardProps { + menuStyle: CSSProperties; + pointerCircleColor: string; + pointerColor: string; + pointerIcon: ReactNode; + primaryColor: string; + statusMenuItems: ContextMenuItem[]; + summaryMenuItems: ContextMenuItem[]; + vitalsMenuItems: ContextMenuItem[]; +} + +const VITALS = [ + { + bg: "bg-rose-50", + color: "text-rose-500", + label: "HR", + unit: "bpm", + value: "72", + }, + { + bg: "bg-emerald-50", + color: "text-emerald-600", + label: "BP", + unit: "", + value: "120/80", + }, + { + bg: "bg-cyan-50", + color: "text-cyan-600", + label: "Temp", + unit: "deg F", + value: "98.6", + }, +]; + +export function HealthcareCard({ + menuStyle, + pointerCircleColor, + pointerColor, + pointerIcon, + primaryColor, + statusMenuItems, + summaryMenuItems, + vitalsMenuItems, +}: HealthcareCardProps) { + const providerTheme = { + menuStyle, + highlightConfig: { + enabled: true, + colors: { + targetColor: primaryColor, + containerColor: `${primaryColor}30`, + }, + }, + }; + + const pointerTheme = { + colors: { + pointerColor, + circleColor: pointerCircleColor, + }, + pointerIcon, + }; -export function HealthcareCard() { return ( -
+
-
-
-
-
-
-
- Active -
-
+ } + scoped + > + +
+
+
+

+ Patient Summary +

+

+ Token PT-80321 +

+

+ Bed B-12 | Intake queue routed +

+
+
+ + Active + +
+
+

+ Right-click to run check-in and identity workflows. +

+
+
+
-
- {[ - { - label: "HR", - value: "72", - unit: "bpm", - color: "text-rose-500", - bg: "bg-rose-50", - }, - { - label: "BP", - value: "120/80", - unit: "", - color: "text-emerald-600", - bg: "bg-emerald-50", - }, - { - label: "Temp", - value: "98.6", - unit: "°F", - color: "text-cyan-600", - bg: "bg-cyan-50", - }, - ].map((vital) => ( + } + scoped + > +
-
- {vital.label} +
+ {VITALS.map((vital) => ( +
+
+ {vital.label} +
+
+ {vital.value} + + {vital.unit} + +
+
+ ))}
-
- {vital.value} - - {vital.unit} - + +
+ + + +
+ +

+ Right-click vitals to alert, recheck, or open trend review. +

- ))} -
+ + + + } + scoped + > + +
+
+
+

+ Care Status +

+

Monitoring stable

+
+
+

Team

+

Primary RN + On-call

+
+
+

Route

+

Unit board task queue

+
+
-
- - - - -
+
+
+
+ +

+ Right-click status to flag urgent, notify care team, or escalate + handoff. +

+
+ +
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..acb29c4 100644 --- a/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx +++ b/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx @@ -1,12 +1,12 @@ -import { PointerProvider } from "@ewjdev/anyclick-pointer"; -import { AnyclickProvider } from "@ewjdev/anyclick-react"; +import { useMemo } from "react"; import { MousePointer2 } from "lucide-react"; import { type MotionValue, motion } from "motion/react"; import { FloatingIcon } from "../FloatingIcon"; -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 { useWorkflowLauncher } from "../workflows/useWorkflowLauncher"; interface HealthcareSectionProps { theme: ImmersiveTheme; @@ -21,6 +21,39 @@ export function HealthcareSection({ isInView, bgY, }: HealthcareSectionProps) { + const { activeWorkflow, closeWorkflow, getMenuItemsForActionIds } = + useWorkflowLauncher("healthcare"); + + const summaryMenuItems = useMemo( + () => + getMenuItemsForActionIds([ + "healthcare.check_in_issue", + "healthcare.verify_identity_token", + "healthcare.coverage_exception", + ]), + [getMenuItemsForActionIds], + ); + + const vitalsMenuItems = useMemo( + () => + getMenuItemsForActionIds([ + "healthcare.vital_alert", + "healthcare.request_vital_recheck", + "healthcare.open_trend_review", + ]), + [getMenuItemsForActionIds], + ); + + const statusMenuItems = useMemo( + () => + getMenuItemsForActionIds([ + "healthcare.flag_urgent", + "healthcare.notify_care_team", + "healthcare.escalate_handoff", + ]), + [getMenuItemsForActionIds], + ); + return ( <> - } - scoped - > - + + -
- - - - Right-click to interact - -
-
-
+ + + Right-click patient summary, vitals, or care status to interact + +
+
@@ -110,6 +126,8 @@ export function HealthcareSection({ background: `linear-gradient(to top, rgba(16, 185, 129, 0.1), transparent)`, }} /> + + ); } 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..74b0e8a 100644 --- a/apps/web/src/components/immersive-workstream/themes.tsx +++ b/apps/web/src/components/immersive-workstream/themes.tsx @@ -13,8 +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 +20,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 +75,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 +139,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", @@ -208,12 +196,7 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [ opacity: 0.2, }, ], - cardContent: , - menuItems: [ - { label: "Check-in issue", type: "checkin", showComment: true }, - { label: "Vital alert", type: "vital", showComment: true }, - { label: "Flag urgent", type: "urgent", showComment: false }, - ], + cardContent: null, menuStyle: { "--anyclick-menu-bg": "#ffffff", "--anyclick-menu-border": "#10b981", @@ -280,11 +263,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..f8cd683 --- /dev/null +++ b/apps/web/src/components/immersive-workstream/workflows/WorkflowFlowRenderer.tsx @@ -0,0 +1,2679 @@ +"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", + }, + ], + }, + "healthcare.verify_identity_token": { + successTitle: "Identity verification workflow simulated", + successMessage: + "Verification and mismatch details were prepared using non-PHI context.", + steps: [ + { + title: "Verification Source", + description: "Capture token verification source.", + kind: "form", + fields: [ + { + key: "verificationSource", + kind: "select", + label: "Source", + required: true, + options: [ + { value: "kiosk", label: "Check-in kiosk" }, + { value: "frontdesk", label: "Front desk scan" }, + { value: "portal", label: "Patient portal token" }, + ], + }, + { + key: "verificationMode", + kind: "select", + label: "Verification Mode", + required: true, + options: [ + { value: "qr", label: "QR token" }, + { value: "wristband", label: "Wristband barcode" }, + { value: "manual", label: "Manual confirmation token" }, + ], + }, + ], + }, + { + title: "Mismatch Details", + description: "Document mismatch details (non-PHI only).", + kind: "form", + fields: [ + { + key: "patientTokenRef", + kind: "text", + label: "Patient Token Reference", + placeholder: "PT-TOKEN-49211", + required: true, + }, + { + key: "mismatchDetails", + kind: "textarea", + label: "Mismatch Details", + placeholder: + "Use operational details only. Do not include patient name, DOB, or MRN.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review and complete.", + kind: "review", + }, + ], + }, + "healthcare.coverage_exception": { + successTitle: "Coverage exception workflow simulated", + successMessage: + "Coverage and authorization routing details were staged with context.", + steps: [ + { + title: "Payer & Trigger", + description: "Capture coverage trigger details.", + kind: "form", + fields: [ + { + key: "payerType", + kind: "select", + label: "Payer", + required: true, + options: [ + { value: "commercial", label: "Commercial" }, + { value: "medicare", label: "Medicare" }, + { value: "medicaid", label: "Medicaid" }, + ], + }, + { + key: "exceptionTrigger", + kind: "select", + label: "Eligibility Trigger", + required: true, + options: [ + { value: "inactive", label: "Inactive coverage" }, + { value: "auth", label: "Authorization required" }, + { value: "benefit", label: "Benefit limitation" }, + ], + }, + ], + }, + { + title: "Authorization Path", + description: "Choose routing and follow-up path.", + kind: "form", + fields: [ + { + key: "authorizationQueue", + kind: "select", + label: "Authorization Queue", + required: true, + options: [ + { value: "billing-auth", label: "Billing authorization" }, + { value: "clinical-auth", label: "Clinical authorization" }, + { value: "supervisor", label: "Supervisor review" }, + ], + }, + { + key: "coverageNotes", + kind: "textarea", + label: "Operational Notes", + placeholder: + "Use token references and workflow context only. Exclude PHI.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review and complete.", + kind: "review", + }, + ], + }, + "healthcare.request_vital_recheck": { + successTitle: "Vital recheck workflow simulated", + successMessage: + "Recheck timing and ownership were captured for bedside follow-up.", + steps: [ + { + title: "Vital & Threshold", + description: "Define concerning vital threshold.", + kind: "form", + fields: [ + { + key: "recheckVitalType", + 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: "thresholdRule", + kind: "text", + label: "Threshold Rule", + placeholder: "HR > 120 for 5 min", + required: true, + }, + ], + }, + { + title: "Recheck Timing & Owner", + description: "Assign recheck timing and owner.", + kind: "form", + fields: [ + { + key: "recheckWindow", + kind: "select", + label: "Recheck Window", + required: true, + options: [ + { value: "5m", label: "5 minutes" }, + { value: "10m", label: "10 minutes" }, + { value: "15m", label: "15 minutes" }, + ], + }, + { + key: "recheckOwner", + kind: "select", + label: "Owner", + required: true, + options: [ + { value: "primary-rn", label: "Primary RN" }, + { value: "charge-rn", label: "Charge RN" }, + { value: "rapid-team", label: "Rapid response team" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review and complete.", + kind: "review", + }, + ], + }, + "healthcare.open_trend_review": { + successTitle: "Trend review workflow simulated", + successMessage: + "Trend review request and clinical context were prepared for analysis.", + steps: [ + { + title: "Trend Window", + description: "Select time range for trend review.", + kind: "form", + fields: [ + { + key: "trendWindow", + kind: "select", + label: "Trend Window", + required: true, + options: [ + { value: "30m", label: "Last 30 minutes" }, + { value: "2h", label: "Last 2 hours" }, + { value: "24h", label: "Last 24 hours" }, + ], + }, + { + key: "trendFocus", + kind: "select", + label: "Focus", + required: true, + options: [ + { value: "instability", label: "Instability pattern" }, + { value: "response", label: "Treatment response" }, + { value: "artifact", label: "Potential monitor artifact" }, + ], + }, + ], + }, + { + title: "Clinical Context", + description: "Capture non-PHI context notes.", + kind: "form", + fields: [ + { + key: "clinicalContextNotes", + kind: "textarea", + label: "Clinical Context Notes", + placeholder: + "Operational context only. Use unit/bed tokens and no patient identifiers.", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review and complete.", + kind: "review", + }, + ], + }, + "healthcare.notify_care_team": { + successTitle: "Care team notification workflow simulated", + successMessage: + "Care team notification and urgency context were prepared for routing.", + steps: [ + { + title: "Team Selection", + description: "Select notification recipients.", + kind: "form", + fields: [ + { + key: "careTeamTarget", + kind: "select", + label: "Care Team", + required: true, + options: [ + { value: "primary-team", label: "Primary care team" }, + { value: "consult-team", label: "Consult team" }, + { value: "oncall-team", label: "On-call coverage" }, + ], + }, + { + key: "notifyChannel", + kind: "select", + label: "Channel", + required: true, + options: [ + { value: "ehr-inbox", label: "EHR inbox (demo)" }, + { value: "secure-chat", label: "Secure chat (demo)" }, + { value: "unit-board", label: "Unit board task" }, + ], + }, + ], + }, + { + title: "Message & Priority", + description: "Set priority and write handoff message.", + kind: "form", + fields: [ + { + key: "notifyPriority", + kind: "select", + label: "Priority", + required: true, + options: [ + { value: "routine", label: "Routine" }, + { value: "high", label: "High" }, + { value: "urgent", label: "Urgent" }, + ], + }, + { + key: "notifyMessage", + kind: "textarea", + label: "Message", + placeholder: + "Use operational context and token references only (no PHI).", + required: true, + }, + ], + }, + { + title: "Confirm", + description: "Review and complete.", + kind: "review", + }, + ], + }, + "healthcare.escalate_handoff": { + successTitle: "Handoff escalation workflow simulated", + successMessage: + "Handoff escalation target and SLA requirements were captured.", + steps: [ + { + title: "Handoff Target", + description: "Choose escalation destination.", + kind: "form", + fields: [ + { + key: "handoffTarget", + kind: "select", + label: "Target Team", + required: true, + options: [ + { value: "charge-nurse", label: "Charge nurse" }, + { value: "hospitalist", label: "Hospitalist coverage" }, + { value: "rapid-response", label: "Rapid response coordinator" }, + ], + }, + { + key: "handoffReason", + kind: "textarea", + label: "Escalation Reason", + placeholder: + "Summarize operational urgency and context with non-PHI details.", + required: true, + }, + ], + }, + { + title: "Risk & SLA", + description: "Define risk posture and response timeline.", + kind: "form", + fields: [ + { + key: "handoffRisk", + kind: "select", + label: "Risk Level", + required: true, + options: [ + { value: "moderate", label: "Moderate" }, + { value: "high", label: "High" }, + { value: "critical", label: "Critical" }, + ], + }, + { + key: "handoffSla", + kind: "select", + label: "SLA Target", + required: true, + options: [ + { value: "10m", label: "10 minutes" }, + { value: "30m", label: "30 minutes" }, + { value: "60m", label: "60 minutes" }, + ], + }, + ], + }, + { + title: "Confirm", + description: "Review 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 ( +