Daniel Tu commited on
Commit
f31dad3
·
unverified ·
2 Parent(s): 874259449af520

Merge pull request #8 from danghoangnhan/feat/editable-plan-card

Browse files

feat: editable plan card with inline editing, agent assist, and notes

agents/design_state.py CHANGED
@@ -50,6 +50,7 @@ class DesignPlan(BaseModel):
50
  axis_recommendation: str = ""
51
  machining_notes: list[str] = Field(default_factory=list)
52
  confidence_score: float = 0.0
 
53
 
54
  @classmethod
55
  def from_state(cls, state: "DesignState", confidence_score: float) -> "DesignPlan":
@@ -86,6 +87,8 @@ class DesignPlan(BaseModel):
86
  lines.append(f"Axis: {self.axis_recommendation}")
87
  if self.machining_notes:
88
  lines.append(f"Machining Notes: {'; '.join(self.machining_notes)}")
 
 
89
  lines.append("")
90
  lines.append("This plan has been reviewed and approved by the user.")
91
  lines.append("Generate the model according to these specifications.")
 
50
  axis_recommendation: str = ""
51
  machining_notes: list[str] = Field(default_factory=list)
52
  confidence_score: float = 0.0
53
+ notes: str = ""
54
 
55
  @classmethod
56
  def from_state(cls, state: "DesignState", confidence_score: float) -> "DesignPlan":
 
87
  lines.append(f"Axis: {self.axis_recommendation}")
88
  if self.machining_notes:
89
  lines.append(f"Machining Notes: {'; '.join(self.machining_notes)}")
90
+ if self.notes:
91
+ lines.append(f"User Notes: {self.notes}")
92
  lines.append("")
93
  lines.append("This plan has been reviewed and approved by the user.")
94
  lines.append("Generate the model according to these specifications.")
config.yaml CHANGED
@@ -95,6 +95,14 @@ planning:
95
  approved_agents:
96
  - "cad"
97
  - "cnc"
 
 
 
 
 
 
 
 
98
 
99
  gap_analysis:
100
  category_keywords:
 
95
  approved_agents:
96
  - "cad"
97
  - "cnc"
98
+ field_agents:
99
+ material: engineering
100
+ dimensions: engineering
101
+ features: design
102
+ constraints: cnc
103
+ axis_recommendation: cnc
104
+ machining_notes: cnc
105
+ part_name: design
106
 
107
  gap_analysis:
108
  category_keywords:
config/settings.py CHANGED
@@ -109,6 +109,15 @@ class PlanningConfig(BaseModel):
109
  "plan", "review", "ready", "show plan", "summarize", "what do we have",
110
  ])
111
  approved_agents: list[str] = Field(default_factory=lambda: ["cad", "cnc"])
 
 
 
 
 
 
 
 
 
112
 
113
 
114
  class GapAnalysisConfig(BaseModel):
 
109
  "plan", "review", "ready", "show plan", "summarize", "what do we have",
110
  ])
111
  approved_agents: list[str] = Field(default_factory=lambda: ["cad", "cnc"])
112
+ field_agents: dict[str, str] = Field(default_factory=lambda: {
113
+ "material": "engineering",
114
+ "dimensions": "engineering",
115
+ "features": "design",
116
+ "constraints": "cnc",
117
+ "axis_recommendation": "cnc",
118
+ "machining_notes": "cnc",
119
+ "part_name": "design",
120
+ })
121
 
122
 
123
  class GapAnalysisConfig(BaseModel):
