fix(web): error handling in LibraryView, taskId watch in IngestProgress, type fixes

This commit is contained in:
pyr0ball 2026-05-04 18:02:36 -07:00
parent b4837163d5
commit e401cb5f48
4 changed files with 50 additions and 12 deletions

View file

@ -36,6 +36,11 @@ export interface TaskStatus {
error?: string
}
export interface ChatMessage {
role: string
content: string
}
export const api = {
async getLibrary(): Promise<Document[]> {
const r = await fetch(`${BASE}/api/library`)
@ -72,7 +77,7 @@ export const api = {
},
async chat(
message: string,
history: { role: string; content: string }[],
history: ChatMessage[],
docIds?: string[],
topK = 5,
): Promise<ChatResponse> {

View file

@ -2,7 +2,7 @@
<div class="doc-card" :class="`status-${doc.status}`">
<div class="doc-status-badge">{{ doc.status }}</div>
<div class="doc-title">{{ doc.title }}</div>
<div class="doc-meta" v-if="doc.page_count">{{ doc.page_count }} pages</div>
<div class="doc-meta" v-if="doc.page_count != null">{{ doc.page_count }} pages</div>
<div class="doc-meta path">{{ shortPath }}</div>
<IngestProgress

View file

@ -12,7 +12,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue"
import { computed, onMounted, onUnmounted, ref, watch } from "vue"
import { api, type TaskStatus } from "@/api"
const props = defineProps<{ taskId: string | null }>()
@ -24,13 +24,13 @@ let timer: ReturnType<typeof setInterval> | null = null
const visible = computed(() => props.taskId !== null && status.value?.status !== "complete")
const statusLabel = computed(() => {
if (!status.value) return "Queued..."
if (!status.value) return "Queued"
const map: Record<string, string> = {
running: "Indexing...",
running: "Indexing",
complete: "Done",
error: "Error",
}
return map[status.value.status] ?? "Processing..."
return map[status.value.status] ?? "Processing"
})
const barWidth = computed(() => {
@ -55,12 +55,21 @@ function stopPoll() {
if (timer) { clearInterval(timer); timer = null }
}
onMounted(() => {
function startPoll() {
stopPoll()
status.value = null
if (props.taskId) {
poll()
timer = setInterval(poll, 2000)
}
}
watch(() => props.taskId, (newId) => {
if (newId) startPoll()
else stopPoll()
})
onMounted(startPoll)
onUnmounted(stopPoll)
</script>

View file

@ -7,6 +7,8 @@
</button>
</header>
<p class="error-msg" v-if="error">{{ error }}</p>
<p class="empty-state" v-if="!loading && docs.length === 0">
No books indexed yet. Click "Scan for PDFs" to discover PDFs in your books directory.<br>
Make sure your PDF directory is mounted at <code>/books</code> inside the container.
@ -37,32 +39,53 @@ import DocumentCard from "@/components/DocumentCard.vue"
const docs = ref<Document[]>([])
const loading = ref(true)
const scanning = ref(false)
const error = ref<string | null>(null)
const scanResult = ref<{ discovered: number; queued: number } | null>(null)
async function load() {
loading.value = true
docs.value = await api.getLibrary().finally(() => (loading.value = false))
error.value = null
try {
docs.value = await api.getLibrary()
} catch (e) {
error.value = e instanceof Error ? e.message : "Failed to load library"
} finally {
loading.value = false
}
}
async function scan() {
scanning.value = true
error.value = null
try {
scanResult.value = await api.scanLibrary()
await load()
} catch (e) {
error.value = e instanceof Error ? e.message : "Scan failed"
} finally {
scanning.value = false
}
}
async function reingest(id: string) {
await api.reingestDocument(id)
await load()
error.value = null
try {
await api.reingestDocument(id)
await load()
} catch (e) {
error.value = e instanceof Error ? e.message : "Re-index failed"
}
}
async function remove(id: string) {
if (!confirm("Remove this book from the library? The PDF file is not deleted.")) return
await api.deleteDocument(id)
await load()
error.value = null
try {
await api.deleteDocument(id)
await load()
} catch (e) {
error.value = e instanceof Error ? e.message : "Remove failed"
}
}
onMounted(load)
@ -81,4 +104,5 @@ h1 { font-size: 1.5rem; }
.empty-state { color: var(--color-text-muted); line-height: 1.8; }
.empty-state code { font-family: var(--font-mono); background: var(--color-surface-alt); padding: 2px 6px; border-radius: 3px; }
.scan-result { margin-top: 1rem; color: var(--color-text-muted); font-size: 0.9rem; }
.error-msg { color: var(--color-error); margin-bottom: 1rem; font-size: 0.9rem; }
</style>