# Story 3.3: Badge-Based Element Referencing for Targeted Modifications

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## Story

As a user,
I want to click diagram elements to reference them in my chat messages,
so that I can tell the AI exactly which elements to modify.

## Acceptance Criteria

1. **Given** I click a node on the canvas, **When** the node is selected, **Then** a badge chip appears near the chat input area showing the node's label/name, **And** the badge animates in with a slide effect (200ms ease-out), **And** the chat input placeholder changes to "Describe changes to [node name]..."

2. **Given** I have badge(s) in the chat input area, **When** I type a message and send it, **Then** the AI receives the selected element context along with my message (FR4), **And** the AI operates in targeted scope — only modifying the referenced elements, **And** the AI response specifically addresses the badged elements

3. **Given** I have a badge chip displayed, **When** I click the X on the badge chip or click empty canvas, **Then** the badge is removed, **And** the chat returns to whole-diagram scope (FR3)

4. **Given** I select multiple elements (multi-select or rectangle drag), **When** badges are created, **Then** multiple badge chips appear near the chat input, **And** the AI receives all selected elements as context for the modification

5. **Given** I select an element and type "split this into two steps", **When** the AI processes the scoped request, **Then** it generates a minimal JSON patch affecting only the referenced element and its immediate connections, **And** the canvas updates only the affected area (not full re-render)

## Tasks / Subtasks

