Final fixes before first release
This commit is contained in:
parent
6522d4ad9e
commit
ac1b68ac18
17 changed files with 315 additions and 69 deletions
32
README.md
32
README.md
|
|
@ -3,7 +3,7 @@
|
|||
A visual document analysis studio powered by [Docling](https://github.com/DS4SD/docling).
|
||||
Upload a PDF, configure the extraction pipeline, and visualize the results — text, tables, images, formulas, bounding boxes — all from your browser.
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ Upload a PDF, configure the extraction pipeline, and visualize the results — t
|
|||
|
||||
| Import | Configure | Results |
|
||||
|--------|-----------|---------|
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|
||||
</details>
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ docker compose up --build
|
|||
|
||||
Open [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
> **Note:** First analysis may take a few minutes as Docling downloads its ML models (~40 MB) on first run.
|
||||
> **Note:** The first analysis takes a bit longer as Docling downloads and caches its ML models (~400 MB). Subsequent runs are fast.
|
||||
|
||||
### Local Development
|
||||
|
||||
|
|
@ -122,13 +122,13 @@ All configuration is done via environment variables. See [`.env.example`](.env.e
|
|||
|
||||
## Performance & System Requirements
|
||||
|
||||
Docling runs ML models (layout analysis, OCR, table structure) on **CPU by default**. Processing time depends on document size and complexity.
|
||||
Docling leverages optimized ML models (layout analysis, OCR, table structure) that run efficiently on CPU. The first analysis takes slightly longer as models are downloaded and cached (~400 MB). Subsequent runs are fast, even on large documents.
|
||||
|
||||
| Document type | Pages | Approx. time (CPU) |
|
||||
|---------------|-------|---------------------|
|
||||
| Simple report | 5-10 | 1-3 min |
|
||||
| Research paper | 15-30 | 5-10 min |
|
||||
| Dense PDF with tables | 30+ | 10-20 min |
|
||||
| Simple report | 5-10 | ~30s-1 min |
|
||||
| Research paper | 10-30 | ~1-2 min |
|
||||
| Large document | 100+ | ~2-5 min |
|
||||
|
||||
### Docker Desktop settings
|
||||
|
||||
|
|
@ -144,17 +144,15 @@ The document parser needs **at least 4 GB of RAM**. Recommended Docker Desktop a
|
|||
|
||||
### Platform support
|
||||
|
||||
All Docker images are **multi-arch** (linux/amd64 + linux/arm64). Works natively on:
|
||||
All Docker images are **multi-arch** (linux/amd64 + linux/arm64). All processing runs on **CPU** — no GPU required.
|
||||
|
||||
| Platform | Architecture | GPU acceleration |
|
||||
|----------|-------------|-----------------|
|
||||
| **macOS Apple Silicon** (M1/M2/M3) | arm64 | Not in Docker (MPS unavailable). Run parser locally for GPU. |
|
||||
| **macOS Intel** | amd64 | N/A |
|
||||
| **Linux x86_64** | amd64 | NVIDIA GPU via `docker compose --profile gpu` (coming soon) |
|
||||
| **Linux ARM** (Raspberry Pi 5, Ampere) | arm64 | CPU only |
|
||||
| **Windows + WSL2** | amd64 | NVIDIA GPU passthrough supported |
|
||||
|
||||
> **Tip for Mac users:** For faster processing, run the document parser **locally** (outside Docker) to leverage Apple Silicon's MPS acceleration when supported by PyTorch/Docling.
|
||||
| Platform | Architecture |
|
||||
|----------|-------------|
|
||||
| **macOS Apple Silicon** (M1/M2/M3/M4) | arm64 |
|
||||
| **macOS Intel** | amd64 |
|
||||
| **Linux x86_64** | amd64 |
|
||||
| **Linux ARM** (Raspberry Pi 5, Ampere) | arm64 |
|
||||
| **Windows + WSL2** | amd64 |
|
||||
|
||||
## Tech Stack
|
||||
|
||||
|
|
|
|||
BIN
docs/screenshots/DS-configure-execution.png
Normal file
BIN
docs/screenshots/DS-configure-execution.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
BIN
docs/screenshots/DS-execution-result.png
Normal file
BIN
docs/screenshots/DS-execution-result.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 KiB |
BIN
docs/screenshots/DS-load-document.png
Normal file
BIN
docs/screenshots/DS-load-document.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
|
|
@ -10,6 +10,9 @@
|
|||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import { AppSidebar } from '../shared/ui/index.js'
|
||||
import { useSettingsStore } from '../features/settings/store.js'
|
||||
|
||||
useSettingsStore()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
@ -46,6 +49,29 @@ import { AppSidebar } from '../shared/ui/index.js'
|
|||
--transition: 150ms ease;
|
||||
}
|
||||
|
||||
html.light {
|
||||
--bg: #FAFAFA;
|
||||
--bg-surface: #FFFFFF;
|
||||
--bg-elevated: #F4F4F5;
|
||||
--bg-hover: #E4E4E7;
|
||||
|
||||
--accent: #F97316;
|
||||
--accent-hover: #EA580C;
|
||||
--accent-muted: rgba(249, 115, 22, 0.10);
|
||||
|
||||
--text: #18181B;
|
||||
--text-secondary: #52525B;
|
||||
--text-muted: #A1A1AA;
|
||||
|
||||
--border: #E4E4E7;
|
||||
--border-light: #D4D4D8;
|
||||
|
||||
--success: #16A34A;
|
||||
--error: #DC2626;
|
||||
--warning: #CA8A04;
|
||||
--info: #2563EB;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="image-gallery">
|
||||
<div v-if="images.length === 0" class="gallery-empty">
|
||||
No images detected in this document
|
||||
{{ t('results.noImages') }}
|
||||
</div>
|
||||
<div v-else class="gallery-grid">
|
||||
<div
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
>
|
||||
<div class="card-header">
|
||||
<span class="card-type">Picture</span>
|
||||
<span class="card-page">Page {{ img.page }}</span>
|
||||
<span class="card-page">{{ t('results.page') }} {{ img.page }}</span>
|
||||
</div>
|
||||
<div class="card-content" v-if="img.content">
|
||||
{{ img.content.substring(0, 100) }}
|
||||
|
|
@ -27,6 +27,9 @@
|
|||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from '../../../shared/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
pages: { type: Array, default: () => [] }
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { useI18n } from '../../../shared/i18n.js'
|
||||
|
||||
const props = defineProps({ content: String })
|
||||
const { t } = useI18n()
|
||||
|
||||
const rendered = computed(() => {
|
||||
if (!props.content) return '<p class="empty">No markdown content</p>'
|
||||
if (!props.content) return `<p class="empty">${t('results.noMarkdown')}</p>`
|
||||
return marked.parse(props.content)
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<!-- Page chip -->
|
||||
<div class="page-indicator" v-if="totalPages > 0">
|
||||
<span class="page-chip">Page {{ currentPage }} sur {{ totalPages }}</span>
|
||||
<span class="page-chip">{{ t('results.pageOf', { current: currentPage, total: totalPages }) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
|
|
@ -27,17 +27,17 @@
|
|||
</div>
|
||||
<div v-else-if="store.currentAnalysis?.status === 'RUNNING'" class="result-placeholder">
|
||||
<div class="spinner-large" />
|
||||
<span>Analyse en cours...</span>
|
||||
<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>
|
||||
<span>{{ store.currentAnalysis.errorMessage || 'L\'analyse a échoué' }}</span>
|
||||
<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>
|
||||
<span>Lancez une analyse pour voir les résultats</span>
|
||||
<span>{{ t('results.runAnalysis') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -46,19 +46,21 @@ import { ref, computed } from 'vue'
|
|||
import { useAnalysisStore } from '../store.js'
|
||||
import MarkdownViewer from './MarkdownViewer.vue'
|
||||
import ImageGallery from './ImageGallery.vue'
|
||||
import { useI18n } from '../../../shared/i18n.js'
|
||||
|
||||
const props = defineProps({
|
||||
currentPage: { type: Number, default: 1 }
|
||||
})
|
||||
|
||||
const store = useAnalysisStore()
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref('text')
|
||||
|
||||
const tabs = [
|
||||
{ id: 'text', label: 'Résultat du texte' },
|
||||
{ id: 'markdown', label: 'Markdown' },
|
||||
{ id: 'images', label: 'Images' }
|
||||
]
|
||||
const tabs = computed(() => [
|
||||
{ id: 'text', label: t('results.textResult') },
|
||||
{ id: 'markdown', label: t('results.markdown') },
|
||||
{ id: 'images', label: t('results.images') }
|
||||
])
|
||||
|
||||
const totalPages = computed(() => store.currentPages.length)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@
|
|||
<input ref="fileInput" type="file" accept=".pdf" hidden @change="onFileSelect" />
|
||||
<div v-if="store.uploading" class="upload-state">
|
||||
<div class="spinner" />
|
||||
<span>Uploading...</span>
|
||||
<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">
|
||||
<path d="M12 16V4m0 0L8 8m4-4l4 4M4 17v2a1 1 0 001 1h14a1 1 0 001-1v-2" />
|
||||
</svg>
|
||||
<span class="upload-text">Drop a PDF here or click to upload</span>
|
||||
<span class="upload-hint">Max 50MB</span>
|
||||
<span class="upload-text">{{ t('upload.drop') }}</span>
|
||||
<span class="upload-hint">{{ t('upload.maxSize') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -25,8 +25,10 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useDocumentStore } from '../store.js'
|
||||
import { useI18n } from '../../../shared/i18n.js'
|
||||
|
||||
const store = useDocumentStore()
|
||||
const { t } = useI18n()
|
||||
const fileInput = ref(null)
|
||||
const dragging = ref(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="history-list">
|
||||
<div v-if="store.analyses.length === 0" class="history-empty">
|
||||
No analyses yet. Go to Studio to analyze your first document.
|
||||
{{ t('history.empty') }}
|
||||
</div>
|
||||
<div v-else class="history-items">
|
||||
<div
|
||||
|
|
@ -31,8 +31,10 @@
|
|||
|
||||
<script setup>
|
||||
import { useHistoryStore } from '../store.js'
|
||||
import { useI18n } from '../../../shared/i18n.js'
|
||||
|
||||
const store = useHistoryStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
function statusClass(status) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,20 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch, watchEffect } from 'vue'
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const apiUrl = ref('http://localhost:8000')
|
||||
const theme = ref(localStorage.getItem('docling-theme') || 'dark')
|
||||
const locale = ref(localStorage.getItem('docling-locale') || 'fr')
|
||||
|
||||
return { apiUrl }
|
||||
watch(theme, (v) => localStorage.setItem('docling-theme', v))
|
||||
watch(locale, (v) => localStorage.setItem('docling-locale', v))
|
||||
|
||||
watchEffect(() => {
|
||||
document.documentElement.classList.toggle('light', theme.value === 'light')
|
||||
})
|
||||
|
||||
function setTheme(t) { theme.value = t }
|
||||
function setLocale(l) { locale.value = l }
|
||||
|
||||
return { apiUrl, theme, locale, setTheme, setLocale }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,27 @@
|
|||
<template>
|
||||
<div class="settings-panel">
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">API URL</label>
|
||||
<input class="setting-input" v-model="store.apiUrl" readonly />
|
||||
<label class="setting-label">{{ t('settings.theme') }}</label>
|
||||
<div class="setting-toggle">
|
||||
<button :class="{ active: store.theme === 'dark' }" @click="store.setTheme('dark')">
|
||||
{{ t('settings.themeDark') }}
|
||||
</button>
|
||||
<button :class="{ active: store.theme === 'light' }" @click="store.setTheme('light')">
|
||||
{{ t('settings.themeLight') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">Version</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">{{ t('settings.version') }}</label>
|
||||
<span class="setting-value">0.1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -13,7 +29,10 @@
|
|||
|
||||
<script setup>
|
||||
import { useSettingsStore } from '../store.js'
|
||||
import { useI18n } from '../../../shared/i18n.js'
|
||||
|
||||
const store = useSettingsStore()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -38,6 +57,36 @@ const store = useSettingsStore()
|
|||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.setting-toggle {
|
||||
display: flex;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setting-toggle button {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.setting-toggle button:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.setting-toggle button.active {
|
||||
background: var(--accent-muted);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="history-page">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Analysis History</h1>
|
||||
<h1 class="page-title">{{ t('history.title') }}</h1>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<HistoryList />
|
||||
|
|
@ -12,8 +12,10 @@
|
|||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { HistoryList, useHistoryStore } from '../features/history/index.js'
|
||||
import { useI18n } from '../shared/i18n.js'
|
||||
|
||||
const store = useHistoryStore()
|
||||
const { t } = useI18n()
|
||||
onMounted(() => store.load())
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="settings-page">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Settings</h1>
|
||||
<h1 class="page-title">{{ t('settings.title') }}</h1>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<SettingsPanel />
|
||||
|
|
@ -11,6 +11,9 @@
|
|||
|
||||
<script setup>
|
||||
import { SettingsPanel } from '../features/settings/index.js'
|
||||
import { useI18n } from '../shared/i18n.js'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
<div class="import-logo">
|
||||
<span class="logo-icon">D</span>
|
||||
</div>
|
||||
<h1 class="import-title">Intelligence des documents</h1>
|
||||
<p class="import-subtitle">Importez un document PDF pour commencer l'analyse avec Docling</p>
|
||||
<h1 class="import-title">{{ t('studio.title') }}</h1>
|
||||
<p class="import-subtitle">{{ t('studio.subtitle') }}</p>
|
||||
<DocumentUpload />
|
||||
<div class="import-docs" v-if="documentStore.documents.length">
|
||||
<label class="section-label">Documents récents</label>
|
||||
<label class="section-label">{{ t('studio.recentDocs') }}</label>
|
||||
<DocumentList />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -20,25 +20,25 @@
|
|||
<!-- Top bar -->
|
||||
<div class="studio-topbar">
|
||||
<div class="topbar-left">
|
||||
<h1 class="topbar-title">Intelligence des documents</h1>
|
||||
<h1 class="topbar-title">{{ t('studio.title') }}</h1>
|
||||
<div class="mode-toggle">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: mode === 'configurer' }"
|
||||
@click="mode = 'configurer'"
|
||||
>Configurer</button>
|
||||
>{{ t('studio.configure') }}</button>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: mode === 'verifier' }"
|
||||
@click="mode = 'verifier'"
|
||||
:disabled="!analysisStore.currentAnalysis"
|
||||
>Vérifier</button>
|
||||
>{{ t('studio.verify') }}</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>
|
||||
Ajouter des fichiers
|
||||
{{ t('studio.addFiles') }}
|
||||
</button>
|
||||
<button
|
||||
class="topbar-btn primary"
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
>
|
||||
<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>
|
||||
{{ analysisStore.running ? 'Analyse...' : 'Ex\u00e9cuter' }}
|
||||
{{ analysisStore.running ? t('studio.analyzing') : t('studio.run') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
<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>
|
||||
<span class="doc-filename">{{ selectedDoc.filename }}</span>
|
||||
<span class="doc-status-chip loaded">Chargé</span>
|
||||
<span class="doc-status-chip loaded">{{ t('studio.loaded') }}</span>
|
||||
</div>
|
||||
<div class="doc-info-right" v-if="analysisStore.currentAnalysis">
|
||||
<span class="info-badge" v-if="analysisStore.currentAnalysis.status === 'COMPLETED'">
|
||||
|
|
@ -67,11 +67,11 @@
|
|||
</span>
|
||||
<span class="info-badge" v-if="analysisStore.currentAnalysis.status === 'RUNNING'">
|
||||
<div class="spinner-xs" />
|
||||
Analyse en cours...
|
||||
{{ t('studio.analysisRunning') }}
|
||||
</span>
|
||||
<span class="info-badge error" v-if="analysisStore.currentAnalysis.status === 'FAILED'">
|
||||
<span class="info-dot error" />
|
||||
Échec
|
||||
{{ t('studio.failed') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
@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>
|
||||
Visuel
|
||||
{{ t('studio.visual') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
<div v-if="mode === 'configurer'" class="config-panel">
|
||||
<div class="config-section">
|
||||
<label class="config-label">
|
||||
Modèle
|
||||
{{ t('config.model') }}
|
||||
<span class="config-hint">?</span>
|
||||
</label>
|
||||
<div class="config-select-display">
|
||||
|
|
@ -149,26 +149,26 @@
|
|||
|
||||
<div class="config-section">
|
||||
<label class="config-label">
|
||||
Pages
|
||||
{{ t('config.pages') }}
|
||||
<span class="config-hint">?</span>
|
||||
</label>
|
||||
<input type="text" class="config-input" placeholder='par ex. "1-4,8"' v-model="pageRange" />
|
||||
<input type="text" class="config-input" :placeholder="t('config.pagesPlaceholder')" v-model="pageRange" />
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<label class="config-label">
|
||||
Extraire les tableaux
|
||||
{{ t('config.extractTables') }}
|
||||
<span class="config-hint">?</span>
|
||||
</label>
|
||||
<select class="config-select" v-model="tableMode">
|
||||
<option value="markdown">Markdown intégré</option>
|
||||
<option value="markdown">{{ t('config.markdownIntegrated') }}</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="csv">CSV</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<label class="config-label">Extraire</label>
|
||||
<label class="config-label">{{ t('config.extract') }}</label>
|
||||
<div class="extract-options">
|
||||
<button
|
||||
v-for="opt in extractOptions"
|
||||
|
|
@ -187,15 +187,15 @@
|
|||
|
||||
<div class="config-section">
|
||||
<label class="config-label">
|
||||
Annoter les images
|
||||
{{ t('config.annotateImages') }}
|
||||
<span class="config-hint">?</span>
|
||||
</label>
|
||||
<button class="config-add-btn">Ajouter +</button>
|
||||
<button class="config-add-btn">{{ t('config.add') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Documents list at bottom -->
|
||||
<div class="config-section config-docs">
|
||||
<label class="config-label">Documents</label>
|
||||
<label class="config-label">{{ t('config.documents') }}</label>
|
||||
<DocumentList />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -217,9 +217,11 @@ import { DocumentUpload, DocumentList } from '../features/document/index.js'
|
|||
import { ResultTabs } from '../features/analysis/index.js'
|
||||
import BboxOverlay from '../features/analysis/ui/BboxOverlay.vue'
|
||||
import { getPreviewUrl } from '../features/document/api.js'
|
||||
import { useI18n } from '../shared/i18n.js'
|
||||
|
||||
const documentStore = useDocumentStore()
|
||||
const analysisStore = useAnalysisStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const mode = ref('configurer')
|
||||
const currentPage = ref(1)
|
||||
|
|
@ -242,11 +244,11 @@ function onPdfImageLoad() {
|
|||
nextTick(() => bboxOverlayRef.value?.draw())
|
||||
}
|
||||
|
||||
const extractOptions = [
|
||||
{ id: 'images', label: 'Images', icon: 'image' },
|
||||
{ id: 'header', label: 'En-t\u00eate', icon: 'header' },
|
||||
{ id: 'footer', label: 'Pied de page', icon: 'footer' }
|
||||
]
|
||||
const extractOptions = computed(() => [
|
||||
{ id: 'images', label: t('config.images'), icon: 'image' },
|
||||
{ id: 'header', label: t('config.header'), icon: 'header' },
|
||||
{ id: 'footer', label: t('config.footer'), icon: 'footer' }
|
||||
])
|
||||
const activeExtracts = reactive(new Set(['images']))
|
||||
|
||||
function toggleExtract(id) {
|
||||
|
|
|
|||
140
frontend/src/shared/i18n.js
Normal file
140
frontend/src/shared/i18n.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { useSettingsStore } from '../features/settings/store.js'
|
||||
|
||||
const messages = {
|
||||
fr: {
|
||||
// Sidebar
|
||||
'nav.studio': 'Studio',
|
||||
'nav.history': 'Historique',
|
||||
'nav.settings': 'Paramètres',
|
||||
|
||||
// Studio — import
|
||||
'studio.title': 'Intelligence des documents',
|
||||
'studio.subtitle': 'Importez un document PDF pour commencer l\'analyse avec Docling',
|
||||
'studio.recentDocs': 'Documents récents',
|
||||
|
||||
// Studio — workspace
|
||||
'studio.configure': 'Configurer',
|
||||
'studio.verify': 'Vérifier',
|
||||
'studio.addFiles': 'Ajouter des fichiers',
|
||||
'studio.analyzing': 'Analyse...',
|
||||
'studio.run': 'Exécuter',
|
||||
'studio.loaded': 'Chargé',
|
||||
'studio.analysisRunning': 'Analyse en cours...',
|
||||
'studio.failed': 'Échec',
|
||||
'studio.visual': 'Visuel',
|
||||
|
||||
// Config panel
|
||||
'config.model': 'Modèle',
|
||||
'config.pages': 'Pages',
|
||||
'config.pagesPlaceholder': 'par ex. "1-4,8"',
|
||||
'config.extractTables': 'Extraire les tableaux',
|
||||
'config.markdownIntegrated': 'Markdown intégré',
|
||||
'config.extract': 'Extraire',
|
||||
'config.images': 'Images',
|
||||
'config.header': 'En-tête',
|
||||
'config.footer': 'Pied de page',
|
||||
'config.annotateImages': 'Annoter les images',
|
||||
'config.add': 'Ajouter +',
|
||||
'config.documents': 'Documents',
|
||||
|
||||
// Results
|
||||
'results.textResult': 'Résultat du texte',
|
||||
'results.markdown': 'Markdown',
|
||||
'results.images': 'Images',
|
||||
'results.pageOf': 'Page {current} sur {total}',
|
||||
'results.noImages': 'Aucune image détectée dans ce document',
|
||||
'results.noMarkdown': 'Pas de contenu markdown',
|
||||
'results.runAnalysis': 'Lancez une analyse pour voir les résultats',
|
||||
'results.analysisFailed': 'L\'analyse a échoué',
|
||||
'results.page': 'Page',
|
||||
|
||||
// Upload
|
||||
'upload.drop': 'Déposez un PDF ici ou cliquez pour importer',
|
||||
'upload.uploading': 'Import en cours...',
|
||||
'upload.maxSize': 'Max 50Mo',
|
||||
|
||||
// History
|
||||
'history.title': 'Historique des analyses',
|
||||
'history.empty': 'Aucune analyse. Allez dans Studio pour analyser votre premier document.',
|
||||
|
||||
// Settings
|
||||
'settings.title': 'Paramètres',
|
||||
'settings.apiUrl': 'API URL',
|
||||
'settings.version': 'Version',
|
||||
'settings.theme': 'Thème',
|
||||
'settings.themeDark': 'Sombre',
|
||||
'settings.themeLight': 'Clair',
|
||||
'settings.language': 'Langue',
|
||||
},
|
||||
en: {
|
||||
'nav.studio': 'Studio',
|
||||
'nav.history': 'History',
|
||||
'nav.settings': 'Settings',
|
||||
|
||||
'studio.title': 'Document Intelligence',
|
||||
'studio.subtitle': 'Upload a PDF document to start analyzing with Docling',
|
||||
'studio.recentDocs': 'Recent documents',
|
||||
|
||||
'studio.configure': 'Configure',
|
||||
'studio.verify': 'Verify',
|
||||
'studio.addFiles': 'Add files',
|
||||
'studio.analyzing': 'Analyzing...',
|
||||
'studio.run': 'Run',
|
||||
'studio.loaded': 'Loaded',
|
||||
'studio.analysisRunning': 'Analysis running...',
|
||||
'studio.failed': 'Failed',
|
||||
'studio.visual': 'Visual',
|
||||
|
||||
'config.model': 'Model',
|
||||
'config.pages': 'Pages',
|
||||
'config.pagesPlaceholder': 'e.g. "1-4,8"',
|
||||
'config.extractTables': 'Extract tables',
|
||||
'config.markdownIntegrated': 'Inline Markdown',
|
||||
'config.extract': 'Extract',
|
||||
'config.images': 'Images',
|
||||
'config.header': 'Header',
|
||||
'config.footer': 'Footer',
|
||||
'config.annotateImages': 'Annotate images',
|
||||
'config.add': 'Add +',
|
||||
'config.documents': 'Documents',
|
||||
|
||||
'results.textResult': 'Text result',
|
||||
'results.markdown': 'Markdown',
|
||||
'results.images': 'Images',
|
||||
'results.pageOf': 'Page {current} of {total}',
|
||||
'results.noImages': 'No images detected in this document',
|
||||
'results.noMarkdown': 'No markdown content',
|
||||
'results.runAnalysis': 'Run an analysis to see results',
|
||||
'results.analysisFailed': 'Analysis failed',
|
||||
'results.page': 'Page',
|
||||
|
||||
'upload.drop': 'Drop a PDF here or click to upload',
|
||||
'upload.uploading': 'Uploading...',
|
||||
'upload.maxSize': 'Max 50MB',
|
||||
|
||||
'history.title': 'Analysis History',
|
||||
'history.empty': 'No analyses yet. Go to Studio to analyze your first document.',
|
||||
|
||||
'settings.title': 'Settings',
|
||||
'settings.apiUrl': 'API URL',
|
||||
'settings.version': 'Version',
|
||||
'settings.theme': 'Theme',
|
||||
'settings.themeDark': 'Dark',
|
||||
'settings.themeLight': 'Light',
|
||||
'settings.language': 'Language',
|
||||
}
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const settings = useSettingsStore()
|
||||
|
||||
function t(key, params = {}) {
|
||||
let str = messages[settings.locale]?.[key] || messages['fr'][key] || key
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
str = str.replace(`{${k}}`, v)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
return { t }
|
||||
}
|
||||
|
|
@ -10,17 +10,17 @@
|
|||
<nav class="sidebar-nav">
|
||||
<RouterLink to="/" 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>
|
||||
<span>Studio</span>
|
||||
<span>{{ t('nav.studio') }}</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>
|
||||
<span>History</span>
|
||||
<span>{{ 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>
|
||||
<span>Settings</span>
|
||||
<span>{{ t('nav.settings') }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
|
|
@ -32,7 +32,10 @@
|
|||
|
||||
<script setup>
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { useI18n } from '../i18n.js'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
Loading…
Reference in a new issue