Format all frontend files with Prettier
Batch reformat to eliminate 23 files with inconsistent formatting that were accumulating across feature branches.
This commit is contained in:
parent
1c82a80389
commit
226ae5994e
23 changed files with 741 additions and 288 deletions
|
|
@ -1,9 +1,17 @@
|
|||
<template>
|
||||
<div class="app-layout">
|
||||
<header class="topbar">
|
||||
<button class="burger-btn" @click="sidebarOpen = !sidebarOpen" :title="sidebarOpen ? t('nav.collapse') : t('nav.expand')">
|
||||
<button
|
||||
class="burger-btn"
|
||||
@click="sidebarOpen = !sidebarOpen"
|
||||
:title="sidebarOpen ? t('nav.collapse') : t('nav.expand')"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="burger-icon">
|
||||
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="topbar-logo">
|
||||
|
|
@ -12,7 +20,11 @@
|
|||
</div>
|
||||
<div class="topbar-spacer" />
|
||||
<button class="new-analysis-btn" @click="newAnalysis">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="new-analysis-icon"><path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="new-analysis-icon">
|
||||
<path
|
||||
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('topbar.newAnalysis') }}
|
||||
</button>
|
||||
</header>
|
||||
|
|
@ -52,26 +64,26 @@ function newAnalysis() {
|
|||
|
||||
:root {
|
||||
/* Dark mode palette - MistralAI Studio inspired */
|
||||
--bg: #0A0A0B;
|
||||
--bg: #0a0a0b;
|
||||
--bg-surface: #111113;
|
||||
--bg-elevated: #1A1A1D;
|
||||
--bg-elevated: #1a1a1d;
|
||||
--bg-hover: #222226;
|
||||
|
||||
--accent: #F97316;
|
||||
--accent-hover: #FB923C;
|
||||
--accent: #f97316;
|
||||
--accent-hover: #fb923c;
|
||||
--accent-muted: rgba(249, 115, 22, 0.15);
|
||||
|
||||
--text: #ECECEF;
|
||||
--text-secondary: #A1A1AA;
|
||||
--text-muted: #63636E;
|
||||
--text: #ececef;
|
||||
--text-secondary: #a1a1aa;
|
||||
--text-muted: #63636e;
|
||||
|
||||
--border: #27272A;
|
||||
--border-light: #3F3F46;
|
||||
--border: #27272a;
|
||||
--border-light: #3f3f46;
|
||||
|
||||
--success: #22C55E;
|
||||
--error: #EF4444;
|
||||
--warning: #EAB308;
|
||||
--info: #3B82F6;
|
||||
--success: #22c55e;
|
||||
--error: #ef4444;
|
||||
--warning: #eab308;
|
||||
--info: #3b82f6;
|
||||
|
||||
--radius: 8px;
|
||||
--radius-sm: 6px;
|
||||
|
|
@ -83,32 +95,40 @@ function newAnalysis() {
|
|||
}
|
||||
|
||||
html.light {
|
||||
--bg: #FAFAFA;
|
||||
--bg-surface: #FFFFFF;
|
||||
--bg-elevated: #F4F4F5;
|
||||
--bg-hover: #E4E4E7;
|
||||
--bg: #fafafa;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-elevated: #f4f4f5;
|
||||
--bg-hover: #e4e4e7;
|
||||
|
||||
--accent: #F97316;
|
||||
--accent-hover: #EA580C;
|
||||
--accent-muted: rgba(249, 115, 22, 0.10);
|
||||
--accent: #f97316;
|
||||
--accent-hover: #ea580c;
|
||||
--accent-muted: rgba(249, 115, 22, 0.1);
|
||||
|
||||
--text: #18181B;
|
||||
--text-secondary: #52525B;
|
||||
--text-muted: #A1A1AA;
|
||||
--text: #18181b;
|
||||
--text-secondary: #52525b;
|
||||
--text-muted: #a1a1aa;
|
||||
|
||||
--border: #E4E4E7;
|
||||
--border-light: #D4D4D8;
|
||||
--border: #e4e4e7;
|
||||
--border-light: #d4d4d8;
|
||||
|
||||
--success: #16A34A;
|
||||
--error: #DC2626;
|
||||
--warning: #CA8A04;
|
||||
--info: #2563EB;
|
||||
--success: #16a34a;
|
||||
--error: #dc2626;
|
||||
--warning: #ca8a04;
|
||||
--info: #2563eb;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
|
|
|
|||
|
|
@ -19,10 +19,7 @@ export function createAnalysis(
|
|||
})
|
||||
}
|
||||
|
||||
export function rechunkAnalysis(
|
||||
jobId: string,
|
||||
chunkingOptions: ChunkingOptions,
|
||||
): Promise<Chunk[]> {
|
||||
export function rechunkAnalysis(jobId: string, chunkingOptions: ChunkingOptions): Promise<Chunk[]> {
|
||||
return apiFetch<Chunk[]>(`/api/analyses/${jobId}/rechunk`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ chunkingOptions }),
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ describe('computeScale', () => {
|
|||
})
|
||||
|
||||
it('sx equals sy when aspect ratio is preserved', () => {
|
||||
const pageW = 612, pageH = 792
|
||||
const pageW = 612,
|
||||
pageH = 792
|
||||
const displayW = 700
|
||||
const displayH = displayW * pageH / pageW
|
||||
const displayH = (displayW * pageH) / pageW
|
||||
const s = computeScale(displayW, displayH, pageW, pageH)
|
||||
expect(s.sx).toBeCloseTo(s.sy, 5)
|
||||
})
|
||||
|
|
@ -165,17 +166,17 @@ describe('pointInRect', () => {
|
|||
})
|
||||
|
||||
it('returns true for point on edge', () => {
|
||||
expect(pointInRect(10, 20, rect)).toBe(true) // top-left
|
||||
expect(pointInRect(110, 80, rect)).toBe(true) // bottom-right
|
||||
expect(pointInRect(10, 80, rect)).toBe(true) // bottom-left
|
||||
expect(pointInRect(110, 20, rect)).toBe(true) // top-right
|
||||
expect(pointInRect(10, 20, rect)).toBe(true) // top-left
|
||||
expect(pointInRect(110, 80, rect)).toBe(true) // bottom-right
|
||||
expect(pointInRect(10, 80, rect)).toBe(true) // bottom-left
|
||||
expect(pointInRect(110, 20, rect)).toBe(true) // top-right
|
||||
})
|
||||
|
||||
it('returns false for point outside', () => {
|
||||
expect(pointInRect(5, 50, rect)).toBe(false) // left
|
||||
expect(pointInRect(50, 15, rect)).toBe(false) // above
|
||||
expect(pointInRect(115, 50, rect)).toBe(false) // right
|
||||
expect(pointInRect(50, 85, rect)).toBe(false) // below
|
||||
expect(pointInRect(5, 50, rect)).toBe(false) // left
|
||||
expect(pointInRect(50, 15, rect)).toBe(false) // above
|
||||
expect(pointInRect(115, 50, rect)).toBe(false) // right
|
||||
expect(pointInRect(50, 85, rect)).toBe(false) // below
|
||||
})
|
||||
|
||||
it('returns false for EMPTY_RECT (degenerate bbox not hoverable)', () => {
|
||||
|
|
@ -203,8 +204,10 @@ describe('pointInRect', () => {
|
|||
describe('integration: bbox pipeline', () => {
|
||||
it('full A4 page: element is clickable at correct position', () => {
|
||||
// Simulate: A4 page rendered at 700px wide
|
||||
const pageW = 595.28, pageH = 841.89
|
||||
const displayW = 700, displayH = 700 * pageH / pageW
|
||||
const pageW = 595.28,
|
||||
pageH = 841.89
|
||||
const displayW = 700,
|
||||
displayH = (700 * pageH) / pageW
|
||||
|
||||
const scale = computeScale(displayW, displayH, pageW, pageH)
|
||||
// Element at center of page in PDF points
|
||||
|
|
@ -212,8 +215,8 @@ describe('integration: bbox pipeline', () => {
|
|||
const rect = bboxToRect(bbox, scale)
|
||||
|
||||
// Center of the element in display pixels
|
||||
const cx = (200 + 400) / 2 * scale.sx
|
||||
const cy = (350 + 500) / 2 * scale.sy
|
||||
const cx = ((200 + 400) / 2) * scale.sx
|
||||
const cy = ((350 + 500) / 2) * scale.sy
|
||||
|
||||
expect(pointInRect(cx, cy, rect)).toBe(true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import * as api from './api'
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('createAnalysis — pipeline options body construction', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset the real module to get the unmocked version
|
||||
|
|
@ -107,7 +106,6 @@ describe('createAnalysis — pipeline options body construction', () => {
|
|||
})
|
||||
})
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Store → API integration — pipeline options forwarding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@
|
|||
</div>
|
||||
|
||||
<div class="panel-actions" v-if="selectedDoc">
|
||||
<button
|
||||
class="btn-analyze"
|
||||
:disabled="analysisStore.running"
|
||||
@click="runAnalysis"
|
||||
>
|
||||
<button class="btn-analyze" :disabled="analysisStore.running" @click="runAnalysis">
|
||||
<div v-if="analysisStore.running" class="spinner" />
|
||||
<svg v-else viewBox="0 0 20 20" fill="currentColor" class="btn-icon">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{{ analysisStore.running ? 'Analyzing...' : 'Analyze' }}
|
||||
</button>
|
||||
|
|
@ -55,7 +55,7 @@ const analysisStore = useAnalysisStore()
|
|||
const currentPage = ref(1)
|
||||
|
||||
const selectedDoc = computed(() => {
|
||||
return documentStore.documents.find(d => d.id === documentStore.selectedId)
|
||||
return documentStore.documents.find((d) => d.id === documentStore.selectedId)
|
||||
})
|
||||
|
||||
const statusClass = computed(() => {
|
||||
|
|
@ -64,7 +64,7 @@ const statusClass = computed(() => {
|
|||
'status-pending': status === 'PENDING',
|
||||
'status-running': status === 'RUNNING',
|
||||
'status-completed': status === 'COMPLETED',
|
||||
'status-failed': status === 'FAILED'
|
||||
'status-failed': status === 'FAILED',
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -115,20 +115,32 @@ async function runAnalysis() {
|
|||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.btn-analyze:hover:not(:disabled) { background: var(--accent-hover); }
|
||||
.btn-analyze:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.btn-icon { width: 18px; height: 18px; }
|
||||
.btn-analyze:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
.btn-analyze:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
|
|
@ -143,8 +155,20 @@ async function runAnalysis() {
|
|||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.status-pending { background: rgba(234, 179, 8, 0.15); color: var(--warning); }
|
||||
.status-running { background: rgba(59, 130, 246, 0.15); color: var(--info); }
|
||||
.status-completed { background: rgba(34, 197, 94, 0.15); color: var(--success); }
|
||||
.status-failed { background: rgba(239, 68, 68, 0.15); color: var(--error); }
|
||||
.status-pending {
|
||||
background: rgba(234, 179, 8, 0.15);
|
||||
color: var(--warning);
|
||||
}
|
||||
.status-running {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: var(--info);
|
||||
}
|
||||
.status-completed {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: var(--success);
|
||||
}
|
||||
.status-failed {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -20,14 +20,16 @@
|
|||
ref="canvasRef"
|
||||
class="overlay-canvas"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseleave="hoveredElement = null; emit('highlight-element', -1)"
|
||||
@mouseleave="
|
||||
hoveredElement = null
|
||||
emit('highlight-element', -1)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="hoveredElement"
|
||||
class="tooltip"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
<span class="tooltip-type" :style="{ color: ELEMENT_COLORS[hoveredElement.type] || ELEMENT_COLORS.text }">
|
||||
<div v-if="hoveredElement" class="tooltip" :style="tooltipStyle">
|
||||
<span
|
||||
class="tooltip-type"
|
||||
:style="{ color: ELEMENT_COLORS[hoveredElement.type] || ELEMENT_COLORS.text }"
|
||||
>
|
||||
{{ hoveredElement.type }}
|
||||
</span>
|
||||
<span class="tooltip-content">{{ hoveredElement.content?.substring(0, 150) }}</span>
|
||||
|
|
@ -49,7 +51,7 @@ const ELEMENT_COLORS: Record<string, string> = {
|
|||
list: '#06B6D4',
|
||||
formula: '#EC4899',
|
||||
code: '#14B8A6',
|
||||
caption: '#EAB308'
|
||||
caption: '#EAB308',
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
@ -116,7 +118,12 @@ function draw(): void {
|
|||
|
||||
if (!props.pageData) return
|
||||
|
||||
const scale = computeScale(img.clientWidth, img.clientHeight, props.pageData.width, props.pageData.height)
|
||||
const scale = computeScale(
|
||||
img.clientWidth,
|
||||
img.clientHeight,
|
||||
props.pageData.width,
|
||||
props.pageData.height,
|
||||
)
|
||||
|
||||
const hasHighlight = props.highlightedIndex >= 0
|
||||
const hasChunkHighlight = props.highlightedBboxes.length > 0
|
||||
|
|
@ -160,7 +167,12 @@ function onMouseMove(e: MouseEvent): void {
|
|||
const mx = e.clientX - canvasRect.left
|
||||
const my = e.clientY - canvasRect.top
|
||||
|
||||
const scale = computeScale(img.clientWidth, img.clientHeight, props.pageData.width, props.pageData.height)
|
||||
const scale = computeScale(
|
||||
img.clientWidth,
|
||||
img.clientHeight,
|
||||
props.pageData.width,
|
||||
props.pageData.height,
|
||||
)
|
||||
|
||||
let found: PageElement | null = null
|
||||
let foundIdx = -1
|
||||
|
|
@ -177,7 +189,7 @@ function onMouseMove(e: MouseEvent): void {
|
|||
if (found) {
|
||||
tooltipStyle.value = {
|
||||
left: `${Math.min(mx + 12, canvas.width - 250)}px`,
|
||||
top: `${my + 12}px`
|
||||
top: `${my + 12}px`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,9 +206,18 @@ onBeforeUnmount(() => {
|
|||
})
|
||||
|
||||
// Redraw when data or image changes
|
||||
watch([() => props.pageData, () => props.imageEl, () => props.highlightedIndex, () => props.highlightedBboxes, hiddenTypes], () => {
|
||||
nextTick(draw)
|
||||
})
|
||||
watch(
|
||||
[
|
||||
() => props.pageData,
|
||||
() => props.imageEl,
|
||||
() => props.highlightedIndex,
|
||||
() => props.highlightedBboxes,
|
||||
hiddenTypes,
|
||||
],
|
||||
() => {
|
||||
nextTick(draw)
|
||||
},
|
||||
)
|
||||
|
||||
// Expose draw so parent can call it after image load
|
||||
defineExpose({ draw })
|
||||
|
|
@ -240,8 +261,13 @@ defineExpose({ draw })
|
|||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.legend-chip:hover { opacity: 1; background: var(--bg-hover); }
|
||||
.legend-chip.dimmed { opacity: 0.35; }
|
||||
.legend-chip:hover {
|
||||
opacity: 1;
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.legend-chip.dimmed {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 6px;
|
||||
|
|
@ -273,7 +299,7 @@ defineExpose({ draw })
|
|||
max-width: 250px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tooltip-type {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@
|
|||
{{ t('results.noImages') }}
|
||||
</div>
|
||||
<div v-else class="gallery-grid">
|
||||
<div
|
||||
v-for="(img, idx) in images"
|
||||
:key="idx"
|
||||
class="gallery-card"
|
||||
>
|
||||
<div v-for="(img, idx) in images" :key="idx" class="gallery-card">
|
||||
<div class="card-header">
|
||||
<span class="card-type">Picture</span>
|
||||
<span class="card-page">{{ t('results.page') }} {{ img.page }}</span>
|
||||
|
|
@ -18,7 +14,7 @@
|
|||
</div>
|
||||
<div class="card-bbox">
|
||||
<span class="bbox-label">bbox:</span>
|
||||
<span class="bbox-value">{{ img.bbox.map(v => Math.round(v)).join(', ') }}</span>
|
||||
<span class="bbox-value">{{ img.bbox.map((v) => Math.round(v)).join(', ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -33,13 +29,13 @@ import type { Page, PageElement } from '../../../shared/types'
|
|||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
pages: { type: Array as () => Page[], default: () => [] }
|
||||
pages: { type: Array as () => Page[], default: () => [] },
|
||||
})
|
||||
|
||||
const images = computed(() => {
|
||||
const result: (PageElement & { page: number })[] = []
|
||||
for (const page of props.pages) {
|
||||
for (const el of (page.elements || [])) {
|
||||
for (const el of page.elements || []) {
|
||||
if (el.type === 'picture') {
|
||||
result.push({ ...el, page: page.page_number })
|
||||
}
|
||||
|
|
@ -109,5 +105,7 @@ const images = computed(() => {
|
|||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.bbox-label { margin-right: 4px; }
|
||||
.bbox-label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -33,11 +33,19 @@ const rendered = computed(() => {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-viewer :deep(h1) { font-size: 24px; }
|
||||
.markdown-viewer :deep(h2) { font-size: 20px; }
|
||||
.markdown-viewer :deep(h3) { font-size: 16px; }
|
||||
.markdown-viewer :deep(h1) {
|
||||
font-size: 24px;
|
||||
}
|
||||
.markdown-viewer :deep(h2) {
|
||||
font-size: 20px;
|
||||
}
|
||||
.markdown-viewer :deep(h3) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown-viewer :deep(p) { margin: 8px 0; }
|
||||
.markdown-viewer :deep(p) {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.markdown-viewer :deep(code) {
|
||||
background: var(--bg-elevated);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
|
||||
<!-- Page chip -->
|
||||
<div class="page-indicator" v-if="totalPages > 0">
|
||||
<span class="page-chip">{{ t('results.pageOf', { current: currentPage, total: totalPages }) }}</span>
|
||||
<span class="page-chip">{{
|
||||
t('results.pageOf', { current: currentPage, total: totalPages })
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
|
|
@ -32,7 +34,10 @@
|
|||
@mouseleave="$emit('highlight-element', -1)"
|
||||
>
|
||||
<div class="element-header">
|
||||
<span class="element-type" :style="{ color: ELEMENT_COLORS[el.type] || ELEMENT_COLORS.text }">
|
||||
<span
|
||||
class="element-type"
|
||||
:style="{ color: ELEMENT_COLORS[el.type] || ELEMENT_COLORS.text }"
|
||||
>
|
||||
{{ el.type }}
|
||||
</span>
|
||||
<span class="element-level" v-if="el.level">L{{ el.level }}</span>
|
||||
|
|
@ -42,12 +47,23 @@
|
|||
:title="t('results.copy')"
|
||||
@click.stop="copyElement(idx, el.content)"
|
||||
>
|
||||
<svg v-if="!copiedElements[idx]" viewBox="0 0 20 20" fill="currentColor" class="copy-icon">
|
||||
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z"/>
|
||||
<path d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z"/>
|
||||
<svg
|
||||
v-if="!copiedElements[idx]"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="copy-icon"
|
||||
>
|
||||
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z" />
|
||||
<path
|
||||
d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z"
|
||||
/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 20 20" fill="currentColor" class="copy-icon copied">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -57,7 +73,7 @@
|
|||
<span v-else>{{ el.content }}</span>
|
||||
</div>
|
||||
<div class="element-bbox">
|
||||
{{ el.bbox.map(v => Math.round(v)).join(', ') }}
|
||||
{{ el.bbox.map((v) => Math.round(v)).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -66,11 +82,17 @@
|
|||
<div v-else-if="activeTab === 'markdown'" class="raw-markdown">
|
||||
<button class="copy-btn copy-btn-block" :title="t('results.copy')" @click="copyMarkdown">
|
||||
<svg v-if="!copiedMarkdown" viewBox="0 0 20 20" fill="currentColor" class="copy-icon">
|
||||
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z"/>
|
||||
<path d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z"/>
|
||||
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z" />
|
||||
<path
|
||||
d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z"
|
||||
/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 20 20" fill="currentColor" class="copy-icon copied">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<pre class="raw-content">{{ pageMarkdown }}</pre>
|
||||
|
|
@ -85,12 +107,26 @@
|
|||
<span>{{ t('studio.analysisRunning') }}</span>
|
||||
</div>
|
||||
<div v-else-if="store.currentAnalysis?.status === 'FAILED'" class="result-placeholder error">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="error-icon"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="error-icon">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ store.currentAnalysis.errorMessage || t('results.analysisFailed') }}</span>
|
||||
</div>
|
||||
<div v-else class="result-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="empty-icon">
|
||||
<path d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9zm3.75 11.625a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="empty-icon"
|
||||
>
|
||||
<path
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9zm3.75 11.625a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ t('results.runAnalysis') }}</span>
|
||||
</div>
|
||||
|
|
@ -113,12 +149,12 @@ const ELEMENT_COLORS: Record<string, string> = {
|
|||
list: '#06B6D4',
|
||||
formula: '#EC4899',
|
||||
code: '#14B8A6',
|
||||
caption: '#EAB308'
|
||||
caption: '#EAB308',
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
currentPage: { type: Number, default: 1 },
|
||||
highlightedIndex: { type: Number, default: -1 }
|
||||
highlightedIndex: { type: Number, default: -1 },
|
||||
})
|
||||
|
||||
defineEmits(['highlight-element'])
|
||||
|
|
@ -130,17 +166,17 @@ const activeTab = ref('elements')
|
|||
const tabs = computed(() => [
|
||||
{ id: 'elements', label: t('results.elements') },
|
||||
{ id: 'markdown', label: t('results.markdown') },
|
||||
{ id: 'images', label: t('results.images') }
|
||||
{ id: 'images', label: t('results.images') },
|
||||
])
|
||||
|
||||
const totalPages = computed(() => store.currentPages.length)
|
||||
|
||||
const currentPageData = computed(() => {
|
||||
return store.currentPages.find(p => p.page_number === props.currentPage) || null
|
||||
return store.currentPages.find((p) => p.page_number === props.currentPage) || null
|
||||
})
|
||||
|
||||
const currentElements = computed(() => {
|
||||
return (currentPageData.value?.elements || []).filter(el => el.content)
|
||||
return (currentPageData.value?.elements || []).filter((el) => el.content)
|
||||
})
|
||||
|
||||
const currentPageAsArray = computed(() => {
|
||||
|
|
@ -153,7 +189,7 @@ const pageMarkdown = computed(() => {
|
|||
if (!page) return ''
|
||||
|
||||
return (page.elements || [])
|
||||
.map(el => formatElement(el))
|
||||
.map((el) => formatElement(el))
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
})
|
||||
|
|
@ -191,16 +227,24 @@ async function copyMarkdown() {
|
|||
try {
|
||||
await navigator.clipboard.writeText(pageMarkdown.value)
|
||||
copiedMarkdown.value = true
|
||||
setTimeout(() => { copiedMarkdown.value = false }, 1500)
|
||||
} catch { /* clipboard not available */ }
|
||||
setTimeout(() => {
|
||||
copiedMarkdown.value = false
|
||||
}, 1500)
|
||||
} catch {
|
||||
/* clipboard not available */
|
||||
}
|
||||
}
|
||||
|
||||
async function copyElement(idx: number, content: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content)
|
||||
copiedElements[idx] = true
|
||||
setTimeout(() => { copiedElements[idx] = false }, 1500)
|
||||
} catch { /* clipboard not available */ }
|
||||
setTimeout(() => {
|
||||
copiedElements[idx] = false
|
||||
}, 1500)
|
||||
} catch {
|
||||
/* clipboard not available */
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -234,7 +278,9 @@ async function copyElement(idx: number, content: string) {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-btn:hover { color: var(--text-secondary); }
|
||||
.tab-btn:hover {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.tab-btn.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
|
|
@ -368,8 +414,13 @@ async function copyElement(idx: number, content: string) {
|
|||
background: var(--accent-muted);
|
||||
}
|
||||
|
||||
.copy-icon { width: 14px; height: 14px; }
|
||||
.copy-icon.copied { color: var(--success); }
|
||||
.copy-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.copy-icon.copied {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.copy-btn-element {
|
||||
margin-left: auto;
|
||||
|
|
@ -377,7 +428,9 @@ async function copyElement(idx: number, content: string) {
|
|||
transition: opacity var(--transition);
|
||||
}
|
||||
|
||||
.element-card:hover .copy-btn-element { opacity: 1; }
|
||||
.element-card:hover .copy-btn-element {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-btn-block {
|
||||
position: absolute;
|
||||
|
|
@ -389,7 +442,9 @@ async function copyElement(idx: number, content: string) {
|
|||
transition: opacity var(--transition);
|
||||
}
|
||||
|
||||
.raw-markdown:hover .copy-btn-block { opacity: 1; }
|
||||
.raw-markdown:hover .copy-btn-block {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* --- Raw markdown --- */
|
||||
.raw-markdown {
|
||||
|
|
@ -423,9 +478,18 @@ async function copyElement(idx: number, content: string) {
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-placeholder.error { color: var(--error); }
|
||||
.error-icon { width: 32px; height: 32px; }
|
||||
.empty-icon { width: 48px; height: 48px; color: var(--border-light); }
|
||||
.result-placeholder.error {
|
||||
color: var(--error);
|
||||
}
|
||||
.error-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.empty-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--border-light);
|
||||
}
|
||||
|
||||
.spinner-large {
|
||||
width: 32px;
|
||||
|
|
@ -436,5 +500,9 @@ async function copyElement(idx: number, content: string) {
|
|||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -44,12 +44,11 @@
|
|||
@mouseleave="hoveredElement = null"
|
||||
/>
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
v-if="hoveredElement"
|
||||
class="tooltip"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
<span class="tooltip-type" :style="{ color: ELEMENT_COLORS[hoveredElement.type] || ELEMENT_COLORS.text }">
|
||||
<div v-if="hoveredElement" class="tooltip" :style="tooltipStyle">
|
||||
<span
|
||||
class="tooltip-type"
|
||||
:style="{ color: ELEMENT_COLORS[hoveredElement.type] || ELEMENT_COLORS.text }"
|
||||
>
|
||||
{{ hoveredElement.type }}
|
||||
</span>
|
||||
<span class="tooltip-content">{{ hoveredElement.content?.substring(0, 150) }}</span>
|
||||
|
|
@ -71,12 +70,12 @@ const ELEMENT_COLORS: Record<string, string> = {
|
|||
picture: '#22C55E',
|
||||
list: '#06B6D4',
|
||||
formula: '#EC4899',
|
||||
caption: '#EAB308'
|
||||
caption: '#EAB308',
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
pages: { type: Array as () => Page[], default: () => [] },
|
||||
documentId: String
|
||||
documentId: String,
|
||||
})
|
||||
|
||||
const selectedPage = ref(1)
|
||||
|
|
@ -89,12 +88,12 @@ const tooltipStyle = ref<Record<string, string>>({})
|
|||
const imageSize = ref({ width: 0, height: 0 })
|
||||
|
||||
const currentPageData = computed(() => {
|
||||
return props.pages.find(p => p.page_number === selectedPage.value)
|
||||
return props.pages.find((p) => p.page_number === selectedPage.value)
|
||||
})
|
||||
|
||||
const visibleElements = computed(() => {
|
||||
if (!currentPageData.value) return []
|
||||
return currentPageData.value.elements.filter(e => !hiddenTypes.has(e.type))
|
||||
return currentPageData.value.elements.filter((e) => !hiddenTypes.has(e.type))
|
||||
})
|
||||
|
||||
const previewUrl = computed(() => {
|
||||
|
|
@ -110,7 +109,7 @@ function toggleType(type: string) {
|
|||
|
||||
function countElements(type: string) {
|
||||
if (!currentPageData.value) return 0
|
||||
return currentPageData.value.elements.filter(e => e.type === type).length
|
||||
return currentPageData.value.elements.filter((e) => e.type === type).length
|
||||
}
|
||||
|
||||
function onImageLoad() {
|
||||
|
|
@ -174,7 +173,7 @@ function onMouseMove(e: MouseEvent) {
|
|||
if (found) {
|
||||
tooltipStyle.value = {
|
||||
left: `${Math.min(mx + 12, canvas.width - 250)}px`,
|
||||
top: `${my + 12}px`
|
||||
top: `${my + 12}px`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -213,7 +212,9 @@ watch([() => props.pages, selectedPage, hiddenTypes], () => {
|
|||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.page-btn:hover { background: var(--bg-hover); }
|
||||
.page-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.page-btn.active {
|
||||
background: var(--accent-muted);
|
||||
border-color: var(--accent);
|
||||
|
|
@ -240,8 +241,12 @@ watch([() => props.pages, selectedPage, hiddenTypes], () => {
|
|||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.legend-item:hover { background: var(--bg-hover); }
|
||||
.legend-item.dimmed { opacity: 0.4; }
|
||||
.legend-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.legend-item.dimmed {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
|
|
@ -286,7 +291,7 @@ watch([() => props.pages, selectedPage, hiddenTypes], () => {
|
|||
max-width: 250px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tooltip-type {
|
||||
|
|
|
|||
|
|
@ -68,9 +68,7 @@
|
|||
|
||||
<!-- Chunks list -->
|
||||
<div class="chunk-results" v-if="pageChunks.length">
|
||||
<div class="chunk-summary">
|
||||
{{ pagination.totalItems.value }} {{ t('chunking.chunks') }}
|
||||
</div>
|
||||
<div class="chunk-summary">{{ pagination.totalItems.value }} {{ t('chunking.chunks') }}</div>
|
||||
<div class="chunk-list">
|
||||
<div
|
||||
class="chunk-card"
|
||||
|
|
@ -85,9 +83,7 @@
|
|||
<span class="chunk-tokens" v-if="chunk.tokenCount">
|
||||
{{ chunk.tokenCount }} tokens
|
||||
</span>
|
||||
<span class="chunk-page" v-if="chunk.sourcePage">
|
||||
p.{{ chunk.sourcePage }}
|
||||
</span>
|
||||
<span class="chunk-page" v-if="chunk.sourcePage"> p.{{ chunk.sourcePage }} </span>
|
||||
</div>
|
||||
<div class="chunk-headings" v-if="chunk.headings.length">
|
||||
<span class="chunk-heading" v-for="h in chunk.headings" :key="h">{{ h }}</span>
|
||||
|
|
@ -100,9 +96,7 @@
|
|||
<div class="chunk-empty" v-else-if="!analysisStore.rechunking">
|
||||
<p>
|
||||
{{
|
||||
analysisStore.currentChunks.length
|
||||
? t('chunking.noChunksOnPage')
|
||||
: t('chunking.noChunks')
|
||||
analysisStore.currentChunks.length ? t('chunking.noChunksOnPage') : t('chunking.noChunks')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -356,15 +350,17 @@ async function doRechunk() {
|
|||
border-radius: var(--radius);
|
||||
padding: 10px 12px;
|
||||
cursor: default;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
}
|
||||
|
||||
.chunk-card:hover {
|
||||
border-color: #F59E0B;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.chunk-card.highlighted {
|
||||
border-color: #F59E0B;
|
||||
border-color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ describe('useDocumentStore', () => {
|
|||
})
|
||||
|
||||
it('load() fetches and sets documents', async () => {
|
||||
const docs = [{ id: '1', filename: 'a.pdf' }, { id: '2', filename: 'b.pdf' }]
|
||||
const docs = [
|
||||
{ id: '1', filename: 'a.pdf' },
|
||||
{ id: '2', filename: 'b.pdf' },
|
||||
]
|
||||
api.fetchDocuments.mockResolvedValue(docs)
|
||||
|
||||
const store = useDocumentStore()
|
||||
|
|
@ -61,7 +64,12 @@ describe('useDocumentStore', () => {
|
|||
|
||||
it('upload() sets uploading to true during upload', async () => {
|
||||
let resolveUpload
|
||||
api.uploadDocument.mockImplementation(() => new Promise(r => { resolveUpload = r }))
|
||||
api.uploadDocument.mockImplementation(
|
||||
() =>
|
||||
new Promise((r) => {
|
||||
resolveUpload = r
|
||||
}),
|
||||
)
|
||||
|
||||
const store = useDocumentStore()
|
||||
const promise = store.upload(new File([], 'test.pdf'))
|
||||
|
|
|
|||
|
|
@ -9,15 +9,28 @@
|
|||
>
|
||||
<div class="doc-info">
|
||||
<svg class="doc-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="doc-meta">
|
||||
<span class="doc-name">{{ doc.filename }}</span>
|
||||
<span class="doc-size">{{ formatSize(doc.fileSize) }}{{ doc.pageCount ? ` — ${doc.pageCount} pages` : '' }}</span>
|
||||
<span class="doc-size"
|
||||
>{{ formatSize(doc.fileSize)
|
||||
}}{{ doc.pageCount ? ` — ${doc.pageCount} pages` : '' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<button class="doc-delete" @click.stop="store.remove(doc.id)" title="Delete">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -101,7 +114,15 @@ const store = useDocumentStore()
|
|||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.doc-item:hover .doc-delete { opacity: 1; }
|
||||
.doc-delete:hover { color: var(--error); background: rgba(239, 68, 68, 0.1); }
|
||||
.doc-delete svg { width: 14px; height: 14px; }
|
||||
.doc-item:hover .doc-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
.doc-delete:hover {
|
||||
color: var(--error);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
.doc-delete svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,13 @@
|
|||
<span>{{ t('upload.uploading') }}</span>
|
||||
</div>
|
||||
<div v-else class="upload-state">
|
||||
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<svg
|
||||
class="upload-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path d="M12 16V4m0 0L8 8m4-4l4 4M4 17v2a1 1 0 001 1h14a1 1 0 001-1v-2" />
|
||||
</svg>
|
||||
<span class="upload-text">{{ t('upload.drop') }}</span>
|
||||
|
|
@ -42,10 +48,7 @@ function openFilePicker() {
|
|||
async function onFileSelect(e: Event) {
|
||||
const target = e.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (
|
||||
file &&
|
||||
(file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'))
|
||||
) {
|
||||
if (file && (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'))) {
|
||||
try {
|
||||
store.clearError()
|
||||
const doc = await store.upload(file)
|
||||
|
|
@ -133,6 +136,8 @@ async function onDrop(e: DragEvent) {
|
|||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,26 @@
|
|||
<span class="preview-label">Page {{ page }}</span>
|
||||
<div class="preview-nav">
|
||||
<button class="nav-btn" :disabled="page <= 1" @click="$emit('update:page', page - 1)">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" :disabled="!!(pageCount && page >= pageCount)" @click="$emit('update:page', page + 1)">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
|
||||
<button
|
||||
class="nav-btn"
|
||||
:disabled="!!(pageCount && page >= pageCount)"
|
||||
@click="$emit('update:page', page + 1)"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -30,7 +46,7 @@ import { getPreviewUrl } from '../api'
|
|||
const props = defineProps({
|
||||
documentId: String,
|
||||
page: { type: Number, default: 1 },
|
||||
pageCount: Number
|
||||
pageCount: Number,
|
||||
})
|
||||
|
||||
defineEmits(['update:page', 'imageLoaded'])
|
||||
|
|
@ -76,9 +92,18 @@ const previewUrl = computed(() => {
|
|||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.nav-btn:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); }
|
||||
.nav-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
.nav-btn svg { width: 16px; height: 16px; }
|
||||
.nav-btn:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
.nav-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
.nav-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.preview-image-wrapper {
|
||||
background: var(--bg-elevated);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@
|
|||
class="item-status status-completed"
|
||||
:title="analysis.status"
|
||||
>
|
||||
<svg class="status-dot" viewBox="0 0 8 8" fill="currentColor"><circle cx="4" cy="4" r="4"/></svg>
|
||||
<svg class="status-dot" viewBox="0 0 8 8" fill="currentColor">
|
||||
<circle cx="4" cy="4" r="4" />
|
||||
</svg>
|
||||
</span>
|
||||
<span v-else class="item-status" :class="statusClass(analysis.status)">
|
||||
{{ analysis.status }}
|
||||
|
|
@ -30,7 +32,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<button class="item-delete" @click.stop="store.remove(analysis.id)" title="Delete">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -56,7 +64,7 @@ function statusClass(status: string) {
|
|||
'status-pending': status === 'PENDING',
|
||||
'status-running': status === 'RUNNING',
|
||||
'status-completed': status === 'COMPLETED',
|
||||
'status-failed': status === 'FAILED'
|
||||
'status-failed': status === 'FAILED',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +117,10 @@ function duration(analysis: Analysis) {
|
|||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.item-main { flex: 1; min-width: 0; }
|
||||
.item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
|
|
@ -140,10 +151,23 @@ function duration(analysis: Analysis) {
|
|||
height: 8px;
|
||||
}
|
||||
|
||||
.status-pending { background: rgba(234, 179, 8, 0.15); color: var(--warning); }
|
||||
.status-running { background: rgba(59, 130, 246, 0.15); color: var(--info); }
|
||||
.status-completed { background: none; color: var(--success); padding: 0; }
|
||||
.status-failed { background: rgba(239, 68, 68, 0.15); color: var(--error); }
|
||||
.status-pending {
|
||||
background: rgba(234, 179, 8, 0.15);
|
||||
color: var(--warning);
|
||||
}
|
||||
.status-running {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: var(--info);
|
||||
}
|
||||
.status-completed {
|
||||
background: none;
|
||||
color: var(--success);
|
||||
padding: 0;
|
||||
}
|
||||
.status-failed {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
font-size: 12px;
|
||||
|
|
@ -164,7 +188,15 @@ function duration(analysis: Analysis) {
|
|||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.history-item:hover .item-delete { opacity: 1; }
|
||||
.item-delete:hover { color: var(--error); background: rgba(239, 68, 68, 0.1); }
|
||||
.item-delete svg { width: 16px; height: 16px; }
|
||||
.history-item:hover .item-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
.item-delete:hover {
|
||||
color: var(--error);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
.item-delete svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@
|
|||
<div class="setting-group">
|
||||
<label class="setting-label">{{ t('settings.language') }}</label>
|
||||
<div class="setting-toggle">
|
||||
<button :class="{ active: store.locale === 'fr' }" @click="store.setLocale('fr')">FR</button>
|
||||
<button :class="{ active: store.locale === 'en' }" @click="store.setLocale('en')">EN</button>
|
||||
<button :class="{ active: store.locale === 'fr' }" @click="store.setLocale('fr')">
|
||||
FR
|
||||
</button>
|
||||
<button :class="{ active: store.locale === 'en' }" @click="store.setLocale('en')">
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@
|
|||
{{ t('history.emptyDocs') }}
|
||||
</div>
|
||||
<div v-else class="doc-items">
|
||||
<div
|
||||
v-for="doc in docStore.documents"
|
||||
:key="doc.id"
|
||||
class="doc-row"
|
||||
>
|
||||
<div v-for="doc in docStore.documents" :key="doc.id" class="doc-row">
|
||||
<div class="doc-row-info">
|
||||
<svg class="doc-row-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="doc-row-meta">
|
||||
<span class="doc-row-name">{{ doc.filename }}</span>
|
||||
|
|
@ -27,7 +27,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<button class="doc-row-delete" @click="docStore.remove(doc.id)">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -158,7 +164,15 @@ onMounted(() => {
|
|||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.doc-row:hover .doc-row-delete { opacity: 1; }
|
||||
.doc-row-delete:hover { color: var(--error); background: rgba(239, 68, 68, 0.1); }
|
||||
.doc-row-delete svg { width: 16px; height: 16px; }
|
||||
.doc-row:hover .doc-row-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
.doc-row-delete:hover {
|
||||
color: var(--error);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
.doc-row-delete svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,12 +15,24 @@
|
|||
<!-- Stats -->
|
||||
<div class="home-stats" v-if="docCount > 0 || analysisCount > 0">
|
||||
<div class="stat-card" @click="$router.push('/documents')">
|
||||
<svg class="stat-icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/></svg>
|
||||
<svg class="stat-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="stat-value">{{ docCount }}</div>
|
||||
<div class="stat-label">{{ t('home.documents') }}</div>
|
||||
</div>
|
||||
<div class="stat-card" @click="$router.push('/history')">
|
||||
<svg class="stat-icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
|
||||
<svg class="stat-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="stat-value">{{ analysisCount }}</div>
|
||||
<div class="stat-label">{{ t('home.analyses') }}</div>
|
||||
</div>
|
||||
|
|
@ -37,14 +49,22 @@
|
|||
@click="openInStudio(doc)"
|
||||
>
|
||||
<svg class="recent-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="recent-meta">
|
||||
<span class="recent-name">{{ doc.filename }}</span>
|
||||
<span class="recent-detail">{{ doc.pageCount ? doc.pageCount + ' pages' : '' }}</span>
|
||||
</div>
|
||||
<svg class="recent-arrow" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,13 @@
|
|||
:class="{ active: mode === 'configurer' }"
|
||||
@click="mode = 'configurer'"
|
||||
>
|
||||
<svg class="toggle-icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/></svg>
|
||||
<svg class="toggle-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('studio.configure') }}
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -34,7 +40,13 @@
|
|||
@click="mode = 'verifier'"
|
||||
:disabled="!analysisStore.currentAnalysis"
|
||||
>
|
||||
<svg class="toggle-icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
|
||||
<svg class="toggle-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('studio.verify') }}
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -44,14 +56,22 @@
|
|||
@click="mode = 'preparer'"
|
||||
:disabled="!analysisStore.currentAnalysis"
|
||||
>
|
||||
<svg class="toggle-icon" viewBox="0 0 20 20" fill="currentColor"><path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
|
||||
<svg class="toggle-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('studio.prepare') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<button class="topbar-btn" @click="addMore">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="btn-icon"><path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="btn-icon">
|
||||
<path
|
||||
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('studio.addFiles') }}
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -61,7 +81,13 @@
|
|||
v-if="mode === 'configurer'"
|
||||
>
|
||||
<div v-if="analysisStore.running" class="spinner-sm" />
|
||||
<svg v-else viewBox="0 0 20 20" fill="currentColor" class="btn-icon"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>
|
||||
<svg v-else viewBox="0 0 20 20" fill="currentColor" class="btn-icon">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{{ analysisStore.running ? t('studio.analyzing') : t('studio.run') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -70,7 +96,13 @@
|
|||
<!-- Document info bar -->
|
||||
<div class="doc-infobar">
|
||||
<div class="doc-info-left">
|
||||
<svg class="doc-icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/></svg>
|
||||
<svg class="doc-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="doc-filename">{{ selectedDoc.filename }}</span>
|
||||
<span class="doc-status-chip loaded">{{ t('studio.loaded') }}</span>
|
||||
</div>
|
||||
|
|
@ -96,7 +128,13 @@
|
|||
<div class="pdf-viewer-panel">
|
||||
<div class="pdf-nav-bar">
|
||||
<button class="pdf-nav-btn" :disabled="currentPage <= 1" @click="currentPage--">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="pdf-page-input-wrap">
|
||||
<input
|
||||
|
|
@ -109,8 +147,18 @@
|
|||
/>
|
||||
</div>
|
||||
<span class="pdf-page-total">/ {{ selectedDoc.pageCount || '?' }}</span>
|
||||
<button class="pdf-nav-btn" :disabled="!selectedDoc.pageCount || currentPage >= selectedDoc.pageCount" @click="currentPage++">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
|
||||
<button
|
||||
class="pdf-nav-btn"
|
||||
:disabled="!selectedDoc.pageCount || currentPage >= selectedDoc.pageCount"
|
||||
@click="currentPage++"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="pdf-separator" />
|
||||
<span class="pdf-zoom">100%</span>
|
||||
|
|
@ -121,7 +169,14 @@
|
|||
:class="{ active: visualMode }"
|
||||
@click="visualMode = !visualMode"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="btn-icon"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="btn-icon">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('studio.visual') }}
|
||||
</button>
|
||||
</template>
|
||||
|
|
@ -150,10 +205,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Resize handle -->
|
||||
<div
|
||||
class="resize-handle"
|
||||
@mousedown="onResizeStart"
|
||||
>
|
||||
<div class="resize-handle" @mousedown="onResizeStart">
|
||||
<div class="resize-grip" />
|
||||
</div>
|
||||
|
||||
|
|
@ -181,16 +233,26 @@
|
|||
<span class="toggle-switch" />
|
||||
<span class="toggle-text">{{ t('config.ocr') }}</span>
|
||||
</label>
|
||||
<span class="config-hint"><span class="config-tooltip">{{ t('config.ocrHint') }}</span>?</span>
|
||||
<span class="config-hint"
|
||||
><span class="config-tooltip">{{ t('config.ocrHint') }}</span
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="config-toggle-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" v-model="pipelineOptions.do_table_structure" class="toggle-input" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="pipelineOptions.do_table_structure"
|
||||
class="toggle-input"
|
||||
/>
|
||||
<span class="toggle-switch" />
|
||||
<span class="toggle-text">{{ t('config.tableStructure') }}</span>
|
||||
</label>
|
||||
<span class="config-hint"><span class="config-tooltip">{{ t('config.tableStructureHint') }}</span>?</span>
|
||||
<span class="config-hint"
|
||||
><span class="config-tooltip">{{ t('config.tableStructureHint') }}</span
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="config-sub-option" v-if="pipelineOptions.do_table_structure">
|
||||
|
|
@ -208,20 +270,34 @@
|
|||
|
||||
<div class="config-toggle-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" v-model="pipelineOptions.do_code_enrichment" class="toggle-input" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="pipelineOptions.do_code_enrichment"
|
||||
class="toggle-input"
|
||||
/>
|
||||
<span class="toggle-switch" />
|
||||
<span class="toggle-text">{{ t('config.codeEnrichment') }}</span>
|
||||
</label>
|
||||
<span class="config-hint"><span class="config-tooltip">{{ t('config.codeEnrichmentHint') }}</span>?</span>
|
||||
<span class="config-hint"
|
||||
><span class="config-tooltip">{{ t('config.codeEnrichmentHint') }}</span
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="config-toggle-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" v-model="pipelineOptions.do_formula_enrichment" class="toggle-input" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="pipelineOptions.do_formula_enrichment"
|
||||
class="toggle-input"
|
||||
/>
|
||||
<span class="toggle-switch" />
|
||||
<span class="toggle-text">{{ t('config.formulaEnrichment') }}</span>
|
||||
</label>
|
||||
<span class="config-hint"><span class="config-tooltip">{{ t('config.formulaEnrichmentHint') }}</span>?</span>
|
||||
<span class="config-hint"
|
||||
><span class="config-tooltip">{{ t('config.formulaEnrichmentHint') }}</span
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -231,41 +307,72 @@
|
|||
|
||||
<div class="config-toggle-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" v-model="pipelineOptions.do_picture_classification" class="toggle-input" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="pipelineOptions.do_picture_classification"
|
||||
class="toggle-input"
|
||||
/>
|
||||
<span class="toggle-switch" />
|
||||
<span class="toggle-text">{{ t('config.pictureClassification') }}</span>
|
||||
</label>
|
||||
<span class="config-hint"><span class="config-tooltip">{{ t('config.pictureClassificationHint') }}</span>?</span>
|
||||
<span class="config-hint"
|
||||
><span class="config-tooltip">{{ t('config.pictureClassificationHint') }}</span
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="config-toggle-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" v-model="pipelineOptions.do_picture_description" class="toggle-input" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="pipelineOptions.do_picture_description"
|
||||
class="toggle-input"
|
||||
/>
|
||||
<span class="toggle-switch" />
|
||||
<span class="toggle-text">{{ t('config.pictureDescription') }}</span>
|
||||
</label>
|
||||
<span class="config-hint"><span class="config-tooltip">{{ t('config.pictureDescriptionHint') }}</span>?</span>
|
||||
<span class="config-hint"
|
||||
><span class="config-tooltip">{{ t('config.pictureDescriptionHint') }}</span
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="config-toggle-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" v-model="pipelineOptions.generate_picture_images" class="toggle-input" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="pipelineOptions.generate_picture_images"
|
||||
class="toggle-input"
|
||||
/>
|
||||
<span class="toggle-switch" />
|
||||
<span class="toggle-text">{{ t('config.generatePictureImages') }}</span>
|
||||
</label>
|
||||
<span class="config-hint"><span class="config-tooltip">{{ t('config.generatePictureImagesHint') }}</span>?</span>
|
||||
<span class="config-hint"
|
||||
><span class="config-tooltip">{{ t('config.generatePictureImagesHint') }}</span
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="config-toggle-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" v-model="pipelineOptions.generate_page_images" class="toggle-input" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="pipelineOptions.generate_page_images"
|
||||
class="toggle-input"
|
||||
/>
|
||||
<span class="toggle-switch" />
|
||||
<span class="toggle-text">{{ t('config.generatePageImages') }}</span>
|
||||
</label>
|
||||
<span class="config-hint"><span class="config-tooltip">{{ t('config.generatePageImagesHint') }}</span>?</span>
|
||||
<span class="config-hint"
|
||||
><span class="config-tooltip">{{ t('config.generatePageImagesHint') }}</span
|
||||
>?</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="config-sub-option" v-if="pipelineOptions.generate_picture_images || pipelineOptions.generate_page_images">
|
||||
<div
|
||||
class="config-sub-option"
|
||||
v-if="pipelineOptions.generate_picture_images || pipelineOptions.generate_page_images"
|
||||
>
|
||||
<label class="config-label-sm">{{ t('config.imagesScale') }}</label>
|
||||
<select class="config-select" v-model.number="pipelineOptions.images_scale">
|
||||
<option :value="0.5">0.5x</option>
|
||||
|
|
@ -379,12 +486,14 @@ const pipelineOptions = reactive<PipelineOptions>({
|
|||
})
|
||||
|
||||
const hasAnalysisResults = computed(() => {
|
||||
return analysisStore.currentAnalysis?.status === 'COMPLETED' && analysisStore.currentPages?.length > 0
|
||||
return (
|
||||
analysisStore.currentAnalysis?.status === 'COMPLETED' && analysisStore.currentPages?.length > 0
|
||||
)
|
||||
})
|
||||
|
||||
const currentPageData = computed(() => {
|
||||
if (!analysisStore.currentPages) return null
|
||||
return analysisStore.currentPages.find(p => p.page_number === currentPage.value) || null
|
||||
return analysisStore.currentPages.find((p) => p.page_number === currentPage.value) || null
|
||||
})
|
||||
|
||||
function onPdfImageLoad() {
|
||||
|
|
@ -392,7 +501,7 @@ function onPdfImageLoad() {
|
|||
}
|
||||
|
||||
const selectedDoc = computed(() => {
|
||||
return documentStore.documents.find(d => d.id === documentStore.selectedId)
|
||||
return documentStore.documents.find((d) => d.id === documentStore.selectedId)
|
||||
})
|
||||
|
||||
const previewUrl = computed(() => {
|
||||
|
|
@ -427,12 +536,15 @@ watch(currentPage, () => {
|
|||
})
|
||||
|
||||
// Auto-switch to verifier when analysis completes + refresh document data (pageCount)
|
||||
watch(() => analysisStore.currentAnalysis?.status, (status) => {
|
||||
if (status === 'COMPLETED') {
|
||||
mode.value = 'verifier'
|
||||
documentStore.load()
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => analysisStore.currentAnalysis?.status,
|
||||
(status) => {
|
||||
if (status === 'COMPLETED') {
|
||||
mode.value = 'verifier'
|
||||
documentStore.load()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await documentStore.load()
|
||||
|
|
@ -658,7 +770,7 @@ onBeforeUnmount(() => {
|
|||
.spinner-sm {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
|
|
@ -730,8 +842,12 @@ onBeforeUnmount(() => {
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info-dot.success { background: var(--success); }
|
||||
.info-dot.error { background: var(--error); }
|
||||
.info-dot.success {
|
||||
background: var(--success);
|
||||
}
|
||||
.info-dot.error {
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
.spinner-xs {
|
||||
width: 12px;
|
||||
|
|
@ -830,9 +946,18 @@ onBeforeUnmount(() => {
|
|||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.pdf-nav-btn:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); }
|
||||
.pdf-nav-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
.pdf-nav-btn svg { width: 16px; height: 16px; }
|
||||
.pdf-nav-btn:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
.pdf-nav-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
.pdf-nav-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.pdf-page-input-wrap {
|
||||
display: flex;
|
||||
|
|
@ -931,7 +1056,7 @@ onBeforeUnmount(() => {
|
|||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
box-shadow: 0 2px 20px rgba(0,0,0,0.4);
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
|
|
@ -1178,5 +1303,9 @@ onBeforeUnmount(() => {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,12 @@ describe('apiFetch', () => {
|
|||
|
||||
await apiFetch('/api/test')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('/api/test', expect.objectContaining({
|
||||
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
|
||||
}))
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
'/api/test',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('skips Content-Type when skipContentType is true', async () => {
|
||||
|
|
@ -73,10 +76,13 @@ describe('apiFetch', () => {
|
|||
const body = JSON.stringify({ documentId: '42' })
|
||||
await apiFetch('/api/analyses', { method: 'POST', body })
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('/api/analyses', expect.objectContaining({
|
||||
method: 'POST',
|
||||
body,
|
||||
}))
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
'/api/analyses',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('merges custom headers with Content-Type', async () => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ const messages: Messages = {
|
|||
|
||||
// Home
|
||||
'home.title': 'Docling Studio',
|
||||
'home.subtitle': 'Analysez, explorez et validez la structure de vos documents PDF grâce à Docling.',
|
||||
'home.subtitle':
|
||||
'Analysez, explorez et validez la structure de vos documents PDF grâce à Docling.',
|
||||
'home.documents': 'Documents',
|
||||
'home.analyses': 'Analyses',
|
||||
'home.recentDocs': 'Documents récents',
|
||||
|
|
@ -45,26 +46,34 @@ const messages: Messages = {
|
|||
'config.model': 'Modèle',
|
||||
'config.pipeline': 'Pipeline',
|
||||
'config.ocr': 'OCR',
|
||||
'config.ocrHint': "Applique la reconnaissance optique de caractères sur les pages scannées ou les images intégrées. Indispensable pour les PDF non-natifs.",
|
||||
'config.ocrHint':
|
||||
'Applique la reconnaissance optique de caractères sur les pages scannées ou les images intégrées. Indispensable pour les PDF non-natifs.',
|
||||
'config.tableStructure': 'Extraction des tableaux',
|
||||
'config.tableStructureHint': "Détecte les tableaux dans le document et reconstruit leur structure lignes/colonnes via le modèle TableFormer, avec correspondance des cellules.",
|
||||
'config.tableStructureHint':
|
||||
'Détecte les tableaux dans le document et reconstruit leur structure lignes/colonnes via le modèle TableFormer, avec correspondance des cellules.',
|
||||
'config.tableMode': 'Mode tableaux',
|
||||
'config.tableModeAccurate': 'Précis',
|
||||
'config.tableModeFast': 'Rapide',
|
||||
'config.enrichment': 'Enrichissement',
|
||||
'config.codeEnrichment': 'Code',
|
||||
'config.codeEnrichmentHint': "Active un modèle OCR spécialisé pour les blocs de code, préservant l'indentation et la syntaxe.",
|
||||
'config.codeEnrichmentHint':
|
||||
"Active un modèle OCR spécialisé pour les blocs de code, préservant l'indentation et la syntaxe.",
|
||||
'config.formulaEnrichment': 'Formules',
|
||||
'config.formulaEnrichmentHint': "Reconnaît les formules mathématiques et les convertit en LaTeX via un modèle dédié.",
|
||||
'config.formulaEnrichmentHint':
|
||||
'Reconnaît les formules mathématiques et les convertit en LaTeX via un modèle dédié.',
|
||||
'config.pictures': 'Images',
|
||||
'config.pictureClassification': 'Classification',
|
||||
'config.pictureClassificationHint': "Classe chaque image détectée par type (graphique, photo, diagramme, logo…) via un modèle de classification.",
|
||||
'config.pictureClassificationHint':
|
||||
'Classe chaque image détectée par type (graphique, photo, diagramme, logo…) via un modèle de classification.',
|
||||
'config.pictureDescription': 'Description',
|
||||
'config.pictureDescriptionHint': "Génère une description textuelle de chaque image via un Vision Language Model (VLM). Utile pour l'accessibilité et l'indexation.",
|
||||
'config.pictureDescriptionHint':
|
||||
"Génère une description textuelle de chaque image via un Vision Language Model (VLM). Utile pour l'accessibilité et l'indexation.",
|
||||
'config.generatePictureImages': 'Extraire les images',
|
||||
'config.generatePictureImagesHint': "Extrait les images détectées du document et les sauvegarde en tant que fichiers séparés. Nécessaire pour l'export d'images.",
|
||||
'config.generatePictureImagesHint':
|
||||
"Extrait les images détectées du document et les sauvegarde en tant que fichiers séparés. Nécessaire pour l'export d'images.",
|
||||
'config.generatePageImages': 'Images de pages',
|
||||
'config.generatePageImagesHint': "Rasterise chaque page du PDF en image. Utile pour la visualisation ou le post-traitement visuel.",
|
||||
'config.generatePageImagesHint':
|
||||
'Rasterise chaque page du PDF en image. Utile pour la visualisation ou le post-traitement visuel.',
|
||||
'config.imagesScale': 'Échelle images',
|
||||
'config.documents': 'Documents',
|
||||
|
||||
|
|
@ -133,7 +142,8 @@ const messages: Messages = {
|
|||
'topbar.newAnalysis': 'New analysis',
|
||||
|
||||
'home.title': 'Docling Studio',
|
||||
'home.subtitle': 'Analyze, explore and validate the structure of your PDF documents with Docling.',
|
||||
'home.subtitle':
|
||||
'Analyze, explore and validate the structure of your PDF documents with Docling.',
|
||||
'home.documents': 'Documents',
|
||||
'home.analyses': 'Analyses',
|
||||
'home.recentDocs': 'Recent documents',
|
||||
|
|
@ -155,26 +165,34 @@ const messages: Messages = {
|
|||
'config.model': 'Model',
|
||||
'config.pipeline': 'Pipeline',
|
||||
'config.ocr': 'OCR',
|
||||
'config.ocrHint': 'Applies Optical Character Recognition on scanned pages or embedded images. Essential for non-native PDFs.',
|
||||
'config.ocrHint':
|
||||
'Applies Optical Character Recognition on scanned pages or embedded images. Essential for non-native PDFs.',
|
||||
'config.tableStructure': 'Table extraction',
|
||||
'config.tableStructureHint': 'Detects tables in the document and reconstructs their row/column structure using the TableFormer model, with cell matching.',
|
||||
'config.tableStructureHint':
|
||||
'Detects tables in the document and reconstructs their row/column structure using the TableFormer model, with cell matching.',
|
||||
'config.tableMode': 'Table mode',
|
||||
'config.tableModeAccurate': 'Accurate',
|
||||
'config.tableModeFast': 'Fast',
|
||||
'config.enrichment': 'Enrichment',
|
||||
'config.codeEnrichment': 'Code',
|
||||
'config.codeEnrichmentHint': 'Activates a specialized OCR model for code blocks, preserving indentation and syntax.',
|
||||
'config.codeEnrichmentHint':
|
||||
'Activates a specialized OCR model for code blocks, preserving indentation and syntax.',
|
||||
'config.formulaEnrichment': 'Formulas',
|
||||
'config.formulaEnrichmentHint': 'Recognizes mathematical formulas and converts them to LaTeX using a dedicated model.',
|
||||
'config.formulaEnrichmentHint':
|
||||
'Recognizes mathematical formulas and converts them to LaTeX using a dedicated model.',
|
||||
'config.pictures': 'Pictures',
|
||||
'config.pictureClassification': 'Classification',
|
||||
'config.pictureClassificationHint': 'Classifies each detected image by type (chart, photo, diagram, logo…) using a classification model.',
|
||||
'config.pictureClassificationHint':
|
||||
'Classifies each detected image by type (chart, photo, diagram, logo…) using a classification model.',
|
||||
'config.pictureDescription': 'Description',
|
||||
'config.pictureDescriptionHint': 'Generates a text description for each image using a Vision Language Model (VLM). Useful for accessibility and indexing.',
|
||||
'config.pictureDescriptionHint':
|
||||
'Generates a text description for each image using a Vision Language Model (VLM). Useful for accessibility and indexing.',
|
||||
'config.generatePictureImages': 'Extract pictures',
|
||||
'config.generatePictureImagesHint': 'Extracts detected images from the document and saves them as separate files. Required for image export.',
|
||||
'config.generatePictureImagesHint':
|
||||
'Extracts detected images from the document and saves them as separate files. Required for image export.',
|
||||
'config.generatePageImages': 'Page images',
|
||||
'config.generatePageImagesHint': 'Rasterizes each PDF page as an image. Useful for visual preview or post-processing.',
|
||||
'config.generatePageImagesHint':
|
||||
'Rasterizes each PDF page as an image. Useful for visual preview or post-processing.',
|
||||
'config.imagesScale': 'Images scale',
|
||||
'config.documents': 'Documents',
|
||||
|
||||
|
|
|
|||
|
|
@ -2,27 +2,53 @@
|
|||
<aside class="sidebar" :class="{ open }">
|
||||
<nav class="sidebar-nav">
|
||||
<RouterLink to="/" class="nav-item" :class="{ active: route.name === 'home' }">
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="nav-label">{{ t('nav.home') }}</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/studio" class="nav-item" :class="{ active: route.name === 'studio' }">
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor"><path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838l-2.727 1.17 1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zm5.99 7.176A9.026 9.026 0 007 15.96v-4.5l.61.26a2.5 2.5 0 001.98 0l.61-.26v4.5a9.026 9.026 0 00-1.7.613z"/></svg>
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838l-2.727 1.17 1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zm5.99 7.176A9.026 9.026 0 007 15.96v-4.5l.61.26a2.5 2.5 0 001.98 0l.61-.26v4.5a9.026 9.026 0 00-1.7.613z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="nav-label">{{ t('nav.studio') }}</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/documents" class="nav-item" :class="{ active: route.name === 'documents' }">
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/></svg>
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="nav-label">{{ t('nav.documents') }}</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/history" class="nav-item" :class="{ active: route.name === 'history' }">
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="nav-label">{{ t('nav.history') }}</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/settings" class="nav-item" :class="{ active: route.name === 'settings' }">
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/></svg>
|
||||
<svg class="nav-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="nav-label">{{ t('nav.settings') }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
|
@ -42,7 +68,7 @@ const route = useRoute()
|
|||
const { t } = useI18n()
|
||||
|
||||
defineProps({
|
||||
open: { type: Boolean, default: false }
|
||||
open: { type: Boolean, default: false },
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -56,7 +82,9 @@ defineProps({
|
|||
overflow: hidden;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
transition: width 250ms ease, min-width 250ms ease;
|
||||
transition:
|
||||
width 250ms ease,
|
||||
min-width 250ms ease;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
|
|
|
|||
Loading…
Reference in a new issue