mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(booking-import): retry a failed import with AI parsing
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Loader2, CheckCircle2, AlertCircle, X } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { addListener, removeListener } from '../../api/websocket'
|
||||
import { reservationsApi } from '../../api/client'
|
||||
import { reservationsApi, healthApi } from '../../api/client'
|
||||
import { useBackgroundTasksStore, type BackgroundImportTask } from '../../store/backgroundTasksStore'
|
||||
|
||||
/**
|
||||
@@ -23,6 +23,25 @@ export default function BackgroundTasksWidget() {
|
||||
const setError = useBackgroundTasksStore((s) => s.setError)
|
||||
const requestReview = useBackgroundTasksStore((s) => s.requestReview)
|
||||
const dismiss = useBackgroundTasksStore((s) => s.dismiss)
|
||||
const addTask = useBackgroundTasksStore((s) => s.addTask)
|
||||
|
||||
const [aiParsing, setAiParsing] = useState(false)
|
||||
useEffect(() => {
|
||||
healthApi.features().then((f) => setAiParsing(!!f.aiParsing)).catch(() => setAiParsing(false))
|
||||
}, [])
|
||||
|
||||
// Re-runs the same files with force-ai: the LLM sees every file, kitinerary is skipped.
|
||||
const retryWithAi = (task: BackgroundImportTask) => {
|
||||
if (!task.sourceFiles || task.sourceFiles.length === 0) return
|
||||
reservationsApi
|
||||
.importBookingAsync(task.tripId, task.sourceFiles, 'force-ai')
|
||||
.then(({ jobId }) => {
|
||||
dismiss(task.id)
|
||||
addTask({ id: jobId, tripId: task.tripId, label: task.label, total: task.sourceFiles!.length, files: task.sourceFiles, mode: 'force-ai' })
|
||||
})
|
||||
// 409 when the addon is enabled but this user has no model configured.
|
||||
.catch((err) => setError(task.id, task.tripId, err?.response?.data?.error ?? t('reservations.import.error')))
|
||||
}
|
||||
|
||||
// On (re)load, reconcile tasks restored from localStorage with the server: a parse
|
||||
// that was still running when the page reloaded must keep its widget, so re-fetch each
|
||||
@@ -136,12 +155,23 @@ export default function BackgroundTasksWidget() {
|
||||
{t('common.import')}
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', marginTop: 1 }}>
|
||||
{t('reservations.import.previewEmpty')}
|
||||
{(task.warnings?.length ?? 0) > 0 && (
|
||||
<div style={{ color: '#b45309', marginTop: 3, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 96, overflowY: 'auto' }}>
|
||||
{task.warnings!.join('\n')}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', marginTop: 1 }}>
|
||||
{t('reservations.import.previewEmpty')}
|
||||
{(task.warnings?.length ?? 0) > 0 && (
|
||||
<div style={{ color: '#b45309', marginTop: 3, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 96, overflowY: 'auto' }}>
|
||||
{task.warnings!.join('\n')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{aiParsing && task.mode !== 'force-ai' && task.sourceFiles && task.sourceFiles.length > 0 && (
|
||||
<button
|
||||
onClick={() => retryWithAi(task)}
|
||||
className="bg-surface-tertiary text-content"
|
||||
style={{ marginTop: 4, border: 'none', borderRadius: 8, padding: '4px 12px', fontSize: 'calc(11.5px * var(--fs-scale-caption, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}
|
||||
>
|
||||
{t('reservations.import.tryAi')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function BookingImportModal({ isOpen, onClose, tripId }: BookingI
|
||||
// Keep the uploaded files so the review can attach each source document to its booking —
|
||||
// in memory for the immediate path, and in IndexedDB so it survives a reload mid-parse.
|
||||
await saveImportFiles(jobId, files)
|
||||
addTask({ id: jobId, tripId: String(tripId), label: files.map((f) => f.name).join(', '), total: files.length, files })
|
||||
addTask({ id: jobId, tripId: String(tripId), label: files.map((f) => f.name).join(', '), total: files.length, files, mode })
|
||||
handleClose()
|
||||
} catch (err: any) {
|
||||
setError(err?.response?.data?.error ?? t('reservations.import.error'))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import type { BookingImportPreviewItem } from '@trek/shared'
|
||||
import type { BookingImportPreviewItem, BookingImportMode } from '@trek/shared'
|
||||
|
||||
/**
|
||||
* Tracks booking-import parses that run in the BACKGROUND (the async endpoint).
|
||||
@@ -30,11 +30,13 @@ export interface BackgroundImportTask {
|
||||
/** The uploaded files this parse ran on — kept in memory so the review can attach the
|
||||
* source document to each created booking. Not persisted (a File can't survive a reload). */
|
||||
sourceFiles?: File[]
|
||||
/** Mode this run used — the widget only offers an AI retry if it wasn't already one. Not persisted. */
|
||||
mode?: BookingImportMode
|
||||
}
|
||||
|
||||
interface BackgroundTasksState {
|
||||
tasks: BackgroundImportTask[]
|
||||
addTask: (task: { id: string; tripId: string; label: string; total: number; files?: File[] }) => void
|
||||
addTask: (task: { id: string; tripId: string; label: string; total: number; files?: File[]; mode?: BookingImportMode }) => void
|
||||
setProgress: (id: string, tripId: string, done: number, total: number) => void
|
||||
setDone: (id: string, tripId: string, items: BookingImportPreviewItem[], warnings: string[]) => void
|
||||
setError: (id: string, tripId: string, error: string) => void
|
||||
@@ -61,7 +63,7 @@ export const useBackgroundTasksStore = create<BackgroundTasksState>()(
|
||||
|
||||
return {
|
||||
tasks: [],
|
||||
addTask: ({ id, tripId, label, total, files }) => upsert(id, tripId, { label, total, status: 'running', done: 0, sourceFiles: files }),
|
||||
addTask: ({ id, tripId, label, total, files, mode }) => upsert(id, tripId, { label, total, status: 'running', done: 0, sourceFiles: files, mode }),
|
||||
setProgress: (id, tripId, done, total) => upsert(id, tripId, { done, total, status: 'running' }),
|
||||
setDone: (id, tripId, items, warnings) => upsert(id, tripId, { status: 'done', items, warnings, done: items?.length ?? 0 }),
|
||||
setError: (id, tripId, error) => upsert(id, tripId, { status: 'error', error }),
|
||||
|
||||
Reference in New Issue
Block a user