Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# GovDoc SecureFlow

**AI-powered government document processing with intelligent routing, classification, and multi-role workflow.**
<div align="center">
<img src="docs/logo.svg" alt="GovDoc SecureFlow Logo" width="120" height="120" />
<h1>GovDoc SecureFlow</h1>
<p><strong>AI-powered government document processing with intelligent routing, classification, and multi-role workflow.</strong></p>
</div>

![Python](https://img.shields.io/badge/python-3.12-blue)
![React](https://img.shields.io/badge/react-19-61dafb)
Expand Down Expand Up @@ -53,6 +55,51 @@ The result: faster processing, fewer routing errors, built-in audit trails, and

---

## Screenshots

Overview of the main UI flows. **Click an image** to open the full-size file in the repository.

<table>
<thead>
<tr>
<th align="center">Homepage Dashboard</th>
<th align="center">Upload &amp; Status Tracking</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center" valign="top">
<a href="docs/screenshots/1.homepage.png" title="Open full size — Homepage Dashboard">
<img src="docs/screenshots/1.homepage.png" alt="Homepage Dashboard" width="440" />
</a>
</td>
<td align="center" valign="top">
<a href="docs/screenshots/2.upload-status.png" title="Open full size — Upload &amp; status">
<img src="docs/screenshots/2.upload-status.png" alt="Upload and document status tracking" width="440" />
</a>
</td>
</tr>
<tr>
<th align="center">AI Classification &amp; Analysis</th>
<th align="center">Consultation Workflow</th>
</tr>
<tr>
<td align="center" valign="top">
<a href="docs/screenshots/3.classification.png" title="Open full size — Classification">
<img src="docs/screenshots/3.classification.png" alt="AI classification and analysis" width="440" />
</a>
</td>
<td align="center" valign="top">
<a href="docs/screenshots/4.consultantion.png" title="Open full size — Consultation">
<img src="docs/screenshots/4.consultantion.png" alt="Consultation workflow between departments" width="440" />
</a>
</td>
</tr>
</tbody>
</table>

---

## Architecture

```
Expand Down
36 changes: 36 additions & 0 deletions api/app/api/v1/endpoints/demo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from pathlib import Path

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from app.api import deps
from app.api.deps import require_action, CurrentRole
Expand All @@ -9,6 +12,39 @@

router = APIRouter(prefix="/demo", tags=["demo"])

# api/app/api/v1/endpoints/demo.py → parents[5] = repo root (govdoc/)
_PROJECT_ROOT = Path(__file__).resolve().parents[5]
_DEMO_SAMPLE_FILES = frozenset(
{
"sample-cong-van-dong-nai.pdf",
"sample-bao-cao-dong-nai.pdf",
}
)


def _demo_sample_pdf_dir() -> Path:
"""Prefer Docker-bundled PDFs; fall back to web/public/demo in local dev."""
docker_dir = Path("/app/demo-sample-pdfs")
if docker_dir.is_dir() and any(docker_dir.glob("*.pdf")):
return docker_dir
return _PROJECT_ROOT / "web" / "public" / "demo"


@router.get("/sample-files/{filename}")
async def download_demo_sample_file(filename: str) -> FileResponse:
"""Serve bundled demo PDFs via the API so downloads work through /api/ (nginx proxy) with correct MIME type."""
if filename not in _DEMO_SAMPLE_FILES:
raise HTTPException(status_code=404, detail="Unknown sample file")
path = _demo_sample_pdf_dir() / filename
if not path.is_file():
raise HTTPException(status_code=404, detail="Sample file not available on server")
return FileResponse(
path,
media_type="application/pdf",
filename=filename,
content_disposition_type="attachment",
)


@router.get("/scenarios", response_model=list[DemoScenarioOut])
async def list_scenarios(db: Session = Depends(deps.get_db)):
Expand Down
23 changes: 23 additions & 0 deletions api/tests/unit/test_demo_sample_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Demo sample PDF downloads — served via API for reliable MIME and proxy routing."""

import pytest
from fastapi.testclient import TestClient

from app.main import app


@pytest.mark.unit
def test_demo_sample_file_returns_pdf() -> None:
with TestClient(app) as client:
r = client.get("/api/v1/demo/sample-files/sample-cong-van-dong-nai.pdf")
assert r.status_code == 200
assert r.headers.get("content-type", "").startswith("application/pdf")
assert len(r.content) > 10_000
assert r.content[:4] == b"%PDF"


@pytest.mark.unit
def test_demo_sample_file_unknown_name_404() -> None:
with TestClient(app) as client:
r = client.get("/api/v1/demo/sample-files/not-a-real-file.pdf")
assert r.status_code == 404
4 changes: 4 additions & 0 deletions deploy/Dockerfile.api
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ RUN pip install --no-cache-dir -r /app/api/requirements.txt
COPY api/ /app/api/
COPY data/ /app/demo-data/

# Demo PDFs for /api/v1/demo/sample-files/* (same files as web/public/demo — web static can still serve /demo/)
RUN mkdir -p /app/demo-sample-pdfs
COPY web/public/demo/sample-cong-van-dong-nai.pdf web/public/demo/sample-bao-cao-dong-nai.pdf /app/demo-sample-pdfs/

# Ensure Python can find the 'app' package inside api/
ENV PYTHONPATH=/app/api

Expand Down
23 changes: 23 additions & 0 deletions docs/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/1.homepage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/2.upload-status.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/3.classification.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/4.consultantion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions web/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ server {
root /usr/share/nginx/html;
index index.html;

# Bundled demo PDFs live under /demo/ — must be real files in the image, never the SPA
# fallback (otherwise try_files serves index.html and browsers report a failed download).
location ^~ /demo/ {
try_files $uri =404;
}

# Proxy API calls to the FastAPI backend
location /api/ {
proxy_pass http://api:8000/api/;
Expand Down
Binary file added web/public/demo/sample-bao-cao-dong-nai.pdf
Binary file not shown.
Binary file added web/public/demo/sample-cong-van-dong-nai.pdf
Binary file not shown.
60 changes: 60 additions & 0 deletions web/src/components/demo-walkthrough.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Link } from 'react-router-dom';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui-card';
import { Sparkles } from 'lucide-react';

/** Shown only on the dashboard — home links here via `/dashboard#demo-walkthrough`. */
export function DemoWalkthrough() {
return (
<Card
id="demo-walkthrough"
className="scroll-mt-24 border-blue-100 bg-gradient-to-br from-blue-50/80 to-white shadow-sm"
>
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2">
<Sparkles className="h-4 w-4 text-blue-600" />
Demo walkthrough
</CardTitle>
<p className="text-xs font-medium text-slate-500">
Downloadable <span className="font-semibold text-slate-600">PDF</span> samples (sourced from the same incoming
corpus as <code className="rounded bg-slate-100 px-1 py-0.5 text-[11px]">./data/incoming/</code>) are on{' '}
<Link to="/intake#demo-samples" className="text-blue-700 underline-offset-2 hover:underline">
Intake → Demo sample files
</Link>
. Your full local pack under <code className="rounded bg-slate-100 px-1 py-0.5 text-[11px]">./data</code> may
contain additional PDFs for stress tests (not committed to git).
</p>
</CardHeader>
<CardContent className="space-y-3 text-sm text-slate-700">
<ol className="list-decimal space-y-2 pl-5">
<li>
Optional for workshops: on the{' '}
<Link to="/" className="font-semibold text-blue-700 underline-offset-2 hover:underline">
home page
</Link>
, use <span className="font-semibold">Seed Baseline Data</span> to reset stored documents to a known starting
queue so a guided session stays in sync—figures below are computed from those records like any other
deployment.
</li>
<li>
Set <span className="font-semibold">Active Role</span> to <span className="font-semibold">Intake Clerk</span>, open{' '}
<Link to="/intake" className="font-semibold text-blue-700 underline-offset-2 hover:underline">
Intake
</Link>
, and upload a <span className="font-semibold">PDF</span> from the demo downloads.
</li>
<li>
Switch to <span className="font-semibold">Department Reviewer</span> →{' '}
<Link to="/review" className="font-semibold text-blue-700 underline-offset-2 hover:underline">
Review
</Link>{' '}
to validate AI routing and workflow actions.
</li>
<li>
Select <span className="font-semibold">Supervisor</span> in the header and return here for aggregate metrics
(trend badges are illustrative in this MVP).
</li>
</ol>
</CardContent>
</Card>
);
}
30 changes: 30 additions & 0 deletions web/src/lib/demo-samples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** Demo PDFs are served by GET `/api/v1/demo/sample-files/{filename}` so nginx proxies them with correct headers. */

import { API_BASE_URL } from '@/lib/api';

export type DemoSampleFile = {
filename: string;
title: string;
/** Corresponding path in the local full data pack (not in git). */
dataPackSource: string;
description: string;
};

export function demoSampleFileUrl(filename: string): string {
return `${API_BASE_URL}/demo/sample-files/${encodeURIComponent(filename)}`;
}

export const DEMO_SAMPLE_FILES: DemoSampleFile[] = [
{
filename: 'sample-cong-van-dong-nai.pdf',
title: 'Công văn (Đồng Nai — mẫu)',
dataPackSource: 'data/incoming/cong-van/cong-van_dong-nai_mau-cong-van-di.pdf',
description: 'Official-style letter PDF from the incoming corpus.',
},
{
filename: 'sample-bao-cao-dong-nai.pdf',
title: 'Báo cáo (Đồng Nai — văn thư)',
dataPackSource: 'data/incoming/bao-cao/bao-cao_dong-nai_cong-tac-van-thu-luu-tru.pdf',
description: 'Periodic report PDF suitable for extraction and routing demos.',
},
];
3 changes: 3 additions & 0 deletions web/src/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui-card';
import { BarChart3, PieChart, TrendingUp, Users, FileCheck, Layers } from 'lucide-react';
import { apiGet } from '@/lib/api';
import { useRole } from '@/hooks/use-role';
import { DemoWalkthrough } from '@/components/demo-walkthrough';

type DashboardMetrics = {
total_received: number;
Expand Down Expand Up @@ -87,6 +88,8 @@ export default function DashboardPage() {
</div>
</div>

<DemoWalkthrough />

<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch">
{stats.map((stat) => (
<Card key={stat.label} className="flex h-full min-h-0 flex-col">
Expand Down
13 changes: 10 additions & 3 deletions web/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRole } from '@/hooks/use-role';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui-card';
import { Badge } from '@/components/ui-badge';
import { ArrowRight, CheckCircle2, Clock, AlertCircle, FileUp, ListChecks, MessageSquare, LayoutDashboard, Database, Loader2 } from 'lucide-react';
import { ArrowRight, CheckCircle2, Clock, AlertCircle, FileUp, ListChecks, MessageSquare, LayoutDashboard, Database, Loader2, Sparkles } from 'lucide-react';
import { Link } from 'react-router-dom';
import { useState } from 'react';
import { apiPost } from '@/lib/api';
Expand Down Expand Up @@ -34,7 +34,7 @@ export default function HomePage() {
<p className="text-xl text-slate-600 font-medium">
Automated document intake, AI-driven triage, and secure administrative workflow for government departments.
</p>
<div className="pt-4 flex items-center justify-center gap-4">
<div className="pt-4 flex flex-wrap items-center justify-center gap-3">
<button
onClick={handleSeed}
disabled={seeding}
Expand All @@ -43,7 +43,14 @@ export default function HomePage() {
{seeding ? <Loader2 size={14} className="animate-spin" /> : <Database size={14} />}
Seed Baseline Data
</button>
{seedResult && <span className="text-[10px] font-bold text-blue-600 animate-in fade-in">{seedResult}</span>}
<Link
to="/dashboard#demo-walkthrough"
className="px-6 py-2 bg-blue-50 border border-blue-200 rounded-xl text-xs font-bold uppercase tracking-widest text-blue-700 hover:bg-blue-100 transition flex items-center gap-2"
>
<Sparkles size={14} />
Demo walkthrough
</Link>
{seedResult && <span className="w-full text-center text-[10px] font-bold text-blue-600 animate-in fade-in sm:w-auto">{seedResult}</span>}
</div>
</section>

Expand Down
Loading
Loading