docs/superpowers/plans/2026-04-12-editable-plan-card.md ADDED
@@ -0,0 +1,848 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Editable Plan Card Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make the plan review card editable with per-field edit/ask-agent buttons, a notes area, dual score display, Save & Continue action, and unified confirm/revise behavior.
6
+
7
+ **Architecture:** Extend the existing inline plan card in `web/index.html` with edit toggles and agent-ask buttons per field. Add a `notes` field to `DesignPlan` and `field_agents` config to `PlanningConfig`. Modify `confirmRecommendation()` to auto-fill plan card fields when a plan is visible. No new API endpoints — Save & Continue is frontend-only, Approve already accepts the full plan object.
8
+
9
+ **Tech Stack:** Python/Pydantic (backend models), FastAPI (routes), vanilla JS/HTML/CSS (frontend)
10
+
11
+ ---
12
+
13
+ ### Task 1: Add `notes` field to `DesignPlan` and `field_agents` to config
14
+
15
+ **Files:**
16
+ - Modify: `agents/design_state.py:42-92`
17
+ - Modify: `config/settings.py:98-112`
18
+ - Modify: `config.yaml:74-97`
19
+ - Test: `tests/test_design_state.py`
20
+ - Test: `tests/test_settings.py`
21
+
22
+ - [ ] **Step 1: Write failing test for `notes` on DesignPlan**
23
+
24
+ Add to `tests/test_design_state.py` in the `TestDesignPlan` class:
25
+
26
+ ```python
27
+ def test_plan_notes_default_empty(self):
28
+ plan = DesignPlan(part_name="test")
29
+ assert plan.notes == ""
30
+
31
+ def test_plan_notes_in_render(self):
32
+ plan = DesignPlan(
33
+ part_name="bracket",
34
+ material="aluminum",
35
+ notes="Check if 304 is overkill",
36
+ )
37
+ rendered = plan.render_approved()
38
+ assert "USER NOTES" in rendered
39
+ assert "Check if 304 is overkill" in rendered
40
+
41
+ def test_plan_notes_empty_not_in_render(self):
42
+ plan = DesignPlan(part_name="bracket", material="aluminum", notes="")
43
+ rendered = plan.render_approved()
44
+ assert "USER NOTES" not in rendered
45
+
46
+ def test_plan_from_state_no_notes(self):
47
+ state = DesignState(part_name="bracket", material="steel")
48
+ plan = DesignPlan.from_state(state, confidence_score=5.0)
49
+ assert plan.notes == ""
50
+ ```
51
+
52
+ - [ ] **Step 2: Run tests to verify they fail**
53
+
54
+ Run: `pytest tests/test_design_state.py::TestDesignPlan -v`
55
+ Expected: FAIL — `DesignPlan` has no `notes` field
56
+
57
+ - [ ] **Step 3: Add `notes` field to `DesignPlan`**
58
+
59
+ In `agents/design_state.py`, add the field after `confidence_score` (line 52):
60
+
61
+ ```python
62
+ confidence_score: float = 0.0
63
+ notes: str = ""
64
+ ```
65
+
66
+ Update `render_approved()` — add before the final two lines (`"This plan has been reviewed..."`):
67
+
68
+ ```python
69
+ if self.notes:
70
+ lines.append(f"User Notes: {self.notes}")
71
+ ```
72
+
73
+ - [ ] **Step 4: Run tests to verify they pass**
74
+
75
+ Run: `pytest tests/test_design_state.py::TestDesignPlan -v`
76
+ Expected: PASS
77
+
78
+ - [ ] **Step 5: Write failing test for `field_agents` config**
79
+
80
+ Add to `tests/test_settings.py`:
81
+
82
+ ```python
83
+ def test_planning_field_agents_defaults():
84
+ from config.settings import PlanningConfig
85
+ cfg = PlanningConfig()
86
+ assert cfg.field_agents["material"] == "engineering"
87
+ assert cfg.field_agents["features"] == "design"
88
+ assert cfg.field_agents["constraints"] == "cnc"
89
+ assert cfg.field_agents["axis_recommendation"] == "cnc"
90
+ assert cfg.field_agents["part_name"] == "design"
91
+ ```
92
+
93
+ - [ ] **Step 6: Run test to verify it fails**
94
+
95
+ Run: `pytest tests/test_settings.py::test_planning_field_agents_defaults -v`
96
+ Expected: FAIL — `PlanningConfig` has no `field_agents`
97
+
98
+ - [ ] **Step 7: Add `field_agents` to `PlanningConfig`**
99
+
100
+ In `config/settings.py`, add after `approved_agents` (line 111):
101
+
102
+ ```python
103
+ field_agents: dict[str, str] = Field(default_factory=lambda: {
104
+ "material": "engineering",
105
+ "dimensions": "engineering",
106
+ "features": "design",
107
+ "constraints": "cnc",
108
+ "axis_recommendation": "cnc",
109
+ "machining_notes": "cnc",
110
+ "part_name": "design",
111
+ })
112
+ ```
113
+
114
+ Add to `config.yaml` under `planning:` (after `approved_agents`):
115
+
116
+ ```yaml
117
+ field_agents:
118
+ material: engineering
119
+ dimensions: engineering
120
+ features: design
121
+ constraints: cnc
122
+ axis_recommendation: cnc
123
+ machining_notes: cnc
124
+ part_name: design
125
+ ```
126
+
127
+ - [ ] **Step 8: Run tests to verify they pass**
128
+
129
+ Run: `pytest tests/test_settings.py::test_planning_field_agents_defaults tests/test_design_state.py -v`
130
+ Expected: PASS
131
+
132
+ - [ ] **Step 9: Commit**
133
+
134
+ ```bash
135
+ git add agents/design_state.py config/settings.py config.yaml tests/test_design_state.py tests/test_settings.py
136
+ git commit -m "feat: add notes field to DesignPlan and field_agents config"
137
+ ```
138
+
139
+ ---
140
+
141
+ ### Task 2: Update `plan/approve` endpoint to handle `notes`
142
+
143
+ **Files:**
144
+ - Modify: `server/routes.py:164-178`
145
+ - Test: `tests/test_api_routes.py`
146
+
147
+ - [ ] **Step 1: Write failing test**
148
+
149
+ Add to `tests/test_api_routes.py` in `TestPlanApproveEndpoint`:
150
+
151
+ ```python
152
+ def test_approve_preserves_notes(self):
153
+ resp = client.post("/api/plan/approve", json={
154
+ "plan": {
155
+ "part_name": "bracket",
156
+ "description": "test",
157
+ "material": "aluminum 6061",
158
+ "dimensions": {"width": 60},
159
+ "features": [],
160
+ "constraints": [],
161
+ "axis_recommendation": "3-axis",
162
+ "machining_notes": [],
163
+ "confidence_score": 9.0,
164
+ "notes": "Check material compatibility",
165
+ },
166
+ "design_state": {"phase": "planning"},
167
+ })
168
+ assert resp.status_code == 200
169
+ data = resp.json()
170
+ assert data["design_state"]["plan"]["notes"] == "Check material compatibility"
171
+ ```
172
+
173
+ - [ ] **Step 2: Run test to verify it passes (no backend change needed)**
174
+
175
+ Run: `pytest tests/test_api_routes.py::TestPlanApproveEndpoint::test_approve_preserves_notes -v`
176
+ Expected: PASS — Pydantic already handles the new `notes` field automatically since the endpoint does `DesignPlan(**body.plan)` and `state.plan = plan`
177
+
178
+ - [ ] **Step 3: Commit**
179
+
180
+ ```bash
181
+ git add tests/test_api_routes.py
182
+ git commit -m "test: verify plan/approve preserves notes field"
183
+ ```
184
+
185
+ ---
186
+
187
+ ### Task 3: Add plan card CSS for edit controls, notes, and dual score
188
+
189
+ **Files:**
190
+ - Modify: `web/index.html` (CSS section, lines ~1286-1317)
191
+
192
+ - [ ] **Step 1: Add CSS styles**
193
+
194
+ In `web/index.html`, after the existing `.plan-card-reject` rule (line 1317), add:
195
+
196
+ ```css
197
+ .plan-card-save {
198
+ background: var(--accent); color: var(--bg-void); border-color: var(--accent);
199
+ }
200
+ .plan-field-actions {
201
+ display: inline-flex; gap: 4px; margin-left: 6px; vertical-align: middle;
202
+ }
203
+ .plan-field-btn {
204
+ background: none; border: 1px solid var(--border); border-radius: 3px;
205
+ color: var(--text-secondary); cursor: pointer; font-size: 10px;
206
+ padding: 1px 5px; font-family: var(--font-mono); line-height: 1.4;
207
+ }
208
+ .plan-field-btn:hover { border-color: var(--accent); color: var(--accent); }
209
+ .plan-field-input {
210
+ background: var(--bg-void); border: 1px solid var(--accent); border-radius: 3px;
211
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
212
+ padding: 2px 6px; width: 60%;
213
+ }
214
+ .plan-field-input:focus { outline: none; border-color: var(--success); }
215
+ .plan-dim-group { display: inline-flex; gap: 4px; }
216
+ .plan-dim-input {
217
+ background: var(--bg-void); border: 1px solid var(--accent); border-radius: 3px;
218
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
219
+ padding: 2px 4px; width: 60px;
220
+ }
221
+ .plan-dim-input:focus { outline: none; border-color: var(--success); }
222
+ .plan-dim-label {
223
+ font-size: 9px; color: var(--text-secondary); font-family: var(--font-mono);
224
+ }
225
+ .plan-notes {
226
+ width: 100%; min-height: 48px; margin-top: 8px; padding: 6px 8px;
227
+ background: var(--bg-void); border: 1px solid var(--border); border-radius: 5px;
228
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
229
+ resize: vertical;
230
+ }
231
+ .plan-notes:focus { outline: none; border-color: var(--accent); }
232
+ .plan-score-dual {
233
+ display: flex; gap: 16px; font-family: var(--font-mono); font-size: 11px;
234
+ color: var(--text-secondary); margin-top: 8px;
235
+ }
236
+ .plan-score-current { font-weight: 600; }
237
+ .plan-score-current.score-ok { color: var(--success); }
238
+ .plan-score-current.score-low { color: var(--warning, #ffab40); }
239
+ ```
240
+
241
+ - [ ] **Step 2: Verify the page loads without CSS errors**
242
+
243
+ Open the web app in a browser and confirm no visual regressions. The new styles have no effect yet since no HTML uses them.
244
+
245
+ - [ ] **Step 3: Commit**
246
+
247
+ ```bash
248
+ git add web/index.html
249
+ git commit -m "feat: add CSS for editable plan card controls"
250
+ ```
251
+
252
+ ---
253
+
254
+ ### Task 4: Add i18n strings for editable plan card
255
+
256
+ **Files:**
257
+ - Modify: `web/index.html` (i18n section, lines ~2020-2126)
258
+
259
+ - [ ] **Step 1: Add translation keys**
260
+
261
+ In the English translations (after `planReject` around line 2051), add:
262
+
263
+ ```javascript
264
+ planSave: 'Save & Continue',
265
+ planNotesPlaceholder: 'Add notes about this plan...',
266
+ planScoreOriginal: 'Original',
267
+ planScoreCurrent: 'Current',
268
+ planAskAgent: 'Ask',
269
+ planEdit: '\u270e',
270
+ ```
271
+
272
+ In the Chinese translations (after `planReject` around line 2088), add:
273
+
274
+ ```javascript
275
+ planSave: '\u5132\u5b58\u4e26\u7e7c\u7e8c',
276
+ planNotesPlaceholder: '\u65b0\u589e\u8a08\u756b\u5099\u8a3b...',
277
+ planScoreOriginal: '\u539f\u59cb',
278
+ planScoreCurrent: '\u7576\u524d',
279
+ planAskAgent: '\u8a62\u554f',
280
+ planEdit: '\u270e',
281
+ ```
282
+
283
+ In the Vietnamese translations (after `planReject` around line 2125), add:
284
+
285
+ ```javascript
286
+ planSave: 'L\u01b0u & Ti\u1ebfp T\u1ee5c',
287
+ planNotesPlaceholder: 'Th\u00eam ghi ch\u00fa v\u1ec1 k\u1ebf ho\u1ea1ch...',
288
+ planScoreOriginal: 'G\u1ed1c',
289
+ planScoreCurrent: 'Hi\u1ec7n t\u1ea1i',
290
+ planAskAgent: 'H\u1ecfi',
291
+ planEdit: '\u270e',
292
+ ```
293
+
294
+ - [ ] **Step 2: Verify `t()` returns new keys**
295
+
296
+ Open browser console: `t('planSave')` should return `'Save & Continue'`.
297
+
298
+ - [ ] **Step 3: Commit**
299
+
300
+ ```bash
301
+ git add web/index.html
302
+ git commit -m "feat: add i18n strings for editable plan card"
303
+ ```
304
+
305
+ ---
306
+
307
+ ### Task 5: Rewrite `renderPlanCard()` with edit controls and notes
308
+
309
+ **Files:**
310
+ - Modify: `web/index.html` (lines ~1840-1862)
311
+
312
+ - [ ] **Step 1: Add field-to-agent mapping and question templates**
313
+
314
+ Add above the `renderPlanCard` function:
315
+
316
+ ```javascript
317
+ const PLAN_FIELD_AGENTS = {
318
+ part_name: 'design', material: 'engineering', dimensions: 'engineering',
319
+ features: 'design', constraints: 'cnc', axis_recommendation: 'cnc',
320
+ machining_notes: 'cnc',
321
+ };
322
+ const PLAN_FIELD_QUESTIONS = {
323
+ part_name: 'What should this part be called?',
324
+ material: 'What material do you recommend?',
325
+ dimensions: 'What dimensions are appropriate?',
326
+ features: 'What features should this part have?',
327
+ constraints: 'What manufacturing constraints should we consider?',
328
+ axis_recommendation: 'What axis strategy do you recommend?',
329
+ machining_notes: 'Any machining notes to consider?',
330
+ };
331
+ ```
332
+
333
+ - [ ] **Step 2: Replace `renderPlanCard()` function**
334
+
335
+ Replace the entire `renderPlanCard` function (lines 1840-1862) with:
336
+
337
+ ```javascript
338
+ function renderPlanCard(plan) {
339
+ const fields = [
340
+ ['Part', 'part_name', plan.part_name, 'text'],
341
+ ['Material', 'material', plan.material, 'text'],
342
+ ['Dimensions', 'dimensions', plan.dimensions, 'dimensions'],
343
+ ['Features', 'features', plan.features, 'list'],
344
+ ['Constraints', 'constraints', plan.constraints, 'list'],
345
+ ['Axis', 'axis_recommendation', plan.axis_recommendation || 'Auto', 'text'],
346
+ ];
347
+ if (plan.machining_notes && plan.machining_notes.length) {
348
+ fields.push(['Notes', 'machining_notes', plan.machining_notes, 'list']);
349
+ }
350
+ let html = '<div class="plan-card" id="active-plan-card" data-original-score="' + (plan.confidence_score || 0) + '">';
351
+ html += '<div class="plan-card-title">\u25c6 ' + t('planReady') + '</div>';
352
+ for (const [label, key, value, type] of fields) {
353
+ html += '<div class="wizard-review-field" data-field="' + key + '">';
354
+ html += '<span class="wizard-review-label">' + escapeHtml(label) + '</span>';
355
+ html += '<span class="wizard-review-value" id="plan-val-' + key + '">';
356
+ if (type === 'dimensions') {
357
+ html += escapeHtml(Object.entries(value || {}).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014');
358
+ } else if (type === 'list') {
359
+ html += escapeHtml((value || []).join(', ') || '\u2014');
360
+ } else {
361
+ html += escapeHtml(value || '\u2014');
362
+ }
363
+ html += '</span>';
364
+ html += '<span class="plan-field-actions">';
365
+ html += '<button class="plan-field-btn" onclick="toggleFieldEdit(\'' + key + '\', \'' + type + '\')" title="Edit">' + t('planEdit') + '</button>';
366
+ html += '<button class="plan-field-btn" onclick="askAgentForField(\'' + key + '\')" title="Ask Agent">' + t('planAskAgent') + '</button>';
367
+ html += '</span>';
368
+ html += '</div>';
369
+ }
370
+ // Notes area
371
+ html += '<textarea class="plan-notes" id="plan-notes" placeholder="' + t('planNotesPlaceholder') + '">' + escapeHtml(plan.notes || '') + '</textarea>';
372
+ // Dual score
373
+ var origScore = (plan.confidence_score || 0).toFixed(0);
374
+ html += '<div class="plan-score-dual">';
375
+ html += '<span>' + t('planScoreOriginal') + ': ' + origScore + '/8</span>';
376
+ html += '<span>\u2192</span>';
377
+ html += '<span class="plan-score-current score-ok" id="plan-current-score">' + t('planScoreCurrent') + ': ' + origScore + '/8</span>';
378
+ html += '</div>';
379
+ // Action buttons
380
+ html += '<div class="plan-card-actions">';
381
+ html += '<button class="plan-card-btn plan-card-approve" onclick="approvePlanCard()">' + t('planApprove') + '</button>';
382
+ html += '<button class="plan-card-btn plan-card-save" onclick="savePlanAndContinue()">' + t('planSave') + '</button>';
383
+ html += '<button class="plan-card-btn plan-card-reject" onclick="rejectPlanCard()">' + t('planReject') + '</button>';
384
+ html += '</div></div>';
385
+ return html;
386
+ }
387
+ ```
388
+
389
+ - [ ] **Step 3: Verify the plan card renders correctly**
390
+
391
+ Trigger a plan in the UI (chat until score >= 8 or type a trigger keyword). Confirm:
392
+ - All fields show values with pencil + Ask buttons
393
+ - Notes textarea appears
394
+ - Dual score shows Original and Current (same value initially)
395
+ - Three buttons: Approve, Save & Continue, Reject
396
+
397
+ - [ ] **Step 4: Commit**
398
+
399
+ ```bash
400
+ git add web/index.html
401
+ git commit -m "feat: render editable plan card with field actions, notes, dual score"
402
+ ```
403
+
404
+ ---
405
+
406
+ ### Task 6: Implement `toggleFieldEdit()` and `recalcPlanScore()`
407
+
408
+ **Files:**
409
+ - Modify: `web/index.html` (add new functions after `renderPlanCard`)
410
+
411
+ - [ ] **Step 1: Add `toggleFieldEdit()` function**
412
+
413
+ Add after `renderPlanCard`:
414
+
415
+ ```javascript
416
+ function toggleFieldEdit(fieldKey, fieldType) {
417
+ var valEl = document.getElementById('plan-val-' + fieldKey);
418
+ if (!valEl) return;
419
+ var plan = designState.plan;
420
+ if (!plan) return;
421
+
422
+ // If already editing (has input), save and switch back to display
423
+ var existingInput = valEl.querySelector('input, .plan-dim-group');
424
+ if (existingInput) {
425
+ // Read value from input(s) and update plan
426
+ if (fieldType === 'dimensions') {
427
+ var dimInputs = valEl.querySelectorAll('.plan-dim-input');
428
+ var dims = {};
429
+ dimInputs.forEach(function(inp) { if (inp.value) dims[inp.dataset.dim] = parseFloat(inp.value) || 0; });
430
+ plan.dimensions = dims;
431
+ valEl.textContent = Object.entries(dims).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014';
432
+ } else if (fieldType === 'list') {
433
+ var raw = existingInput.value;
434
+ plan[fieldKey] = raw ? raw.split(',').map(function(s) { return s.trim(); }).filter(Boolean) : [];
435
+ valEl.textContent = plan[fieldKey].join(', ') || '\u2014';
436
+ } else {
437
+ plan[fieldKey] = existingInput.value;
438
+ valEl.textContent = plan[fieldKey] || '\u2014';
439
+ }
440
+ recalcPlanScore();
441
+ return;
442
+ }
443
+
444
+ // Switch to edit mode
445
+ if (fieldType === 'dimensions') {
446
+ var dims = plan.dimensions || {};
447
+ var dimKeys = Object.keys(dims).length ? Object.keys(dims) : ['width', 'height', 'depth'];
448
+ var groupHtml = '<span class="plan-dim-group">';
449
+ dimKeys.forEach(function(dk) {
450
+ groupHtml += '<span><span class="plan-dim-label">' + dk + '</span><input class="plan-dim-input" type="number" data-dim="' + dk + '" value="' + (dims[dk] || '') + '"></span>';
451
+ });
452
+ groupHtml += '</span>';
453
+ valEl.innerHTML = groupHtml;
454
+ } else if (fieldType === 'list') {
455
+ var current = (plan[fieldKey] || []).join(', ');
456
+ valEl.innerHTML = '<input class="plan-field-input" type="text" value="' + escapeHtml(current) + '">';
457
+ } else {
458
+ var current = plan[fieldKey] || '';
459
+ valEl.innerHTML = '<input class="plan-field-input" type="text" value="' + escapeHtml(current) + '">';
460
+ }
461
+ var firstInput = valEl.querySelector('input');
462
+ if (firstInput) firstInput.focus();
463
+ }
464
+ ```
465
+
466
+ - [ ] **Step 2: Add `recalcPlanScore()` function**
467
+
468
+ Add after `toggleFieldEdit`:
469
+
470
+ ```javascript
471
+ function recalcPlanScore() {
472
+ var plan = designState.plan;
473
+ if (!plan) return;
474
+ // Compute score using same logic as wizComputeScore but from plan object
475
+ var s = 0;
476
+ if (plan.material) s += 3;
477
+ if (plan.part_name) s += 1;
478
+ if (plan.description) s += 1;
479
+ if (plan.axis_recommendation) s += 2;
480
+ s += Math.min(Object.keys(plan.dimensions || {}).length, 4);
481
+ s += Math.min((plan.features || []).length, 4);
482
+ s += Math.min((plan.constraints || []).length, 2);
483
+ var el = document.getElementById('plan-current-score');
484
+ if (el) {
485
+ el.textContent = t('planScoreCurrent') + ': ' + s.toFixed(0) + '/8';
486
+ el.className = 'plan-score-current ' + (s >= 8 ? 'score-ok' : 'score-low');
487
+ }
488
+ }
489
+ ```
490
+
491
+ - [ ] **Step 3: Verify field editing works**
492
+
493
+ In the browser, trigger a plan card, then:
494
+ - Click pencil on Material — should show text input with current value
495
+ - Type a new value, click pencil again — should save and show updated text
496
+ - Click pencil on Dimensions — should show number inputs for each dimension key
497
+ - Verify Current score updates when clearing/adding fields
498
+
499
+ - [ ] **Step 4: Commit**
500
+
501
+ ```bash
502
+ git add web/index.html
503
+ git commit -m "feat: implement toggleFieldEdit and recalcPlanScore for plan card"
504
+ ```
505
+
506
+ ---
507
+
508
+ ### Task 7: Implement `askAgentForField()`
509
+
510
+ **Files:**
511
+ - Modify: `web/index.html` (add function after `recalcPlanScore`)
512
+
513
+ - [ ] **Step 1: Add `askAgentForField()` function**
514
+
515
+ ```javascript
516
+ function askAgentForField(fieldKey) {
517
+ var plan = designState.plan;
518
+ if (!plan) return;
519
+ var agentId = PLAN_FIELD_AGENTS[fieldKey] || 'design';
520
+ var question = PLAN_FIELD_QUESTIONS[fieldKey] || 'Can you help with this field?';
521
+ var partCtx = plan.part_name ? ' for "' + plan.part_name + '"' : '';
522
+ var msg = '@' + agentId + ' Regarding the plan' + partCtx + ': ' + question;
523
+ // Switch to chat tab if on guided tab
524
+ var chatTab = document.querySelector('[data-tab="chat"]');
525
+ if (chatTab) chatTab.click();
526
+ sendMessage(msg);
527
+ }
528
+ ```
529
+
530
+ - [ ] **Step 2: Verify Ask Agent sends correct message**
531
+
532
+ In the browser:
533
+ - Trigger a plan card
534
+ - Click "Ask" next to Material field
535
+ - Verify a message like `@engineering Regarding the plan for "bracket": What material do you recommend?` appears in chat
536
+ - Verify the engineering agent responds
537
+
538
+ - [ ] **Step 3: Commit**
539
+
540
+ ```bash
541
+ git add web/index.html
542
+ git commit -m "feat: implement askAgentForField for plan card agent assist"
543
+ ```
544
+
545
+ ---
546
+
547
+ ### Task 8: Implement `savePlanAndContinue()` and update `approvePlanCard()`
548
+
549
+ **Files:**
550
+ - Modify: `web/index.html` (modify `approvePlanCard`, add `savePlanAndContinue`)
551
+
552
+ - [ ] **Step 1: Add `savePlanAndContinue()` function**
553
+
554
+ Add after `askAgentForField`:
555
+
556
+ ```javascript
557
+ function savePlanAndContinue() {
558
+ var plan = designState.plan;
559
+ if (!plan) return;
560
+ // Read notes from textarea
561
+ var notesEl = document.getElementById('plan-notes');
562
+ if (notesEl) plan.notes = notesEl.value;
563
+ // Merge plan fields into designState
564
+ designState.part_name = plan.part_name;
565
+ designState.description = plan.description;
566
+ designState.material = plan.material;
567
+ designState.dimensions = Object.assign({}, plan.dimensions);
568
+ designState.features = (plan.features || []).slice();
569
+ designState.constraints = (plan.constraints || []).slice();
570
+ designState.axis_recommendation = plan.axis_recommendation;
571
+ // Reset to exploring
572
+ designState.phase = 'exploring';
573
+ designState.plan = null;
574
+ saveState();
575
+ var card = document.getElementById('active-plan-card');
576
+ if (card) card.remove();
577
+ }
578
+ ```
579
+
580
+ - [ ] **Step 2: Update `approvePlanCard()` to read edited values**
581
+
582
+ Replace the existing `approvePlanCard` function (lines 1865-1883) with:
583
+
584
+ ```javascript
585
+ async function approvePlanCard() {
586
+ var plan = designState.plan;
587
+ if (!plan) return;
588
+ // Read notes from textarea
589
+ var notesEl = document.getElementById('plan-notes');
590
+ if (notesEl) plan.notes = notesEl.value;
591
+ try {
592
+ var resp = await fetch('/api/plan/approve', {
593
+ method: 'POST',
594
+ headers: { 'Content-Type': 'application/json' },
595
+ body: JSON.stringify({ plan: plan, design_state: designState }),
596
+ });
597
+ var data = await resp.json();
598
+ designState = data.design_state;
599
+ saveState();
600
+ var card = document.getElementById('active-plan-card');
601
+ if (card) card.remove();
602
+ await sendMessage('Generate the approved design');
603
+ } catch (err) {
604
+ console.error('Plan approve failed:', err);
605
+ }
606
+ }
607
+ ```
608
+
609
+ - [ ] **Step 3: Verify Save & Continue and Approve work**
610
+
611
+ In the browser:
612
+ - Trigger a plan card
613
+ - Edit the material field, type notes
614
+ - Click "Save & Continue" — plan card should disappear, designState should retain edited material, phase should be "exploring"
615
+ - Trigger the plan again — verify edited material appears
616
+ - Click "Approve" — verify the edited plan is sent to the API with notes
617
+
618
+ - [ ] **Step 4: Commit**
619
+
620
+ ```bash
621
+ git add web/index.html
622
+ git commit -m "feat: implement savePlanAndContinue and update approvePlanCard to read edits"
623
+ ```
624
+
625
+ ---
626
+
627
+ ### Task 9: Implement `updatePlanCardField()` for external auto-fill
628
+
629
+ **Files:**
630
+ - Modify: `web/index.html` (add function after `savePlanAndContinue`)
631
+
632
+ - [ ] **Step 1: Add `updatePlanCardField()` function**
633
+
634
+ ```javascript
635
+ function updatePlanCardField(fieldKey, value) {
636
+ var plan = designState.plan;
637
+ if (!plan) return;
638
+ var valEl = document.getElementById('plan-val-' + fieldKey);
639
+ // Close any open edit mode first
640
+ var existingInput = valEl ? valEl.querySelector('input, .plan-dim-group') : null;
641
+ if (existingInput) {
642
+ // Cancel edit mode by resetting display
643
+ }
644
+ // Update plan object
645
+ if (fieldKey === 'dimensions' && typeof value === 'object') {
646
+ Object.assign(plan.dimensions, value);
647
+ if (valEl) valEl.textContent = Object.entries(plan.dimensions).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014';
648
+ } else if (fieldKey === 'features' || fieldKey === 'constraints' || fieldKey === 'machining_notes') {
649
+ if (Array.isArray(value)) {
650
+ plan[fieldKey] = value;
651
+ } else {
652
+ if (!plan[fieldKey].includes(value)) plan[fieldKey].push(value);
653
+ }
654
+ if (valEl) valEl.textContent = plan[fieldKey].join(', ') || '\u2014';
655
+ } else {
656
+ plan[fieldKey] = value;
657
+ if (valEl) valEl.textContent = value || '\u2014';
658
+ }
659
+ recalcPlanScore();
660
+ }
661
+ ```
662
+
663
+ - [ ] **Step 2: Commit**
664
+
665
+ ```bash
666
+ git add web/index.html
667
+ git commit -m "feat: add updatePlanCardField for external auto-fill of plan fields"
668
+ ```
669
+
670
+ ---
671
+
672
+ ### Task 10: Implement `detectPlanField()` and `extractFieldValue()`
673
+
674
+ **Files:**
675
+ - Modify: `web/index.html` (add functions after `updatePlanCardField`)
676
+
677
+ - [ ] **Step 1: Add `detectPlanField()` function**
678
+
679
+ This reuses the `category_keywords` from `GapAnalysisConfig` via a frontend mapping:
680
+
681
+ ```javascript
682
+ var CATEGORY_KEYWORDS = {
683
+ dimension: ['width', 'height', 'depth', 'length', 'diameter', 'dimension', 'size'],
684
+ material: ['material', 'alloy', 'grade', 'aluminum', 'steel', 'metal', 'brass', 'titanium', 'nylon'],
685
+ feature: ['hole', 'fillet', 'chamfer', 'pocket', 'slot', 'feature'],
686
+ constraint: ['tolerance', 'wall thickness', 'constraint', 'min wall', 'max size'],
687
+ machining: ['axis', 'machine', 'setup', 'fixture', 'tool access'],
688
+ };
689
+ var CATEGORY_TO_FIELD = {
690
+ material: 'material',
691
+ dimension: 'dimensions',
692
+ feature: 'features',
693
+ constraint: 'constraints',
694
+ machining: 'axis_recommendation',
695
+ };
696
+
697
+ function detectPlanField(agentId, content) {
698
+ var lower = content.toLowerCase();
699
+ // Check each category — if the agent matches AND content contains keywords, return the field
700
+ for (var cat in CATEGORY_KEYWORDS) {
701
+ var expectedAgent = PLAN_FIELD_AGENTS[CATEGORY_TO_FIELD[cat]];
702
+ if (expectedAgent !== agentId) continue;
703
+ var keywords = CATEGORY_KEYWORDS[cat];
704
+ for (var i = 0; i < keywords.length; i++) {
705
+ if (lower.indexOf(keywords[i]) !== -1) {
706
+ return CATEGORY_TO_FIELD[cat];
707
+ }
708
+ }
709
+ }
710
+ return null;
711
+ }
712
+ ```
713
+
714
+ - [ ] **Step 2: Add `extractFieldValue()` function**
715
+
716
+ ```javascript
717
+ function extractFieldValue(fieldKey, content) {
718
+ // Try quoted values first
719
+ var quoted = content.match(/"([^"]+)"/);
720
+ if (quoted) return quoted[1];
721
+
722
+ if (fieldKey === 'material') {
723
+ // Match known materials
724
+ var matList = ['aluminum 6061', 'aluminum 7075', 'stainless steel 304', 'stainless steel 316',
725
+ 'aluminum', 'steel', 'brass', 'copper', 'titanium', 'nylon', 'delrin', 'peek', 'polycarbonate'];
726
+ var lower = content.toLowerCase();
727
+ for (var i = 0; i < matList.length; i++) {
728
+ if (lower.indexOf(matList[i]) !== -1) return matList[i];
729
+ }
730
+ }
731
+
732
+ if (fieldKey === 'axis_recommendation') {
733
+ var axisMatch = content.match(/(3-axis|3\+2[\s-]*axis|5-axis)/i);
734
+ if (axisMatch) return axisMatch[1].toLowerCase();
735
+ }
736
+
737
+ if (fieldKey === 'dimensions') {
738
+ // Extract dimension values as object
739
+ var dims = {};
740
+ var dimPattern = /(\d+\.?\d*)\s*mm\s+(wide|width|tall|height|high|thick|thickness|deep|depth|long|length|diameter)/gi;
741
+ var m;
742
+ while ((m = dimPattern.exec(content)) !== null) {
743
+ var dimMap = {wide:'width',width:'width',tall:'height',height:'height',high:'height',
744
+ thick:'thickness',thickness:'thickness',deep:'depth',depth:'depth',long:'length',length:'length',diameter:'diameter'};
745
+ dims[dimMap[m[2].toLowerCase()] || m[2].toLowerCase()] = parseFloat(m[1]);
746
+ }
747
+ if (Object.keys(dims).length) return dims;
748
+ }
749
+
750
+ // Fallback: text after "recommend"/"suggest"/"use"
751
+ var recMatch = content.match(/(?:recommend|suggest|use)\s+(.+?)(?:\.|,|$)/i);
752
+ if (recMatch) return recMatch[1].trim();
753
+
754
+ return null;
755
+ }
756
+ ```
757
+
758
+ - [ ] **Step 3: Commit**
759
+
760
+ ```bash
761
+ git add web/index.html
762
+ git commit -m "feat: add detectPlanField and extractFieldValue for confirm auto-fill"
763
+ ```
764
+
765
+ ---
766
+
767
+ ### Task 11: Update `confirmRecommendation()` to auto-fill plan card
768
+
769
+ **Files:**
770
+ - Modify: `web/index.html` (lines ~2759-2769)
771
+
772
+ - [ ] **Step 1: Replace `confirmRecommendation()` function**
773
+
774
+ Replace the existing function (lines 2759-2769) with:
775
+
776
+ ```javascript
777
+ function confirmRecommendation(msgId, agentId) {
778
+ var el = document.querySelector('[data-msg-id="' + msgId + '"]');
779
+ if (!el) return;
780
+ var content = el.dataset.content;
781
+ var actions = document.getElementById(msgId + '-actions');
782
+ if (actions) {
783
+ actions.innerHTML = '<div class="msg-confirmed">' + t('confirmed') + '</div>';
784
+ }
785
+ // Send confirmation as user message so it updates design state
786
+ var agentName = (AGENTS[agentId] || {}).name || agentId;
787
+ sendMessage(t('confirmedMsg').replace('{agent}', agentName).replace('{content}', content));
788
+ // Auto-fill plan card field if plan is visible
789
+ if (designState.phase === 'planning' && designState.plan && document.getElementById('active-plan-card')) {
790
+ var field = detectPlanField(agentId, content);
791
+ if (field) {
792
+ var value = extractFieldValue(field, content);
793
+ if (value != null) {
794
+ updatePlanCardField(field, value);
795
+ }
796
+ }
797
+ }
798
+ }
799
+ ```
800
+
801
+ - [ ] **Step 2: Verify unified confirm/plan card behavior**
802
+
803
+ In the browser:
804
+ 1. Chat until agents provide recommendations but don't yet reach plan threshold
805
+ 2. Chat more until plan card appears
806
+ 3. Scroll to an engineering agent message about material — click Confirm
807
+ 4. Verify the plan card's material field updates with the extracted value
808
+ 5. Verify Current score recalculates
809
+ 6. Click Confirm on a message when no plan card is visible — verify it works as before (just sends the confirmation message)
810
+
811
+ - [ ] **Step 3: Commit**
812
+
813
+ ```bash
814
+ git add web/index.html
815
+ git commit -m "feat: confirmRecommendation auto-fills plan card when plan is visible"
816
+ ```
817
+
818
+ ---
819
+
820
+ ### Task 12: Run full test suite and verify end-to-end
821
+
822
+ **Files:** None (verification only)
823
+
824
+ - [ ] **Step 1: Run backend tests**
825
+
826
+ Run: `pytest tests/ -x -q`
827
+ Expected: All tests pass
828
+
829
+ - [ ] **Step 2: End-to-end browser verification**
830
+
831
+ Start the server: `python -m server.web`
832
+
833
+ Test the following flows:
834
+ 1. **Edit flow:** Trigger plan → edit material → edit dimensions → add notes → Approve → verify model generates with edited values
835
+ 2. **Save & Continue flow:** Trigger plan → edit features → Save & Continue → verify state preserved → chat more → re-trigger plan → verify edits are in new plan
836
+ 3. **Reject flow:** Trigger plan → edit fields → Reject → verify edits discarded, phase reset
837
+ 4. **Ask Agent flow:** Trigger plan → click Ask next to material → verify engineering agent responds in chat
838
+ 5. **Confirm auto-fill flow:** While plan is visible, Confirm an agent recommendation → verify plan field updates
839
+ 6. **Dual score:** Edit fields to reduce completeness → verify Current score drops, Original stays
840
+
841
+ - [ ] **Step 3: Commit any fixes found during verification**
842
+
843
+ ```bash
844
+ git add -A
845
+ git commit -m "fix: address issues found during end-to-end verification"
846
+ ```
847
+
848
+ (Skip this step if no fixes are needed.)
docs/superpowers/specs/2026-04-12-editable-plan-card-design.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Editable Plan Card Design Spec
2
+
3
+ **Date:** 2026-04-12
4
+ **Status:** Approved
5
+
6
+ ## Problem
7
+
8
+ The plan card in the planning review gate is read-only. Users can only approve or reject the entire plan. There is no way to:
9
+ - Edit individual field values before approving
10
+ - Add notes or comments about the plan
11
+ - Ask a specific agent to help fill or refine a field
12
+ - Save partial edits and continue chatting before re-triggering a plan
13
+
14
+ ## Solution
15
+
16
+ Extend the existing inline plan card with per-field editing, an "Ask Agent" button per field, a notes text area, dual score display, and a third "Save & Continue" action button.
17
+
18
+ ## Design
19
+
20
+ ### 1. Per-Field Editing
21
+
22
+ Each plan field displays two icon buttons next to its value:
23
+
24
+ **Edit icon (pencil)** — toggles the value between read-only text and an editable input:
25
+ - `part_name`, `material`, `axis_recommendation` — text input
26
+ - `dimensions` — group of number inputs (width/height/depth in mm)
27
+ - `features`, `constraints`, `machining_notes` — comma-separated text input (parsed back to list)
28
+
29
+ **Ask Agent icon** — sends a pre-formatted, mention-targeted message to the chat. The message includes current plan context for agent awareness:
30
+ - Template: `@{agent} Regarding the plan for "{part_name}": {field-specific question}`
31
+ - Example: `@engineering Regarding the plan for "mounting_bracket": What material do you recommend given the current constraints: [min wall 3mm]?`
32
+
33
+ When the user edits a field, the plan card's local state updates immediately (no API call). Edited values are sent on Approve or Save & Continue.
34
+
35
+ **Agent-to-field mapping** (configurable in `config.yaml` under `planning.field_agents`):
36
+
37
+ | Field | Agent | Question |
38
+ |-------|-------|----------|
39
+ | `part_name` | design | What should this part be called? |
40
+ | `material` | engineering | What material do you recommend? |
41
+ | `dimensions` | engineering | What dimensions are appropriate? |
42
+ | `features` | design | What features should this part have? |
43
+ | `constraints` | cnc | What manufacturing constraints should we consider? |
44
+ | `axis_recommendation` | cnc | What axis strategy do you recommend? |
45
+ | `machining_notes` | cnc | Any machining notes to consider? |
46
+
47
+ ### 2. Notes Area
48
+
49
+ A text area at the bottom of the plan card, above the action buttons.
50
+ - Placeholder text: "Add notes about this plan..."
51
+ - Stored in the plan object as a new `notes: str` field on `DesignPlan`
52
+ - Passed to agents as additional context when the plan is approved (appended to the `render_approved()` output)
53
+ - Persisted in `designState` on Save & Continue
54
+
55
+ ### 3. Dual Score Display
56
+
57
+ Replaces the current single score display with two scores side by side:
58
+ - **Original** — the score when the plan was generated (static)
59
+ - **Current** — recalculated live as fields are edited, using the existing frontend `computeScore()` logic
60
+
61
+ Display format: `Original: 8/8 → Current: 6/8`
62
+
63
+ Current score is color-coded:
64
+ - Green if >= threshold (default 8)
65
+ - Yellow/red if below threshold
66
+
67
+ ### 4. Action Buttons
68
+
69
+ Three buttons replace the current two:
70
+
71
+ - **Approve** (green) — sends the edited plan (with field changes + notes) to `POST /api/plan/approve`. Sets `phase = "approved"`. Auto-sends "Generate the approved design" to chat.
72
+ - **Save & Continue** (secondary/blue) — merges edited field values back into `designState`, saves notes, sets `phase = "exploring"`, removes the plan card from DOM. User can keep chatting; edits preserved for the next plan trigger. Frontend-only operation (no API call), updates `designState` in localStorage.
73
+ - **Reject** (secondary/red) — discards all edits made in the plan card. Sets `phase = "exploring"`, `plan = null`.
74
+
75
+ ### 5. Ask Agent Chat Integration
76
+
77
+ When the user clicks "Ask Agent" on a field:
78
+
79
+ 1. Frontend constructs a mention-targeted message using the existing `sendMessage()` function with current plan context.
80
+ 2. Chat tab activates automatically (if user is on wizard tab).
81
+ 3. User reads the agent response in chat, then scrolls up to the plan card and edits the field manually.
82
+
83
+ No new API endpoints — reuses existing `POST /api/chat` with `@mentions` routing.
84
+
85
+ ### 6. Unified Confirm/Revise with Plan Card
86
+
87
+ Currently there are three disconnected approval mechanisms:
88
+ - **Plan card** — Approve / Reject (whole plan)
89
+ - **Wizard step 7** — Approve & Generate (same as plan card)
90
+ - **Per-message** — Confirm / Revise on every agent response (sends chat message, updates design state indirectly via `extract_decisions()`)
91
+
92
+ The per-message Confirm/Revise buttons must be aware of the plan card.
93
+
94
+ **When a plan card is visible (phase = "planning"):**
95
+
96
+ `confirmRecommendation()` behavior changes:
97
+ 1. Sends the confirmation message to chat (same as today — keeps design state updated)
98
+ 2. **Auto-fills the corresponding plan card field** — detects which field the agent message relates to using the `field_agents` mapping + keyword matching from `GapAnalysisConfig.category_keywords`
99
+ 3. Recalculates the plan card's current score
100
+
101
+ Field detection logic (reuses existing gap analysis categories):
102
+ - Agent is `engineering` + message contains dimension keywords → update `dimensions`
103
+ - Agent is `engineering` + message contains material keywords → update `material`
104
+ - Agent is `design` + message contains feature keywords → update `features`
105
+ - Agent is `cnc` + message contains constraint keywords → update `constraints`
106
+ - Agent is `cnc` + message contains axis/machining keywords → update `axis_recommendation`
107
+ - If no field match is found, skip the auto-fill (just send the chat message as today)
108
+
109
+ Value extraction: parse the confirmed message content for the relevant value (e.g., extract "Al 6061" from "I recommend Al 6061 for its machinability"). Uses simple heuristics — first quoted value, or value after "recommend"/"suggest"/"use".
110
+
111
+ `reviseRecommendation()` — no change. Works as today (pre-fills input with context).
112
+
113
+ **When no plan card is visible (phase = "exploring"):**
114
+
115
+ Confirm/Revise works exactly as it does today — no plan card interaction.
116
+
117
+ ## Data Model Changes
118
+
119
+ ### `DesignPlan` (`agents/design_state.py`)
120
+
121
+ Add field:
122
+ ```python
123
+ notes: str = ""
124
+ ```
125
+
126
+ Update `render_approved()` to include notes:
127
+ ```
128
+ ## USER NOTES
129
+ {self.notes}
130
+ ```
131
+
132
+ ### `config.yaml`
133
+
134
+ Add under `planning`:
135
+ ```yaml
136
+ planning:
137
+ field_agents:
138
+ material: engineering
139
+ dimensions: engineering
140
+ features: design
141
+ constraints: cnc
142
+ axis_recommendation: cnc
143
+ machining_notes: cnc
144
+ part_name: design
145
+ ```
146
+
147
+ ### `PlanningConfig` (`config/settings.py`)
148
+
149
+ Add field:
150
+ ```python
151
+ field_agents: dict[str, str] = {
152
+ "material": "engineering",
153
+ "dimensions": "engineering",
154
+ "features": "design",
155
+ "constraints": "cnc",
156
+ "axis_recommendation": "cnc",
157
+ "machining_notes": "cnc",
158
+ "part_name": "design",
159
+ }
160
+ ```
161
+
162
+ ## Backend Changes
163
+
164
+ - `POST /api/plan/approve` — already accepts full plan object. Add `notes` field handling: merge into `DesignState`, include in `render_approved()`.
165
+ - `POST /api/plan/reject` — no change.
166
+ - No new endpoint for Save & Continue (frontend-only operation).
167
+
168
+ ## Frontend Changes (`web/index.html`)
169
+
170
+ ### `renderPlanCard(plan)`
171
+ - Each `wizard-review-field` value gets pencil icon + agent icon buttons
172
+ - Pencil toggles between `<span>` and `<input>` for the value
173
+ - Agent icon calls `askAgentForField(fieldName)`
174
+ - Notes `<textarea>` added before action buttons
175
+ - Score area shows original + current side by side
176
+ - Three action buttons: Approve, Save & Continue, Reject
177
+
178
+ ### New functions
179
+ - `toggleFieldEdit(fieldName)` — switches field between view/edit mode
180
+ - `askAgentForField(fieldName)` — constructs mention-targeted message, calls `sendMessage()`, switches to chat tab
181
+ - `savePlanAndContinue()` — merges edited values into `designState`, saves to localStorage, removes plan card, resets phase to exploring
182
+ - `recalcPlanScore()` — reads current field values from plan card inputs, runs `computeScore()`, updates current score display
183
+ - `detectPlanField(agentId, content)` — maps an agent message to a plan field using `field_agents` mapping + `category_keywords`. Returns field name or null.
184
+ - `extractFieldValue(fieldName, content)` — parses agent message content to extract a value for the given field (quoted values, values after "recommend"/"suggest"/"use")
185
+ - `updatePlanCardField(fieldName, value)` — programmatically sets a plan card field's value and triggers `recalcPlanScore()`
186
+
187
+ ### Modified functions
188
+ - `approvePlanCard()` — reads edited field values + notes from plan card inputs before sending to API
189
+ - `confirmRecommendation()` — when phase is "planning", also calls `detectPlanField()` → `extractFieldValue()` → `updatePlanCardField()` to auto-fill the plan card
190
+ - `computeScore()` — no logic change, just called by `recalcPlanScore()`
191
+
192
+ ## Testing
193
+
194
+ - Verify field toggling between view/edit mode for all field types
195
+ - Verify Ask Agent sends correct mention-targeted message with plan context
196
+ - Verify notes persist through Save & Continue and appear in agent context on Approve
197
+ - Verify dual score: original stays static, current recalculates on field edit
198
+ - Verify Save & Continue preserves edits in designState and resets phase
199
+ - Verify Reject discards all edits
200
+ - Verify Approve sends edited values (not original values)
201
+ - Verify Confirm on agent message auto-fills corresponding plan card field when plan is visible
202
+ - Verify Confirm on agent message works as before when no plan card is visible
203
+ - Verify field detection correctly maps agent + keyword to plan field
204
+ - Verify value extraction handles common patterns (quoted, after "recommend", etc.)
205
+ - Verify plan card score recalculates after Confirm auto-fill
tests/test_api_routes.py CHANGED
@@ -173,6 +173,26 @@ class TestPlanApproveEndpoint:
173
  assert data["design_state"]["part_name"] == "gear"
