mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-07-16 03:13:36 +00:00
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:
165
apps/webapp/src/app/dashboard/demo-data.ts
Normal file
165
apps/webapp/src/app/dashboard/demo-data.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { Document, Submission, InsightsResult } from '@/libs/api';
|
||||
|
||||
const NOW = new Date().toISOString();
|
||||
const D = (daysAgo: number) => new Date(Date.now() - daysAgo * 86_400_000).toISOString();
|
||||
|
||||
const ROOT_VERSION_ID = 'demo-v1';
|
||||
const ML_VERSION_ID = 'demo-v2';
|
||||
const BACKEND_VERSION_ID = 'demo-v3';
|
||||
export const DEMO_DOC_ID = 'demo-doc-1';
|
||||
|
||||
export const DEMO_DOCUMENTS: Document[] = [
|
||||
{
|
||||
id: DEMO_DOC_ID,
|
||||
title: 'Alex Rivera — Software Engineer',
|
||||
description: 'Main CV, ATS-safe baseline',
|
||||
owner_id: 'demo-user',
|
||||
root_version_id: ROOT_VERSION_ID,
|
||||
created_at: D(45),
|
||||
updated_at: D(3),
|
||||
versions: [
|
||||
{
|
||||
id: ROOT_VERSION_ID,
|
||||
branch_name: 'root',
|
||||
version_label: 'v1.0 baseline',
|
||||
parent_version_id: null,
|
||||
structured_blocks: [
|
||||
{ path: 'heading[1]', block_type: 'heading', text: 'Alex Rivera', keywords: [] },
|
||||
{ path: 'summary[1]', block_type: 'summary', text: 'Software engineer with 5 years of experience building distributed systems and ML pipelines at scale.', keywords: ['distributed', 'systems', 'machine', 'learning'] },
|
||||
{ path: 'heading[2]', block_type: 'heading', text: 'Experience', keywords: [] },
|
||||
{ path: 'bullet[1]', block_type: 'bullet', text: 'Led migration of monolithic data pipeline to distributed microservices, reducing p99 latency by 40%.', keywords: ['distributed', 'microservices', 'latency', 'pipeline'] },
|
||||
{ path: 'bullet[2]', block_type: 'bullet', text: 'Designed feature flag system used by 50+ engineers across 3 teams.', keywords: ['system', 'design', 'engineers'] },
|
||||
{ path: 'heading[3]', block_type: 'heading', text: 'Skills', keywords: [] },
|
||||
{ path: 'skills[1]', block_type: 'skills', text: 'Python, Go, TypeScript, SQL, Kubernetes, AWS, PyTorch', keywords: ['python', 'go', 'typescript', 'pytorch', 'kubernetes'] },
|
||||
],
|
||||
artifact_docx_key: 'demo/demo-cv.docx',
|
||||
patches: [],
|
||||
public_assets: [],
|
||||
created_at: D(45),
|
||||
updated_at: D(45),
|
||||
},
|
||||
{
|
||||
id: ML_VERSION_ID,
|
||||
branch_name: 'ml-engineer',
|
||||
version_label: 'ML-focused variant',
|
||||
parent_version_id: ROOT_VERSION_ID,
|
||||
structured_blocks: [
|
||||
{ path: 'heading[1]', block_type: 'heading', text: 'Alex Rivera', keywords: [] },
|
||||
{ path: 'summary[1]', block_type: 'summary', text: 'ML engineer specialising in large-scale PyTorch training pipelines, distributed inference, and production-grade MLOps.', keywords: ['pytorch', 'distributed', 'mlops', 'inference'] },
|
||||
{ path: 'heading[2]', block_type: 'heading', text: 'Experience', keywords: [] },
|
||||
{ path: 'bullet[1]', block_type: 'bullet', text: 'Contributed PyTorch anomaly detection model achieving 92% precision on production traffic at 2M events/day.', keywords: ['pytorch', 'machine learning', 'production', 'precision'] },
|
||||
{ path: 'bullet[2]', block_type: 'bullet', text: 'Built streaming data ingestion system (Kafka + Flink) powering real-time ML feature store.', keywords: ['kafka', 'flink', 'streaming', 'feature store'] },
|
||||
{ path: 'heading[3]', block_type: 'heading', text: 'Skills', keywords: [] },
|
||||
{ path: 'skills[1]', block_type: 'skills', text: 'PyTorch, Python, Go, Kubernetes, Spark, dbt, AWS SageMaker', keywords: ['pytorch', 'python', 'kubernetes', 'spark', 'sagemaker'] },
|
||||
],
|
||||
artifact_docx_key: 'demo/demo-cv.docx',
|
||||
patches: [
|
||||
{ id: 'dp1', target_path: 'summary[1]', operation: 'replace_text', old_value: 'Software engineer…', new_value: 'ML engineer specialising…', created_at: D(30) },
|
||||
{ id: 'dp2', target_path: 'skills[1]', operation: 'boost_keyword', old_value: null, new_value: 'PyTorch', created_at: D(30) },
|
||||
],
|
||||
public_assets: [{
|
||||
id: 'demo-asset-1', slug: 'alex-ml', artifact_key: 'public/alex-ml.docx',
|
||||
is_public: true, url: '/demo-cv.docx', version_id: ML_VERSION_ID, submission_id: null, created_at: D(20),
|
||||
}],
|
||||
created_at: D(30),
|
||||
updated_at: D(3),
|
||||
},
|
||||
{
|
||||
id: BACKEND_VERSION_ID,
|
||||
branch_name: 'backend-engineer',
|
||||
version_label: 'Backend-focused variant',
|
||||
parent_version_id: ROOT_VERSION_ID,
|
||||
structured_blocks: [
|
||||
{ path: 'heading[1]', block_type: 'heading', text: 'Alex Rivera', keywords: [] },
|
||||
{ path: 'summary[1]', block_type: 'summary', text: 'Backend engineer focused on high-throughput API design, distributed systems, and reliability engineering.', keywords: ['backend', 'api', 'distributed', 'reliability'] },
|
||||
{ path: 'bullet[1]', block_type: 'bullet', text: 'Led migration to microservices, reducing p99 latency by 40% under 10k RPS sustained load.', keywords: ['microservices', 'latency', 'rps', 'distributed'] },
|
||||
{ path: 'skills[1]', block_type: 'skills', text: 'Go, Python, PostgreSQL, Redis, gRPC, Kubernetes, AWS', keywords: ['go', 'postgresql', 'redis', 'grpc', 'kubernetes'] },
|
||||
],
|
||||
artifact_docx_key: 'demo/demo-cv.docx',
|
||||
patches: [
|
||||
{ id: 'dp3', target_path: 'summary[1]', operation: 'replace_text', old_value: 'Software engineer…', new_value: 'Backend engineer…', created_at: D(25) },
|
||||
],
|
||||
public_assets: [],
|
||||
created_at: D(25),
|
||||
updated_at: D(10),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const DEMO_SUBMISSIONS: Submission[] = [
|
||||
{
|
||||
id: 'ds1', version_id: ML_VERSION_ID, company_name: 'Anthropic', role_title: 'ML Research Engineer',
|
||||
job_url: null, job_description: null, status: 'pending_review', created_at: D(18),
|
||||
suggestions: [
|
||||
{ id: 's1', target_path: 'summary[1]', operation: 'boost_keyword', proposed_text: 'constitutional ai', rationale: 'Highlight alignment research experience', accepted: true, metadata_json: { confidence: 0.82 } },
|
||||
{ id: 's2', target_path: 'bullet[1]', operation: 'replace_text', proposed_text: 'Built distributed PyTorch training pipeline handling constitutional AI fine-tuning at scale.', rationale: 'Align with Anthropic stack', accepted: true, metadata_json: { confidence: 0.74 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ds2', version_id: ML_VERSION_ID, company_name: 'Google DeepMind', role_title: 'Senior ML Engineer',
|
||||
job_url: null, job_description: null, status: 'pending_review', created_at: D(14),
|
||||
suggestions: [
|
||||
{ id: 's3', target_path: 'skills[1]', operation: 'boost_keyword', proposed_text: 'JAX', rationale: 'DeepMind uses JAX heavily', accepted: true, metadata_json: { confidence: 0.71 } },
|
||||
{ id: 's4', target_path: 'bullet[2]', operation: 'replace_text', proposed_text: 'Built large-scale streaming pipeline underpinning real-time feature store for JAX model serving.', rationale: 'Add JAX context', accepted: true, metadata_json: { confidence: 0.68 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ds3', version_id: ML_VERSION_ID, company_name: 'OpenAI', role_title: 'Research Engineer',
|
||||
job_url: null, job_description: null, status: 'published', created_at: D(10),
|
||||
suggestions: [
|
||||
{ id: 's5', target_path: 'summary[1]', operation: 'replace_text', proposed_text: 'ML engineer with track record in large-scale training infrastructure and RLHF pipelines.', rationale: 'OpenAI focus on RLHF', accepted: true, metadata_json: { confidence: 0.77 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ds4', version_id: ML_VERSION_ID, company_name: 'Meta AI', role_title: 'ML Infrastructure Engineer',
|
||||
job_url: null, job_description: null, status: 'archived', created_at: D(22),
|
||||
suggestions: [
|
||||
{ id: 's6', target_path: 'bullet[1]', operation: 'boost_keyword', proposed_text: 'PyTorch', rationale: 'Meta maintains PyTorch', accepted: true, metadata_json: { confidence: 0.55 } },
|
||||
{ id: 's7', target_path: 'summary[1]', operation: 'suppress_block', proposed_text: null, rationale: 'Summary too generic', accepted: false, metadata_json: { confidence: 0.3 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ds5', version_id: BACKEND_VERSION_ID, company_name: 'Stripe', role_title: 'Senior Backend Engineer',
|
||||
job_url: null, job_description: null, status: 'pending_review', created_at: D(8),
|
||||
suggestions: [
|
||||
{ id: 's8', target_path: 'bullet[1]', operation: 'replace_text', proposed_text: 'Led migration to microservices achieving 99.99% uptime across Stripe-scale payment processing.', rationale: 'Emphasise reliability', accepted: true, metadata_json: { confidence: 0.79 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ds6', version_id: BACKEND_VERSION_ID, company_name: 'Cloudflare', role_title: 'Staff Engineer',
|
||||
job_url: null, job_description: null, status: 'archived', created_at: D(20),
|
||||
suggestions: [
|
||||
{ id: 's9', target_path: 'skills[1]', operation: 'boost_keyword', proposed_text: 'Rust', rationale: 'Cloudflare uses Rust', accepted: true, metadata_json: { confidence: 0.4 } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const DEMO_INSIGHTS: InsightsResult = {
|
||||
total_submissions: 6,
|
||||
positive_count: 4,
|
||||
positive_rate: 0.667,
|
||||
has_data: true,
|
||||
operation_impact: [
|
||||
{ operation: 'replace_text', total: 5, positive: 4, rate: 0.8 },
|
||||
{ operation: 'boost_keyword', total: 5, positive: 3, rate: 0.6 },
|
||||
{ operation: 'suppress_block', total: 1, positive: 0, rate: 0.0 },
|
||||
],
|
||||
top_positive_keywords: [
|
||||
{ keyword: 'pytorch', positive_count: 4, negative_count: 1, lift: 4.0 },
|
||||
{ keyword: 'distributed', positive_count: 3, negative_count: 0, lift: 3.0 },
|
||||
{ keyword: 'pipeline', positive_count: 3, negative_count: 1, lift: 3.0 },
|
||||
{ keyword: 'scale', positive_count: 3, negative_count: 1, lift: 3.0 },
|
||||
{ keyword: 'reliability', positive_count: 2, negative_count: 0, lift: 2.0 },
|
||||
{ keyword: 'inference', positive_count: 2, negative_count: 0, lift: 2.0 },
|
||||
],
|
||||
top_negative_keywords: [
|
||||
{ keyword: 'generic', positive_count: 0, negative_count: 2, lift: 0.0 },
|
||||
{ keyword: 'suppress', positive_count: 0, negative_count: 1, lift: 0.0 },
|
||||
],
|
||||
section_impact: [
|
||||
{ section: 'summary', positive_rate: 0.83, count: 6 },
|
||||
{ section: 'bullet', positive_rate: 0.75, count: 4 },
|
||||
{ section: 'skills', positive_rate: 0.5, count: 4 },
|
||||
],
|
||||
};
|
||||
@@ -3,12 +3,15 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import CVTree from '@/components/cv/CVTree';
|
||||
import DiffViewer from '@/components/cv/DiffViewer';
|
||||
import InsightsPanel from '@/components/cv/InsightsPanel';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
appendPatches,
|
||||
createBranch, createSubmission, deleteDocument, deleteVersion,
|
||||
Document, downloadVersionUrl,
|
||||
fetchDocuments, fetchSubmissions, fetchPublicAssetAnalytics, getPublicPdfUrl,
|
||||
fetchDocuments, fetchInsights, fetchSubmissions, fetchPublicAssetAnalytics, getPublicPdfUrl,
|
||||
InsightsResult,
|
||||
IS_DEMO,
|
||||
publishVersion, PublicAsset, PublicAssetAnalytics,
|
||||
requestAiSuggestions,
|
||||
Submission,
|
||||
@@ -20,6 +23,9 @@ import {
|
||||
uploadDocument,
|
||||
Version,
|
||||
} from '@/libs/api';
|
||||
import {
|
||||
DEMO_DOCUMENTS, DEMO_DOC_ID, DEMO_INSIGHTS, DEMO_SUBMISSIONS,
|
||||
} from './demo-data';
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -548,7 +554,7 @@ function SubmissionsTab({
|
||||
// ── main dashboard ────────────────────────────────────────────────────────────
|
||||
|
||||
type Modal = 'upload' | 'branch' | 'submission' | 'publish' | null;
|
||||
type Tab = 'content' | 'patches' | 'submissions';
|
||||
type Tab = 'content' | 'patches' | 'submissions' | 'insights';
|
||||
|
||||
export default function Dashboard() {
|
||||
const [docs, setDocs] = useState<Document[]>([]);
|
||||
@@ -568,8 +574,17 @@ export default function Dashboard() {
|
||||
const [docHovered, setDocHovered] = useState<string | null>(null);
|
||||
const [applyLoading, setApplyLoading] = useState(false);
|
||||
const [applyError, setApplyError] = useState('');
|
||||
const [insights, setInsights] = useState<InsightsResult | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO) {
|
||||
setDocs(DEMO_DOCUMENTS);
|
||||
setAllSubmissions(DEMO_SUBMISSIONS);
|
||||
setSelectedDocId(DEMO_DOC_ID);
|
||||
setInsights(DEMO_INSIGHTS);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
Promise.all([fetchDocuments(), fetchSubmissions().catch(() => [])])
|
||||
.then(([d, allSubs]) => {
|
||||
setDocs(d);
|
||||
@@ -580,6 +595,11 @@ export default function Dashboard() {
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO || !selectedDocId) return;
|
||||
fetchInsights().then(setInsights).catch(() => setInsights(null));
|
||||
}, [selectedDocId]);
|
||||
|
||||
useEffect(() => {
|
||||
setPendingEdits(new Map());
|
||||
setApplyError('');
|
||||
@@ -691,6 +711,7 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
const handleDeleteDoc = async (docId: string) => {
|
||||
if (IS_DEMO) return;
|
||||
if (!confirm('Delete this CV and all its branches? This cannot be undone.')) return;
|
||||
try {
|
||||
await deleteDocument(docId);
|
||||
@@ -706,6 +727,7 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
const handleDeleteVersion = async (versionId: string) => {
|
||||
if (IS_DEMO) return;
|
||||
const hasChildren = selectedDoc?.versions.some(v => v.parent_version_id === versionId);
|
||||
const msg = hasChildren
|
||||
? 'Delete this branch and all its sub-branches? This cannot be undone.'
|
||||
@@ -758,12 +780,21 @@ export default function Dashboard() {
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button className="btn btn-primary" style={{ padding: '4px 10px', fontSize: 12 }} onClick={() => setModal('upload')}>
|
||||
+ Upload CV
|
||||
</button>
|
||||
<button className="btn btn-ghost" style={{ padding: '4px 10px', fontSize: 12 }} onClick={logout}>
|
||||
Sign out
|
||||
</button>
|
||||
{IS_DEMO && (
|
||||
<span style={{ fontSize: 11, padding: '2px 10px', background: '#7c3aed', color: '#fff', borderRadius: 9999, fontWeight: 600, letterSpacing: '0.04em' }}>
|
||||
DEMO
|
||||
</span>
|
||||
)}
|
||||
{!IS_DEMO && (
|
||||
<button className="btn btn-primary" style={{ padding: '4px 10px', fontSize: 12 }} onClick={() => setModal('upload')}>
|
||||
+ Upload CV
|
||||
</button>
|
||||
)}
|
||||
{!IS_DEMO && (
|
||||
<button className="btn btn-ghost" style={{ padding: '4px 10px', fontSize: 12 }} onClick={logout}>
|
||||
Sign out
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -900,6 +931,13 @@ export default function Dashboard() {
|
||||
onSelect={selectVersion}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{insights?.has_data && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div className="label" style={{ marginBottom: 8 }}>NLP insights</div>
|
||||
<InsightsPanel data={insights} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ paddingTop: 60, textAlign: 'center', color: 'var(--text-faint)', fontSize: 13 }}>
|
||||
@@ -938,10 +976,15 @@ export default function Dashboard() {
|
||||
|
||||
{/* action buttons */}
|
||||
<div className="action-buttons">
|
||||
<button className="btn btn-ghost" onClick={() => setModal('branch')}>Branch</button>
|
||||
<button className="btn btn-ghost" onClick={() => { setModal('submission'); }}>Submit</button>
|
||||
<button className="btn btn-ghost" onClick={() => setModal('publish')}>Publish</button>
|
||||
{selectedVersion.artifact_docx_key && selectedDoc && (
|
||||
{!IS_DEMO && <button className="btn btn-ghost" onClick={() => setModal('branch')}>Branch</button>}
|
||||
{!IS_DEMO && <button className="btn btn-ghost" onClick={() => { setModal('submission'); }}>Submit</button>}
|
||||
{!IS_DEMO && <button className="btn btn-ghost" onClick={() => setModal('publish')}>Publish</button>}
|
||||
{IS_DEMO && (
|
||||
<a href="/demo-cv.docx" download="alex-rivera-cv.docx" className="btn btn-ghost">
|
||||
↓ DOCX
|
||||
</a>
|
||||
)}
|
||||
{!IS_DEMO && selectedVersion.artifact_docx_key && selectedDoc && (
|
||||
<a href={downloadVersionUrl(selectedDoc.id, selectedVersion.id)} download className="btn btn-ghost">
|
||||
↓ DOCX
|
||||
</a>
|
||||
@@ -1044,7 +1087,7 @@ export default function Dashboard() {
|
||||
|
||||
{/* tabs */}
|
||||
<div style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--border)', overflowX: 'auto' }}>
|
||||
{(['content', 'patches', 'submissions'] as Tab[]).map(t => (
|
||||
{(['content', 'patches', 'submissions', 'insights'] as Tab[]).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setActiveTab(t)}
|
||||
@@ -1076,13 +1119,21 @@ export default function Dashboard() {
|
||||
)}
|
||||
{activeTab === 'submissions' && (
|
||||
<SubmissionsTab
|
||||
submissions={submissions}
|
||||
submissions={IS_DEMO
|
||||
? DEMO_SUBMISSIONS.filter(s => {
|
||||
const doc = DEMO_DOCUMENTS.find(d => d.id === selectedDocId);
|
||||
return doc?.versions.some(v => v.id === s.version_id);
|
||||
})
|
||||
: submissions}
|
||||
loading={subsLoading}
|
||||
onNewSubmission={() => setModal('submission')}
|
||||
onRefresh={refreshSubs}
|
||||
onNewSubmission={() => !IS_DEMO && setModal('submission')}
|
||||
onRefresh={() => !IS_DEMO && refreshSubs()}
|
||||
onStatusChange={handleSubmissionStatusChange}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'insights' && (
|
||||
<InsightsPanel data={insights} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user