mirror of
https://github.com/PlatypusPus/MushroomEmpire.git
synced 2026-02-07 22:18:59 +00:00
frontend
This commit is contained in:
18
frontend/nordic-privacy-ai/.gitignore
vendored
Normal file
18
frontend/nordic-privacy-ai/.gitignore
vendored
Normal 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*
|
||||
28
frontend/nordic-privacy-ai/README.md
Normal file
28
frontend/nordic-privacy-ai/README.md
Normal 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
|
||||
46
frontend/nordic-privacy-ai/app/globals.css
Normal file
46
frontend/nordic-privacy-ai/app/globals.css
Normal 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 */
|
||||
}
|
||||
18
frontend/nordic-privacy-ai/app/layout.tsx
Normal file
18
frontend/nordic-privacy-ai/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
frontend/nordic-privacy-ai/app/page.tsx
Normal file
59
frontend/nordic-privacy-ai/app/page.tsx
Normal 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 ecosystem—built 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">We’re 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>
|
||||
);
|
||||
}
|
||||
23
frontend/nordic-privacy-ai/app/try/page.tsx
Normal file
23
frontend/nordic-privacy-ai/app/try/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
63
frontend/nordic-privacy-ai/components/Navbar.tsx
Normal file
63
frontend/nordic-privacy-ai/components/Navbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
42
frontend/nordic-privacy-ai/components/common/Reveal.tsx
Normal file
42
frontend/nordic-privacy-ai/components/common/Reveal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
17
frontend/nordic-privacy-ai/components/landing/Footer.tsx
Normal file
17
frontend/nordic-privacy-ai/components/landing/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
frontend/nordic-privacy-ai/components/landing/Steps.tsx
Normal file
32
frontend/nordic-privacy-ai/components/landing/Steps.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
21
frontend/nordic-privacy-ai/components/landing/ValueProps.tsx
Normal file
21
frontend/nordic-privacy-ai/components/landing/ValueProps.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
238
frontend/nordic-privacy-ai/components/try/CenterPanel.tsx
Normal file
238
frontend/nordic-privacy-ai/components/try/CenterPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
42
frontend/nordic-privacy-ai/components/try/ChatbotPanel.tsx
Normal file
42
frontend/nordic-privacy-ai/components/try/ChatbotPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
50
frontend/nordic-privacy-ai/components/try/Sidebar.tsx
Normal file
50
frontend/nordic-privacy-ai/components/try/Sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
frontend/nordic-privacy-ai/next-env.d.ts
vendored
Normal file
5
frontend/nordic-privacy-ai/next-env.d.ts
vendored
Normal 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.
|
||||
9
frontend/nordic-privacy-ai/next.config.mjs
Normal file
9
frontend/nordic-privacy-ai/next.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
typedRoutes: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6078
frontend/nordic-privacy-ai/package-lock.json
generated
Normal file
6078
frontend/nordic-privacy-ai/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
frontend/nordic-privacy-ai/package.json
Normal file
28
frontend/nordic-privacy-ai/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
frontend/nordic-privacy-ai/postcss.config.js
Normal file
6
frontend/nordic-privacy-ai/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
29
frontend/nordic-privacy-ai/tailwind.config.ts
Normal file
29
frontend/nordic-privacy-ai/tailwind.config.ts
Normal 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;
|
||||
41
frontend/nordic-privacy-ai/tsconfig.json
Normal file
41
frontend/nordic-privacy-ai/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user