diff --git a/frontend/src/features/analysis/ui/ResultTabs.vue b/frontend/src/features/analysis/ui/ResultTabs.vue index 94031c3..afb7897 100644 --- a/frontend/src/features/analysis/ui/ResultTabs.vue +++ b/frontend/src/features/analysis/ui/ResultTabs.vue @@ -109,20 +109,54 @@
-
- {{ t('studio.analysisRunning') }} +
-
-
+
+ + + + + {{ progressPercent }}% +
+
+ {{ t('studio.analysisRunning') }} +
+
+
+ + {{ store.currentAnalysis.progressCurrent ?? 0 }} + / + {{ store.currentAnalysis.progressTotal }} pages +
- - Pages {{ store.currentAnalysis.progressCurrent ?? 0 }} / - {{ store.currentAnalysis.progressTotal }} -
+ +
{ return Math.min(100, Math.round(((a.progressCurrent ?? 0) / a.progressTotal) * 100)) }) +/** Number of batch segments to render in the segmented progress bar. */ +const batchSegments = computed(() => { + const a = store.currentAnalysis + if (!a?.progressTotal || a.progressTotal <= 0) return 0 + // Each segment = batch_page_size pages. We infer count from total & current. + // Backend updates progressCurrent in increments, so we derive segment count. + const current = a.progressCurrent ?? 0 + if (current <= 0) return 0 + // Guess batch size from the first non-zero progressCurrent value. + // Fallback: assume ~5 segments for a clean visual. + const total = a.progressTotal + // We can't know batch_page_size on frontend, so compute a clean segment count + // by aiming for 3-8 segments (visually optimal). + let count = Math.round(total / Math.max(1, current || total / 5)) + if (count < 2) count = Math.ceil(total / Math.ceil(total / 5)) + return Math.max(2, Math.min(8, count)) +}) + +/** How many segments are "filled" (completed batches). */ +const filledSegments = computed(() => { + const total = batchSegments.value + if (total <= 0) return 0 + return Math.round((progressPercent.value / 100) * total) +}) + const currentPageData = computed(() => { return store.currentPages.find((p) => p.page_number === props.currentPage) || null }) @@ -534,29 +593,98 @@ async function copyElement(idx: number, content: string) { } } +/* ── Batch progress: ring + segmented bar ── */ .batch-progress { display: flex; - flex-direction: column; align-items: center; - gap: 6px; - margin-top: 8px; - width: 200px; + gap: 20px; } -.progress-bar { + +/* Circular ring */ +.batch-progress-ring { + position: relative; + width: 56px; + height: 56px; + flex-shrink: 0; +} +.progress-ring-svg { width: 100%; - height: 6px; - background: var(--border-light); - border-radius: 3px; - overflow: hidden; -} -.progress-fill { height: 100%; - background: var(--accent); - border-radius: 3px; - transition: width 0.3s ease; + transform: rotate(-90deg); } -.progress-text { - font-size: 0.8rem; - color: var(--text-secondary); +.progress-ring-fill { + transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1); + filter: drop-shadow(0 0 4px rgba(249, 115, 22, 0.4)); +} +.progress-ring-label { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + font-family: 'IBM Plex Mono', monospace; + color: var(--text); +} + +/* Right side: text + segments */ +.batch-progress-detail { + display: flex; + flex-direction: column; + gap: 6px; +} +.batch-progress-title { + font-size: 13px; + font-weight: 500; + color: var(--text); +} + +/* Segmented bar */ +.batch-segments { + display: flex; + gap: 3px; +} +.batch-segment { + width: 28px; + height: 5px; + border-radius: 2.5px; + background: var(--border); + transition: + background 0.4s ease, + box-shadow 0.4s ease; +} +.batch-segment.filled { + background: var(--accent); + box-shadow: 0 0 6px rgba(249, 115, 22, 0.3); +} +.batch-segment.active { + background: var(--accent-muted); + animation: segment-pulse 1.5s ease-in-out infinite; +} +@keyframes segment-pulse { + 0%, + 100% { + background: var(--accent-muted); + } + 50% { + background: var(--accent); + box-shadow: 0 0 8px rgba(249, 115, 22, 0.35); + } +} + +/* Page counter */ +.batch-progress-sub { + font-size: 12px; + font-family: 'IBM Plex Mono', monospace; + color: var(--text-muted); +} +.batch-progress-pages { + color: var(--accent); + font-weight: 600; +} +.batch-progress-sep { + margin: 0 2px; + opacity: 0.4; } diff --git a/frontend/src/pages/StudioPage.vue b/frontend/src/pages/StudioPage.vue index 51040bf..b0801c5 100644 --- a/frontend/src/pages/StudioPage.vue +++ b/frontend/src/pages/StudioPage.vue @@ -118,6 +118,35 @@
{{ t('studio.analysisRunning') }} + + + + + {{ analysisStore.currentAnalysis.progressCurrent ?? 0 }}/{{ + analysisStore.currentAnalysis.progressTotal + }} + @@ -867,6 +896,32 @@ onBeforeUnmount(() => { animation: spin 0.6s linear infinite; } +/* Inline mini progress in top bar */ +.info-badge-progress { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: 4px; +} +.info-badge-bar { + width: 48px; + height: 3px; + background: var(--border); + border-radius: 1.5px; + overflow: hidden; +} +.info-badge-fill { + display: block; + height: 100%; + background: var(--accent); + border-radius: 1.5px; + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} +.info-badge-count { + font-size: 11px; + color: var(--text-muted); +} + /* Main content */ .studio-main { flex: 1;