From 2d1024dd33f710b25f2def791e2911caecd7a4c1 Mon Sep 17 00:00:00 2001 From: coreci Date: Sun, 27 Sep 2026 04:50:24 +0000 Subject: [PATCH 1/4] =?UTF-8?q?docs(P07):=202nd=20owner=20gate=20feedback?= =?UTF-8?q?=20=E2=80=94=20ask=20intelligence=20phase=20(REQ-67..68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---ci--- phase: 7 milestone: v0.1 status: execute escalation: type: prototype_approval hook: prototype_approval resolution: binding_extension message: >- Owner returned 2nd gate feedback: the New Research view must surface follow-up query suggestions from unanswered questions, and offer a refine option that crafts a better research query through back-and-forth dialogue. Feedback becomes binding requirements REQ-67/68 as execution phase 7 (+ P8 final re-review); the milestone merge to main stays halted until the extended prototype is approved at the REQ-62 gate. requirements: covered: [] partial: [REQ-67, REQ-68, REQ-62] ---/ci--- ROADMAP: P6 marked shipped-superseded; P7 (ask suggestions + refine dialogue) + P8 (final re-review) inserted; releases table extended (v0.1.7, v0.1.8). REQUIREMENTS: Ask Intelligence Extension section (REQ-67/68); traceability updated. PLAN.md: phase-7 waves with MVP/UX sections. Checkpoint: phase 7 execute. --- .ciagent/CHECKPOINT.json | 8 +++--- .ciagent/PLAN.md | 62 ++++++++++++++++++---------------------- .ciagent/REQUIREMENTS.md | 7 ++++- .ciagent/ROADMAP.md | 24 +++++++++++++--- 4 files changed, 58 insertions(+), 43 deletions(-) diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index 7b400fd..14018a5 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,8 +1,8 @@ { - "phase": 6, - "stage": "approval_pending", + "phase": 7, + "stage": "execute", "milestone": "v0.1", - "phase_role": "final", + "phase_role": "execution", "attempts": 0, - "updated_at": "2026-09-27T04:40:00Z" + "updated_at": "2026-09-27T05:10:00Z" } \ No newline at end of file diff --git a/.ciagent/PLAN.md b/.ciagent/PLAN.md index 27bb1aa..a0ce778 100644 --- a/.ciagent/PLAN.md +++ b/.ciagent/PLAN.md @@ -1,54 +1,48 @@ -# PLAN — Milestone v0.1 Phase 6 (FINAL): Review + Approval Re-presentation + Ship +# PLAN — Milestone v0.1 Phase 7: Ask Screen Intelligence — Suggestions + Refine --- -phase: 6 +phase: 7 milestone: v0.1 -status: executed +status: planned must_haves: - - "Full-milestone review: PASS-WITH-FIXES — deep-link P0 (stored-report cards resolved to the wrong run) + 5 P1s fixed and verified in-phase; 75/75 tests" - - "Audit: PASS-WITH-NOTES — reconstruction, tags, file/branch/commit discipline, secrets sweep all green; notes applied (version bump, ROADMAP status, ARCHITECTURE surfaces)" - - "Final phase patch v0.1.6 shipped — milestone release candidate, presented at the REQ-62 owner approval gate" + - "Follow-up query suggestions on the New Research view, derived from unanswered questions (open task-inbox items joined with source reports); click fills the question box — editable, never auto-starts (REQ-67)" + - "Refine option: opt-in back-and-forth dialogue in an inline drawer (scripted clarifying questions → answers → composed refined question + suggested context); accept fills the form, or keep original (REQ-68)" + - "Both features opt-in — zero required clicks added to the ≤3-click happy path (pinned by test)" + - "All new data access extends the ApiClient boundary + MockApiClient fixtures (REQ-53/54 discipline)" personas: + - frontend-engineer - ci-lead-developer -requirements: [REQ-62] +requirements: [REQ-67, REQ-68] --- ## User-Facing Surface -The extended prototype presented for owner approval: the full 5-screen click-path (Ask → Choose → Plan & Run → Reading Room → Library) now inside the persistent app shell (sidebar + top bar + avatar), the folder-structure research library with cross-report connections, the settings surface (API keys, storage, methodology defaults, appearance, about), and the ⌘K navigation palette. Releases v0.1.0–v0.1.5; v0.1.6 is the extended-prototype release candidate. +1. **Ask suggestions** (`/ask`, below the primary form): "Pick up an open question" — cards for each open task-inbox item (question text + source report title + "from your research" attribution). Click fills the question textarea (editable, no auto-submit, focus moves to the box). Honest empty state: "No open questions yet — they collect here as you run research." +2. **Refine drawer** (`/ask`, inline): "Refine question" button (disabled until a draft exists) opens the established dials-drawer pattern — one clarifying question at a time with answer input, progress indicator (1/2/3), then the composed refined question + suggested context preview. **Use this** fills question + context and closes; **Keep original** dismisses; **Restart** re-runs the exchange. Escape closes. Honest caption: "Scripted refinement in the prototype — model-driven refine lands in v0.2." -## Happy Path (owner review script) +## Happy Path -1. `cd frontend && pnpm install && pnpm dev` → http://localhost:3000 → shell renders on /ask -2. Ask → question → Research this (click 1) -3. Choose → Deep Report card → Start research (click 2) -4. Plan & Run → Approve plan (click 3) → live trace → pause/resume/cancel -5. Read the report → hover [n] → exact quote; ledger; export Markdown/JSON -6. Sidebar → Library → folder tree → open a stored report → matches its card title -7. Library → Connections → shared-source ⇄ and related-topic → edges -8. ⌘K → type "lith" → jump to the stored report -9. Avatar → user menu → Settings → API keys masked, save round-trips -10. Owner decision: approve → v0.1.6 published + milestone merge to main; or request changes - -## What this phase did - -1. **Review** (multi-persona, full milestone diff `main..HEAD`, P5-weighted): verdict PASS-WITH-FIXES. P0-1: stored-report deep links resolved to the wrong run (every library card + palette entry opened the demo report under a mismatched id) — fixed via the `#storedRun` resolver + card-title override in the scripted builders; probe + boundary tests prove run/report/ledger/export agree. P1s fixed in-phase: avatar-menu dismiss race (toggleRef guard, pinned by a new test), lost v2-preview pills on graph/inbox panels, stale root README, settings stale-after-save + non-live dial preview (now previews the live selection with formatted values), doc rot (run-page header, VERSIONING.md table). Suite 72 → 75. -2. **Audit**: PASS-WITH-NOTES — tags v0.1.0–v0.1.5 verified annotated + on-branch and mapped to ship records; REQ traceability consistent with ---ci--- blocks; secrets clean across full history; branch hygiene exact. Notes applied in this phase: settings About bumped to v0.1.6 (P1-watch), ROADMAP P6 → in_progress, ARCHITECTURE.md §7 + Build Order extended with P5/P6 surfaces, release README example bumped; `---/ci---` closer convention restored going forward. -3. **Ship (this commit sequence)**: PR #6 (phase/06 → milestone), squash-merge, tag v0.1.6, release with full milestone summary. Per REQ-62 and the prototype_approval escalation hook, the milestone merge to main does NOT proceed autonomously — the pipeline halts at the gate for the recorded owner decision. +1. `cd frontend && pnpm install && pnpm dev` → http://localhost:3000/ask (shell present) +2. Default path unchanged: question → Research this (click 1) → Choose → Start research (click 2) → Approve plan (click 3) +3. Suggestions: scroll to "Pick up an open question" → click a card → question box fills with the open question → edit if desired → Research this as usual +4. Refine: type a draft → "Refine question" → answer the 3 clarifying questions → review the composed question + suggested context → "Use this" → both boxes filled → Research this +5. Keep-original: open the drawer, answer partway, Escape → the original draft is untouched ## UX Acceptance Criteria -- Owner demo script runs clean on the shipped code (deep-link P0 fix verified by test) -- No placeholder/stub text in owner-visible surfaces; preview pills honest -- The 3-click criterion still pinned green post-shell -- Avatar menu opens/closes cleanly; keyboard accessible throughout +- ≤3-click default happy path still green (both features opt-in; zero required interactions) +- Suggestions never compete with the primary action (below the form, not modal, no auto-start) +- Refine drawer: draft visible behind, Escape-safe, honest scripted-prototype caption +- Keyboard accessible; focus ring in accent (global standard) +- All data via `api.*` — components never import MockApiClient; v0.2 OpenAPI swap unaffected -## Verification +## Waves -- Gates: typecheck ✓ lint ✓ tests 75/75 ✓ build ✓ (re-run after review fixes, plus by audit) -- Deep-link boundary test: every stored report resolves to its own id/question/methodology/title -- Post-hoc v0.2 log: demo-run fallback 404 semantics, shared scripted report body disclosure, palette scroll-into-view, raw methodology slugs on cards, pt-[14vh] token +1. **T-7.1 Boundary**: types (`SuggestedQuery`, `RefinementQuestion`, `RefinedQuestion`) → ApiClient gains `getSuggestedQueries` (mock: open `taskInboxFixture` items joined with `reportLibraryFixture`; v0.2: GET /api/suggestions), `getRefinementQuestions(draft)` (mock: scripted 3-question set; v0.2: POST /api/refine step 1), `composeRefinedQuestion(draft, answers)` (mock: deterministic composer; v0.2: model-driven) → co-located boundary tests +2. **T-7.2 Suggestions UI**: `AskSuggestions` component on /ask; click-fills-box contract; empty state; tests +3. **T-7.3 Refine drawer**: `RefineQuestionButton` + drawer component; exchange state machine; use/keep/restart; tests +4. **T-7.4 Regression + polish**: 3-click re-pin, README, typography pass, gates green ## Scope Guard -Milestone merge to main, v0.1.6 publication, v0.2 unblocking, and all v0.2 P1 work wait on the owner approval gate (REQ-62, prototype_approval escalation hook). The pipeline does not proceed autonomously past this commit. \ No newline at end of file +P8 (final review + approval re-presentation) follows P7 ship + UAT deploy. Milestone merge to main remains gated on the owner's recorded approval (REQ-62, prototype_approval escalation hook). No backend work, no v0.2 scope, no model calls (scripted refine only — the model-driven version is v0.2). \ No newline at end of file diff --git a/.ciagent/REQUIREMENTS.md b/.ciagent/REQUIREMENTS.md index 1f99df9..212db5d 100644 --- a/.ciagent/REQUIREMENTS.md +++ b/.ciagent/REQUIREMENTS.md @@ -25,6 +25,10 @@ - **REQ-65**: Settings surface behind the ApiClient boundary: API keys (model/search, masked), storage endpoint (local directory), methodology defaults (resolved dial display), appearance slot, about/version - **REQ-66**: Avatar button (top bar) opening a user menu: local single-user profile (no auth), links to settings and about; future slots visible but honest (preview-marked) +### Ask Intelligence Extension (P7 — owner gate feedback, directed 2026-09-27) +- **REQ-67**: New Research view surfaces follow-up query suggestions derived from unanswered questions (open task-inbox items from past runs); one click loads a suggestion into the question box for editing — never auto-starts a run +- **REQ-68**: Refine option on the question: an opt-in back-and-forth dialogue (clarifying questions → answers → composed refined question + suggested context); accepting fills the form, or the original is kept + ## MVP (Milestone v0.2 — core loop, gated on prototype approval) ### Methodology Framework @@ -115,8 +119,9 @@ | REQ-57 (Plan & Run) | v0.1 | P2 | complete | | REQ-58, REQ-59 (Reading Room, exports) | v0.1 | P2 | complete | | REQ-60, REQ-61 (v2 mockups, 3-click) | v0.1 | P3 | complete | -| REQ-62 (approval gate) | v0.1 | P6 | in_progress (awaiting owner decision on extended prototype) | +| REQ-62 (approval gate) | v0.1 | P8 | in_progress (awaiting owner decision on extended prototype) | | REQ-63, REQ-64, REQ-65, REQ-66 (app shell, library, settings, avatar) | v0.1 | P5 | complete | +| REQ-67, REQ-68 (ask suggestions, refine dialogue) | v0.1 | P7 | in_progress | | REQ-01..06, REQ-08..11, REQ-31 (framework, adapters) | v0.2 | P1 | blocked | | REQ-07, REQ-12, REQ-13..24 (probing, engine, trust) | v0.2 | P2 | blocked | | REQ-25, REQ-26, REQ-33 (API, client, tooling) | v0.2 | P3 | blocked | diff --git a/.ciagent/ROADMAP.md b/.ciagent/ROADMAP.md index 5d60989..6fbcc47 100644 --- a/.ciagent/ROADMAP.md +++ b/.ciagent/ROADMAP.md @@ -47,12 +47,26 @@ Two milestones, owner-directed 2026-09-27. **Milestone v0.1 — Prototype**: the - **Depends on**: 4 - **Status**: complete (PR #5 squash-merged to milestone/v0.1-prototype, tag v0.1.5, release created; verify PASS with one P1 fixed in-phase — main landmark restored) -### Phase 6 — Final Review + Approval Re-presentation + Ship (FINAL) +### Phase 6 — Final Review + Approval Re-presentation + Ship (shipped v0.1.6 — superseded by 2nd extension) - **Description**: Multi-persona review of the full milestone diff (now including P5), audit, then re-present the **extended prototype** at the REQ-62 owner approval gate. On recorded approval: milestone merge to main, tag v0.1.6 — the milestone release. (v0.2 P1 branches only after this tag.) - **Requirements covered**: REQ-62 (gate) + milestone approval of REQ-52..66 - **Success criteria**: review PASS (P0s fixed in-phase); audit PASS; v0.1.6 tagged + release; pipeline halts at the approval gate for the owner's recorded decision - **Depends on**: 5 -- **Status**: in_progress (review PASS-WITH-FIXES — deep-link P0 + 5 P1s fixed; audit PASS-WITH-NOTES; shipping as v0.1.6, then the gate halts for the owner decision) +- **Status**: shipped (PR #6 squash-merged, tag v0.1.6 + release created; review PASS-WITH-FIXES — deep-link P0 + 5 P1s fixed; audit PASS; owner directed UAT deploy then returned 2nd gate feedback → binding extension cycle P7/P8; milestone merge to main stays halted) + +### Phase 7 — Ask Screen Intelligence: Suggestions + Refine (2nd owner gate feedback) +- **Description**: New Research view gains follow-up query suggestions derived from unanswered questions (open task-inbox items joined with their source reports); one click fills the question box — editable, never auto-starts. An opt-in Refine dialogue (inline drawer, one clarifying question at a time, scripted in the prototype) composes a refined question + suggested context; accept fills the form, or keep the original. Both are opt-in affordances — zero required clicks added to the ≤3-click happy path. All data through the ApiClient boundary. +- **Requirements covered**: REQ-67, REQ-68 +- **Success criteria**: suggestions render from open inbox items with source attribution; click fills the box (no auto-submit); refine drawer completes a scripted exchange and composes a refined question; both flows keyboard-accessible; 3-click path pinned green; gates green +- **Depends on**: 6 +- **Status**: in_progress + +### Phase 8 — Final Review + Approval Re-presentation + Ship (FINAL) +- **Description**: Multi-persona review of the full milestone diff (now including P7), audit, then re-present the extended prototype at the REQ-62 owner approval gate. On recorded approval: milestone merge to main, tag v0.1.8 — the milestone release. (v0.2 P1 branches only after this tag.) +- **Requirements covered**: REQ-62 (gate) + milestone approval of REQ-52..68 +- **Success criteria**: review PASS (P0s fixed in-phase); audit PASS; v0.1.8 tagged + release; UAT staged per owner directive; pipeline halts at the approval gate +- **Depends on**: 7 +- **Status**: planned ## Milestone v0.2 — MVP (feature) — BLOCKED until prototype approval @@ -87,9 +101,11 @@ Phases renumbered from the original v1.0 plan (1-based within the milestone): - **Status**: blocked ## Milestone Releases -- **v0.1.4 = extended-prototype candidate** (feature milestone; final phase patch — superseded by the P5/P6 extension cycle) +- **v0.1.4 = extended-prototype candidate** (superseded by the P5/P6 extension cycle) - **v0.1.5 = app shell + research library + settings + avatar** (P5 patch) -- **v0.1.6 = milestone release** (P6 final phase patch — the approved extended prototype release, pending owner decision) +- **v0.1.6 = extended-prototype candidate** (superseded by the P7/P8 extension cycle) +- **v0.1.7 = ask suggestions + refine dialogue** (P7 patch) +- **v0.1.8 = milestone release** (P8 final phase patch — the approved extended prototype release, pending owner decision) - **v0.2.x = MVP release** (MAJOR still 0; ship only if all v0.2 phases complete) - If MVP scope extends: additional 0.x minor milestones (v0.3, …), each a full milestone cycle - **v1.0.0**: owner-approved release only — not created by the pipeline autonomously -- 2.43.0 From 6e57b17dbf8fa8e30f35976a78a43d8ea677be2d Mon Sep 17 00:00:00 2001 From: coreci Date: Sun, 27 Sep 2026 04:51:36 +0000 Subject: [PATCH 2/4] =?UTF-8?q?feat(P07):=20ask-intelligence=20ApiClient?= =?UTF-8?q?=20boundary=20=E2=80=94=20suggestions=20+=20refine=20(T-7.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---ci--- phase: 7 milestone: v0.1 status: execute persona: frontend-engineer task: T-7.1 requirements: covered: [] partial: [REQ-67, REQ-68] ---/ci--- Types: SuggestedQuery (question + source run/report attribution), RefinementQuestion (prompt/placeholder/optional), RefinedQuestion (question + context). ApiClient gains getSuggestedQueries (v0.2: GET /api/suggestions), getRefinementQuestions (v0.2: POST /api/refine/questions), composeRefinedQuestion (v0.2: POST /api/refine/compose — model-driven there, deterministic here). Mock: suggestions derive live from OPEN taskInboxFixture items joined with reportLibraryFixture for titles — done items drop automatically; refine exchange is a scripted 3-question set (scope/decision/depth) with a composer that weaves the scope into the question and the optional answers into context, degrading gracefully on skips. 6 boundary tests; gates green (81/81). --- frontend/src/lib/api/client.ts | 29 +++++ .../src/lib/api/mock/ask-boundary.test.ts | 85 +++++++++++++++ frontend/src/lib/api/mock/ask-fixtures.ts | 100 ++++++++++++++++++ frontend/src/lib/api/mock/mock-client.ts | 30 ++++++ frontend/src/lib/types.ts | 37 +++++++ 5 files changed, 281 insertions(+) create mode 100644 frontend/src/lib/api/mock/ask-boundary.test.ts create mode 100644 frontend/src/lib/api/mock/ask-fixtures.ts diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 2edffff..1f6249d 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -14,11 +14,14 @@ import type { InboxItem, Methodology, ProjectGraph, + RefinementQuestion, + RefinedQuestion, Report, ReportConnection, ReportLibrary, ResearchRun, Settings, + SuggestedQuery, TraceEvent, UserProfile, } from "@/lib/types"; @@ -35,6 +38,8 @@ export type { InboxItem, Methodology, ProjectGraph, + RefinementQuestion, + RefinedQuestion, ReportConnection, ReportConnectionKind, ReportFolder, @@ -45,6 +50,7 @@ export type { RunPlan, RunStatus, Settings, + SuggestedQuery, Task, TraceEvent, UserProfile, @@ -148,6 +154,29 @@ export interface ApiClient { * responses — masked values only. */ saveSettings(update: Partial): Promise; + + /** + * Follow-up query suggestions for the New Research view — + * derived from unanswered open questions of past runs + * (maps to GET /api/suggestions in v0.2 — REQ-67). + */ + getSuggestedQueries(): Promise; + + /** + * The clarifying questions for the refine dialogue, keyed to the + * draft (maps to POST /api/refine/questions in v0.2 — REQ-68). + */ + getRefinementQuestions(draft: string): Promise; + + /** + * Compose the refined question + suggested context from the + * exchange answers (maps to POST /api/refine/compose in v0.2, + * model-driven there; deterministic in the prototype — REQ-68). + */ + composeRefinedQuestion( + draft: string, + answers: Record, + ): Promise; } /** diff --git a/frontend/src/lib/api/mock/ask-boundary.test.ts b/frontend/src/lib/api/mock/ask-boundary.test.ts new file mode 100644 index 0000000..c91b79d --- /dev/null +++ b/frontend/src/lib/api/mock/ask-boundary.test.ts @@ -0,0 +1,85 @@ +/** + * Boundary tests for the P7 ask-intelligence surface (T-7.1, REQ-67, REQ-68). + * + * Suggestions derive from OPEN inbox items with source attribution; + * done items drop out. The refine dialogue returns its scripted + * question set and composes deterministically — skipped optional + * answers degrade gracefully, the non-optional scope always weaves in. + */ + +import { describe, expect, it } from "vitest"; +import { api } from "@/lib/api/client"; +import type { ApiClient } from "@/lib/api/client"; +import { taskInboxFixture } from "@/lib/api/mock/library-fixtures"; + +const client: ApiClient = api; + +describe("getSuggestedQueries (REQ-67)", () => { + it("derives suggestions from open inbox items only", async () => { + const suggestions = await client.getSuggestedQueries(); + const openItems = taskInboxFixture.filter((i) => i.status !== "done"); + expect(suggestions).toHaveLength(openItems.length); + const openQuestions = new Set(openItems.map((i) => i.question)); + for (const suggestion of suggestions) { + expect(openQuestions).toContain(suggestion.question); + } + }); + + it("attributes each suggestion to its source report", async () => { + const suggestions = await client.getSuggestedQueries(); + expect(suggestions.length).toBeGreaterThan(0); + for (const suggestion of suggestions) { + expect(suggestion.sourceRunId).toBeTruthy(); + expect(suggestion.sourceReportTitle.length).toBeGreaterThan(0); + } + }); +}); + +describe("getRefinementQuestions (REQ-68)", () => { + it("returns the scripted 3-question exchange", async () => { + const questions = await client.getRefinementQuestions("How do batteries work?"); + expect(questions).toHaveLength(3); + const first = questions[0]; + if (first === undefined) throw new Error("no refinement questions"); + expect(first.prompt.length).toBeGreaterThan(0); + expect(first.placeholder.length).toBeGreaterThan(0); + // The scope question is required; the rest are skippable. + expect(questions.filter((q) => !q.optional).map((q) => q.id)).toEqual([ + "scope", + ]); + }); +}); + +describe("composeRefinedQuestion (REQ-68)", () => { + const DRAFT = "What are the latest advances in solid-state batteries"; + + it("weaves the scope into the question and answers into context", async () => { + const refined = await client.composeRefinedQuestion(DRAFT, { + scope: "automotive applications", + decision: "choosing a supplier for a 2027 EV program", + depth: "an executive summary with verifiable evidence", + }); + expect(refined.question).toBe( + "What are the latest advances in solid-state batteries — focusing on automotive applications", + ); + expect(refined.context).toContain("choosing a supplier for a 2027 EV program"); + expect(refined.context).toContain( + "an executive summary with verifiable evidence", + ); + }); + + it("degrades gracefully when optional answers are skipped", async () => { + const refined = await client.composeRefinedQuestion(DRAFT, { + scope: "grid storage", + }); + expect(refined.question).toBe(`${DRAFT} — focusing on grid storage`); + expect(refined.context).not.toContain("Feeds this decision"); + expect(refined.context).not.toContain("Depth wanted"); + }); + + it("keeps the draft intact when no answers are given", async () => { + const refined = await client.composeRefinedQuestion(DRAFT, {}); + expect(refined.question).toBe(DRAFT); + expect(refined.context).toContain("Refined through the question dialogue"); + }); +}); \ No newline at end of file diff --git a/frontend/src/lib/api/mock/ask-fixtures.ts b/frontend/src/lib/api/mock/ask-fixtures.ts new file mode 100644 index 0000000..9394c2e --- /dev/null +++ b/frontend/src/lib/api/mock/ask-fixtures.ts @@ -0,0 +1,100 @@ +/** + * Ask-intelligence fixtures (T-7.1, REQ-67, REQ-68). + * + * Suggested queries derive from OPEN task-inbox items joined with + * their source reports (the unanswered-questions contract). The + * refine dialogue is a scripted 3-question exchange with a + * deterministic composer — the model-driven version is v0.2. + */ + +import type { + RefinementQuestion, + RefinedQuestion, + SuggestedQuery, +} from "@/lib/types"; +import { reportLibraryFixture } from "@/lib/api/mock/shell-fixtures"; +import { taskInboxFixture } from "@/lib/api/mock/library-fixtures"; + +/* ------------------------------------------------------------------ */ +/* Suggested queries (REQ-67) */ +/* ------------------------------------------------------------------ */ + +/** + * Build suggestions from open inbox items: each unanswered question + * becomes a suggested query, attributed to its source report. + */ +export function buildSuggestedQueries(): SuggestedQuery[] { + const titleByRunId = new Map( + reportLibraryFixture.reports.map((r) => [r.runId, r.title] as const), + ); + return taskInboxFixture + .filter((item) => item.status !== "done") + .map((item) => ({ + id: `sug-${item.id}`, + question: item.question, + sourceRunId: item.sourceRunId, + sourceReportTitle: titleByRunId.get(item.sourceRunId) ?? "a past run", + })); +} + +/* ------------------------------------------------------------------ */ +/* Refine dialogue (REQ-68) — scripted in the prototype */ +/* ------------------------------------------------------------------ */ + +/** + * The scripted clarifying questions. Generic enough to apply to any + * research draft: scope, decision, and depth — the three answers a + * good research question bakes in. + */ +export const refinementQuestionsFixture: RefinementQuestion[] = [ + { + id: "scope", + prompt: "What specific part of this topic matters most to you?", + placeholder: "e.g. just automotive applications, not consumer devices", + optional: false, + }, + { + id: "decision", + prompt: "What decision or action will this research inform?", + placeholder: "e.g. choosing a battery supplier for a 2027 EV program", + optional: true, + }, + { + id: "depth", + prompt: "How deep should the answer go — a quick memo or a full report?", + placeholder: "e.g. executive summary with evidence I can verify", + optional: true, + }, +]; + +/** + * Deterministic composer: weave the answers into the draft question + * plus a suggested context block. Answers are optional-terminated so + * a half-answered exchange still composes (skipped questions drop). + */ +export function composeRefinedQuestion( + draft: string, + answers: Record, +): RefinedQuestion { + const scope = answers.scope?.trim(); + const decision = answers.decision?.trim(); + const depth = answers.depth?.trim(); + + // Question: draft + the specific scope (the one non-optional dial). + const question = + scope && scope.length > 0 + ? `${draft.trim()} — focusing on ${scope}` + : draft.trim(); + + // Context: the remaining answers, assembled as guidance lines. + const contextLines: string[] = []; + if (decision && decision.length > 0) { + contextLines.push(`Feeds this decision: ${decision}.`); + } + if (depth && depth.length > 0) { + contextLines.push(`Depth wanted: ${depth}.`); + } + contextLines.push("Refined through the question dialogue."); + + return { question, context: contextLines.join(" ") }; +} \ No newline at end of file diff --git a/frontend/src/lib/api/mock/mock-client.ts b/frontend/src/lib/api/mock/mock-client.ts index a19312a..434057c 100644 --- a/frontend/src/lib/api/mock/mock-client.ts +++ b/frontend/src/lib/api/mock/mock-client.ts @@ -7,6 +7,11 @@ */ import type { ApiClient, CreateRunInput } from "@/lib/api/client"; +import { + buildSuggestedQueries, + composeRefinedQuestion, + refinementQuestionsFixture, +} from "@/lib/api/mock/ask-fixtures"; import { methodologyFixtures, findMethodologyBySlug } from "@/lib/api/mock/fixtures"; import { projectGraphFixture, @@ -31,11 +36,14 @@ import type { InboxItem, Methodology, ProjectGraph, + RefinementQuestion, + RefinedQuestion, Report, ReportConnection, ReportLibrary, ResearchRun, Settings, + SuggestedQuery, TraceEvent, UserProfile, } from "@/lib/types"; @@ -300,6 +308,28 @@ export class MockApiClient implements ApiClient { /** Session-persisted settings (mock-local). */ #settings: Settings = structuredClone(settingsFixture); + /** + * Follow-up suggestions from unanswered open questions (REQ-67). + * Derived live from the inbox fixtures — a completed item drops + * out of suggestions automatically. + */ + async getSuggestedQueries(): Promise { + return structuredClone(buildSuggestedQueries()); + } + + /** The scripted refine-dialogue questions (REQ-68). */ + async getRefinementQuestions(_draft: string): Promise { + return structuredClone(refinementQuestionsFixture); + } + + /** Deterministic refine composer (REQ-68; model-driven in v0.2). */ + async composeRefinedQuestion( + draft: string, + answers: Record, + ): Promise { + return composeRefinedQuestion(draft, answers); + } + /** The scripted run for the prototype's demo question. */ #defaultRun(): ResearchRun { return buildScriptedRun({ diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index a754d38..d631c6d 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -407,3 +407,40 @@ export interface UserProfile { tagline: string; } +/* ------------------------------------------------------------------ */ +/* Ask intelligence (P7 vocabulary — REQ-67, REQ-68) */ +/* ------------------------------------------------------------------ */ + +/** + * A follow-up query suggestion on the New Research view — derived + * from an unanswered open question (task-inbox item) of a past run. + */ +export interface SuggestedQuery { + id: string; + /** The suggested question text — loaded into the question box. */ + question: string; + /** The run the open question came from. */ + sourceRunId: string; + /** Source report title for attribution ("from your research"). */ + sourceReportTitle: string; +} + +/** One clarifying question in the refine dialogue. */ +export interface RefinementQuestion { + id: string; + /** The clarifying question shown to the user. */ + prompt: string; + /** Input hint for the answer box. */ + placeholder: string; + /** True when an answer may be skipped (the composer tolerates it). */ + optional: boolean; +} + +/** The composed result of a completed refine exchange. */ +export interface RefinedQuestion { + /** The refined research question. */ + question: string; + /** Suggested context assembled from the answers. */ + context: string; +} + -- 2.43.0 From e2a5ad5f058a74b2d82087bf3c4be43f4ceeff68 Mon Sep 17 00:00:00 2001 From: coreci Date: Sun, 27 Sep 2026 04:55:13 +0000 Subject: [PATCH 3/4] feat(P07): ask suggestions + refine dialogue on the New Research view (T-7.2..T-7.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---ci--- phase: 7 milestone: v0.1 status: execute persona: frontend-engineer task: T-7.2 requirements: covered: [REQ-67, REQ-68] partial: [] ---/ci--- /ask restructured: AskView (client) owns question+context state; AskForm retired (its tests move to AskView; the 3-click test drives AskView — criterion green, suggestions/refine are opt-in). REQ-67 AskSuggestions: open questions from past runs as clickable cards (question + 'From your research — ' attribution); click fills the question box for editing, never auto-submits; honest empty state ('they collect here as you run research'). REQ-68 RefineQuestionButton + RefineDrawer: disabled until a draft exists; inline drawer (dials-drawer pattern) shows the draft, asks the scripted clarifying questions one at a time (progress n/3, required scope vs skippable decision/depth, Enter submits), then previews the composed refined question + suggested context; Use this fills the form, Restart re-runs, Keep original / Escape leaves the draft untouched; honest 'scripted in the prototype' caption. Tests: 8 AskView suites (submit carry, suggestions render/join, click-fills-box no-auto-submit, refine open/compose/skip/keep/ escape) + 6 boundary; 3-click re-pinned on AskView. Gates: lint 0/0, typecheck, 86/86, build. README documents both affordances. --- frontend/README.md | 7 + frontend/src/app/ask/page.tsx | 12 +- frontend/src/components/ask-form.test.tsx | 82 ------ frontend/src/components/ask-form.tsx | 78 ----- frontend/src/components/ask-suggestions.tsx | 88 ++++++ frontend/src/components/ask-view.test.tsx | 222 ++++++++++++++ frontend/src/components/ask-view.tsx | 96 ++++++ frontend/src/components/refine-question.tsx | 277 ++++++++++++++++++ .../src/components/three-click-path.test.tsx | 6 +- frontend/src/lib/api/mock/mock-client.ts | 6 +- 10 files changed, 706 insertions(+), 168 deletions(-) delete mode 100644 frontend/src/components/ask-form.test.tsx delete mode 100644 frontend/src/components/ask-form.tsx create mode 100644 frontend/src/components/ask-suggestions.tsx create mode 100644 frontend/src/components/ask-view.test.tsx create mode 100644 frontend/src/components/ask-view.tsx create mode 100644 frontend/src/components/refine-question.tsx diff --git a/frontend/README.md b/frontend/README.md index 6b2184c..b0e7ba1 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -35,6 +35,13 @@ The happy path: The shell adds zero required clicks to that path. Everything else it hosts is wayfinding and accumulated research: +- **Open questions** (`/ask`, below the form) — follow-up suggestions + derived from unanswered questions of your past runs; one click loads + one into the question box for editing (never auto-starts). +- **Refine question** (`/ask`, beside the box) — an opt-in back-and-forth: + a few clarifying questions, then a composed refined question with + suggested context. Use it, or keep your original. Scripted in the + prototype — model-driven refine lands with v0.2. - **Library** (`/library`) — every report you've generated, filed into auto-categorized folders; sort by recency or title; open any card to re-enter its Reading Room. The **Connections** panel draws the links diff --git a/frontend/src/app/ask/page.tsx b/frontend/src/app/ask/page.tsx index 87e77e7..3e2a9bb 100644 --- a/frontend/src/app/ask/page.tsx +++ b/frontend/src/app/ask/page.tsx @@ -1,8 +1,10 @@ -import { AskForm } from "@/components/ask-form"; +import { AskView } from "@/components/ask-view"; /** - * /ask (T-3.1, REQ-55) — the front door: one question box, one optional - * context box, one primary action. Generous whitespace, no competition. + * /ask (T-3.1, REQ-55; T-7.2/7.3, REQ-67/68) — the front door: one + * question box, one optional context box, one primary action. The + * opt-in affordances live below and beside the form — open-question + * suggestions and the refine dialogue — never competing with it. * The app shell (sidebar + top bar) lives in the root layout. */ export default function AskPage() { @@ -21,11 +23,11 @@ export default function AskPage() {

- +

Prototype: runs on scripted mock data — no API keys, no live search.

); -} +} \ No newline at end of file diff --git a/frontend/src/components/ask-form.test.tsx b/frontend/src/components/ask-form.test.tsx deleted file mode 100644 index 42aa577..0000000 --- a/frontend/src/components/ask-form.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/** - * AskForm tests (T-3.3, REQ-55). - * - * - Button disabled until the question is non-empty - * - Submit stores the payload in the ask store (state carry) and - * routes to /choose - */ - -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { AskForm } from "@/components/ask-form"; -import { clearAskState, getAskState } from "@/lib/ask-store"; - -const pushMock = vi.hoisted(() => vi.fn()); - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: pushMock }), -})); - -describe("AskForm", () => { - beforeEach(() => { - pushMock.mockClear(); - clearAskState(); - }); - - afterEach(cleanup); - - it("disables the submit button until a question is typed", async () => { - const user = userEvent.setup(); - render(); - - const button = screen.getByRole("button", { name: "Research this" }); - expect(button).toBeDisabled(); - - const question = screen.getByLabelText(/what do you want to know\?/i); - await user.type(question, "What are the latest advances in solid-state batteries?"); - - expect(button).toBeEnabled(); - }); - - it("submits: stores question + context and routes to /choose", async () => { - const user = userEvent.setup(); - render(); - - await user.type( - screen.getByLabelText(/what do you want to know\?/i), - "What are the latest advances in solid-state batteries?", - ); - await user.type( - screen.getByLabelText(/anything else it should know\?/i), - "Focus on automotive applications", - ); - await user.click(screen.getByRole("button", { name: "Research this" })); - - // State carry (UX criterion): the payload survives navigation. - const carried = getAskState(); - expect(carried?.question).toBe( - "What are the latest advances in solid-state batteries?", - ); - expect(carried?.context).toBe("Focus on automotive applications"); - - await waitFor(() => { - expect(pushMock).toHaveBeenCalledWith("/choose"); - }); - }); - - it("submits without storing an empty context", async () => { - const user = userEvent.setup(); - render(); - - await user.type( - screen.getByLabelText(/what do you want to know\?/i), - "Who makes solid-state cells today?", - ); - await user.click(screen.getByRole("button", { name: "Research this" })); - - const carried = getAskState(); - expect(carried?.question).toBe("Who makes solid-state cells today?"); - expect(carried?.context).toBeUndefined(); - }); -}); \ No newline at end of file diff --git a/frontend/src/components/ask-form.tsx b/frontend/src/components/ask-form.tsx deleted file mode 100644 index bf20f44..0000000 --- a/frontend/src/components/ask-form.tsx +++ /dev/null @@ -1,78 +0,0 @@ -"use client"; - -/** - * AskForm (T-3.1, REQ-55) — the front door. - * - * Exactly ONE question textarea + ONE optional context textarea + one - * primary action. Nothing else competes for attention. Submit stores - * the payload in the ask store and routes to /choose. - */ - -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import type { FormEvent } from "react"; -import { setAskState } from "@/lib/ask-store"; - -export function AskForm() { - const router = useRouter(); - const [question, setQuestion] = useState(""); - const [context, setContext] = useState(""); - const [submitting, setSubmitting] = useState(false); - - const canSubmit = question.trim().length > 0 && !submitting; - - function handleSubmit(event: FormEvent) { - event.preventDefault(); - if (!canSubmit) return; - setSubmitting(true); - setAskState({ - question: question.trim(), - ...(context.trim() !== "" ? { context: context.trim() } : {}), - }); - router.push("/choose"); - } - - return ( -
- -