174
  assert data["design_state"]["plan"]["material"] == "brass"
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
  class TestPlanRejectEndpoint:
178
  def test_reject_resets_phase(self):
 
173
  assert data["design_state"]["part_name"] == "gear"
174
  assert data["design_state"]["plan"]["material"] == "brass"
175
 
176
+ def test_approve_preserves_notes(self):
177
+ resp = client.post("/api/plan/approve", json={
178
+ "plan": {
179
+ "part_name": "bracket",
180
+ "description": "test",
181
+ "material": "aluminum 6061",
182
+ "dimensions": {"width": 60},
183
+ "features": [],
184
+ "constraints": [],
185
+ "axis_recommendation": "3-axis",
186
+ "machining_notes": [],
187
+ "confidence_score": 9.0,
188
+ "notes": "Check material compatibility",
189
+ },
190
+ "design_state": {"phase": "planning"},
191
+ })
192
+ assert resp.status_code == 200
193
+ data = resp.json()
194
+ assert data["design_state"]["plan"]["notes"] == "Check material compatibility"
195
+
196
 
197
  class TestPlanRejectEndpoint:
198
  def test_reject_resets_phase(self):
tests/test_design_state.py CHANGED
@@ -126,6 +126,30 @@ class TestDesignPlan:
126
  assert "aluminum 6061" in rendered
