Commit Graph

18 Commits

Author SHA1 Message Date
Arnas Donauskas
dcdfff938d
feat(flows): canvas / mind-map view (PR 2b of 3 — full editing)
Closes the editing-affordance gap from PR 2a. The canvas is now in
full parity with the list view for everything that fits in a node
graph: connections, deletes, and adding nodes all work on-canvas.

Adds:

  - Per-slot Handle components on the custom node renderer. Single-
    outgoing types get one handle on the right edge; condition gets
    `true` / `false` handles; send_buttons and send_list render slot
    rows inside the card with one handle each, so the user can SEE
    which button or list row they're wiring. Every non-start node
    gets a target handle on the left.

  - Drag-to-connect (`onConnect`). Looks up the source node, dispatches
    the right config patch via the new `applyEdgeConnection(node,
    sourceHandle, target)` helper, and routes through updateNodeConfig.
    Rejects self-loops silently (a button whose target is its own node
    = infinite reprompt).

  - Keyboard delete on nodes (Backspace + Delete, both for cross-OS
    parity). Routes through removeNode, which now ALSO unlinks every
    inbound reference via the new `unlinkNodeReferences(nodes, key)`
    helper. No more dangling arrows after a delete — the validator
    previously flagged these as errors and the user had to chase them
    down manually.

  - Edge delete. Selecting an edge + Delete clears just that slot's
    next_node_key (rather than removing anything), via the same
    applyEdgeConnection helper passing "" as the target.

  - Floating "+ Add node" button (React-Flow Panel, bottom-right).
    Same dropdown as the list view; drops the new node at the visible
    viewport center via reactFlow.screenToFlowPosition.

New pure helpers in src/lib/flows/edges.ts (no React, fully unit-
tested — 18 new tests, 319 total passing):

  - outgoingSlots(node) — list of {id, label} per outgoing slot.
  - applyEdgeConnection(node, sourceHandle, targetKey) — config patch
    for a drag-to-connect, or null for unrecognised handles.
  - unlinkNodeReferences(nodes, deletedKey) — clears every direct +
    nested reference to deletedKey. Returns nodes-by-identity when
    no reference matches to avoid needless re-renders.

The hook's removeNode is the only consumer of unlinkNodeReferences
right now, which means LIST-view deletes also stop leaving dangling
refs — strict improvement for both views.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:32:59 +02:00
Arnas Donauskas
1645034e1a
feat(flows): canvas / mind-map view (PR 2a of 3 — editable foundation)
Builds on PR 1's read-only canvas. This PR brings the canvas to ~75%
of list-view parity: drag-to-reposition (persisted) and click-to-edit
via a side panel that mounts the same per-node forms the list view
uses. Drag-to-connect + add/delete from canvas land in PR 2b.

Architecture (the bulk of the diff):

  - Extracted the per-node forms (SendButtonsForm, SendListForm,
    SendMediaForm, ConditionForm, SetTagForm + inline forms for the
    smaller types) plus the shared field components (TextRow,
    NextNodeRow, NodeKeySelect) from flow-builder.tsx into
    src/components/flows/forms/. Both views now mount the SAME
    NodeConfigForm dispatcher so behaviour can't drift between
    "what the list editor shows" and "what the canvas side panel
    shows".
  - Lifted all editor state into a context/hook
    (src/components/flows/flow-editor-state.tsx) with a
    FlowEditorProvider that wraps both views in flow-editor-shell.
    Single source of truth: toggling Canvas ⇄ List never resets
    unsaved edits, and a drag on the canvas updates the same nodes
    array the list view reads.
  - flow-builder.tsx shrinks from ~2400 lines to ~900 (extracted
    forms) and stops owning state (now consumes via useFlowEditor()).
    List-only UI state — expanded cards, scroll refs, jump-to-node
    flash — stays internal.

Canvas additions:

  - onNodeDragStop writes the new position to context via
    updateNodePosition (flips dirty; serialized in the existing PUT
    /api/flows/[id] body — the route already accepts position_x /
    position_y per migration 010).
  - onNodeClick opens a controlled shadcn Sheet (right-side panel)
    with the per-node form, plus "Set as entry" and "Delete node"
    buttons. Esc / overlay / close button all close.

