feat: Corrections tab — SFT candidate import, review, and JSONL export #15

Merged
pyr0ball merged 99 commits from feat/sft-corrections into main 2026-04-08 22:19:01 -07:00
2 changed files with 115 additions and 0 deletions
Showing only changes of commit 50dd4c2f45 - Show all commits

View file

@ -0,0 +1,62 @@
// src/stores/label.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { useLabelStore } from './label'
import { beforeEach, describe, it, expect } from 'vitest'
const MOCK_ITEM = {
id: 'abc', subject: 'Test', body: 'Body', from: 'a@b.com',
date: '2026-03-01', source: 'imap:test',
}
describe('label store', () => {
beforeEach(() => setActivePinia(createPinia()))
it('starts with empty queue', () => {
const store = useLabelStore()
expect(store.queue).toEqual([])
expect(store.current).toBeNull()
})
it('current returns first item', () => {
const store = useLabelStore()
store.queue = [MOCK_ITEM]
expect(store.current).toEqual(MOCK_ITEM)
})
it('removeCurrentFromQueue removes first item', () => {
const store = useLabelStore()
store.queue = [MOCK_ITEM, { ...MOCK_ITEM, id: 'def' }]
store.removeCurrentFromQueue()
expect(store.queue[0].id).toBe('def')
})
it('tracks lastAction', () => {
const store = useLabelStore()
store.queue = [MOCK_ITEM]
store.setLastAction('label', MOCK_ITEM, 'interview_scheduled')
expect(store.lastAction?.type).toBe('label')
expect(store.lastAction?.label).toBe('interview_scheduled')
})
it('incrementLabeled increases sessionLabeled', () => {
const store = useLabelStore()
store.incrementLabeled()
store.incrementLabeled()
expect(store.sessionLabeled).toBe(2)
})
it('restoreItem adds to front of queue', () => {
const store = useLabelStore()
store.queue = [{ ...MOCK_ITEM, id: 'def' }]
store.restoreItem(MOCK_ITEM)
expect(store.queue[0].id).toBe('abc')
expect(store.queue[1].id).toBe('def')
})
it('clearLastAction nulls lastAction', () => {
const store = useLabelStore()
store.setLastAction('skip', MOCK_ITEM)
store.clearLastAction()
expect(store.lastAction).toBeNull()
})
})

53
web/src/stores/label.ts Normal file
View file

@ -0,0 +1,53 @@
// src/stores/label.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export interface QueueItem {
id: string
subject: string
body: string
from: string
date: string
source: string
}
export interface LastAction {
type: 'label' | 'skip' | 'discard'
item: QueueItem
label?: string
}
export const useLabelStore = defineStore('label', () => {
const queue = ref<QueueItem[]>([])
const totalRemaining = ref(0)
const lastAction = ref<LastAction | null>(null)
const sessionLabeled = ref(0) // for easter eggs
const current = computed(() => queue.value[0] ?? null)
function removeCurrentFromQueue() {
queue.value.shift()
}
function setLastAction(type: LastAction['type'], item: QueueItem, label?: string) {
lastAction.value = { type, item, label }
}
function clearLastAction() {
lastAction.value = null
}
function restoreItem(item: QueueItem) {
queue.value.unshift(item)
}
function incrementLabeled() {
sessionLabeled.value++
}
return {
queue, totalRemaining, lastAction, sessionLabeled, current,
removeCurrentFromQueue, setLastAction, clearLastAction,
restoreItem, incrementLabeled,
}
})