feat: add training export state and actions to fineTune store

This commit is contained in:
pyr0ball 2026-05-03 00:14:22 -07:00
parent 25473aef77
commit 8e6cc02295
2 changed files with 125 additions and 0 deletions

View file

@ -1,8 +1,11 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia' import { setActivePinia, createPinia } from 'pinia'
import { useFineTuneStore } from './fineTune' import { useFineTuneStore } from './fineTune'
import type { DbPair } from './fineTune'
vi.mock('../../composables/useApi', () => ({ useApiFetch: vi.fn() })) 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' import { useApiFetch } from '../../composables/useApi'
const mockFetch = vi.mocked(useApiFetch) const mockFetch = vi.mocked(useApiFetch)
@ -36,4 +39,47 @@ describe('useFineTuneStore', () => {
expect(mockFetch).toHaveBeenCalledWith('/api/settings/fine-tune/status') expect(mockFetch).toHaveBeenCalledWith('/api/settings/fine-tune/status')
store.stopPolling() 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)
})
}) })

View file

@ -10,6 +10,16 @@ export interface TrainingPair {
source_file: string 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', () => { export const useFineTuneStore = defineStore('settings/fineTune', () => {
const step = ref(1) const step = ref(1)
const inFlightJob = ref(false) const inFlightJob = ref(false)
@ -22,6 +32,11 @@ export const useFineTuneStore = defineStore('settings/fineTune', () => {
const pairsLoading = ref(false) const pairsLoading = ref(false)
let _pollTimer: ReturnType<typeof setInterval> | null = null let _pollTimer: ReturnType<typeof setInterval> | null = null
const optedIn = ref(false)
const dbPairs = ref<DbPair[]>([])
const dbPairsLoading = ref(false)
const dbExcludedCount = ref(0)
function resetStep() { step.value = 1 } function resetStep() { step.value = 1 }
async function loadStatus() { async function loadStatus() {
@ -31,6 +46,7 @@ export const useFineTuneStore = defineStore('settings/fineTune', () => {
pairsCount.value = data.pairs_count ?? 0 pairsCount.value = data.pairs_count ?? 0
quotaRemaining.value = data.quota_remaining ?? null quotaRemaining.value = data.quota_remaining ?? null
inFlightJob.value = ['queued', 'running'].includes(data.status) inFlightJob.value = ['queued', 'running'].includes(data.status)
optedIn.value = (data as any).opted_in ?? false
} }
function startPolling() { 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 { return {
step, step,
inFlightJob, inFlightJob,
@ -85,5 +155,14 @@ export const useFineTuneStore = defineStore('settings/fineTune', () => {
submitJob, submitJob,
loadPairs, loadPairs,
deletePair, deletePair,
optedIn,
dbPairs,
dbPairsLoading,
dbExcludedCount,
toggleOptIn,
loadDbPairs,
excludeDbPair,
includeDbPair,
downloadExport,
} }
}) })