From eebfc84a80c64aaeb2edc61b25ea12c153421fec Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sun, 14 Jun 2026 12:13:58 -0700 Subject: [PATCH] =?UTF-8?q?fix(wizard):=20quality=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20store=20encapsulation=20+=20skip=20action=20+=20set?= =?UTF-8?q?tings=20CTA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add keepChatting() action to aiInterview store; replace direct store.complete = false mutation in WizardAIView template with store.keepChatting() - Add skip() action wrapping SKIP_SIGNAL constant; replace magic string store.send('skip') with store.skip() - Fix skip button disabled condition to include || store.complete (was always enabled when wizard was complete, allowing spurious skip after finalize) - Add _persist() call after user bubble append in send() so localStorage draft is written before the async fetch — prevents stale draft on browser refresh during slow LLM call - Fix @click="store.startOver" → @click="store.startOver()" (missing parentheses) - Add 2 tests: skip() sends SKIP_SIGNAL, keepChatting() clears complete without reset - Remove 'ultra' from Tier type in appConfig.ts (violates no-ultra-tier policy) - Add MyProfileView wizard callout banner with tier-aware unlock/upgrade CTAs - Add clarifying comment on wizard route guard in router/index.ts Closes: https://git.opensourcesolarpunk.com/Circuit-Forge/peregrine/issues/77 --- web/src/router/index.ts | 2 +- web/src/stores/appConfig.ts | 2 +- .../wizard/__tests__/aiInterview.test.ts | 38 ++++++ web/src/stores/wizard/aiInterview.ts | 14 +- web/src/views/settings/MyProfileView.vue | 124 +++++++++++++++++- web/src/views/wizard/WizardAIView.vue | 8 +- 6 files changed, 179 insertions(+), 9 deletions(-) diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 89576ca..8fd5a76 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -37,7 +37,7 @@ export const router = createRouter({ { path: 'developer', component: () => import('../views/settings/DeveloperView.vue') }, ], }, - // AI profile wizard — standalone full-page chat + // AI profile wizard — post-setup settings entry point (correctly blocked by wizard gate during onboarding) { path: '/wizard/ai-profile', component: () => import('../views/wizard/WizardAIView.vue') }, // Onboarding wizard — full-page layout, no AppNav { diff --git a/web/src/stores/appConfig.ts b/web/src/stores/appConfig.ts index 0ae85bb..542625b 100644 --- a/web/src/stores/appConfig.ts +++ b/web/src/stores/appConfig.ts @@ -2,7 +2,7 @@ import { ref } from 'vue' import { defineStore } from 'pinia' import { useApiFetch } from '../composables/useApi' -export type Tier = 'free' | 'paid' | 'premium' | 'ultra' +export type Tier = 'free' | 'paid' | 'premium' export type InferenceProfile = 'remote' | 'cpu' | 'single-gpu' | 'dual-gpu' export const useAppConfigStore = defineStore('appConfig', () => { diff --git a/web/src/stores/wizard/__tests__/aiInterview.test.ts b/web/src/stores/wizard/__tests__/aiInterview.test.ts index ae582f4..70278f9 100644 --- a/web/src/stores/wizard/__tests__/aiInterview.test.ts +++ b/web/src/stores/wizard/__tests__/aiInterview.test.ts @@ -157,6 +157,44 @@ describe('useAiInterviewStore', () => { expect(store.error).toBe('Failed to save profile. Please try again.') }) + // ── skip() ───────────────────────────────────────────────────────────────── + + it('skip() sends the skip signal to the backend', async () => { + mockFetch.mockResolvedValue({ + data: { reply: 'No problem, moving on.', extracted_fields: {}, complete: false }, + error: null, + }) + + const store = useAiInterviewStore() + await store.skip() + + expect(mockFetch).toHaveBeenCalledWith( + '/api/wizard/ai/interview', + expect.objectContaining({ method: 'POST' }), + ) + const body = JSON.parse((mockFetch.mock.calls[0][1] as { body: string }).body) + expect(body.history[0]).toEqual({ role: 'user', content: 'skip' }) + }) + + // ── keepChatting() ───────────────────────────────────────────────────────── + + it('keepChatting() clears the complete flag without resetting messages', async () => { + mockFetch.mockResolvedValue({ + data: { reply: 'All done!', extracted_fields: { name: 'Alice' }, complete: true }, + error: null, + }) + + const store = useAiInterviewStore() + await store.send('done') + expect(store.complete).toBe(true) + + store.keepChatting() + + expect(store.complete).toBe(false) + expect(store.messages.length).toBeGreaterThan(0) + expect(store.fields).toEqual({ name: 'Alice' }) + }) + // ── startOver() ──────────────────────────────────────────────────────────── it('startOver() resets all state and clears localStorage', async () => { diff --git a/web/src/stores/wizard/aiInterview.ts b/web/src/stores/wizard/aiInterview.ts index a66fa59..220c226 100644 --- a/web/src/stores/wizard/aiInterview.ts +++ b/web/src/stores/wizard/aiInterview.ts @@ -2,7 +2,8 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { useApiFetch } from '../../composables/useApi' -const LS_KEY = 'peregrine:wizard-draft' +const LS_KEY = 'peregrine:wizard-draft' +const SKIP_SIGNAL = 'skip' export interface ChatMessage { role: 'user' | 'assistant' @@ -40,6 +41,7 @@ export const useAiInterviewStore = defineStore('aiInterview', () => { if (loading.value) return if (userText !== '') { messages.value = [...messages.value, { role: 'user', content: userText }] + _persist() } loading.value = true error.value = null @@ -78,6 +80,14 @@ export const useAiInterviewStore = defineStore('aiInterview', () => { return true } + function skip() { + return send(SKIP_SIGNAL) + } + + function keepChatting() { + complete.value = false + } + function startOver() { messages.value = [] fields.value = {} @@ -86,5 +96,5 @@ export const useAiInterviewStore = defineStore('aiInterview', () => { localStorage.removeItem(LS_KEY) } - return { messages, fields, complete, loading, saving, error, restore, send, finalize, startOver } + return { messages, fields, complete, loading, saving, error, restore, send, skip, finalize, keepChatting, startOver } }) diff --git a/web/src/views/settings/MyProfileView.vue b/web/src/views/settings/MyProfileView.vue index eb238b9..e15727b 100644 --- a/web/src/views/settings/MyProfileView.vue +++ b/web/src/views/settings/MyProfileView.vue @@ -5,6 +5,26 @@

Your identity and preferences used for cover letters, research, and interview prep.

+ +
+
+ +
+

Set up your profile with AI

+

+ + +

+
+
+ + Start AI setup + + + Upgrade + +
+
Loading profile…