127
  assert "No undercuts" in rendered
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  class TestComputeScore:
131
  def test_empty_state_scores_zero(self):
 
126
  assert "aluminum 6061" in rendered
127
  assert "No undercuts" in rendered
128
 
129
+ def test_plan_notes_default_empty(self):
130
+ plan = DesignPlan(part_name="test")
131
+ assert plan.notes == ""
132
+
133
+ def test_plan_notes_in_render(self):
134
+ plan = DesignPlan(
135
+ part_name="bracket",
136
+ material="aluminum",
137
+ notes="Check if 304 is overkill",
138
+ )
139
+ rendered = plan.render_approved()
140
+ assert "User Notes" in rendered
141
+ assert "Check if 304 is overkill" in rendered
142
+
143
+ def test_plan_notes_empty_not_in_render(self):
144
+ plan = DesignPlan(part_name="bracket", material="aluminum", notes="")
145
+ rendered = plan.render_approved()
146
+ assert "User Notes" not in rendered
147
+
148
+ def test_plan_from_state_no_notes(self):
149
+ state = DesignState(part_name="bracket", material="steel")
150
+ plan = DesignPlan.from_state(state, confidence_score=5.0)
151
+ assert plan.notes == ""
152
+
153
 
154
  class TestComputeScore:
155
  def test_empty_state_scores_zero(self):
