Backend — live runner - New `POST /api/documents/:id/rag` endpoint. Loads `document_json` from SQLite, reconstructs the DoclingDocument, wraps the model id in `ModelIdentifier(ollama_name=...)`, and calls `agent._rag_loop` off-thread (blocking sync call). Returns a `RAGResult` in the shape the existing v1 import path already consumes, so the frontend overlay is fully reused. - `_rag_loop` is private upstream; we call it because `run()` wraps the answer in a synthetic DoclingDocument and drops the iteration trace. - Settings: `RAG_ENABLED`, `OLLAMA_HOST`, `RAG_MODEL_ID`. Router mounts unconditionally; handler 503s when the flag is off or deps aren't installed. `rag_available` surfaced in `/api/health`. - Maps known docling-agent bugs to readable HTTP errors: 502 with "the model couldn't produce a parseable answer" when `_rag_loop` raises `IndexError` from `find_json_dicts([])[0]` after 3 + 3 rejection-sampling retries (model-dependent). - Tests: 11 cases (flag off, query empty, no analysis, happy path, model_id wrap, Ollama env, IndexError → 502, other errors → 500, deps missing → 503). Backend — bug fix - Default `BATCH_PAGE_SIZE` flipped from `10` to `0` to match the dataclass default. The old default silently dropped `document_json` (see `domain/services.merge_results`) for any doc > 10 pages, which broke the reasoning tunnel. Set `BATCH_PAGE_SIZE>0` explicitly on memory-constrained deploys if batching is wanted. Frontend — runner UX - `features/reasoning/api.ts:runReasoning()` — POST wrapper. - `RunReasoningDialog.vue` — query textarea + optional model_id override. Blocks close while running, 20-40s loading state, synthesises a sidecar-shaped envelope so the panel surfaces query + model the same way an imported trace would. - `ReasoningWorkspace.vue` — primary "Run reasoning" button; "Import trace" relegated to ghost secondary. - Store: `runDialogOpen`, `running`, `setRunning`. Frontend — answer polish - Answer rendered through `marked` + DOMPurify (models emit markdown lists; `pre-wrap` rendered them as plain "1. …" strings). - Dedicated answer block with orange border, "ANSWER" label, "Copy" button (clipboard + "Copied ✓" feedback). - IterationCard: drop the duplicate `response` block (the main answer is authoritative); style reasons equal to `"fallback"` (docling-agent `select_from_failure` placeholder) as italic muted "— no structured rationale". Frontend — node details contents - Clicking a SectionHeader (or any node with compound children) lists its contained elements in `NodeDetailsPanel` under a new "Contents" block. Children come from the same `parentMap` used for Cytoscape compound parenting (explicit PARENT_OF + synthetic section scope), inverted once and cached as a computed. - Click a child row → pan the viewport to it + swap the selection. Housekeeping - `cytoscape-navigator` removed from `package-lock.json` (follow-up from the minimap removal in the previous commit).
173 lines
3.8 KiB
Vue
173 lines
3.8 KiB
Vue
<template>
|
|
<button
|
|
type="button"
|
|
class="it-card"
|
|
:class="{ active, missing: !iteration.present, converged: iteration.canAnswer }"
|
|
:data-e2e="`reasoning-iteration-${iteration.iteration}`"
|
|
@click="$emit('focus', iteration.iteration)"
|
|
>
|
|
<div class="it-row">
|
|
<span class="it-badge">{{ iteration.iteration }}</span>
|
|
<span class="it-ref" :title="iteration.sectionRef">{{ iteration.sectionRef }}</span>
|
|
<span
|
|
class="it-status"
|
|
:class="{
|
|
ok: iteration.canAnswer,
|
|
more: !iteration.canAnswer && iteration.present,
|
|
missing: !iteration.present,
|
|
}"
|
|
>
|
|
{{ statusLabel }}
|
|
</span>
|
|
</div>
|
|
<p v-if="iteration.reason" class="it-reason" :class="{ placeholder: isPlaceholderReason }">
|
|
{{ isPlaceholderReason ? t('reasoning.reasonPlaceholder') : iteration.reason }}
|
|
</p>
|
|
<div class="it-meta">
|
|
<span v-if="iteration.sectionTextLength">
|
|
{{ t('reasoning.charsLabel').replace('{n}', String(iteration.sectionTextLength)) }}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
|
|
import { useI18n } from '../../../shared/i18n'
|
|
import type { ResolvedIteration } from '../types'
|
|
|
|
const props = defineProps<{
|
|
iteration: ResolvedIteration
|
|
active: boolean
|
|
}>()
|
|
|
|
defineEmits<{ focus: [iteration: number] }>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const statusLabel = computed(() => {
|
|
if (!props.iteration.present) return t('reasoning.statusMissing')
|
|
if (props.iteration.canAnswer) return t('reasoning.statusAnswered')
|
|
return t('reasoning.statusMore')
|
|
})
|
|
|
|
// docling-agent emits the literal string "fallback" for `reason` when its
|
|
// `select_from_failure` branch runs (the model's structured output didn't
|
|
// parse N times in a row). Don't show that noise — render a dash-style
|
|
// placeholder the user can visually skip.
|
|
const isPlaceholderReason = computed(() => {
|
|
const r = (props.iteration.reason || '').trim().toLowerCase()
|
|
return r === '' || r === 'fallback'
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.it-card {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px 12px;
|
|
cursor: pointer;
|
|
transition: all var(--transition);
|
|
font-family: inherit;
|
|
color: inherit;
|
|
}
|
|
|
|
.it-card:hover {
|
|
border-color: var(--accent);
|
|
background: var(--accent-muted);
|
|
}
|
|
|
|
.it-card.active {
|
|
border-color: #ea580c;
|
|
box-shadow: 0 0 0 2px rgba(234, 88, 12, 0.2);
|
|
background: rgba(234, 88, 12, 0.06);
|
|
}
|
|
|
|
.it-card.missing {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.it-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.it-badge {
|
|
flex: 0 0 auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 50%;
|
|
background: #ea580c;
|
|
color: #ffffff;
|
|
font-weight: 700;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.it-card.missing .it-badge {
|
|
background: var(--text-muted);
|
|
}
|
|
|
|
.it-ref {
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
flex: 1 1 auto;
|
|
}
|
|
|
|
.it-status {
|
|
flex: 0 0 auto;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.it-status.ok {
|
|
background: rgba(22, 163, 74, 0.15);
|
|
color: #15803d;
|
|
}
|
|
|
|
.it-status.more {
|
|
background: rgba(234, 179, 8, 0.15);
|
|
color: #a16207;
|
|
}
|
|
|
|
.it-status.missing {
|
|
background: var(--border-light);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.it-reason {
|
|
margin: 8px 0 0;
|
|
font-size: 12px;
|
|
line-height: 1.45;
|
|
color: var(--text);
|
|
}
|
|
|
|
.it-reason.placeholder {
|
|
color: var(--text-muted);
|
|
font-style: italic;
|
|
}
|
|
|
|
.it-meta {
|
|
margin-top: 6px;
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
}
|
|
</style>
|