Allow History consultaiton on click, add documents management tab

This commit is contained in:
pjmalandrino 2026-03-20 09:37:40 +01:00
parent 55f3f22c4b
commit 7821cd397b
5 changed files with 212 additions and 9 deletions

View file

@ -8,6 +8,7 @@
v-for="analysis in store.analyses" v-for="analysis in store.analyses"
:key="analysis.id" :key="analysis.id"
class="history-item" class="history-item"
@click="openAnalysis(analysis)"
> >
<div class="item-main"> <div class="item-main">
<div class="item-header"> <div class="item-header">
@ -21,7 +22,7 @@
<span v-if="analysis.completedAt"> {{ duration(analysis) }}</span> <span v-if="analysis.completedAt"> {{ duration(analysis) }}</span>
</div> </div>
</div> </div>
<button class="item-delete" @click="store.remove(analysis.id)" title="Delete"> <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> </button>
</div> </div>
@ -30,12 +31,18 @@
</template> </template>
<script setup> <script setup>
import { useRouter } from 'vue-router'
import { useHistoryStore } from '../store.js' import { useHistoryStore } from '../store.js'
import { useI18n } from '../../../shared/i18n.js' import { useI18n } from '../../../shared/i18n.js'
const store = useHistoryStore() const store = useHistoryStore()
const router = useRouter()
const { t } = useI18n() const { t } = useI18n()
function openAnalysis(analysis) {
router.push({ name: 'studio', query: { analysisId: analysis.id } })
}
function statusClass(status) { function statusClass(status) {
return { return {
'status-pending': status === 'PENDING', 'status-pending': status === 'PENDING',
@ -83,6 +90,7 @@ function duration(analysis) {
padding: 14px 20px; padding: 14px 20px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
transition: background var(--transition); transition: background var(--transition);
cursor: pointer;
} }
.history-item:hover { background: var(--bg-hover); } .history-item:hover { background: var(--bg-hover); }

View file

@ -2,21 +2,80 @@
<div class="history-page"> <div class="history-page">
<div class="page-header"> <div class="page-header">
<h1 class="page-title">{{ t('history.title') }}</h1> <h1 class="page-title">{{ t('history.title') }}</h1>
<div class="tab-bar">
<button
class="tab-btn"
:class="{ active: tab === 'analyses' }"
@click="tab = 'analyses'"
>{{ t('history.tabAnalyses') }}</button>
<button
class="tab-btn"
:class="{ active: tab === 'documents' }"
@click="tab = 'documents'"
>{{ t('history.tabDocuments') }}</button>
</div>
</div> </div>
<div class="page-content"> <div class="page-content">
<HistoryList /> <HistoryList v-if="tab === 'analyses'" />
<div v-else class="doc-tab">
<div v-if="docStore.documents.length === 0" class="tab-empty">
{{ t('history.emptyDocs') }}
</div>
<div v-else class="doc-items">
<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"/>
</svg>
<div class="doc-row-meta">
<span class="doc-row-name">{{ doc.filename }}</span>
<span class="doc-row-detail">
{{ formatSize(doc.fileSize) }}
<template v-if="doc.pageCount"> {{ doc.pageCount }} pages</template>
<template v-if="doc.createdAt"> {{ formatDate(doc.createdAt) }}</template>
</span>
</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>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { HistoryList, useHistoryStore } from '../features/history/index.js' import { HistoryList, useHistoryStore } from '../features/history/index.js'
import { useDocumentStore } from '../features/document/store.js'
import { useI18n } from '../shared/i18n.js' import { useI18n } from '../shared/i18n.js'
const store = useHistoryStore() const historyStore = useHistoryStore()
const docStore = useDocumentStore()
const { t } = useI18n() const { t } = useI18n()
onMounted(() => store.load()) const tab = ref('analyses')
function formatSize(bytes) {
if (!bytes) return ''
const mb = bytes / (1024 * 1024)
return mb >= 1 ? `${mb.toFixed(1)} MB` : `${(bytes / 1024).toFixed(0)} KB`
}
function formatDate(iso) {
if (!iso) return ''
return new Date(iso).toLocaleString()
}
onMounted(() => {
historyStore.load()
docStore.load()
})
</script> </script>
<style scoped> <style scoped>
@ -31,6 +90,9 @@ onMounted(() => store.load())
padding: 16px 24px; padding: 16px 24px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
} }
.page-title { .page-title {
@ -39,8 +101,115 @@ onMounted(() => store.load())
color: var(--text); color: var(--text);
} }
.tab-bar {
display: flex;
gap: 4px;
background: var(--bg-secondary);
border-radius: 8px;
padding: 3px;
}
.tab-btn {
background: none;
border: none;
padding: 6px 16px;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
border-radius: 6px;
cursor: pointer;
transition: all var(--transition);
}
.tab-btn:hover {
color: var(--text);
}
.tab-btn.active {
background: var(--bg-primary);
color: var(--text);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.page-content { .page-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
.tab-empty {
text-align: center;
color: var(--text-muted);
padding: 60px 20px;
font-size: 14px;
}
.doc-items {
display: flex;
flex-direction: column;
gap: 2px;
}
.doc-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid var(--border);
transition: background var(--transition);
}
.doc-row:hover { background: var(--bg-hover); }
.doc-row-info {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
flex: 1;
}
.doc-row-icon {
width: 18px;
height: 18px;
color: var(--accent);
flex-shrink: 0;
}
.doc-row-meta {
display: flex;
flex-direction: column;
min-width: 0;
}
.doc-row-name {
font-size: 14px;
font-weight: 500;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.doc-row-detail {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
font-family: 'IBM Plex Mono', monospace;
}
.doc-row-delete {
background: none;
border: none;
padding: 6px;
cursor: pointer;
color: var(--text-muted);
border-radius: 4px;
display: flex;
opacity: 0;
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; }
</style> </style>

View file

@ -269,6 +269,7 @@
<script setup> <script setup>
import { ref, computed, watch, nextTick, onMounted, reactive } from 'vue' import { ref, computed, watch, nextTick, onMounted, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useDocumentStore } from '../features/document/store.js' import { useDocumentStore } from '../features/document/store.js'
import { useAnalysisStore } from '../features/analysis/store.js' import { useAnalysisStore } from '../features/analysis/store.js'
import { DocumentUpload, DocumentList } from '../features/document/index.js' import { DocumentUpload, DocumentList } from '../features/document/index.js'
@ -277,6 +278,8 @@ import BboxOverlay from '../features/analysis/ui/BboxOverlay.vue'
import { getPreviewUrl } from '../features/document/api.js' import { getPreviewUrl } from '../features/document/api.js'
import { useI18n } from '../shared/i18n.js' import { useI18n } from '../shared/i18n.js'
const route = useRoute()
const router = useRouter()
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
const analysisStore = useAnalysisStore() const analysisStore = useAnalysisStore()
const { t } = useI18n() const { t } = useI18n()
@ -346,9 +349,24 @@ watch(() => analysisStore.currentAnalysis?.status, (status) => {
} }
}) })
onMounted(() => { onMounted(async () => {
documentStore.load() await documentStore.load()
analysisStore.load() analysisStore.load()
// Restore analysis from history via query param
const analysisId = route.query.analysisId
if (analysisId) {
await analysisStore.select(analysisId)
const analysis = analysisStore.currentAnalysis
if (analysis) {
documentStore.select(analysis.documentId)
if (analysis.status === 'COMPLETED') {
mode.value = 'verifier'
}
}
// Clean query param from URL
router.replace({ query: {} })
}
}) })
</script> </script>

