peregrine/web/src/stores/wizard/aiInterview.ts
pyr0ball eebfc84a80 fix(wizard): quality review fixes — store encapsulation + skip action + settings CTA
- 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: #77
2026-06-14 12:13:58 -07:00

100 lines
2.8 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useApiFetch } from '../../composables/useApi'
const LS_KEY = 'peregrine:wizard-draft'
const SKIP_SIGNAL = 'skip'
export interface ChatMessage {
role: 'user' | 'assistant'
content: string
}
export const useAiInterviewStore = defineStore('aiInterview', () => {
const messages = ref<ChatMessage[]>([])
const fields = ref<Record<string, unknown>>({})
const complete = ref(false)
const loading = ref(false)
const saving = ref(false)
const error = ref<string | null>(null)
function _persist() {
localStorage.setItem(LS_KEY, JSON.stringify({
messages: messages.value,
fields: fields.value,
complete: complete.value,
}))
}
function restore() {
try {
const raw = localStorage.getItem(LS_KEY)
if (!raw) return
const d = JSON.parse(raw) as { messages?: ChatMessage[]; fields?: Record<string, unknown>; complete?: boolean }
messages.value = d.messages ?? []
fields.value = d.fields ?? {}
complete.value = d.complete ?? false
} catch { /* ignore corrupted draft */ }
}
async function send(userText: string) {
if (loading.value) return
if (userText !== '') {
messages.value = [...messages.value, { role: 'user', content: userText }]
_persist()
}
loading.value = true
error.value = null
const { data, error: err } = await useApiFetch<{
reply: string
extracted_fields: Record<string, unknown>
complete: boolean
}>('/api/wizard/ai/interview', {
method: 'POST',
body: JSON.stringify({ history: messages.value, profile_so_far: fields.value }),
})
loading.value = false
if (err || !data) {
error.value = 'Could not reach the assistant. Please try again.'
return
}
messages.value = [...messages.value, { role: 'assistant', content: data.reply }]
fields.value = { ...fields.value, ...data.extracted_fields }
complete.value = data.complete
_persist()
}
async function finalize(): Promise<boolean> {
saving.value = true
error.value = null
const { error: err } = await useApiFetch('/api/wizard/ai/finalize', {
method: 'POST',
body: JSON.stringify({ profile: fields.value }),
})
saving.value = false
if (err) {
error.value = 'Failed to save profile. Please try again.'
return false
}
localStorage.removeItem(LS_KEY)
return true
}
function skip() {
return send(SKIP_SIGNAL)
}
function keepChatting() {
complete.value = false
}
function startOver() {
messages.value = []
fields.value = {}
complete.value = false
error.value = null
localStorage.removeItem(LS_KEY)
}
return { messages, fields, complete, loading, saving, error, restore, send, skip, finalize, keepChatting, startOver }
})