This commit is contained in:
Ashishyg
2025-11-07 10:54:13 +05:30
parent 6bf9f91974
commit 8833186262
23 changed files with 6987 additions and 0 deletions

18
frontend/nordic-privacy-ai/.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
node_modules/
.next/
# Environment
.env
.env.local
.env.*
# OS / Editor
.DS_Store
.vscode/
.idea/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

View File

@@ -0,0 +1,28 @@
# Nordic Privacy AI
AI-Powered GDPR compliance & personal data protection platform tailored for Nordic ecosystems (BankID, MitID, Suomi.fi).
## Tech Stack
- Next.js (App Router, TypeScript)
- Tailwind CSS
## Getting Started
```powershell
npm install
npm run dev
```
Visit http://localhost:3000 to view the landing page.
## Scripts
- `npm run dev` Start dev server
- `npm run build` Production build
- `npm start` Run built app
- `npm run lint` ESLint
## Next Steps
- Implement /try page workflow
- Add feature sections & agent explanations
- Integrate backend services for data upload & scanning
## License
Internal hackathon prototype

View File

@@ -0,0 +1,46 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body { height: 100%; }
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }
/* Custom utilities */
.container-max { @apply max-w-7xl mx-auto px-4; }
/* Smooth scrolling for in-page anchors */
html { scroll-behavior: smooth; }
/* Subtle hero background animations */
@keyframes drift {
0% { transform: translate3d(0,0,0) scale(1) rotate(0deg); }
50% { transform: translate3d(10px,-8px,0) scale(1.02) rotate(8deg); }
100% { transform: translate3d(0,0,0) scale(1) rotate(0deg); }
}
.animate-drift-slow {
animation: drift 16s ease-in-out infinite;
will-change: transform;
}
.animate-drift-slower {
animation: drift 22s ease-in-out infinite;
will-change: transform;
}
/* Reveal on scroll */
.reveal { opacity: 0; transform: translateY(16px); transition: opacity 500ms ease, transform 600ms ease; }
.reveal-visible { opacity: 1; transform: translateY(0); }
/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.animate-drift-slow, .animate-drift-slower { animation: none !important; }
.reveal { opacity: 1; transform: none; transition: none; }
}
/* Performance: avoid rendering off-screen content until needed */
.content-auto {
content-visibility: auto;
contain-intrinsic-size: 1px 1000px; /* reserve space to prevent big jumps */
}

View File

@@ -0,0 +1,18 @@
import './globals.css';
import type { ReactNode } from 'react';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Nordic Privacy AI',
description: 'AI-powered GDPR compliance platform for Nordic ecosystems',
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className="h-full">
<body className="min-h-full bg-gradient-to-br from-brand-50 via-white to-brand-100 text-slate-900 antialiased">
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,59 @@
import Link from 'next/link';
import { Navbar } from '../components/Navbar';
import { FeatureGrid } from '../components/landing/FeatureGrid';
import { AgentsOverview } from '../components/landing/AgentsOverview';
import { Steps } from '../components/landing/Steps';
import { Footer } from '../components/landing/Footer';
export default function HomePage() {
return (
<main className="min-h-screen flex flex-col">
<Navbar />
<section className="relative pt-20 pb-24 overflow-hidden">
<div className="absolute inset-0 pointer-events-none overflow-hidden">
<div className="absolute -top-24 left-1/2 -translate-x-1/2 w-[480px] h-[480px] md:w-[560px] md:h-[560px] rounded-full bg-gradient-to-br from-brand-200/50 to-brand-400/30 blur-xl opacity-60 animate-drift-slow transform-gpu" />
<div className="hidden md:block absolute top-10 right-[10%] w-[340px] h-[340px] rounded-full bg-gradient-to-br from-brand-300/40 to-brand-500/25 blur-xl opacity-50 animate-drift-slower transform-gpu" />
<div className="hidden md:block absolute -bottom-28 left-[8%] w-[300px] h-[300px] rounded-full bg-gradient-to-br from-brand-100/60 to-brand-300/35 blur-xl opacity-60 animate-drift-slower transform-gpu" style={{ animationDelay: '1.2s' }} />
</div>
<div className="container-max relative text-center">
<div className="mx-auto max-w-3xl">
<h1 className="text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tight text-slate-900">
Proactive Nordic Data Privacy
</h1>
<p className="mt-6 text-base sm:text-lg text-slate-700">
AI agents that discover, classify, and remediate personal data across your ecosystembuilt for BankID, MitID, and Suomi.fi contexts.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3 sm:gap-4">
<Link href="/try" className="inline-flex items-center rounded-md bg-brand-600 px-7 py-3 text-white font-semibold shadow-lg hover:bg-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">
Start free scan
</Link>
<Link href="#features" className="inline-flex items-center rounded-md border border-slate-300 px-7 py-3 text-slate-700 font-medium hover:bg-white/60 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">
Explore features
</Link>
</div>
<div className="mt-10 grid grid-cols-3 gap-3 sm:gap-6 text-center text-xs sm:text-sm">
<div className="flex flex-col"><span className="font-semibold text-slate-900">99%</span><span className="text-slate-600">PII labels coverage*</span></div>
<div className="flex flex-col"><span className="font-semibold text-slate-900"><span className="align-middle"></span> Real-time</span><span className="text-slate-600">Monitoring</span></div>
<div className="flex flex-col"><span className="font-semibold text-slate-900">EU-first</span><span className="text-slate-600">Reg alignment</span></div>
</div>
</div>
</div>
</section>
<FeatureGrid />
<Steps />
<AgentsOverview />
<section id="contact" className="container-max py-20 content-auto">
<div className="rounded-2xl border border-brand-200/70 bg-white/80 backdrop-blur p-10 text-center shadow-sm">
<h2 className="text-2xl font-bold text-slate-900">Ready to pilot?</h2>
<p className="mt-4 text-slate-600 max-w-xl mx-auto">Were onboarding early Nordic partners. Get in touch to shape proactive privacy intelligence.</p>
<div className="mt-6">
<Link href="/try" className="inline-flex items-center rounded-md bg-brand-600 px-6 py-3 text-white font-semibold shadow hover:bg-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">
Request access
</Link>
</div>
</div>
</section>
<Footer />
</main>
);
}