What's intentionally still NOT in this PR (PR 2b):

  - Per-slot Handle components on the custom node renderer.
  - Drag-to-connect handler that maps target → updated
    next_node_key for the right slot.
  - Floating "+" button to add nodes from canvas.
  - Keyboard / context-menu delete.

Tests: 9 new pure-helper tests (uniqueNodeKey + defaultConfigFor
self-consistency). Hook integration tests are deferred — would
require @testing-library/react + jsdom infra. Existing 271 tests
still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 20:17:15 +02:00
Arnas Donauskas
bc859bcbcd
feat(flows): canvas / mind-map view (PR 1 of 3 — read-only)
The second half of the user feedback that drove send_media: the
linear-list editor reads as a checklist, not a branching flow, which
made conversational structures hard to understand at a glance. This
introduces a canvas view alongside the list — boxes + arrows, drag-
zoom-pan, labeled edges per branch slot. Canvas is the new default;
the list stays as a fallback toggle (per-browser preference,
persisted to localStorage).

PR 1 scope is intentionally read-only: render, don't edit. The
foundation is testable in isolation before adding drag-to-persist,
side-panel editing, and drag-to-connect (PR 2) and polish like
keyboard shortcuts, mobile fallback, and validator integration
(PR 3).

Pieces:
  - New @xyflow/react (~30 KB gz) + @dagrejs/dagre (auto-layout for
    pre-existing flows whose position_x / position_y are all zero —
    flow_nodes already reserved those columns in migration 010
    precisely for this).
  - Shared module src/components/flows/shared.tsx lifts NodeType,
    BuilderNode, NODE_META, summarizeNode, truncate so both views
    render nodes identically.
  - Pure helpers (no React, fully unit-tested):
      src/lib/flows/edges.ts   — derive labeled canvas edges per
                                 outgoing slot (button, list row,
                                 condition branch)
      src/lib/flows/layout.ts  — dagre wrapper + shouldAutoLayout
                                 guard so manually-positioned flows
                                 never get rearranged on open
  - FlowCanvas — client component, custom node renderer matching the
    list view's collapsed-card styling, MiniMap + Controls, attribution
    hidden.
  - FlowEditorShell — owns the Canvas / List toggle, conditionally
    mounts the right view.

21 new unit tests (layout + edges); existing 271 still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:41:37 +02:00
Arnas Donauskas
62073d9915
feat(flows): add send_media node for image / video / document sends
Lets flows attach an image, video, or document (e.g. PDF invoice /
receipt) to a customer message — the most-requested gap in v1 was that
nodes were text-only. Single node with a `media_type` discriminator
rather than three node types: Meta's wire payload differs only in the
top-level key, so three types would triple the surface area (forms,
engine cases, menu entries) for no behavioural difference.

Files are uploaded directly from the builder to a new public
`flow-media` Supabase Storage bucket (per-user RLS, 16 MB cap, MIME
allowlist) and the resulting public URL is what Meta fetches at send
time. Same trade-off as the existing avatars bucket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:11:29 +02:00
Arnas Donauskas
32022c13fe
feat(theme): sweep remaining violet-* → primary across all surfaces
PR #132 (color themes) only tokenized the high-visibility chrome.
Inbox / contacts / pipelines / broadcasts / automations / auth /
settings sub-forms still had ~150 hard-coded `violet-*` classes
across ~32 files, so picking a non-violet theme felt half-broken —
sidebar and header changed colour, but message bubbles, deal cards,
and the broadcast wizard all stayed violet.

This is the mechanical follow-up: replace every `violet-N(/N)?`
with `primary(/N)?` so the theme picker themes the whole app.

Two non-trivial swaps inside the pass:
- `text-white` on a `bg-primary` button → `text-primary-foreground`.
  Emerald and Amber define a *dark* `--primary-foreground` because
  their primaries are bright; `text-white` over those would tank
  contrast.
- `bg-primary hover:bg-primary` (hover identical to base — original
  code had this latent bug from `hover:bg-violet-500`) → adds `/90`
  on hover so the lift is visible.

Test update:
- `broadcast-status.test.ts` asserted shade numbers (`bg-X-N/10`).
  Relaxed the regex to also accept token form (`bg-X/10`) since
  brand-accent status badges now ride the active theme.

