diff --git a/web/src/stores/settings/fineTune.test.ts b/web/src/stores/settings/fineTune.test.ts index 838a7e5..871e105 100644 --- a/web/src/stores/settings/fineTune.test.ts +++ b/web/src/stores/settings/fineTune.test.ts @@ -1,8 +1,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { useFineTuneStore } from './fineTune' +import type { DbPair } from './fineTune' vi.mock('../../composables/useApi', () => ({ useApiFetch: vi.fn() })) +vi.mock('../appConfig', () => ({ useAppConfigStore: vi.fn(() => ({ isDemo: false })) })) +vi.mock('../../composables/useToast', () => ({ showToast: vi.fn() })) import { useApiFetch } from '../../composables/useApi' const mockFetch = vi.mocked(useApiFetch) @@ -36,4 +39,47 @@ describe('useFineTuneStore', () => { expect(mockFetch).toHaveBeenCalledWith('/api/settings/fine-tune/status') store.stopPolling() }) + + it('toggleOptIn updates optedIn state', async () => { + mockFetch.mockResolvedValue({ data: { ok: true, enabled: true }, error: null }) + const store = useFineTuneStore() + await store.toggleOptIn(true) + expect(store.optedIn).toBe(true) + }) + + it('loadDbPairs no-ops when not opted in', async () => { + const store = useFineTuneStore() + store.optedIn = false + await store.loadDbPairs() + expect(store.dbPairs).toEqual([]) + expect(mockFetch).not.toHaveBeenCalledWith('/api/settings/fine-tune/db-pairs') + }) + + it('loadDbPairs fetches when opted in', async () => { + const pairs: DbPair[] = [{ job_id: 1, title: 'Eng', company: 'Acme', status: 'applied', instruction: 'Write...', input_preview: 'Build', excluded: false }] + mockFetch.mockResolvedValue({ data: { pairs, total: 1, excluded_count: 0 }, error: null }) + const store = useFineTuneStore() + store.optedIn = true + await store.loadDbPairs() + expect(store.dbPairs).toHaveLength(1) + }) + + it('excludeDbPair marks pair excluded and increments count', async () => { + mockFetch.mockResolvedValue({ data: { ok: true }, error: null }) + const store = useFineTuneStore() + store.dbPairs = [{ job_id: 1, title: 'Eng', company: 'Acme', status: 'applied', instruction: 'Write...', input_preview: 'Build', excluded: false }] + await store.excludeDbPair(1) + expect(store.dbPairs[0].excluded).toBe(true) + expect(store.dbExcludedCount).toBe(1) + }) + + it('includeDbPair marks pair included and decrements excludedCount', async () => { + mockFetch.mockResolvedValue({ data: { ok: true }, error: null }) + const store = useFineTuneStore() + store.dbPairs = [{ job_id: 1, title: 'Eng', company: 'Acme', status: 'applied', instruction: 'Write...', input_preview: 'Build', excluded: true }] + store.dbExcludedCount = 1 + await store.includeDbPair(1) + expect(store.dbPairs[0].excluded).toBe(false) + expect(store.dbExcludedCount).toBe(0) + }) }) diff --git a/web/src/stores/settings/fineTune.ts b/web/src/stores/settings/fineTune.ts index c8df9d9..1120b6a 100644 --- a/web/src/stores/settings/fineTune.ts +++ b/web/src/stores/settings/fineTune.ts @@ -10,6 +10,16 @@ export interface TrainingPair { source_file: string } +export interface DbPair { + job_id: number + title: string + company: string + status: string + instruction: string + input_preview: string + excluded: boolean +} + export const useFineTuneStore = defineStore('settings/fineTune', () => { const step = ref(1) const inFlightJob = ref(false) @@ -22,6 +32,11 @@ export const useFineTuneStore = defineStore('settings/fineTune', () => { const pairsLoading = ref(false) let _pollTimer: ReturnType | null = null + const optedIn = ref(false) + const dbPairs = ref([]) + const dbPairsLoading = ref(false) + const dbExcludedCount = ref(0) + function resetStep() { step.value = 1 } async function loadStatus() { @@ -31,6 +46,7 @@ export const useFineTuneStore = defineStore('settings/fineTune', () => { pairsCount.value = data.pairs_count ?? 0 quotaRemaining.value = data.quota_remaining ?? null inFlightJob.value = ['queued', 'running'].includes(data.status) + optedIn.value = (data as any).opted_in ?? false } function startPolling() { @@ -68,6 +84,60 @@ export const useFineTuneStore = defineStore('settings/fineTune', () => { } } + async function toggleOptIn(enabled: boolean) { + const { data } = await useApiFetch<{ ok: boolean; enabled: boolean }>( + '/api/settings/fine-tune/opt-in', + { method: 'PATCH', body: JSON.stringify({ enabled }), headers: { 'Content-Type': 'application/json' } }, + ) + if (data) optedIn.value = data.enabled + } + + async function loadDbPairs() { + if (!optedIn.value) { dbPairs.value = []; return } + dbPairsLoading.value = true + const { data } = await useApiFetch<{ pairs: DbPair[]; total: number; excluded_count: number }>( + '/api/settings/fine-tune/db-pairs', + ) + dbPairsLoading.value = false + if (data) { + dbPairs.value = data.pairs + dbExcludedCount.value = data.excluded_count + } + } + + async function excludeDbPair(jobId: number) { + const { data } = await useApiFetch<{ ok: boolean }>( + `/api/settings/fine-tune/db-pairs/${jobId}/exclude`, + { method: 'PATCH' }, + ) + if (data?.ok) { + dbPairs.value = dbPairs.value.map(p => + p.job_id === jobId ? { ...p, excluded: true } : p, + ) + dbExcludedCount.value += 1 + } + } + + async function includeDbPair(jobId: number) { + const { data } = await useApiFetch<{ ok: boolean }>( + `/api/settings/fine-tune/db-pairs/${jobId}/include`, + { method: 'PATCH' }, + ) + if (data?.ok) { + dbPairs.value = dbPairs.value.map(p => + p.job_id === jobId ? { ...p, excluded: false } : p, + ) + dbExcludedCount.value = Math.max(0, dbExcludedCount.value - 1) + } + } + + function downloadExport() { + const a = document.createElement('a') + a.href = '/api/settings/fine-tune/export' + a.download = 'peregrine_training_pairs.jsonl' + a.click() + } + return { step, inFlightJob, @@ -85,5 +155,14 @@ export const useFineTuneStore = defineStore('settings/fineTune', () => { submitJob, loadPairs, deletePair, + optedIn, + dbPairs, + dbPairsLoading, + dbExcludedCount, + toggleOptIn, + loadDbPairs, + excludeDbPair, + includeDbPair, + downloadExport, } })