View File

@@ -0,0 +1,23 @@
"use client";
import { useState } from "react";
import { Navbar } from "../../components/Navbar";
import { Sidebar, TryTab } from "../../components/try/Sidebar";
import { CenterPanel } from "../../components/try/CenterPanel";
import { ChatbotPanel } from "../../components/try/ChatbotPanel";
export default function TryPage() {
const [tab, setTab] = useState<TryTab>("processing");
return (
<main className="min-h-screen flex flex-col">
<Navbar />
<div className="flex flex-1 min-h-0">
<Sidebar value={tab} onChange={setTab} />
<div className="flex-1 min-h-0 flex">
<div className="flex-1 min-h-0"><CenterPanel tab={tab} /></div>
<div className="w-[360px] hidden xl:block"><ChatbotPanel /></div>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,63 @@
"use client";
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
export function Navbar() {
const pathname = usePathname();
const onTry = pathname?.startsWith('/try');
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 4);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<nav className={`w-full sticky top-0 z-50 transition-colors ${scrolled ? 'bg-white/90 border-b border-slate-200/70 shadow-sm' : 'bg-white/70 border-b border-transparent'}`}>
<div className="container-max flex items-center justify-between h-16">
<Link href="/" className="font-semibold text-brand-700 text-lg tracking-tight">Nordic Privacy AI</Link>
{/* Desktop nav */}
{onTry ? (
<div className="hidden md:flex items-center gap-6 text-sm">
<Link href="/" className="hover:text-brand-600 transition-colors">Home</Link>
</div>
) : (
<div className="hidden md:flex items-center gap-6 text-sm">
<Link href="#features" className="hover:text-brand-600 transition-colors">Features</Link>
<Link href="#agents" className="hover:text-brand-600 transition-colors">Agents</Link>
<Link href="#contact" className="hover:text-brand-600 transition-colors">Contact</Link>
<Link href="/try" className="inline-flex items-center rounded-md bg-brand-600 px-4 py-2 text-white font-medium shadow hover:bg-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">Try me</Link>
</div>
)}
{/* Mobile menu button */}
<button
aria-label="Toggle menu"
className="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-md border border-slate-300 text-slate-700"
onClick={() => setMenuOpen(v => !v)}
>
<span className="sr-only">Menu</span>
{menuOpen ? '✕' : '☰'}
</button>
</div>
{/* Mobile menu panel */}
<div className={`md:hidden border-t border-slate-200 transition-[max-height,opacity] duration-200 overflow-hidden ${menuOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'}`}>
<div className="container-max py-3 flex flex-col gap-3 text-sm">
{onTry ? (
<Link href="/" onClick={() => setMenuOpen(false)} className="py-2">Home</Link>
) : (
<>
<a href="#features" onClick={() => setMenuOpen(false)} className="py-2">Features</a>
<a href="#agents" onClick={() => setMenuOpen(false)} className="py-2">Agents</a>
<a href="#contact" onClick={() => setMenuOpen(false)} className="py-2">Contact</a>
<Link href="/try" onClick={() => setMenuOpen(false)} className="inline-flex items-center justify-center rounded-md bg-brand-600 px-4 py-2 text-white font-medium shadow hover:bg-brand-500">Try me</Link>
</>
)}
</div>
</div>
</nav>
);
}

