llm-router / apps /web /src /api /slip-queue.ts
ClaraSu's picture
Upload folder using huggingface_hub
c5a5343 verified
Raw History Blame Contribute Delete
4.8 kB
import { apiFetch } from './client'
export type SlipStatus = 'pending' | 'processing' | 'success' | 'needs_review' | 'failed' | 'confirmed' | 'duplicate'
// Mirrors ParsedExpense (apps/api/src/services/expense/ports/index.ts)
// exactly — narrower ParsedExpenseResult (./expenses, the older
// /v1/expense/parse response) doesn't carry `reference`/`template_id`.
export interface SlipParsedExpense {
amount: number
currency: string
date: string | null
merchant: string | null
category: string | null
confidence: number
raw_description: string
reference: string | null
template_id?: string | null
}
export interface SlipQueueItem {
id: string
image_base64: string | null
image_url: string | null
status: SlipStatus
retry_count: number
parsed_result: SlipParsedExpense | null
error_message: string | null
// Best-candidate template match info, present whenever a template match
// was attempted (regardless of whether it cleared the confidence bar) —
// see slip-queue-match-review-ui.
match_confidence: number | null
match_template_id: string | null
created_at: string
processed_at: string | null
}
export interface EnqueueSlipsResult {
succeeded: SlipQueueItem[]
failed: Array<{ error: string }>
}
export async function enqueueSlips(images: string[]): Promise<EnqueueSlipsResult> {
const res = await apiFetch('/v1/slip-queue', {
method: 'POST',
body: JSON.stringify({ images, format: 'json' }),
})
if (!res.ok) {
const err = await res.json() as { error?: string }
throw new Error(err.error ?? 'Failed to enqueue slips')
}
return res.json() as Promise<EnqueueSlipsResult>
}
export interface SlipQueueCursor {
createdAt: string
id: string
}
export interface SlipQueuePage {
items: SlipQueueItem[]
hasMore: boolean
}
export async function listSlipQueuePage(status: SlipStatus, opts: { limit?: number; after?: SlipQueueCursor } = {}): Promise<SlipQueuePage> {
const params = new URLSearchParams({ status, limit: String(opts.limit ?? 30) })
if (opts.after) {
params.set('after_created_at', opts.after.createdAt)
params.set('after_id', opts.after.id)
}
const res = await apiFetch(`/v1/slip-queue?${params.toString()}`)
if (!res.ok) throw new Error('Failed to load slip queue')
return res.json() as Promise<SlipQueuePage>
}
export async function getSlipQueueCounts(): Promise<Record<SlipStatus, number>> {
const res = await apiFetch('/v1/slip-queue/counts')
if (!res.ok) throw new Error('Failed to load slip queue counts')
return res.json() as Promise<Record<SlipStatus, number>>
}
export async function retrySlip(id: string): Promise<SlipQueueItem> {
const res = await apiFetch(`/v1/slip-queue/${id}/retry`, { method: 'POST' })
if (!res.ok) {
const err = await res.json() as { error?: string }
throw new Error(err.error ?? 'Failed to retry slip')
}
return res.json() as Promise<SlipQueueItem>
}
export interface SlipConfirmOverrides {
date?: string
merchant?: string
currency?: string
description?: string
shares?: Array<{ person_id: string; amount: number }>
}
export interface SlipConfirmDetails extends SlipConfirmOverrides {
category_id: string
}
export async function confirmSlip(id: string, categoryId: string, overrides?: SlipConfirmOverrides): Promise<void> {
const res = await apiFetch(`/v1/slip-queue/${id}/confirm`, {
method: 'POST',
body: JSON.stringify({ category_id: categoryId, ...overrides }),
})
if (!res.ok) {
const err = await res.json() as { error?: string }
throw new Error(err.error ?? 'Failed to confirm slip')
}
}
export interface ApplyMatchOverride {
parsed_result: SlipParsedExpense
match_confidence: number
match_template_id: string | null
}
// POST /v1/slip-queue/:id/apply-match — resolves a needs_review slip via one
// of slip-queue-match-review-ui's two option boxes. No `override` reuses the
// slip's own already-persisted match ("use matched template"); an `override`
// carries a freshly-taught template's match result instead ("set up a new
// template"). Either way the slip lands on 'success', same as today's
// confident-auto-match path.
export async function applySlipMatch(id: string, override?: ApplyMatchOverride): Promise<SlipQueueItem> {
const res = await apiFetch(`/v1/slip-queue/${id}/apply-match`, {
method: 'POST',
body: JSON.stringify(override ?? {}),
})
if (!res.ok) {
const err = await res.json() as { error?: string }
throw new Error(err.error ?? 'Failed to apply the template match')
}
return res.json() as Promise<SlipQueueItem>
}
export async function discardSlip(id: string): Promise<void> {
const res = await apiFetch(`/v1/slip-queue/${id}`, { method: 'DELETE' })
if (!res.ok) throw new Error('Failed to discard slip')
}