diff --git a/web/app.py b/web/app.py index 7f3c177..f037cdd 100644 --- a/web/app.py +++ b/web/app.py @@ -565,6 +565,62 @@ async def api_learn(case_number: str): return {"status": "final", "message": "לולאת למידה הופעלה — גרסה סופית נקלטה"} +# ── Local files API — research, drafts, proofread ── + + +@app.get("/api/cases/{case_number}/local-files") +async def api_local_files(case_number: str): + """List local files from case subdirectories (research, drafts, proofread).""" + case_dir = config.find_case_dir(case_number) + result = {} + for folder in ("research", "proofread"): + folder_path = case_dir / "documents" / folder + if folder_path.exists(): + files = [] + for f in sorted(folder_path.iterdir()): + if f.is_file() and not f.name.startswith("."): + stat = f.stat() + files.append({ + "filename": f.name, + "size": stat.st_size, + "modified_at": stat.st_mtime, + "folder": folder, + }) + if files: + result[folder] = files + # Drafts are at case level, not under documents + drafts_path = case_dir / "drafts" + if drafts_path.exists(): + files = [] + for f in sorted(drafts_path.iterdir()): + if f.is_file() and not f.name.startswith("."): + stat = f.stat() + files.append({ + "filename": f.name, + "size": stat.st_size, + "modified_at": stat.st_mtime, + "folder": "drafts", + }) + if files: + result["drafts"] = files + return result + + +@app.get("/api/cases/{case_number}/local-files/{folder}/{filename}") +async def api_read_local_file(case_number: str, folder: str, filename: str): + """Read contents of a local case file.""" + if folder not in ("research", "proofread", "drafts"): + raise HTTPException(400, "Invalid folder") + case_dir = config.find_case_dir(case_number) + if folder == "drafts": + path = case_dir / "drafts" / filename + else: + path = case_dir / "documents" / folder / filename + if not path.exists() or not path.is_file(): + raise HTTPException(404, "קובץ לא נמצא") + return FileResponse(path, media_type="text/plain; charset=utf-8", filename=filename) + + # ── Exports API — drafts, versions, download, upload, mark-final ── diff --git a/web/static/index.html b/web/static/index.html index a945b81..a17b5e4 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -154,6 +154,14 @@ header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255, .doc-item .doc-icon { color: #999; } .doc-item .doc-name { flex: 1; } .doc-item .doc-status { font-size: 0.75em; color: #999; } +.doc-status.completed { color: #27ae60; font-weight: 700; font-size: 1em; } +.doc-status.processing { display: inline-block; width: 14px; height: 14px; border: 2px solid #e94560; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; } +.doc-status.pending { color: #ccc; } +.doc-status.failed { color: #e94560; font-weight: 700; } +.btn-retry { background: none; border: 1px solid #e94560; color: #e94560; border-radius: 4px; padding: 2px 8px; font-size: 0.75em; cursor: pointer; margin-right: 6px; } +.btn-retry:hover { background: #e94560; color: #fff; } +.processing-badge { display: inline-flex; align-items: center; gap: 4px; color: #e94560; font-size: 0.78em; font-weight: 500; } +.mini-spinner { display: inline-block; width: 10px; height: 10px; border: 1.5px solid #e94560; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; } /* Upload zone (reusable) */ .upload-zone { @@ -217,6 +225,19 @@ header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255, .toast.error { background: #e94560; } .toast.success { background: #27ae60; } +/* ── Local files (research, drafts, proofread) ───────── */ +.local-file-group { margin-bottom: 12px; } +.local-file-group-header { font-size: 0.82em; font-weight: 600; color: #666; margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid #eee; } +.local-file-item { + display: flex; align-items: center; gap: 10px; padding: 8px 12px; + border: 1px solid #eee; border-radius: 6px; margin-bottom: 4px; background: #fafafa; + cursor: pointer; transition: background 0.15s; +} +.local-file-item:hover { background: #f0f0f0; } +.local-file-item .lf-icon { flex-shrink: 0; font-size: 1.1em; } +.local-file-item .lf-name { flex: 1; font-size: 0.85em; font-weight: 500; word-break: break-all; } +.local-file-item .lf-meta { font-size: 0.72em; color: #999; flex-shrink: 0; } + /* ── Exports / Drafts ─────────────────────────────────── */ .export-item { display: flex; align-items: center; gap: 12px; padding: 10px 14px; @@ -448,6 +469,14 @@ header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255, + +
+
מחקר וניתוח
+
+
אין קבצי מחקר
+
+
+
@@ -544,6 +573,12 @@ function navigate(path) { } function handleRoute() { + // Clean up polling timer when navigating away + if (window._docPollTimer) { + clearInterval(window._docPollTimer); + window._docPollTimer = null; + } + const hash = window.location.hash || '#/'; const pages = document.querySelectorAll('.page'); pages.forEach(p => p.classList.remove('active')); @@ -611,6 +646,7 @@ async function loadCaseList() {
${esc(c.title)}
${c.document_count !== undefined ? `${c.document_count} מסמכים` : ''} + ${c.processing_count > 0 ? ` ${c.processing_count} בעיבוד` : ''} ${c.committee_type ? `${esc(c.committee_type)}` : ''} ${c.hearing_date ? `דיון: ${esc(c.hearing_date)}` : ''}
@@ -834,16 +870,37 @@ async function loadCaseView(caseNumber) { ${groups[type].length}
`; for (const doc of groups[type]) { - const statusIcon = doc.extraction_status === 'completed' ? '✓' : '◯'; + let statusHtml; + if (doc.extraction_status === 'completed') { + statusHtml = ''; + } else if (doc.extraction_status === 'processing') { + statusHtml = ''; + } else if (doc.extraction_status === 'failed') { + statusHtml = ``; + } else { + statusHtml = ''; + } html += `
📄 ${esc(doc.title)} - ${statusIcon} + ${statusHtml}
`; } html += '
'; } document.getElementById('caseDocsList').innerHTML = html; + + // Auto-refresh while documents are still processing + if (data.documents?.some(d => d.extraction_status !== 'completed')) { + if (!window._docPollTimer) { + window._docPollTimer = setInterval(() => loadCaseView(caseNumber), 5000); + } + } else { + if (window._docPollTimer) { + clearInterval(window._docPollTimer); + window._docPollTimer = null; + } + } } } catch (e) { document.getElementById('caseViewTitle').textContent = `תיק ${caseNumber} לא נמצא`; @@ -858,10 +915,58 @@ async function loadCaseView(caseNumber) { } setupCaseUpload(caseNumber); + loadLocalFiles(caseNumber); loadExports(caseNumber); setupExportUpload(caseNumber); } +const FOLDER_LABELS = { research: 'מחקר וניתוח', proofread: 'טקסט מוגה', drafts: 'טיוטות' }; +const FOLDER_ICONS = { research: '📚', proofread: '✅', drafts: '📝' }; + +async function loadLocalFiles(caseNumber) { + const container = document.getElementById('caseLocalFiles'); + try { + const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/local-files'); + const data = await res.json(); + const folders = Object.keys(data); + if (!folders.length) { + container.innerHTML = '
אין קבצי מחקר או ניתוח
'; + return; + } + let html = ''; + for (const folder of ['research', 'drafts', 'proofread']) { + if (!data[folder]) continue; + html += `
+
${FOLDER_LABELS[folder] || folder} (${data[folder].length})
`; + for (const f of data[folder]) { + const date = new Date(f.modified_at * 1000); + const dateStr = date.toLocaleDateString('he-IL'); + const url = API + '/cases/' + encodeURIComponent(caseNumber) + '/local-files/' + encodeURIComponent(folder) + '/' + encodeURIComponent(f.filename); + html += ` +
+ ${FOLDER_ICONS[folder] || '📄'} + ${esc(f.filename)} + ${dateStr} · ${formatSize(f.size)} +
`; + } + html += '
'; + } + container.innerHTML = html; + } catch (e) { + container.innerHTML = '
שגיאה בטעינת קבצים מקומיים
'; + } +} + +async function retryDoc(caseNumber, docId) { + const btn = event.target; + btn.disabled = true; + btn.textContent = '...'; + try { + await fetch(`/api/cases/${caseNumber}/documents/${docId}/reprocess`, {method: 'POST'}); + } catch (e) { /* will show on next refresh */ } + loadCaseView(caseNumber); +} + async function openPaperclip(caseNumber) { // Try to find the Paperclip project URL try {