View File

@@ -0,0 +1,42 @@
"use client";
import { useEffect, useRef, useState } from "react";
interface RevealProps {
children: React.ReactNode;
as?: keyof JSX.IntrinsicElements;
className?: string;
delayMs?: number;
}
export function Reveal({ children, as = "div", className, delayMs = 0 }: RevealProps) {
const Cmp: any = as;
const ref = useRef<HTMLElement | null>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const el = ref.current as Element | null;
if (!el || typeof IntersectionObserver === "undefined") {
setVisible(true);
return;
}
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
if (delayMs > 0) {
const t = setTimeout(() => setVisible(true), delayMs);
return () => clearTimeout(t);
} else {
setVisible(true);
}
}
},
{ threshold: 0.12 }
);
obs.observe(el);
return () => obs.disconnect();
}, [delayMs]);
return (
<Cmp ref={ref} className={(visible ? "reveal-visible " : "reveal ") + (className ?? "")}>{children}</Cmp>
);
}

View File

@@ -0,0 +1,46 @@
import { Reveal } from "../common/Reveal";
export function AgentsOverview() {
const agents = [
{
title: "Discovery Agent",
desc:
"Continuously inventories systems to locate personal and sensitive data across sources, fixing visibility gaps.",
emoji: "🛰️",
},
{
title: "Cleaner Agent",
desc:
"Identifies PII and sensitive attributes, classifies content, and prepares data for remediation and audits.",
emoji: "🧽",
},
{
title: "Remediation Agent",
desc:
"Suggests anonymization, consent validation, or deletion; generates compliance reports and monitors posture.",
emoji: "🛡️",
},
];
return (
<div className="container-max py-16 content-auto" id="agents">
<Reveal className="text-center" as="div">
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900">Our agents</h2>
<p className="mt-3 text-slate-600 max-w-2xl mx-auto">
Modular, AI-driven roles that work together to keep your data compliant.
</p>
</Reveal>
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 sm:gap-6">
{agents.map((a, i) => (
<Reveal key={a.title} delayMs={i * 110}>
<div className="rounded-xl border border-slate-200 bg-white/80 p-6 shadow-sm transition-transform duration-300 hover:-translate-y-0.5">
<div className="text-3xl">{a.emoji}</div>
<h3 className="mt-3 font-semibold text-slate-900">{a.title}</h3>
<p className="mt-2 text-sm text-slate-600">{a.desc}</p>
</div>
</Reveal>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { Reveal } from "../common/Reveal";
export function FeatureGrid() {
const items = [
{
title: "Discovery",
desc: "Continuously map data across apps, logs, DBs, and cloud storage to restore visibility.",
emoji: "🧭",
},
{
title: "Classification",
desc: "Detect PII and sensitive categories (health, finance) with AI-driven labeling.",
emoji: "🔎",
},
{
title: "Remediation",
desc: "Anonymize, minimize, and automate consent workflows to reduce exposure.",
emoji: "🧹",
},
{
title: "Monitoring",
desc: "Continuous compliance checks with alerts and reports aligned to GDPR.",
emoji: "📈",
},
];
return (
<div className="container-max py-16 content-auto" id="features">
<Reveal className="text-center" as="div">
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900">Core capabilities</h2>
<p className="mt-3 text-slate-600 max-w-2xl mx-auto">
Proactive privacy protection tailored for Nordic identity ecosystems and EU data law.
</p>
</Reveal>
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
{items.map((f, i) => (
<Reveal key={f.title} delayMs={i * 90}>
<div className="rounded-xl border border-slate-200 bg-white/80 p-6 shadow-sm transition-transform duration-300 hover:-translate-y-0.5">
<div className="text-3xl">{f.emoji}</div>
<h3 className="mt-3 font-semibold text-slate-900">{f.title}</h3>
<p className="mt-2 text-sm text-slate-600">{f.desc}</p>
</div>
</Reveal>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import Link from 'next/link';
export function Footer() {
return (
<footer className="mt-24 border-t border-slate-200 bg-white/80">
<div className="container-max py-10 flex flex-col sm:flex-row items-center justify-between gap-6">
<div className="text-sm text-slate-600">© {new Date().getFullYear()} Nordic Privacy AI. Hackathon prototype.</div>
<nav className="flex gap-6 text-sm">
<Link href="#features" className="hover:text-brand-600">Features</Link>
<Link href="#agents" className="hover:text-brand-600">Agents</Link>
<Link href="#contact" className="hover:text-brand-600">Contact</Link>
<Link href="/try" className="hover:text-brand-600">Try</Link>
</nav>
</div>
</footer>
);
}

View File

@@ -0,0 +1,32 @@
import { Reveal } from "../common/Reveal";
export function Steps() {
const steps = [
{ title: 'Discover', desc: 'Continuously inventory data across apps, logs, DBs, and cloud.' , emoji: '🧭' },
{ title: 'Classify', desc: 'Detect PII and sensitive categories with AI labeling.', emoji: '🔎' },
{ title: 'Mitigate', desc: 'Anonymize, minimize, and enforce consent governance.', emoji: '🛡️' },
{ title: 'Monitor', desc: 'Continuous checks and reports mapped to GDPR.', emoji: '📈' },
];
return (
<section className="container-max py-16 content-auto">
<Reveal className="text-center" as="div">
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900">How it works</h2>
<p className="mt-3 text-slate-600 max-w-2xl mx-auto">Simple, proactive steps to keep your data compliant.</p>
</Reveal>
<div className="mt-10 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6">
{steps.map((s, i) => (
<Reveal key={s.title} delayMs={i * 100}>
<div className="relative rounded-xl border border-slate-200 bg-white/80 p-6 shadow-sm transition-transform duration-300 hover:-translate-y-0.5">
<div className="text-3xl">{s.emoji}</div>
<div className="mt-3 flex items-baseline gap-2">
<span className="text-xs text-slate-500">Step {i + 1}</span>
<h3 className="font-semibold text-slate-900">{s.title}</h3>
</div>
<p className="mt-2 text-sm text-slate-600">{s.desc}</p>
</div>
</Reveal>
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,21 @@
export function ValueProps() {
const items = [
{ title: 'EU-first compliance', desc: 'Designed around GDPR principles: lawfulness, purpose limitation, minimization, and accountability.', emoji: '🇪🇺' },
{ title: 'Nordic identity ready', desc: 'Built with BankID, MitID, and Suomi.fi contexts in mind for seamless identity-aware workflows.', emoji: '🧩' },
{ title: 'Continuous by default', desc: 'Move from manual audits to ongoing monitoring with alerts and clear reports.', emoji: '⏱️' },
];
return (
<section className="container-max py-16">
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900 text-center">Why it fits the Nordics</h2>
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
{items.map((v) => (
<div key={v.title} className="rounded-xl border border-slate-200 bg-white/70 backdrop-blur p-6 shadow-sm">
<div className="text-3xl">{v.emoji}</div>
<h3 className="mt-3 font-semibold text-slate-900">{v.title}</h3>
<p className="mt-2 text-sm text-slate-600">{v.desc}</p>
</div>
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,238 @@
"use client";
import { TryTab } from "./Sidebar";
import { useState, useRef, useCallback, useEffect } from "react";
import { saveLatestUpload, getLatestUpload, deleteLatestUpload } from "../../lib/idb";
interface CenterPanelProps {
tab: TryTab;
}
interface UploadedFileMeta {
name: string;
size: number;
type: string;
contentPreview: string;
}
export function CenterPanel({ tab }: CenterPanelProps) {
const [fileMeta, setFileMeta] = useState<UploadedFileMeta | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [progress, setProgress] = useState<number>(0);
const inputRef = useRef<HTMLInputElement | null>(null);
const [loadedFromCache, setLoadedFromCache] = useState(false);
const reset = () => {
setFileMeta(null);
setProgress(0);
};
const processFile = useCallback((f: File) => {
if (!f) return;
setProgress(0);
if (f.size > 1024 * 1024) {
setFileMeta({
name: f.name,
size: f.size,
type: f.type,
contentPreview: "File too large for preview (limit 1MB).",
});
return;
}
const reader = new FileReader();
reader.onprogress = (evt) => {
if (evt.lengthComputable) {
const pct = Math.min(100, Math.round((evt.loaded / evt.total) * 100));
setProgress(pct);
} else {
setProgress((p) => (p < 90 ? p + 5 : p));
}
};
reader.onload = async () => {
try {
const buf = reader.result as ArrayBuffer;
const decoder = new TextDecoder();
const text = decoder.decode(buf);
const metaObj: UploadedFileMeta = {
name: f.name,
size: f.size,
type: f.type || "unknown",
contentPreview: text.slice(0, 4000),
};
setFileMeta(metaObj);
// Save file blob and meta to browser cache (IndexedDB)
try {
await saveLatestUpload(f, metaObj);
} catch {}
setProgress(100);
} catch (e) {
const metaObj: UploadedFileMeta = {
name: f.name,
size: f.size,
type: f.type || "unknown",
contentPreview: "Unable to decode preview.",
};
setFileMeta(metaObj);
try {
await saveLatestUpload(f, metaObj);
} catch {}
setProgress(100);
}
};
reader.onerror = () => {
setProgress(0);
};
reader.readAsArrayBuffer(f);
}, []);
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0];
processFile(f as File);
}
const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
};
const onDragLeave = () => setIsDragging(false);
const onDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
const f = e.dataTransfer.files?.[0];
processFile(f as File);
};
// Load last cached upload on mount (processing tab only)
useEffect(() => {
let ignore = false;
if (tab !== "processing") return;
(async () => {
try {
const { meta } = await getLatestUpload();
if (!ignore && meta) {
setFileMeta(meta as UploadedFileMeta);
setLoadedFromCache(true);
}
} catch {}
})();
return () => {
ignore = true;
};
}, [tab]);
function renderTabContent() {
switch (tab) {
case "processing":
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Upload & Process Data</h2>
<p className="text-sm text-slate-600">Upload a CSV / JSON / text file. We will later parse, detect PII, and queue analyses.</p>
<div className="flex flex-col gap-3">
<div
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
className={
"rounded-lg border-2 border-dashed p-6 text-center transition-colors " +
(isDragging ? "border-brand-600 bg-brand-50" : "border-slate-300 hover:border-brand-300")
}
>
<p className="text-sm text-slate-600">Drag & drop a CSV / JSON / TXT here, or click to browse.</p>
<div className="mt-3">
<button
type="button"
onClick={() => inputRef.current?.click()}
className="inline-flex items-center rounded-md bg-brand-600 px-4 py-2 text-white text-sm font-medium shadow hover:bg-brand-500"
>
Choose file
</button>
</div>
</div>
<input
ref={inputRef}
type="file"
accept=".csv,.json,.txt"
onChange={handleFileChange}
className="hidden"
aria-hidden
/>
{progress > 0 && (
<div className="w-full">
<div className="h-2 w-full rounded-full bg-slate-200 overflow-hidden">
<div
className="h-2 bg-brand-600 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<div className="mt-1 text-xs text-slate-500">Processing {progress}%</div>
</div>
)}
{fileMeta && (
<div className="rounded-md border border-slate-200 p-4 bg-white shadow-sm">
<div className="flex items-center justify-between mb-2">
<div className="text-sm font-medium">{fileMeta.name}</div>
<div className="text-xs text-slate-500">{Math.round(fileMeta.size / 1024)} KB</div>
</div>
{loadedFromCache && (
<div className="mb-2 text-[11px] text-brand-700">Loaded from browser cache</div>
)}
<div className="mb-3 text-xs text-slate-500">{fileMeta.type || "Unknown type"}</div>
<pre className="max-h-64 overflow-auto text-xs bg-slate-50 p-3 rounded-md whitespace-pre-wrap leading-relaxed">
{fileMeta.contentPreview || "(no preview)"}
</pre>
<div className="mt-3 flex justify-end">
<button
type="button"
onClick={async () => {
reset();
try { await deleteLatestUpload(); } catch {}
setLoadedFromCache(false);
}}
className="text-xs rounded-md border px-3 py-1.5 hover:bg-slate-50"
>
Clear
</button>
</div>
</div>
)}
</div>
</div>
);
case "bias-analysis":
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Bias Analysis (Placeholder)</h2>
<p className="text-sm text-slate-600">Once processing completes, bias metrics will appear here (distribution, representation, fairness indicators).</p>
</div>
);
case "risk-analysis":
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Risk Analysis (Placeholder)</h2>
<p className="text-sm text-slate-600">Potential privacy exposure, sensitive attribute concentration, consent gaps will be displayed.</p>
</div>
);
case "bias-risk-mitigation":
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Mitigation Suggestions (Placeholder)</h2>
<p className="text-sm text-slate-600">Recommended transformations, anonymization strategies, sampling adjustments, consent workflows.</p>
</div>
);
case "results":
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Results Summary (Placeholder)</h2>
<p className="text-sm text-slate-600">Aggregated findings and downloadable compliance report will appear here.</p>
</div>
);
default:
return null;
}
}
return (
<div className="h-full overflow-y-auto p-6 bg-white/60">
{renderTabContent()}
</div>
);
}

View File

@@ -0,0 +1,42 @@
"use client";
import { useState } from "react";
export function ChatbotPanel() {
const [messages] = useState<{ role: "user" | "assistant"; content: string }[]>([
{ role: "assistant", content: "Hi! I'll help you interpret compliance results soon." },
]);
return (
<div className="flex flex-col h-full border-l border-slate-200 bg-white/80">
<div className="h-14 flex items-center px-4 border-b border-slate-200">
<h2 className="font-semibold text-sm text-brand-700">Privacy Copilot</h2>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.map((m, i) => (
<div
key={i}
className={"rounded-md px-3 py-2 text-sm max-w-[80%] " + (m.role === "assistant" ? "bg-brand-600/10 text-brand-800" : "bg-brand-600 text-white ml-auto")}
>
{m.content}
</div>
))}
</div>
<div className="p-3 border-t border-slate-200">
<form className="flex gap-2" onSubmit={e => e.preventDefault()}>
<input
disabled
placeholder="Chat coming soon..."
className="flex-1 rounded-md border border-slate-300 bg-slate-50 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400 disabled:opacity-60"
/>
<button
type="submit"
disabled
className="rounded-md bg-brand-600 text-white px-4 py-2 text-sm font-medium disabled:opacity-50"
>
Send
</button>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,50 @@
"use client";
import clsx from "clsx";
export type TryTab =
| "processing"
| "bias-analysis"
| "risk-analysis"
| "bias-risk-mitigation"
| "results";
const tabs: { id: TryTab; label: string; description: string }[] = [
{ id: "processing", label: "Processing", description: "Upload & parse" },
{ id: "bias-analysis", label: "Bias Analysis", description: "Detect patterns" },
{ id: "risk-analysis", label: "Risk Analysis", description: "Assess exposure" },
{ id: "bias-risk-mitigation", label: "Bias & Risk Mitigation", description: "Recommend actions" },
{ id: "results", label: "Results", description: "View summaries" },
];
interface SidebarProps {
value: TryTab;
onChange: (tab: TryTab) => void;
}
export function Sidebar({ value, onChange }: SidebarProps) {
return (
<aside className={clsx("flex-none w-64 h-full border-r border-slate-200 bg-white/80 flex flex-col")}>
<div className="flex items-center px-3 h-14 border-b border-slate-200">
<span className="font-semibold text-sm text-brand-700">Workflow</span>
</div>
<nav className="flex-1 overflow-y-auto py-2 space-y-1">
{tabs.map((t) => {
const selected = t.id === value;
return (
<button
key={t.id}
onClick={() => onChange(t.id)}
className={clsx(
"group w-full text-left px-4 py-3 text-sm font-medium flex flex-col gap-0.5 transition-colors",
selected ? "bg-brand-600/10 text-brand-800" : "hover:bg-brand-50 text-slate-700"
)}
>
<span className={clsx("", selected && "font-semibold")}>{t.label}</span>
<span className="text-xs text-slate-500 group-hover:text-slate-600 line-clamp-1">{t.description}</span>
</button>
);
})}
</nav>
</aside>
);
}

View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
typedRoutes: true,
},
};
export default nextConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"name": "nordic-privacy-ai",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"clsx": "^2.1.1",
"next": "14.2.5",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "20.11.30",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.17",
"eslint": "8.57.0",
"eslint-config-next": "14.2.5",
"postcss": "8.4.35",
"tailwindcss": "3.4.3",
"typescript": "5.4.5"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,29 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
colors: {
brand: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e'
}
}
},
},
plugins: [],
};
export default config;

View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "es2020",
"lib": [
"dom",
"dom.iterable",
"es2020"
],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"types": [
"node",
"react"
],
"baseUrl": ".",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}