Files touched: 49. Lint clean (existing warnings only), 173/173
tests, typecheck + build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:00:32 +03:00
Arnas Donauskas
54a969c56d
feat(settings): color theme picker (5 themes)
Adds a new "Appearance" tab in Settings with 5 dark color themes
(Violet, Emerald, Cobalt, Amber, Rose). Pick a theme → applied
immediately and persisted to localStorage. Boot script in the root
layout replays the pick before first paint so there's no
flash-of-default on subsequent loads.

Implementation, ordered the way it's read:

1. `globals.css` — 5 `html[data-theme="..."]` blocks extracted
   verbatim from the design handoff at project/Color Themes.html.
   `:root` mirrors Violet so SSR / first-paint renders correctly
   even before the boot script runs. Three new tokens
   (`--primary-hover`, `--primary-soft`, `--primary-soft-2`) added
   to all themes — used by tinted-primary surfaces.

2. `src/lib/themes.ts` — single source of truth for the catalog
   (id, name, tagline, swatch). Adding a theme = append a CSS
   block + append an entry here.

3. `src/hooks/use-theme.tsx` — ThemeProvider client component. Reads
   what the boot script already applied, syncs across tabs via the
   `storage` event, persists to localStorage. Falls back to a no-op
   setter when used outside the provider.

4. `src/app/layout.tsx` — inline `beforeInteractive` script that
   reads localStorage and sets `documentElement.dataset.theme`
   before React hydrates. Knowledge of valid theme IDs comes from
   the THEME_IDS constant so adding a theme doesn't silently break
   the boot path. Wraps the tree in ThemeProvider.

5. `src/components/settings/appearance-panel.tsx` — picker UI. 5
   cards, swatch + name + tagline + a 4-tone preview strip. Active
   card carries a primary-tinted ring + "Active" chip.

6. `src/app/(dashboard)/settings/page.tsx` — new "appearance" tab
   alongside profile/whatsapp/templates/tags.

7. Key-surface tokenization — replaced hard-coded `violet-*` with
   `primary` tokens on the high-visibility surfaces so the theme
   switch feels real: sidebar logo + active nav + unread dot +
   avatar fallback, header avatar, dashboard-shell loading
   spinner, Flows header icon + entry-node picker icon + entry
   badge + flash ring, Flows list page (template card hover,
   FlowCard icon), Flows editor + runs "back" links, Settings tabs
   active state.

NOT touched (intentional follow-ups):
- Per-node-type colors in flow-builder.tsx NODE_META — those are a
  deliberate per-type rainbow, not a brand accent.
- The inbox/contacts/pipelines surfaces — many `violet-*` usages
  there. Sweep is a contained follow-up; the user explicitly
  scoped this PR to "tokens + tab + key surfaces".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:48:40 +03:00
Arnas Donauskas
47836d2c08
fix(flows): pin validation panel to viewport bottom while editing
"No issues. Ready to activate." (and the equivalent error count)
sat at the very bottom of the editor. With even a handful of nodes
the panel was below the fold — the user had to scroll past every
node card to see whether they could activate.

Wrap the panel in a sticky-bottom container so the activate-readiness
status follows the user as they scroll. Shadow-xl gives it the lift
to read as "pinned" against the node list behind it.