tests/test_settings.py CHANGED
@@ -65,6 +65,16 @@ class TestPlanningConfig:
65
  assert settings.planning.threshold == 8.0
66
 
67
 
 
 
 
 
 
 
 
 
 
 
68
  class TestGapAnalysisConfig:
69
  def test_gap_analysis_defaults(self):
70
  from config.settings import Settings
 
65
  assert settings.planning.threshold == 8.0
66
 
67
 
68
+ def test_planning_field_agents_defaults():
69
+ from config.settings import PlanningConfig
70
+ cfg = PlanningConfig()
71
+ assert cfg.field_agents["material"] == "engineering"
72
+ assert cfg.field_agents["features"] == "design"
73
+ assert cfg.field_agents["constraints"] == "cnc"
74
+ assert cfg.field_agents["axis_recommendation"] == "cnc"
75
+ assert cfg.field_agents["part_name"] == "design"
76
+
77
+
78
  class TestGapAnalysisConfig:
79
  def test_gap_analysis_defaults(self):
80
  from config.settings import Settings
web/index.html CHANGED
@@ -1315,6 +1315,48 @@
1315
  .plan-card-reject {
1316
  background: var(--bg-surface); color: var(--text-secondary);
1317
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1318
 
1319
  /* ---- GAP / QUESTION CARDS ---- */
1320
  .gap-cards {
@@ -1837,44 +1879,265 @@ function syncWizardFromState() {
1837
 
1838
  // ── PLAN CARD ─────────────────────────────────────────
1839
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1840
  function renderPlanCard(plan) {
1841
- const fields = [
1842
- ['Part', plan.part_name],
1843
- ['Material', plan.material],
1844
- ['Dimensions', Object.entries(plan.dimensions || {}).map(([k,v]) => k + '=' + v + 'mm').join(', ')],
1845
- ['Features', (plan.features || []).join(', ')],
1846
- ['Constraints', (plan.constraints || []).join(', ')],
1847
- ['Axis', plan.axis_recommendation || 'Auto'],
1848
  ];
1849
- let html = '<div class="plan-card" id="active-plan-card">';
1850
- html += '<div class="plan-card-title">\u25c6 PLAN READY FOR REVIEW</div>';
1851
- for (const [label, value] of fields) {
1852
- html += '<div class="wizard-review-field"><span class="wizard-review-label">' + escapeHtml(label) + '</span><span class="wizard-review-value">' + escapeHtml(value || '\u2014') + '</span></div>';
1853
- }
1854
  if (plan.machining_notes && plan.machining_notes.length) {
1855
- html += '<div class="wizard-review-field"><span class="wizard-review-label">Notes</span><span class="wizard-review-value">' + escapeHtml(plan.machining_notes.join('; ')) + '</span></div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1856
  }
1857
- html += '<div class="plan-card-score">Score: ' + (plan.confidence_score || 0).toFixed(0) + '/8</div>';
 
 
 
 
 
 
1858
  html += '<div class="plan-card-actions">';
1859
- html += '<button class="plan-card-btn plan-card-approve" onclick="approvePlanCard()">Approve</button>';
1860
- html += '<button class="plan-card-btn plan-card-reject" onclick="rejectPlanCard()">Reject</button>';
 
1861
  html += '</div></div>';
1862
  return html;
1863
  }
1864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1865
  async function approvePlanCard() {
1866
- const plan = designState.plan;
1867
  if (!plan) return;
 
 
1868
  try {
1869
- const resp = await fetch('/api/plan/approve', {
1870
  method: 'POST',
1871
  headers: { 'Content-Type': 'application/json' },
1872
  body: JSON.stringify({ plan: plan, design_state: designState }),
1873
  });
1874
- const data = await resp.json();
1875
  designState = data.design_state;
1876
  saveState();
1877
- const card = document.getElementById('active-plan-card');
1878
  if (card) card.remove();
1879
  await sendMessage('Generate the approved design');
1880
  } catch (err) {
@@ -2049,6 +2312,12 @@ const I18N = {
2049
  planReady: 'PLAN READY FOR REVIEW',
2050
  planApprove: 'Approve',
2051
  planReject: 'Reject',
 
 
 
 
 
 
2052
  },
2053
  'zh-TW': {
2054
  subtitle: '\u591a\u4ee3\u7406\u8a2d\u8a08',
@@ -2086,6 +2355,12 @@ const I18N = {
2086
  planReady: '\u8a08\u756b\u5df2\u6e96\u5099\u5be9\u67e5',
2087
  planApprove: '\u6279\u51c6',
2088
  planReject: '\u62d2\u7d55',
 
 
 
 
 
 
2089
  },
2090
  vi: {
2091
  subtitle: 'Thi\u1ebft K\u1ebf \u0110a T\u00e1c T\u1eed',
@@ -2123,6 +2398,12 @@ const I18N = {
2123
  planReady: 'K\u1ebe HO\u1ea0CH S\u1eb4N S\u00c0NG',
2124
  planApprove: 'Duy\u1ec7t',
2125
  planReject: 'T\u1eeb Ch\u1ed1i',
 
 
 
 
 
 
2126
  },
2127
  };
2128
 
@@ -2757,16 +3038,25 @@ function addMessage(msg) {
2757
  }
2758
 
2759
  function confirmRecommendation(msgId, agentId) {
2760
- const el = document.querySelector('[data-msg-id="' + msgId + '"]');
2761
  if (!el) return;
2762
- const content = el.dataset.content;
2763
- const actions = document.getElementById(msgId + '-actions');
2764
  if (actions) {
2765
  actions.innerHTML = '<div class="msg-confirmed">' + t('confirmed') + '</div>';
2766
  }
2767
- // Send confirmation as user message so it updates design state
2768
- const agentName = (AGENTS[agentId] || {}).name || agentId;
2769
  sendMessage(t('confirmedMsg').replace('{agent}', agentName).replace('{content}', content));
 
 
 
 
 
 
 
 
 
 
2770
  }
2771
 
2772
  function reviseRecommendation(msgId, agentId) {
 
1315
  .plan-card-reject {
1316
  background: var(--bg-surface); color: var(--text-secondary);
1317
  }
1318
+ .plan-card-save {
1319
+ background: var(--accent); color: var(--bg-void); border-color: var(--accent);
1320
+ }
1321
+ .plan-field-actions {
1322
+ display: inline-flex; gap: 4px; margin-left: 6px; vertical-align: middle;
1323
+ }
1324
+ .plan-field-btn {
1325
+ background: none; border: 1px solid var(--border); border-radius: 3px;
1326
+ color: var(--text-secondary); cursor: pointer; font-size: 10px;
1327
+ padding: 1px 5px; font-family: var(--font-mono); line-height: 1.4;
1328
+ }
1329
+ .plan-field-btn:hover { border-color: var(--accent); color: var(--accent); }
1330
+ .plan-field-input {
1331
+ background: var(--bg-void); border: 1px solid var(--accent); border-radius: 3px;
1332
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
1333
+ padding: 2px 6px; width: 60%;
1334
+ }
1335
+ .plan-field-input:focus { outline: none; border-color: var(--success); }
1336
+ .plan-dim-group { display: inline-flex; gap: 4px; }
1337
+ .plan-dim-input {
1338
+ background: var(--bg-void); border: 1px solid var(--accent); border-radius: 3px;
1339
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
1340
+ padding: 2px 4px; width: 60px;
1341
+ }
1342
+ .plan-dim-input:focus { outline: none; border-color: var(--success); }
1343
+ .plan-dim-label {
1344
+ font-size: 9px; color: var(--text-secondary); font-family: var(--font-mono);
1345
+ }
1346
+ .plan-notes {
1347
+ width: 100%; min-height: 48px; margin-top: 8px; padding: 6px 8px;
1348
+ background: var(--bg-void); border: 1px solid var(--border); border-radius: 5px;
1349
+ color: var(--text-primary); font-family: var(--font-mono); font-size: 11px;
1350
+ resize: vertical;
1351
+ }
1352
+ .plan-notes:focus { outline: none; border-color: var(--accent); }
1353
+ .plan-score-dual {
1354
+ display: flex; gap: 16px; font-family: var(--font-mono); font-size: 11px;
1355
+ color: var(--text-secondary); margin-top: 8px;
1356
+ }
1357
+ .plan-score-current { font-weight: 600; }
1358
+ .plan-score-current.score-ok { color: var(--success); }
1359
+ .plan-score-current.score-low { color: var(--warning, #ffab40); }
1360
 
1361
  /* ---- GAP / QUESTION CARDS ---- */
1362
  .gap-cards {
 
1879
 
1880
  // ── PLAN CARD ─────────────────────────────────────────
1881
 
1882
+ var PLAN_FIELD_AGENTS = {
1883
+ part_name: 'design', material: 'engineering', dimensions: 'engineering',
1884
+ features: 'design', constraints: 'cnc', axis_recommendation: 'cnc',
1885
+ machining_notes: 'cnc',
1886
+ };
1887
+ var PLAN_FIELD_QUESTIONS = {
1888
+ part_name: 'What should this part be called?',
1889
+ material: 'What material do you recommend?',
1890
+ dimensions: 'What dimensions are appropriate?',
1891
+ features: 'What features should this part have?',
1892
+ constraints: 'What manufacturing constraints should we consider?',
1893
+ axis_recommendation: 'What axis strategy do you recommend?',
1894
+ machining_notes: 'Any machining notes to consider?',
1895
+ };
1896
+
1897
  function renderPlanCard(plan) {
1898
+ var fields = [
1899
+ ['Part', 'part_name', plan.part_name, 'text'],
1900
+ ['Material', 'material', plan.material, 'text'],
1901
+ ['Dimensions', 'dimensions', plan.dimensions, 'dimensions'],
1902
+ ['Features', 'features', plan.features, 'list'],
1903
+ ['Constraints', 'constraints', plan.constraints, 'list'],
1904
+ ['Axis', 'axis_recommendation', plan.axis_recommendation || 'Auto', 'text'],
1905
  ];
 
 
 
 
 
1906
  if (plan.machining_notes && plan.machining_notes.length) {
1907
+ fields.push(['Notes', 'machining_notes', plan.machining_notes, 'list']);
1908
+ }
1909
+ var html = '<div class="plan-card" id="active-plan-card" data-original-score="' + (plan.confidence_score || 0) + '">';
1910
+ html += '<div class="plan-card-title">\u25c6 ' + t('planReady') + '</div>';
1911
+ for (var i = 0; i < fields.length; i++) {
1912
+ var label = fields[i][0], key = fields[i][1], value = fields[i][2], type = fields[i][3];
1913
+ html += '<div class="wizard-review-field" data-field="' + key + '">';
1914
+ html += '<span class="wizard-review-label">' + escapeHtml(label) + '</span>';
1915
+ html += '<span class="wizard-review-value" id="plan-val-' + key + '">';
1916
+ if (type === 'dimensions') {
1917
+ html += escapeHtml(Object.entries(value || {}).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014');
1918
+ } else if (type === 'list') {
1919
+ html += escapeHtml((value || []).join(', ') || '\u2014');
1920
+ } else {
1921
+ html += escapeHtml(value || '\u2014');
1922
+ }
1923
+ html += '</span>';
1924
+ html += '<span class="plan-field-actions">';
1925
+ html += '<button class="plan-field-btn" onclick="toggleFieldEdit(\'' + key + '\', \'' + type + '\')" title="Edit">' + t('planEdit') + '</button>';
1926
+ html += '<button class="plan-field-btn" onclick="askAgentForField(\'' + key + '\')" title="Ask Agent">' + t('planAskAgent') + '</button>';
1927
+ html += '</span>';
1928
+ html += '</div>';
1929
  }
1930
+ html += '<textarea class="plan-notes" id="plan-notes" placeholder="' + t('planNotesPlaceholder') + '">' + escapeHtml(plan.notes || '') + '</textarea>';
1931
+ var origScore = (plan.confidence_score || 0).toFixed(0);
1932
+ html += '<div class="plan-score-dual">';
1933
+ html += '<span>' + t('planScoreOriginal') + ': ' + origScore + '/8</span>';
1934
+ html += '<span>\u2192</span>';
1935
+ html += '<span class="plan-score-current score-ok" id="plan-current-score">' + t('planScoreCurrent') + ': ' + origScore + '/8</span>';
1936
+ html += '</div>';
1937
  html += '<div class="plan-card-actions">';
1938
+ html += '<button class="plan-card-btn plan-card-approve" onclick="approvePlanCard()">' + t('planApprove') + '</button>';
1939
+ html += '<button class="plan-card-btn plan-card-save" onclick="savePlanAndContinue()">' + t('planSave') + '</button>';
1940
+ html += '<button class="plan-card-btn plan-card-reject" onclick="rejectPlanCard()">' + t('planReject') + '</button>';
1941
  html += '</div></div>';
1942
  return html;
1943
  }
1944
 
1945
+ function toggleFieldEdit(fieldKey, fieldType) {
1946
+ var valEl = document.getElementById('plan-val-' + fieldKey);
1947
+ if (!valEl) return;
1948
+ var plan = designState.plan;
1949
+ if (!plan) return;
1950
+
1951
+ var existingInput = valEl.querySelector('input, .plan-dim-group');
1952
+ if (existingInput) {
1953
+ if (fieldType === 'dimensions') {
1954
+ var dimInputs = valEl.querySelectorAll('.plan-dim-input');
1955
+ var dims = {};
1956
+ dimInputs.forEach(function(inp) { if (inp.value) dims[inp.dataset.dim] = parseFloat(inp.value) || 0; });
1957
+ plan.dimensions = dims;
1958
+ valEl.textContent = Object.entries(dims).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014';
1959
+ } else if (fieldType === 'list') {
1960
+ var raw = existingInput.value;
1961
+ plan[fieldKey] = raw ? raw.split(',').map(function(s) { return s.trim(); }).filter(Boolean) : [];
1962
+ valEl.textContent = plan[fieldKey].join(', ') || '\u2014';
1963
+ } else {
1964
+ plan[fieldKey] = existingInput.value;
1965
+ valEl.textContent = plan[fieldKey] || '\u2014';
1966
+ }
1967
+ recalcPlanScore();
1968
+ return;
1969
+ }
1970
+
1971
+ if (fieldType === 'dimensions') {
1972
+ var dims = plan.dimensions || {};
1973
+ var dimKeys = Object.keys(dims).length ? Object.keys(dims) : ['width', 'height', 'depth'];
1974
+ var groupHtml = '<span class="plan-dim-group">';
1975
+ dimKeys.forEach(function(dk) {
1976
+ groupHtml += '<span><span class="plan-dim-label">' + escapeHtml(dk) + '</span><input class="plan-dim-input" type="number" data-dim="' + dk + '" value="' + (dims[dk] || '') + '"></span>';
1977
+ });
1978
+ groupHtml += '</span>';
1979
+ valEl.innerHTML = groupHtml;
1980
+ } else if (fieldType === 'list') {
1981
+ var current = (plan[fieldKey] || []).join(', ');
1982
+ valEl.innerHTML = '<input class="plan-field-input" type="text" value="' + escapeHtml(current) + '">';
1983
+ } else {
1984
+ var current = plan[fieldKey] || '';
1985
+ valEl.innerHTML = '<input class="plan-field-input" type="text" value="' + escapeHtml(current) + '">';
1986
+ }
1987
+ var firstInput = valEl.querySelector('input');
1988
+ if (firstInput) firstInput.focus();
1989
+ }
1990
+
1991
+ function recalcPlanScore() {
1992
+ var plan = designState.plan;
1993
+ if (!plan) return;
1994
+ var s = 0;
1995
+ if (plan.material) s += 3;
1996
+ if (plan.part_name) s += 1;
1997
+ if (plan.description) s += 1;
1998
+ if (plan.axis_recommendation) s += 2;
1999
+ s += Math.min(Object.keys(plan.dimensions || {}).length, 4);
2000
+ s += Math.min((plan.features || []).length, 4);
2001
+ s += Math.min((plan.constraints || []).length, 2);
2002
+ var el = document.getElementById('plan-current-score');
2003
+ if (el) {
2004
+ el.textContent = t('planScoreCurrent') + ': ' + s.toFixed(0) + '/8';
2005
+ el.className = 'plan-score-current ' + (s >= 8 ? 'score-ok' : 'score-low');
2006
+ }
2007
+ }
2008
+
2009
+ function askAgentForField(fieldKey) {
2010
+ var plan = designState.plan;
2011
+ if (!plan) return;
2012
+ var agentId = PLAN_FIELD_AGENTS[fieldKey] || 'design';
2013
+ var question = PLAN_FIELD_QUESTIONS[fieldKey] || 'Can you help with this field?';
2014
+ var partCtx = plan.part_name ? ' for "' + plan.part_name + '"' : '';
2015
+ var msg = '@' + agentId + ' Regarding the plan' + partCtx + ': ' + question;
2016
+ var chatTab = document.querySelector('[data-tab="chat"]');
2017
+ if (chatTab) chatTab.click();
2018
+ sendMessage(msg);
2019
+ }
2020
+
2021
+ function savePlanAndContinue() {
2022
+ var plan = designState.plan;
2023
+ if (!plan) return;
2024
+ var notesEl = document.getElementById('plan-notes');
2025
+ if (notesEl) plan.notes = notesEl.value;
2026
+ designState.part_name = plan.part_name;
2027
+ designState.description = plan.description;
2028
+ designState.material = plan.material;
2029
+ designState.dimensions = Object.assign({}, plan.dimensions);
2030
+ designState.features = (plan.features || []).slice();
2031
+ designState.constraints = (plan.constraints || []).slice();
2032
+ designState.axis_recommendation = plan.axis_recommendation;
2033
+ designState.phase = 'exploring';
2034
+ saveState();
2035
+ var card = document.getElementById('active-plan-card');
2036
+ if (card) card.remove();
2037
+ }
2038
+
2039
+ function updatePlanCardField(fieldKey, value) {
2040
+ var plan = designState.plan;
2041
+ if (!plan) return;
2042
+ var valEl = document.getElementById('plan-val-' + fieldKey);
2043
+ if (fieldKey === 'dimensions' && typeof value === 'object') {
2044
+ Object.assign(plan.dimensions, value);
2045
+ if (valEl) valEl.textContent = Object.entries(plan.dimensions).map(function(e) { return e[0] + '=' + e[1] + 'mm'; }).join(', ') || '\u2014';
2046
+ } else if (fieldKey === 'features' || fieldKey === 'constraints' || fieldKey === 'machining_notes') {
2047
+ if (Array.isArray(value)) {
2048
+ plan[fieldKey] = value;
2049
+ } else {
2050
+ if (!plan[fieldKey].includes(value)) plan[fieldKey].push(value);
2051
+ }
2052
+ if (valEl) valEl.textContent = plan[fieldKey].join(', ') || '\u2014';
2053
+ } else {
2054
+ plan[fieldKey] = value;
2055
+ if (valEl) valEl.textContent = value || '\u2014';
2056
+ }
2057
+ recalcPlanScore();
2058
+ }
2059
+
2060
+ var CATEGORY_KEYWORDS = {
2061
+ dimension: ['width', 'height', 'depth', 'length', 'diameter', 'dimension', 'size'],
2062
+ material: ['material', 'alloy', 'grade', 'aluminum', 'steel', 'metal', 'brass', 'titanium', 'nylon'],
2063
+ feature: ['hole', 'fillet', 'chamfer', 'pocket', 'slot', 'feature'],
2064
+ constraint: ['tolerance', 'wall thickness', 'constraint', 'min wall', 'max size'],
2065
+ machining: ['axis', 'machine', 'setup', 'fixture', 'tool access'],
2066
+ };
2067
+ var CATEGORY_TO_FIELD = {
2068
+ material: 'material',
2069
+ dimension: 'dimensions',
2070
+ feature: 'features',
2071
+ constraint: 'constraints',
2072
+ machining: 'axis_recommendation',
2073
+ };
2074
+
2075
+ function detectPlanField(agentId, content) {
2076
+ var lower = content.toLowerCase();
2077
+ for (var cat in CATEGORY_KEYWORDS) {
2078
+ var expectedAgent = PLAN_FIELD_AGENTS[CATEGORY_TO_FIELD[cat]];
2079
+ if (expectedAgent !== agentId) continue;
2080
+ var keywords = CATEGORY_KEYWORDS[cat];
2081
+ for (var i = 0; i < keywords.length; i++) {
2082
+ if (lower.indexOf(keywords[i]) !== -1) {
2083
+ return CATEGORY_TO_FIELD[cat];
2084
+ }
2085
+ }
2086
+ }
2087
+ return null;
2088
+ }
2089
+
2090
+ function extractFieldValue(fieldKey, content) {
2091
+ if (fieldKey === 'material') {
2092
+ var matList = ['aluminum 6061', 'aluminum 7075', 'stainless steel 304', 'stainless steel 316',
2093
+ 'aluminum', 'steel', 'brass', 'copper', 'titanium', 'nylon', 'delrin', 'peek', 'polycarbonate'];
2094
+ var lower = content.toLowerCase();
2095
+ for (var i = 0; i < matList.length; i++) {
2096
+ if (lower.indexOf(matList[i]) !== -1) return matList[i];
2097
+ }
2098
+ }
2099
+
2100
+ if (fieldKey === 'axis_recommendation') {
2101
+ var axisMatch = content.match(/(3-axis|3\+2[\s-]*axis|5-axis)/i);
2102
+ if (axisMatch) return axisMatch[1].toLowerCase();
2103
+ }
2104
+
2105
+ if (fieldKey === 'dimensions') {
2106
+ var dims = {};
2107
+ var dimPattern = /(\d+\.?\d*)\s*mm\s+(wide|width|tall|height|high|thick|thickness|deep|depth|long|length|diameter)/gi;
2108
+ var m;
2109
+ while ((m = dimPattern.exec(content)) !== null) {
2110
+ var dimMap = {wide:'width',width:'width',tall:'height',height:'height',high:'height',
2111
+ thick:'thickness',thickness:'thickness',deep:'depth',depth:'depth',long:'length',length:'length',diameter:'diameter'};
2112
+ dims[dimMap[m[2].toLowerCase()] || m[2].toLowerCase()] = parseFloat(m[1]);
2113
+ }
2114
+ if (Object.keys(dims).length) return dims;
2115
+ }
2116
+
2117
+ var recMatch = content.match(/(?:recommend|suggest|use)\s+(.+?)(?:\.|,|$)/i);
2118
+ if (recMatch) return recMatch[1].trim();
2119
+
2120
+ var quoted = content.match(/"([^"]+)"/);
2121
+ if (quoted) return quoted[1];
2122
+
2123
+ return null;
2124
+ }
2125
+
2126
  async function approvePlanCard() {
2127
+ var plan = designState.plan;
2128
  if (!plan) return;
2129
+ var notesEl = document.getElementById('plan-notes');
2130
+ if (notesEl) plan.notes = notesEl.value;
2131
  try {
2132
+ var resp = await fetch('/api/plan/approve', {
2133
  method: 'POST',
2134
  headers: { 'Content-Type': 'application/json' },
2135
  body: JSON.stringify({ plan: plan, design_state: designState }),
2136
  });
2137
+ var data = await resp.json();
2138
  designState = data.design_state;
2139
  saveState();
2140
+ var card = document.getElementById('active-plan-card');
2141
  if (card) card.remove();
2142
  await sendMessage('Generate the approved design');
2143
  } catch (err) {
 
2312
  planReady: 'PLAN READY FOR REVIEW',
2313
  planApprove: 'Approve',
2314
  planReject: 'Reject',
2315
+ planSave: 'Save & Continue',
2316
+ planNotesPlaceholder: 'Add notes about this plan...',
2317
+ planScoreOriginal: 'Original',
2318
+ planScoreCurrent: 'Current',
2319
+ planAskAgent: 'Ask',
2320
+ planEdit: '\u270e',
2321
  },
2322
  'zh-TW': {
2323
  subtitle: '\u591a\u4ee3\u7406\u8a2d\u8a08',
 
2355
  planReady: '\u8a08\u756b\u5df2\u6e96\u5099\u5be9\u67e5',
2356
  planApprove: '\u6279\u51c6',
2357
  planReject: '\u62d2\u7d55',
2358
+ planSave: '\u5132\u5b58\u4e26\u7e7c\u7e8c',
2359
+ planNotesPlaceholder: '\u65b0\u589e\u8a08\u756b\u5099\u8a3b...',
2360
+ planScoreOriginal: '\u539f\u59cb',
2361
+ planScoreCurrent: '\u7576\u524d',
2362
+ planAskAgent: '\u8a62\u554f',
2363
+ planEdit: '\u270e',
2364
  },
2365
  vi: {
2366
  subtitle: 'Thi\u1ebft K\u1ebf \u0110a T\u00e1c T\u1eed',
 
2398
  planReady: 'K\u1ebe HO\u1ea0CH S\u1eb4N S\u00c0NG',
2399
  planApprove: 'Duy\u1ec7t',
2400
  planReject: 'T\u1eeb Ch\u1ed1i',
2401
+ planSave: 'L\u01b0u & Ti\u1ebfp T\u1ee5c',
2402
+ planNotesPlaceholder: 'Th\u00eam ghi ch\u00fa v\u1ec1 k\u1ebf ho\u1ea1ch...',
2403
+ planScoreOriginal: 'G\u1ed1c',
2404
+ planScoreCurrent: 'Hi\u1ec7n t\u1ea1i',
2405
+ planAskAgent: 'H\u1ecfi',
2406
+ planEdit: '\u270e',
2407
  },
2408
  };
2409
 
 
3038
  }
3039
 
3040
  function confirmRecommendation(msgId, agentId) {
3041
+ var el = document.querySelector('[data-msg-id="' + msgId + '"]');
3042
  if (!el) return;
3043
+ var content = el.dataset.content;
3044
+ var actions = document.getElementById(msgId + '-actions');
3045
  if (actions) {
3046
  actions.innerHTML = '<div class="msg-confirmed">' + t('confirmed') + '</div>';
3047
  }
3048
+ var agentName = (AGENTS[agentId] || {}).name || agentId;
 
3049
  sendMessage(t('confirmedMsg').replace('{agent}', agentName).replace('{content}', content));
3050
+ // Auto-fill plan card field if plan is visible
3051
+ if (designState.phase === 'planning' && designState.plan && document.getElementById('active-plan-card')) {
3052
+ var field = detectPlanField(agentId, content);
3053
+ if (field) {
3054
+ var value = extractFieldValue(field, content);
3055
+ if (value != null) {
3056
+ updatePlanCardField(field, value);
3057
+ }
3058
+ }
3059
+ }
3060
  }
3061
 
3062
  function reviseRecommendation(msgId, agentId) {