- [x] Task 1: Create BadgeChip component (AC: #1, #3)
  - [x] 1.1 Create `apps/web/src/modules/copilot/components/BadgeChip.tsx` — shadcn/ui Badge variant with dismiss (X) button, truncated node label, diagram-type-aware icon
  - [x] 1.2 Implement slide-in animation (200ms ease-out) using Motion `animate` + `exit` with `AnimatePresence`
  - [x] 1.3 Click badge → scroll canvas to element + highlight (reuse existing `highlightedNodeId` store state)
  - [x] 1.4 Click X → call `setSelectedNodeIds` to remove that node ID from selection array
  - [x] 1.5 Add ARIA label `"Selected element: [name]"`, keyboard dismissible via Backspace/Delete
  - [x] 1.6 Style with `--badge-chip-bg`, `--badge-chip-border`, `--badge-chip-text` CSS custom properties (from UX design tokens)

- [x] Task 2: Integrate BadgeChip display into CopilotPanel (AC: #1, #3, #4)
  - [x] 2.1 Subscribe to `selectedNodeIds` from `useGraphStore` in CopilotPanel
  - [x] 2.2 Map selected IDs to node data (label, type) by reading `nodes` from store
  - [x] 2.3 Render badge chips in a flex-wrap container above the textarea input, inside the border-t input area
  - [x] 2.4 Update placeholder text: when badges present → "Describe changes to [first badge name]..." (or "Describe changes to N elements..." for multi-select)
  - [x] 2.5 Wrap badge chips in `AnimatePresence` for enter/exit animations
  - [x] 2.6 Handle Escape key in textarea: clear all badges (deselect all nodes on canvas)

- [x] Task 3: Extend copilotMessageSchema with selectedElements (AC: #2, #4)
  - [x] 3.1 Add `selectedElements` optional field to `copilotMessageSchema` in `packages/ai/src/modules/copilot/schema.ts` — array of `{ id: string, type: string, label: string }`
  - [x] 3.2 Export `SelectedElement` type from `packages/ai/src/modules/copilot/types.ts`

- [x] Task 4: Pass selected element context from CopilotPanel to API (AC: #2, #4)
  - [x] 4.1 In `prepareSendMessagesRequest`, serialize selected nodes: for each `selectedNodeIds`, extract the node's graph-level data (id, type, label, connected edges, neighbor node labels) via `flowToGraph` filtered to selected
  - [x] 4.2 Include `selectedElements` in the request body alongside existing `graphContext`
  - [x] 4.3 Include connected edges of selected nodes as `selectedEdges` for neighbor context

- [x] Task 5: Update system prompt for scoped AI context (AC: #2, #5)
  - [x] 5.1 Extend `buildCopilotSystemPrompt` options to accept `selectedElements?: SelectedElement[]` and `selectedContext?: string` (JSON of selected nodes + their edges + 1-hop neighbors)
  - [x] 5.2 When selectedElements are provided, add a `## Scoped context` section to the system prompt specifying: "The user has selected specific elements for targeted modification. Focus your changes on these elements and their immediate connections. Include ALL nodes and edges in your output, but only modify the selected ones."
  - [x] 5.3 Include selected nodes' full data (properties, connected edges, neighbor labels) in the scoped context section
  - [x] 5.4 Add instruction: "When elements are selected, prefer minimal changes. Modify, split, merge, or restructure only the referenced elements. Preserve all other nodes and edges unchanged."
  - [x] 5.5 Update `system-prompt.test.ts` to cover scoped context variations

- [x] Task 6: Update API handler to pass selectedElements (AC: #2)
  - [x] 6.1 In `streamCopilot` in `packages/ai/src/modules/copilot/api.ts`, destructure `selectedElements` from the validated payload
  - [x] 6.2 Pass `selectedElements` and formatted `selectedContext` to `buildCopilotSystemPrompt`

- [x] Task 7: Add CSS custom properties for badge chip tokens (AC: #1)
  - [x] 7.1 Add `--badge-chip-bg`, `--badge-chip-border`, `--badge-chip-text` to the diagram editor's CSS (in the existing design token location) — values from UX spec: `oklch(0.623 0.214 260 / 10%)`, `oklch(0.623 0.214 260 / 30%)`, `oklch(0.45 0.20 260)`
  - [x] 7.2 Add dark mode variants for badge chip tokens

- [x] Task 8: Write tests (AC: all)
  - [x] 8.1 System prompt tests: scoped context presence when selectedElements provided, absence when not, correct element data formatting (8 tests)
  - [x] 8.2 Schema tests: selectedElements validation — valid arrays, empty arrays, missing optional field (5 tests)
  - [x] 8.3 Component rendering tests deferred to E2E (per project standards from Story 3.1)

## Dev Notes

### Architecture Compliance

- **AI mutations through CRDT**: Per Winston architecture decision, all graph mutations flow through the Zustand store (which syncs to Liveblocks CRDT in future Epic 4). Badge-based targeted modifications use the same mutation pipeline as whole-diagram operations — the AI still outputs a complete `GraphData` patch via the `generateDiagram` tool. The "targeted" scope is an AI behavioral constraint (system prompt), NOT a different code path.
- **ELK.js in Web Worker**: Layout continues to run in Web Worker after targeted modifications. Use the existing `requestLayout()` trigger from Story 3.2.
- **Unified graph model**: Selected elements are referenced by their `DiagramNode` properties from the unified `GraphData` interface. Badge chips display the node's `label` field.
- **Selection state already exists**: `useGraphStore` has `selectedNodeIds: string[]` and `setSelectedNodeIds(ids: string[])` — already wired to `onSelectionChange` in `DiagramCanvas.tsx` (Story 2.9). This story CONSUMES the existing selection state; it does NOT create new selection mechanisms.

### Critical Implementation Patterns (from Stories 3.1 & 3.2)

**Transport pattern — extend body with selectedElements:**
```typescript
// In CopilotPanel prepareSendMessagesRequest:
const selectedNodeIds = useGraphStore.getState().selectedNodeIds;
const allNodes = useGraphStore.getState().nodes;
const allEdges = useGraphStore.getState().edges;

// Build selected element context for AI
const selectedElements = selectedNodeIds.length > 0
  ? selectedNodeIds.map(id => {
      const node = allNodes.find(n => n.id === id);
      if (!node) return null;
      const data = node.data as { type?: string; label?: string };
      return { id: node.id, type: data.type ?? "unknown", label: data.label ?? node.id };
    }).filter(Boolean)
  : undefined;

return {
  body: {
    ...lastMessage,
    chatId: id,
    diagramId,
    diagramType,
    graphContext,
    selectedElements, // NEW
  },
};
```

**System prompt scoping pattern:**
```typescript
// In buildCopilotSystemPrompt, when selectedElements provided:
## Scoped context — targeted modification
The user has selected ${selectedElements.length} element(s) for modification:
${selectedElements.map(e => `- ${e.label} (${e.type})`).join('\n')}

Connected edges: [edges connecting to/from selected nodes]
Neighbor nodes: [1-hop neighbors for context]

IMPORTANT: The user wants to modify ONLY these elements. Include ALL nodes
and edges in your generateDiagram output, but focus changes on the selected
elements and their immediate connections. Preserve everything else unchanged.
```

**Badge chip click → canvas scroll pattern:**
```typescript
// BadgeChip onClick handler — use React Flow's fitView or setCenter
// CopilotPanel is OUTSIDE ReactFlowProvider, so direct React Flow hooks won't work.
// Instead, use the same Zustand store pattern from Story 3.2:
// Set highlightedNodeId in store → DiagramCanvas reacts to it.
const handleBadgeClick = (nodeId: string) => {
  useGraphStore.getState().setHighlightedNodeId(nodeId);
};
```

### Existing Selection Infrastructure (Story 2.9)

The selection system is already fully wired:

1. `DiagramCanvas.tsx:314-319` — `handleSelectionChange` callback from `@xyflow/react` updates `selectedNodeIds` in Zustand store
2. `useGraphStore.ts:26,37,76` — `selectedNodeIds: string[]` state + `setSelectedNodeIds` action
3. `DiagramCanvas.tsx:332-333` — `onSelectionChange` + `onPaneClick` (clear) bound to ReactFlow
4. `@xyflow/react` handles multi-select (Cmd+Click) and lasso (rectangle drag) natively

**What this story adds:** Consuming `selectedNodeIds` in CopilotPanel to show badge chips and pass scoped context to the AI. No changes to the canvas selection logic itself.

### Unified Graph Model (Source of Truth)

Location: `apps/web/src/modules/diagram/types/graph.ts`

```typescript
type DiagramType = "bpmn" | "er" | "orgchart" | "architecture" | "sequence" | "flowchart";

interface DiagramNode {
  id: string;      // Required
  type: string;    // Required - diagram-type-specific
  label: string;   // Required - display text (shown on badge chip)
  // ... (see Story 3.2 dev notes for full interface)
}

interface DiagramEdge {
  id: string;      // Required
  from: string;    // Required - source node ID
  to: string;      // Required - target node ID
  label?: string;
  type?: string;
  cardinality?: string;
}
```

### Badge Chip UX Requirements (from UX Spec)

| Property | Value |
|---|---|
| Animation | Slide-in 200ms ease-out (entry), fade-out (exit) |
| Content | Node icon + truncated label + X dismiss button |
| Click badge | Scroll canvas to element + highlight |
| Click X | Deselect element on canvas (remove from `selectedNodeIds`) |
| Backspace/Delete | Remove last badge (like tag input pattern) |
| Escape (in textarea) | Clear all badges |
| Max display | Show all selected (scroll horizontally if overflow) |
| Placeholder | "Describe changes to [name]..." or "Describe changes to N elements..." |
| ARIA | `role="listitem"`, `aria-label="Selected element: [name]"` |
| Tokens | `--badge-chip-bg: oklch(0.623 0.214 260 / 10%)`, `--badge-chip-border: oklch(0.623 0.214 260 / 30%)`, `--badge-chip-text: oklch(0.45 0.20 260)` |

### Scope Indicator (UX Spec)

When badges are active, display a subtle scope indicator in the chat: "Context: [node name] + N connected edges" — so users know what the AI sees. This is a small text below the badge area, not a modal or tooltip.

### File Structure

**Files to CREATE:**
```
apps/web/src/modules/copilot/components/
  └── BadgeChip.tsx              # Badge chip component with animation + dismiss
```

**Files to MODIFY:**
```
apps/web/src/modules/copilot/components/
  └── CopilotPanel.tsx           # Add badge display area, selectedElements in transport, placeholder

packages/ai/src/modules/copilot/
  ├── schema.ts                  # Add selectedElements optional field
  ├── types.ts                   # Export SelectedElement type
  ├── system-prompt.ts           # Add scoped context section when elements selected
  ├── system-prompt.test.ts      # Add scoped context tests
  └── api.ts                     # Pass selectedElements to system prompt
```

**Files to REFERENCE (read-only):**
```
apps/web/src/modules/diagram/stores/useGraphStore.ts     # selectedNodeIds state (consume)
apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx  # Selection handling (no changes)
apps/web/src/modules/diagram/lib/graph-converter.ts      # flowToGraph, flowNodeToGraphNode
apps/web/src/modules/diagram/types/graph.ts              # DiagramNode, DiagramEdge interfaces
```

### Project Structure Notes

- BadgeChip goes in `apps/web/src/modules/copilot/components/` — co-located with CopilotPanel, NOT in diagram module (badge is a copilot UI concern)
- Per the epics file, the suggested path was `apps/web/src/modules/chat/BadgeChip.tsx` — but the project uses `copilot` not `chat` as the module name. Use `apps/web/src/modules/copilot/components/BadgeChip.tsx`
- Schema changes go in `packages/ai/src/modules/copilot/schema.ts` — extend existing `copilotMessageSchema`
- System prompt changes go in `packages/ai/src/modules/copilot/system-prompt.ts` — extend `buildCopilotSystemPrompt` options

### Anti-Patterns to AVOID

- **Do NOT create a separate API endpoint** for scoped modifications — extend the existing copilot POST. The AI already receives `graphContext`; this story adds `selectedElements` as additional context.
- **Do NOT modify canvas selection behavior** — selection is already working via `@xyflow/react` built-in + Zustand store from Story 2.9. This story READS from the store, not writes.
- **Do NOT create a "patch mode" that only sends partial graph data** — the AI always outputs a COMPLETE GraphData. "Targeted" scope is a system prompt instruction that tells the AI to focus changes on selected elements while preserving everything else.
- **Do NOT use `require()` or CommonJS** — all packages are ESM-only
- **Do NOT inline `.parse()` in Hono handlers** — use `validate()` middleware
- **Do NOT put business logic in API routers** — handlers call domain package functions
- **Do NOT re-create selection UI** — use `@xyflow/react`'s existing `elementsSelectable` + `onSelectionChange`
- **Do NOT use `uuid()` column type** — always `text().primaryKey().$defaultFn(generateId)`
- **Do NOT run ELK.js on main thread** — use existing Web Worker

### Performance Requirements

- Badge chip animation: ≤ 200ms (UX spec)
- Badge appear after selection: < 50ms (instant feel — just Zustand state read)
- AI targeted mutation applied and rendered < 3 seconds (NFR4, same as whole-diagram)
- Respect `prefers-reduced-motion` — badges appear instantly instead of slide-in

### Security Requirements

- `enforceAuth` middleware on all endpoints (already in place)
- `deductCredits` middleware before AI generation calls (already in place)
- `selectedElements` data comes from client-side store (trusted) — validate schema structure only
- AI output still validated through `validateGraphPatch` mutation schema before applying to graph

### Testing Standards

- Test runner: Vitest with explicit imports (`import { describe, it, expect } from 'vitest'`)
- Test location: co-located with source files
- Factory pattern for test data
- Component rendering tests deferred to E2E per project standards
- Workspace command: `pnpm --filter @turbostarter/ai test`
- Expected new tests: ~10-15 (system prompt scoped context + schema selectedElements validation)

### Previous Story Intelligence (Story 3.2)

**What was built:**
- `generateDiagramTool` with `graphPatchSchema` — AI outputs full GraphData via tool call
- `useGraphMutation.ts` hook — applies AI patches to Zustand store + persists to DB
- System prompt with generation/modification instructions, node/edge type references, examples
- Tool invocation detection in CopilotPanel via `isGenerateDiagramTool` type guard
- `graphContext` serialization in transport's `prepareSendMessagesRequest`
- `layoutRequestId` counter for cross-component layout triggering

**Key learnings:**
- AI SDK v4 uses `tool-${toolName}` type pattern (e.g., `tool-generateDiagram`)
- AI SDK v4 tool part states: `input-streaming`, `input-available`, `output-available`, `output-error`
- `useGraphStore.getState()` in transport callback avoids transport re-creation
- Cross-component communication via Zustand store counters (not React Flow hooks)

**Files created in 3.2 (do NOT recreate):**
- `packages/ai/src/modules/copilot/mutation-schema.ts` — Zod schema + validation
- `packages/ai/src/modules/copilot/mutation-schema.test.ts` — 38 tests
- `apps/web/src/modules/copilot/hooks/useGraphMutation.ts` — graph patch application hook

**Review fixes from 3.2 (already in codebase):**
- `diagramType` validated as `z.enum(VALID_DIAGRAM_TYPES)` in mutation schema
- `toChatMessage()` preserves tool invocation parts
- `validateUniqueIds()` prevents duplicate node/edge IDs
- `graphData` structurally validated in `updateDiagramBodySchema`

**No new dependencies needed** — Motion (for animations) is already in workspace, shadcn/ui Badge is available.

### Git Intelligence

Recent commit pattern: `feat: implement Story X.Y — <description>`. Follow this convention.

Last 5 commits all follow the pattern of implementing one story per commit. Files modified in Story 3.2:
- CopilotPanel.tsx (extended — will extend further)
- system-prompt.ts (extended — will extend further)
- api.ts (extended — will extend further)
- schema.ts (extended — will extend further, minor)
- types.ts (extended — will extend further, minor)

### References

- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.3] — Acceptance criteria, technical notes
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#BadgeChip] — Component spec: animation, content, states, interactions, accessibility, tokens
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Core User Experience] — Badge referencing as signature interaction, conversational design loop
- [Source: _bmad-output/planning-artifacts/architecture.md] — AI mutation pipeline (client-side relay), soft-lock, bidirectional canvas↔chat state
- [Source: _bmad-output/project-context.md] — Framework rules, coding standards, anti-patterns
- [Source: apps/web/src/modules/diagram/stores/useGraphStore.ts] — selectedNodeIds state (lines 26, 37, 76)
- [Source: apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx] — onSelectionChange handler (lines 314-319)
- [Source: _bmad-output/implementation-artifacts/3-2-ai-diagram-generation-from-natural-language.md] — Previous story patterns, review fixes, established conventions

## Dev Agent Record

### Agent Model Used

Claude Opus 4.6

### Debug Log References

- Fixed BadgeChip icon names: `Icons.User` → `Icons.User2`, `Icons.Box` → `Icons.Server`, `Icons.Layers` → `Icons.Package` (not exported in project's icons.tsx)
- Pre-existing TS errors in bpmn-layout.ts and CopilotPanel setMessages type (not from this story)

### Completion Notes List

- All 8 tasks completed, all subtasks done
- 103 copilot tests passing (33 system-prompt, 38 mutation-schema, 15 schema, 17 chunking)
- 8 scoped context tests + 6 buildSelectedContext tests + 6 selectedElements schema tests = 20 new tests
- No new TypeScript errors introduced (verified via `tsc --noEmit`)
- Badge chips use Motion animations with `AnimatePresence` for enter/exit
- AI receives `selectedElements` + `selectedContext` (selected nodes, connected edges, 1-hop neighbors)
- System prompt adds "Scoped context" section instructing AI to focus modifications on selected elements

### Code Review Fixes Applied (2026-02-28)

- **H1**: Added `prefers-reduced-motion` support via `useReducedMotion()` hook in BadgeChip — animations skip when OS reduced-motion enabled
- **H2**: Added scope indicator text below badges: "Context: [name] + N connected edges"
- **M1/M2**: Fixed `handleDismissBadge` to use `getState()` instead of closure — eliminates stale closure risk and prevents memo invalidation
- **M3**: Moved `buildSelectedContext` from api.ts to system-prompt.ts for testability; added 6 unit tests
- **L1**: Changed X dismiss button `tabIndex` from -1 to 0 for keyboard accessibility
- **L2**: Replaced O(N*M) `.find()` in selectedElements memo with O(N) `Map` lookup
- **L3**: Added `.min(1)` constraints to `selectedElementSchema` string fields
- **L4**: Added MAX_NEIGHBOR_NODES=10 limit to prevent prompt bloat from highly-connected nodes

### File List

**Created:**
- `apps/web/src/modules/copilot/components/BadgeChip.tsx`

**Modified:**
- `apps/web/src/modules/copilot/components/CopilotPanel.tsx` — badge display, Escape key, selectedElements in transport, dynamic placeholder, scope indicator
- `packages/ai/src/modules/copilot/schema.ts` — `selectedElementSchema`, `selectedElements` field
- `packages/ai/src/modules/copilot/types.ts` — `SelectedElement` type export
- `packages/ai/src/modules/copilot/system-prompt.ts` — `buildScopedContextSection`, `buildSelectedContext`, extended options
- `packages/ai/src/modules/copilot/system-prompt.test.ts` — 8 scoped context tests + 6 buildSelectedContext tests
- `packages/ai/src/modules/copilot/schema.test.ts` — 6 selectedElements validation tests
- `packages/ai/src/modules/copilot/api.ts` — selectedElements passthrough (buildSelectedContext moved to system-prompt.ts)
- `apps/web/src/assets/styles/globals.css` — badge chip CSS tokens (light + dark)