Side fix while I was in there: the success-state panel used
bg-emerald-500/10 (10% emerald on whatever's behind). Fine when
nothing's behind, but sticky-positioned over a slate-900 node card
the green tint bled through ugly. Switched both states to a
bg-slate-950 base with the severity expressed in the border + text
color only — readable on any backdrop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:19:19 +03:00
Arnas Donauskas
31bd967861
Merge remote-tracking branch 'origin/main' into feat/flows-unsaved-changes-guard
# Conflicts:
#	src/components/flows/flow-builder.tsx
2026-05-22 15:16:35 +03:00
Arnas Donauskas
323e0f06cb
Merge pull request #121 from ArnasDon/feat/flows-clickable-validation-issues
feat(flows): clickable validation issues jump to node
2026-05-22 15:10:30 +03:00
Arnas Donauskas
ad91d205fb
feat(flows): unsaved-changes indicator + reload guard
Editing 5 node configs and then refreshing the tab silently wiped them.
Activate also silently invoked Save first — useful, but invisible: users
had no way to know if pending edits had landed.

Adds:
- A `dirty` boolean flipped true on any user edit, false on a successful
  PUT. Internal `state.status` changes from the activate API path use
  raw setState and don't dirty the form.
- An "● Edited" amber chip next to the StatusBadge while dirty.
- A `beforeunload` listener that prompts the native confirm on
  reload / tab-close / external-link click when dirty.

SPA navigation (sidebar, back button) isn't covered — Next 16's App
Router doesn't fire beforeunload on client-side routes. Worth a
follow-up but the accidental-refresh case is the noisier one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:10:06 +03:00
Arnas Donauskas
b95ce8b372
Merge pull request #120 from ArnasDon/feat/flows-node-card-previews
feat(flows): show content preview in collapsed node cards
2026-05-22 15:06:38 +03:00
Arnas Donauskas
edbbb894f6
feat(flows): make validation issues clickable to jump to node
The validation panel sits at the bottom of the editor. When it surfaced
an error ("node X is missing next_node_key") the user had to scroll up,
find that node, expand it, find the broken field. Long feedback loop.

Each node-scoped IssueLine becomes a button: click it and the editor
expands the offending node, scrolls it into view, and briefly rings
its border so the eye lands on the right card. Trigger-scoped issues
stay non-interactive (the trigger panel is already at the top of the
page).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:05:44 +03:00
Arnas Donauskas
9c7f635a4d
Merge pull request #119 from ArnasDon/feat/flows-send-list-add-section
feat(flows): add multi-section support to send_list builder
2026-05-22 15:02:20 +03:00
Arnas Donauskas
6079bf2046
feat(flows): show content preview in collapsed node cards
A flow with 8 send_message nodes used to show 8 identical-looking pills
in the editor — the only way to tell them apart was to expand each
one. Adds a one-line summary below the meta label when the card is
collapsed:

- send_message → first 80 chars of text
- send_buttons → text + button titles
- send_list → text + option count
- collect_input → prompt + target var
- condition → "var.email == 'support@…'"
- set_tag → "Add tag 1f8c…"
- handoff → first 80 chars of note

Empty nodes (just added, no fields set) keep the old compact look so
you can tell at a glance what still needs configuration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:01:53 +03:00
Arnas Donauskas
5a9a69a3c5
feat(flows): add multi-section support to send_list builder
The data model and the runner already supported multiple sections in a
WhatsApp interactive list, but the builder UI exposed no way to add one
— it only let you add rows within the single default section. So users
couldn't build a categorized menu (Billing / Support / Sales) without
editing the DB directly.

Adds an "Add section" button below the sections list (capped at the
WhatsApp spec's 10) and a per-section remove button (shown when more
than one section exists).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:56:52 +03:00
Arnas Donauskas
268ae7076f
feat(flows): hide internal node_key + reply_id behind Advanced disclosure
Node key and per-button/row reply IDs are auto-generated and consumed
only by the runner — exposing them in the main form read as developer
leakage. Move them behind a per-node "Show advanced" toggle so the
default UI shows only customer-facing fields. Power users who need
stable IDs for analytics can still get them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:52:44 +03:00
Arnas Donauskas
ed15493f95
feat(flows): templates + v1.5 nodes (collect_input/condition/set_tag) + run-history viewer (PR 4/4)
Final feature PR before GA cleanup. Closes the gap between "flow
runner works" (PR 2) and "users have everything they need to build
real automation" — three new node types, plain-text sends actually
sending, three starter templates, a run-history page for debugging.

What landed:

  1. engineSendText helper (src/lib/flows/meta-send.ts) — wraps
     sendTextMessage with the phone-variant retry + DB persistence
     pattern. Unblocks send_message + collect_input nodes that
     previously auto-advanced without sending.

  2. Three new node types in the engine:
     - collect_input: sends a prompt, suspends, captures the next
       text reply into flow_runs.vars[var_key], advances. Supports
       {{vars.X}} interpolation in downstream prompts + handoff
       notes.
     - condition: branches on subject (var / tag / contact_field)
       with operators (equals / contains / present / absent).
       Auto-advancing, no Meta call. Pure predicate logic exported
       as evaluateConditionPredicate with 7 new unit tests.
     - set_tag: upsert/delete on contact_tags then advance. Non-
       fatal on write failure — logged + advances anyway so a tag
       glitch doesn't strand the customer.

  3. Real send_message — calls engineSendText, logs the result,
     handles errors by ending the run with status='failed'. Same
     {{vars.X}} interpolation.

  4. Three starter templates (src/lib/flows/templates.ts):
     - Welcome menu (the owner's brief example)
     - FAQ bot (keyword → list → answers → end)
     - Lead capture (first_inbound → collect_input chain → handoff
       with vars in the note)
     POST /api/flows now accepts {template_slug} to clone a
     template into a fresh draft. GET /api/flows/templates exposes
     the gallery shape (no nodes payload — fetched on actual clone).

  5. Builder UI extensions:
     - NODE_META + config sub-forms for collect_input, condition,
       set_tag.
     - Template gallery on the /flows New-flow dialog (loads from
       /api/flows/templates, falls back gracefully if absent).
     - "Runs" button in the editor header → /flows/[id]/runs.

  6. Run-history viewer at /flows/[id]/runs — newest-first list of
     the 50 most recent runs, with expandable per-run event
     timeline (started, node_entered, message_sent,
     reply_received, fallback_fired, handoff, timeout, error,
     completed). Useful for debugging "why didn't my flow advance?"
     from the brief's use case.

  7. Validator updates: new error rules for each new node type
     (prompt_text required, var_key alphanumeric, operator valid,
     true_next/false_next exist, etc.). outgoingEdges helper now
     covers collect_input/condition/set_tag for reachability.

All gated behind isFlowsEnabled(profile). Non-beta accounts see no
change. Run-history API is also 404 for non-beta.

Validation: 173/173 tests pass (added 7 new for the predicate);
typecheck + lint + build all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:27:01 +03:00
Arnas Donauskas
4871d7aa06
feat(flows): no-code builder UI + flows API + validator (PR 3/4)
Ships the surface that lets opted-in beta accounts build, save, and
activate WhatsApp conversation flows entirely from the web UI — no
SQL fixtures needed. Sidebar gets a "Flows" entry (hidden for
non-beta accounts), the /flows list page surfaces existing flows,
/flows/[id] is the linear-list editor.

Wraps the runner from PR 2 with everything a real user needs to
build the welcome-menu example end-to-end.

What landed:

  1. `src/lib/flows/validate.ts` — pure validator. Checks entry
     node exists, all next_node_key refs resolve, no unreachable
     nodes, Meta-limit violations (button title ≤20, ≤3 buttons,
     ≤10 list rows, ≤24 chars per list row). 24 unit tests.

  2. `src/app/api/flows/route.ts` — GET (list user's flows),
     POST (create draft).

  3. `src/app/api/flows/[id]/route.ts` — GET (load with nodes),
     PUT (replace name + trigger + nodes; delete-then-insert),
     DELETE (cascade clean-up).

  4. `src/app/api/flows/[id]/activate/route.ts` — status toggle.
     Activation runs the validator; returns 422 + the full issue
     list when blocked.

  5. `src/app/(dashboard)/flows/page.tsx` — list of cards, "New
     flow" dialog, delete with confirm.

  6. `src/app/(dashboard)/flows/[id]/page.tsx` — editor shell
     that loads data and hands it to FlowBuilder.

  7. `src/components/flows/flow-builder.tsx` — the editor itself.
     Header with name + save/activate/pause/delete; trigger
     panel; entry-node picker; node list with expandable per-node
     config forms (start, send_buttons, send_list, send_message,
     handoff, end); bottom validation panel that mirrors what the
     server's activate endpoint would reject.

  8. `src/components/layout/sidebar.tsx` — Flows entry between
     Automations and Settings, rendered only when isFlowsEnabled.

Every API route + page is gated server-side by isFlowsEnabled —
non-beta accounts get 404 (not 403) so the routes don't even hint
at existing. UI gate is for snappy redirect; security is at the
API layer.

Validation: 166/166 tests pass (added 24 validator tests);
typecheck + lint + build all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:57:38 +03:00