feat: NLP patch insights + standalone demo mode

- dlib/ai/insights.py: pure-Python NLP analysis that correlates accepted
  AI suggestion operations/keywords/sections with submission outcomes
  (pending_review / published = positive, archived = negative)
- Backend: GET /api/v1/insights route + service + Pydantic schema
- Frontend: InsightsPanel component with bar charts for operation impact,
  section impact, and keyword signal lift scores
- Insights tab added to the version panel; compact preview on doc overview
- NEXT_PUBLIC_DEMO=true makes the webapp fully standalone: loads
  DEMO_DOCUMENTS / DEMO_SUBMISSIONS / DEMO_INSIGHTS from demo-data.ts,
  disables all mutating actions, shows a DEMO badge in the top bar
- apps/webapp/public/demo-cv.docx: static dummy CV (Alex Rivera) for demo
- scripts/gen_demo_cv.py: script to regenerate the demo DOCX
- .env.example: document NEXT_PUBLIC_DEMO flag

https://claude.ai/code/session_01LWxu2qrwY6BRjUFXXn7NiM
This commit is contained in:
Claude
2026-04-05 09:34:01 +00:00
parent 0f32d46404
commit 615d1bdb9e
12 changed files with 780 additions and 17 deletions

View File

@@ -1,4 +1,5 @@
const API = "";
export const IS_DEMO = process.env.NEXT_PUBLIC_DEMO === 'true';
export type StructuredBlock = {
path: string;
@@ -87,6 +88,21 @@ export type PublicAssetAnalytics = {
last_viewed_at?: string | null;
};
export type OperationImpact = { operation: string; total: number; positive: number; rate: number };
export type KeywordSignal = { keyword: string; positive_count: number; negative_count: number; lift: number };
export type SectionImpact = { section: string; positive_rate: number; count: number };
export type InsightsResult = {
total_submissions: number;
positive_count: number;
positive_rate: number;
operation_impact: OperationImpact[];
top_positive_keywords: KeywordSignal[];
top_negative_keywords: KeywordSignal[];
section_impact: SectionImpact[];
has_data: boolean;
};
// reads OIDC bearer token from client-readable cookie (set by /api/auth/callback)
function getAuthHeader(): Record<string, string> {
if (typeof document === 'undefined') return {};
@@ -238,6 +254,9 @@ export async function deleteDocument(documentId: string): Promise<void> {
}
}
export const fetchInsights = (): Promise<InsightsResult> =>
req<InsightsResult>('/api/v1/insights');
export async function deleteVersion(versionId: string): Promise<void> {
const res = await fetch(`${API}/api/v1/versions/${versionId}`, {
method: 'DELETE',