View file

@ -67,8 +67,12 @@ const messages = {
'upload.maxSize': 'Max 50Mo', 'upload.maxSize': 'Max 50Mo',
// History // History
'history.title': 'Historique des analyses', 'history.title': 'Historique',
'history.tabAnalyses': 'Analyses',
'history.tabDocuments': 'Documents',
'history.empty': 'Aucune analyse. Allez dans Studio pour analyser votre premier document.', 'history.empty': 'Aucune analyse. Allez dans Studio pour analyser votre premier document.',
'history.emptyDocs': 'Aucun document. Importez un document depuis le Studio.',
'history.open': 'Ouvrir',
// Settings // Settings
'settings.title': 'Paramètres', 'settings.title': 'Paramètres',
@ -138,8 +142,12 @@ const messages = {
'upload.uploading': 'Uploading...', 'upload.uploading': 'Uploading...',
'upload.maxSize': 'Max 50MB', 'upload.maxSize': 'Max 50MB',
'history.title': 'Analysis History', 'history.title': 'History',
'history.tabAnalyses': 'Analyses',
'history.tabDocuments': 'Documents',
'history.empty': 'No analyses yet. Go to Studio to analyze your first document.', 'history.empty': 'No analyses yet. Go to Studio to analyze your first document.',
'history.emptyDocs': 'No documents yet. Upload a document from the Studio.',
'history.open': 'Open',
'settings.title': 'Settings', 'settings.title': 'Settings',
'settings.apiUrl': 'API URL', 'settings.apiUrl': 'API URL',