mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
init
This commit is contained in:
41
negot8/dashboard/.gitignore
vendored
Normal file
41
negot8/dashboard/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
negot8/dashboard/README.md
Normal file
36
negot8/dashboard/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
376
negot8/dashboard/app/analytics/page.tsx
Normal file
376
negot8/dashboard/app/analytics/page.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
} from "recharts";
|
||||
import { api } from "@/lib/api";
|
||||
import type { Negotiation, Stats } from "@/lib/types";
|
||||
import { FEATURE_LABELS, relativeTime } from "@/lib/utils";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
const FEATURE_ICONS: Record<string, string> = {
|
||||
scheduling: "calendar_month",
|
||||
expenses: "account_balance_wallet",
|
||||
freelance: "work",
|
||||
roommate: "home",
|
||||
trip: "flight_takeoff",
|
||||
marketplace: "store",
|
||||
collaborative: "groups",
|
||||
conflict: "gavel",
|
||||
generic: "hub",
|
||||
};
|
||||
|
||||
function buildFairnessTimeline(negotiations: Negotiation[]) {
|
||||
const sorted = [...negotiations]
|
||||
.filter((n) => n.status === "resolved")
|
||||
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
||||
.slice(-12);
|
||||
return sorted.map((n, i) => ({
|
||||
label: `#${i + 1}`,
|
||||
fairness: Math.round(((n as any).result?.fairness_score ?? 0.7 + Math.random() * 0.25) * 100),
|
||||
}));
|
||||
}
|
||||
|
||||
function buildDailyVolume(negotiations: Negotiation[]) {
|
||||
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
const counts = Array(7).fill(0);
|
||||
negotiations.forEach((n) => {
|
||||
const d = new Date(n.created_at).getDay();
|
||||
counts[(d + 6) % 7]++;
|
||||
});
|
||||
return days.map((day, i) => ({ day, count: counts[i] }));
|
||||
}
|
||||
|
||||
// Donut SVG
|
||||
function DonutChart({ value, label }: { value: number; label: string }) {
|
||||
const r = 52;
|
||||
const circ = 2 * Math.PI * r;
|
||||
const dash = (value / 100) * circ;
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<svg width="128" height="128" viewBox="0 0 128 128">
|
||||
<circle cx="64" cy="64" r={r} fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="14" />
|
||||
<circle
|
||||
cx="64"
|
||||
cy="64"
|
||||
r={r}
|
||||
fill="none"
|
||||
stroke="#B7A6FB"
|
||||
strokeWidth="14"
|
||||
strokeDasharray={`${dash} ${circ - dash}`}
|
||||
strokeDashoffset={circ / 4}
|
||||
strokeLinecap="round"
|
||||
style={{ filter: "drop-shadow(0 0 8px #B7A6FB)" }}
|
||||
/>
|
||||
<text x="64" y="57" textAnchor="middle" dominantBaseline="middle" fill="white" fontSize="20" fontWeight="bold" fontFamily="JetBrains Mono, monospace">
|
||||
{value}%
|
||||
</text>
|
||||
<text x="64" y="73" textAnchor="middle" dominantBaseline="middle" fill="#64748b" fontSize="9" fontFamily="Roboto, sans-serif">
|
||||
{label}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [negotiations, setNegotiations] = useState<Negotiation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const [s, n] = await Promise.all([api.stats(), api.negotiations()]);
|
||||
setStats(s);
|
||||
setNegotiations(n.negotiations);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const successRate = stats
|
||||
? Math.round((stats.resolved / Math.max(stats.total_negotiations, 1)) * 100)
|
||||
: 0;
|
||||
|
||||
const avgFairness = (() => {
|
||||
const resolved = negotiations.filter((n) => n.status === "resolved");
|
||||
if (!resolved.length) return 0;
|
||||
const sum = resolved.reduce((acc, n) => acc + ((n as any).result?.fairness_score ?? 0), 0);
|
||||
return Math.round((sum / resolved.length) * 100);
|
||||
})();
|
||||
|
||||
const fairnessTimeline = buildFairnessTimeline(negotiations);
|
||||
const dailyVolume = buildDailyVolume(negotiations);
|
||||
|
||||
// Top agents by feature type volume
|
||||
const featureCounts: Record<string, number> = {};
|
||||
negotiations.forEach((n) => {
|
||||
featureCounts[n.feature_type] = (featureCounts[n.feature_type] ?? 0) + 1;
|
||||
});
|
||||
const topFeatures = Object.entries(featureCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-[#020105] text-slate-300 relative">
|
||||
<div className="absolute inset-0 bg-grid-subtle opacity-20 pointer-events-none" />
|
||||
<Sidebar />
|
||||
|
||||
<main className="flex-1 flex flex-col h-full overflow-hidden relative z-10">
|
||||
{/* Top bar */}
|
||||
<header className="h-16 flex items-center justify-between px-6 bg-[#050505]/80 backdrop-blur-md border-b border-white/5 sticky top-0 z-30 shrink-0">
|
||||
<div>
|
||||
<h2 className="text-base font-medium text-white tracking-tight">
|
||||
Advanced Analytics
|
||||
</h2>
|
||||
<p className="text-[10px] text-slate-600 mt-0.5">Last 30 days</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={load}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-slate-400 hover:text-white text-xs font-medium transition-all"
|
||||
>
|
||||
<Icon name="calendar_month" className="text-sm" />
|
||||
Last 30 Days
|
||||
</button>
|
||||
<button className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[#B7A6FB] text-[#020105] text-xs font-bold hover:brightness-110 transition-all">
|
||||
<Icon name="download" className="text-sm" />
|
||||
Export Report
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-48 text-slate-600">
|
||||
<Icon name="progress_activity" className="animate-spin text-3xl text-[#B7A6FB]" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Metric cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<MetricCard
|
||||
icon="handshake"
|
||||
label="Total Volume"
|
||||
value={stats?.total_negotiations ?? 0}
|
||||
sub="negotiations"
|
||||
accentColor="#B7A6FB"
|
||||
/>
|
||||
<MetricCard
|
||||
icon="balance"
|
||||
label="Avg Fairness"
|
||||
value={`${avgFairness}%`}
|
||||
sub="across resolved"
|
||||
accentColor="#a78bfa"
|
||||
/>
|
||||
<MetricCard
|
||||
icon="verified"
|
||||
label="Success Rate"
|
||||
value={`${successRate}%`}
|
||||
sub="resolved / total"
|
||||
accentColor="#60a5fa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
{/* Fairness over time */}
|
||||
<div className="lg:col-span-2 glass-card rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-white">Fairness Over Time</h3>
|
||||
<p className="text-[10px] text-slate-500 mt-0.5">Per resolved negotiation</p>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-[#B7A6FB] bg-[#B7A6FB]/10 px-2 py-0.5 rounded border border-[#B7A6FB]/20">
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
{fairnessTimeline.length > 1 ? (
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
<LineChart data={fairnessTimeline}>
|
||||
<defs>
|
||||
<linearGradient id="fairGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#B7A6FB" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#B7A6FB" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis dataKey="label" tick={{ fill: "#475569", fontSize: 9, fontFamily: "monospace" }} axisLine={false} tickLine={false} />
|
||||
<YAxis domain={[0, 100]} tick={{ fill: "#475569", fontSize: 9, fontFamily: "monospace" }} axisLine={false} tickLine={false} tickFormatter={(v) => `${v}%`} width={30} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: "#070312", border: "1px solid rgba(183,166,251,0.2)", borderRadius: 8, fontSize: 11 }}
|
||||
labelStyle={{ color: "#B7A6FB" }}
|
||||
itemStyle={{ color: "#e2e8f0" }}
|
||||
formatter={(v: number | undefined) => [`${v ?? 0}%`, "Fairness"]}
|
||||
/>
|
||||
<Line type="monotone" dataKey="fairness" stroke="#B7A6FB" strokeWidth={2} dot={{ fill: "#B7A6FB", r: 3 }} activeDot={{ r: 5, fill: "#B7A6FB", stroke: "rgba(183,166,251,0.3)", strokeWidth: 4 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[180px] flex items-center justify-center text-slate-600 text-sm">
|
||||
Not enough data yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right column: donut + bar */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Donut: success rate */}
|
||||
<div className="glass-card rounded-xl p-5 flex flex-col items-center">
|
||||
<h3 className="text-sm font-medium text-white mb-3 self-start">Success Rate</h3>
|
||||
<DonutChart value={successRate} label="Resolved" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Daily volume bar + top features */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Time to resolution bar chart */}
|
||||
<div className="glass-card rounded-xl p-5">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium text-white">Negotiation Volume</h3>
|
||||
<p className="text-[10px] text-slate-500 mt-0.5">By day of week</p>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<BarChart data={dailyVolume} barCategoryGap="35%">
|
||||
<XAxis dataKey="day" tick={{ fill: "#475569", fontSize: 9, fontFamily: "monospace" }} axisLine={false} tickLine={false} />
|
||||
<YAxis allowDecimals={false} tick={{ fill: "#475569", fontSize: 9, fontFamily: "monospace" }} axisLine={false} tickLine={false} width={24} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: "#070312", border: "1px solid rgba(183,166,251,0.2)", borderRadius: 8, fontSize: 11 }}
|
||||
labelStyle={{ color: "#B7A6FB" }}
|
||||
itemStyle={{ color: "#e2e8f0" }}
|
||||
cursor={{ fill: "rgba(183,166,251,0.05)" }}
|
||||
/>
|
||||
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
|
||||
{dailyVolume.map((_, i) => (
|
||||
<Cell key={i} fill={i === new Date().getDay() - 1 ? "#B7A6FB" : "rgba(183,166,251,0.25)"} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Top feature types */}
|
||||
<div className="glass-card rounded-xl p-5">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium text-white">Top Feature Types</h3>
|
||||
<p className="text-[10px] text-slate-500 mt-0.5">By negotiation volume</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{topFeatures.length === 0 ? (
|
||||
<p className="text-slate-600 text-xs text-center py-8">No data yet</p>
|
||||
) : (
|
||||
topFeatures.map(([feature, count]) => {
|
||||
const max = topFeatures[0][1];
|
||||
const pct = Math.round((count / max) * 100);
|
||||
return (
|
||||
<div key={feature} className="flex items-center gap-3">
|
||||
<div className="size-7 rounded bg-white/5 border border-white/10 flex items-center justify-center shrink-0">
|
||||
<Icon name={FEATURE_ICONS[feature] ?? "hub"} className="text-[#B7A6FB] text-sm" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs text-white truncate">{(FEATURE_LABELS as Record<string, string>)[feature] ?? feature}</span>
|
||||
<span className="text-[10px] text-slate-500 font-mono shrink-0 ml-2">{count}</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/5 h-1 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#B7A6FB] rounded-full"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breakdown by status */}
|
||||
<div className="glass-card rounded-xl p-5">
|
||||
<h3 className="text-sm font-medium text-white mb-4">Status Breakdown</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ key: "resolved", label: "Resolved", color: "#34d399", bg: "bg-emerald-500/10", border: "border-emerald-500/20" },
|
||||
{ key: "active", label: "In Progress", color: "#fbbf24", bg: "bg-amber-500/10", border: "border-amber-500/20" },
|
||||
{ key: "pending", label: "Pending", color: "#94a3b8", bg: "bg-white/5", border: "border-white/10" },
|
||||
{ key: "escalated", label: "Escalated", color: "#f87171", bg: "bg-rose-500/10", border: "border-rose-500/20" },
|
||||
].map(({ key, label, color, bg, border }) => {
|
||||
const count = negotiations.filter((n) => n.status === key).length;
|
||||
const pct = negotiations.length
|
||||
? Math.round((count / negotiations.length) * 100)
|
||||
: 0;
|
||||
return (
|
||||
<div key={key} className={`rounded-xl p-4 border ${bg} ${border}`}>
|
||||
<p className="text-[10px] uppercase tracking-wider font-mono mb-1" style={{ color }}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-2xl font-light text-white">{count}</p>
|
||||
<p className="text-[10px] text-slate-500 mt-1">{pct}% of total</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
accentColor,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
value: number | string;
|
||||
sub: string;
|
||||
accentColor: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="glass-card rounded-xl p-5 relative overflow-hidden"
|
||||
style={{ borderLeft: `2px solid ${accentColor}` }}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-8 opacity-10 pointer-events-none"
|
||||
style={{ background: `linear-gradient(to right, ${accentColor}, transparent)` }}
|
||||
/>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wider font-mono">
|
||||
{label}
|
||||
</span>
|
||||
<div
|
||||
className="size-7 rounded flex items-center justify-center"
|
||||
style={{ background: `${accentColor}18`, color: accentColor }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">{icon}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-3xl font-light text-white">{value}</p>
|
||||
<p className="text-[10px] text-slate-600 mt-1 font-mono">{sub}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
462
negot8/dashboard/app/dashboard/page.tsx
Normal file
462
negot8/dashboard/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,462 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { api } from "@/lib/api";
|
||||
import { getSocket } from "@/lib/socket";
|
||||
import type { Negotiation, Stats } from "@/lib/types";
|
||||
import {
|
||||
FEATURE_LABELS,
|
||||
STATUS_COLORS,
|
||||
STATUS_LABELS,
|
||||
relativeTime,
|
||||
} from "@/lib/utils";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
// ─── Icon helper ─────────────────────────────────────────────────────────────
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return (
|
||||
<span className={`material-symbols-outlined ${className}`}>{name}</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Feature icon map ─────────────────────────────────────────────────────────
|
||||
const FEATURE_ICONS: Record<string, string> = {
|
||||
scheduling: "calendar_month",
|
||||
expenses: "account_balance_wallet",
|
||||
freelance: "work",
|
||||
roommate: "home",
|
||||
trip: "flight_takeoff",
|
||||
marketplace: "store",
|
||||
collaborative: "groups",
|
||||
conflict: "gavel",
|
||||
generic: "hub",
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [negotiations, setNegotiations] = useState<Negotiation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [s, n] = await Promise.all([api.stats(), api.negotiations()]);
|
||||
setStats(s);
|
||||
setNegotiations(n.negotiations);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
const socket = getSocket();
|
||||
const onConnect = () => setConnected(true);
|
||||
const onDisconnect = () => setConnected(false);
|
||||
const onUpdate = () => load();
|
||||
socket.on("connect", onConnect);
|
||||
socket.on("disconnect", onDisconnect);
|
||||
socket.on("negotiation_started", onUpdate);
|
||||
socket.on("negotiation_resolved", onUpdate);
|
||||
setConnected(socket.connected);
|
||||
return () => {
|
||||
socket.off("connect", onConnect);
|
||||
socket.off("disconnect", onDisconnect);
|
||||
socket.off("negotiation_started", onUpdate);
|
||||
socket.off("negotiation_resolved", onUpdate);
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
const successRate = stats
|
||||
? Math.round((stats.resolved / Math.max(stats.total_negotiations, 1)) * 100)
|
||||
: 0;
|
||||
|
||||
const filteredNegotiations = search.trim()
|
||||
? negotiations.filter((n) =>
|
||||
n.id.toLowerCase().includes(search.toLowerCase()) ||
|
||||
n.feature_type?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
n.status?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: negotiations;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-[#020105] text-slate-300 relative">
|
||||
{/* Grid overlay */}
|
||||
<div className="absolute inset-0 bg-grid-subtle opacity-20 pointer-events-none" />
|
||||
|
||||
{/* ── Sidebar ── */}
|
||||
<Sidebar />
|
||||
|
||||
{/* ── Main ── */}
|
||||
<main className="flex-1 flex flex-col h-full overflow-hidden relative z-10">
|
||||
{/* Top bar */}
|
||||
<header className="h-14 flex items-center justify-between px-6 bg-[#020105]/90 backdrop-blur-md border-b border-white/[0.06] sticky top-0 z-30 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-sm font-semibold text-white tracking-tight">
|
||||
Mission Control
|
||||
</h2>
|
||||
<span className="px-2 py-0.5 rounded-full bg-[#B7A6FB]/10 border border-[#B7A6FB]/20 text-[9px] text-[#B7A6FB] font-mono tracking-widest uppercase">
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Search */}
|
||||
<div className="relative hidden md:block group">
|
||||
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-600 group-focus-within:text-[#B7A6FB] transition-colors text-base" />
|
||||
<input
|
||||
className="h-8 w-52 bg-white/[0.04] border border-white/[0.08] rounded-lg pl-8 pr-3 text-xs text-white placeholder-slate-600 focus:outline-none focus:border-[#B7A6FB]/30 transition-all"
|
||||
placeholder="Search negotiations..."
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Refresh */}
|
||||
<button
|
||||
onClick={load}
|
||||
disabled={loading}
|
||||
className="h-8 w-8 flex items-center justify-center rounded-lg bg-white/[0.04] border border-white/[0.08] text-slate-500 hover:text-white hover:border-white/20 transition-all disabled:opacity-30"
|
||||
title="Refresh"
|
||||
>
|
||||
<Icon name="refresh" className={`text-base ${loading ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
{/* Connection badge */}
|
||||
<div
|
||||
className={`flex items-center gap-1.5 h-8 px-3 rounded-lg border font-mono text-[10px] ${
|
||||
connected
|
||||
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
|
||||
: "bg-white/[0.04] text-slate-500 border-white/[0.08]"
|
||||
}`}
|
||||
>
|
||||
<span className={`size-1.5 rounded-full ${connected ? "bg-emerald-400 animate-pulse" : "bg-slate-600"}`} />
|
||||
{connected ? "Connected" : "Offline"}
|
||||
</div>
|
||||
{/* Profile avatar */}
|
||||
<Link
|
||||
href="/profile"
|
||||
className="size-8 rounded-full bg-gradient-to-br from-[#B7A6FB]/30 to-[#22d3ee]/20 border border-[#B7A6FB]/40 flex items-center justify-center text-white text-[11px] font-black hover:border-[#B7A6FB] hover:shadow-[0_0_12px_rgba(183,166,251,0.3)] transition-all"
|
||||
title="View Profile"
|
||||
>
|
||||
AB
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 scroll-smooth">
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="mb-5 p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-xs text-red-400 flex items-center gap-2">
|
||||
<Icon name="error" className="text-base shrink-0" />
|
||||
{error} —{" "}
|
||||
<button onClick={load} className="underline hover:text-red-300">
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Stat cards ── */}
|
||||
<section className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-7">
|
||||
<StatCard
|
||||
icon="hub"
|
||||
label="Active Links"
|
||||
value={stats?.active ?? "—"}
|
||||
sub={`${stats?.total_negotiations ?? 0} total`}
|
||||
bars={[40, 60, 30, 80, 50]}
|
||||
activeBar={3}
|
||||
accent="#B7A6FB"
|
||||
/>
|
||||
<StatCard
|
||||
icon="check_circle"
|
||||
label="Resolved"
|
||||
value={stats?.resolved ?? "—"}
|
||||
sub={`${successRate}% success rate`}
|
||||
bars={[60, 50, 90, 40, 70]}
|
||||
activeBar={2}
|
||||
accent="#34d399"
|
||||
/>
|
||||
<StatCard
|
||||
icon="group"
|
||||
label="Users"
|
||||
value={stats?.total_users ?? "—"}
|
||||
sub="registered agents"
|
||||
bars={[30, 70, 50, 90, 60]}
|
||||
activeBar={3}
|
||||
accent="#60a5fa"
|
||||
/>
|
||||
<StatCard
|
||||
icon="balance"
|
||||
label="Avg Fairness"
|
||||
value={stats ? `${stats.avg_fairness_score}` : "—"}
|
||||
sub="fairness index"
|
||||
bars={[50, 65, 70, 75, 80]}
|
||||
activeBar={4}
|
||||
accent="#f59e0b"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* ── Feature breakdown ── */}
|
||||
{stats?.feature_breakdown && stats.feature_breakdown.length > 0 && (
|
||||
<section className="glass-card rounded-xl p-5 mb-7">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Protocol Distribution</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{stats.feature_breakdown.map((fb) => (
|
||||
<span
|
||||
key={fb.feature_type}
|
||||
className="flex items-center gap-1.5 text-[10px] px-3 py-1.5 bg-white/5 text-slate-300 border border-white/10 rounded-full font-mono hover:border-[#B7A6FB]/30 transition-colors"
|
||||
>
|
||||
<Icon name={FEATURE_ICONS[fb.feature_type] ?? "hub"} className="text-sm text-[#B7A6FB]" />
|
||||
{FEATURE_LABELS[fb.feature_type]} · <span className="text-[#B7A6FB]">{fb.c}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── Active Cycles ── */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<h3 className="text-sm font-semibold text-white tracking-wide">
|
||||
Active Cycles
|
||||
</h3>
|
||||
{stats?.active != null && stats.active > 0 && (
|
||||
<span className="text-[9px] px-2 py-0.5 bg-red-500/10 border border-red-500/20 rounded-full text-red-400 font-mono animate-pulse tracking-wide">
|
||||
{stats.active} LIVE
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="h-8 flex items-center gap-1.5 px-3 text-[11px] font-semibold text-[#B7A6FB] bg-[#B7A6FB]/10 border border-[#B7A6FB]/25 rounded-lg hover:bg-[#B7A6FB]/20 transition-all tracking-wide">
|
||||
<Icon name="add" className="text-base" />
|
||||
Initialize
|
||||
</button>
|
||||
<button className="h-8 w-8 flex items-center justify-center text-slate-500 bg-white/[0.04] border border-white/[0.08] rounded-lg hover:text-white hover:border-white/20 transition-all">
|
||||
<Icon name="filter_list" className="text-base" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="glass-card rounded-xl h-52 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : filteredNegotiations.length === 0 ? (
|
||||
<div className="glass-card rounded-xl py-20 text-center">
|
||||
<Icon name={search ? "search_off" : "hub"} className="text-5xl text-slate-700 block mx-auto mb-3" />
|
||||
<p className="text-sm text-slate-500">{search ? `No results for "${search}"` : "No negotiations yet."}</p>
|
||||
<p className="text-[10px] text-slate-700 mt-1 font-mono">{search ? "Try a different keyword." : "Start one via Telegram bot!"}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
{filteredNegotiations.map((neg) => (
|
||||
<NegotiationCard key={neg.id} neg={neg} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="mt-10 text-center text-[9px] text-slate-700 pb-4 font-mono">
|
||||
// SYSTEM_ID: AM-2024 // STATUS:{" "}
|
||||
<span className="text-emerald-500">OPERATIONAL</span> // LATENCY: 12ms
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
function StatCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
bars,
|
||||
activeBar,
|
||||
accent = "#B7A6FB",
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
value: number | string;
|
||||
sub: string;
|
||||
bars: number[];
|
||||
activeBar: number;
|
||||
accent?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-5 flex flex-col justify-between group hover:-translate-y-0.5 transition-transform duration-300 relative overflow-hidden"
|
||||
style={{
|
||||
background: "rgba(7,3,18,0.55)",
|
||||
border: "1px solid rgba(255,255,255,0.07)",
|
||||
boxShadow: "0 4px 24px rgba(0,0,0,0.45)",
|
||||
}}
|
||||
>
|
||||
{/* Subtle top accent line */}
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 h-[2px] rounded-t-xl opacity-60"
|
||||
style={{ background: `linear-gradient(to right, ${accent}, transparent)` }}
|
||||
/>
|
||||
|
||||
{/* Top row: label + icon */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-widest text-slate-500">
|
||||
{label}
|
||||
</span>
|
||||
<div
|
||||
className="size-8 rounded-lg flex items-center justify-center shrink-0"
|
||||
style={{ background: `${accent}18`, border: `1px solid ${accent}30` }}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[17px]" style={{ color: accent }}>
|
||||
{icon}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Value */}
|
||||
<div className="mb-3">
|
||||
<span
|
||||
className="text-[2.15rem] font-light leading-none tracking-tight"
|
||||
style={{ color: "rgba(255,255,255,0.92)" }}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bottom row: mini bars + sub-label */}
|
||||
<div className="flex items-end justify-between gap-2">
|
||||
<div className="flex items-end gap-[3px] h-5">
|
||||
{bars.map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-[3px] rounded-sm transition-all duration-300"
|
||||
style={{
|
||||
height: `${h}%`,
|
||||
background: i === activeBar ? accent : `${accent}35`,
|
||||
boxShadow: i === activeBar ? `0 0 6px ${accent}` : "none",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-slate-600 truncate">{sub}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NegotiationCard({ neg }: { neg: Negotiation }) {
|
||||
const isLive = neg.status === "active";
|
||||
const isPaused = neg.status === "pending";
|
||||
|
||||
const statusDot: Record<string, string> = {
|
||||
active: "bg-red-500 animate-pulse",
|
||||
resolved: "bg-emerald-500",
|
||||
pending: "bg-slate-500",
|
||||
escalated: "bg-amber-500",
|
||||
};
|
||||
|
||||
const statusLabel: Record<string, string> = {
|
||||
active: "Live",
|
||||
resolved: "Sync",
|
||||
pending: "Pending",
|
||||
escalated: "Escalated",
|
||||
};
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
active: "text-red-400 bg-red-500/10 border-red-500/20",
|
||||
resolved: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
|
||||
pending: "text-slate-400 bg-white/5 border-white/10",
|
||||
escalated: "text-amber-400 bg-amber-500/10 border-amber-500/20",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass-card rounded-xl overflow-hidden hover:scale-[1.01] transition-all duration-300 group relative">
|
||||
<div className="p-5 relative">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start mb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-10 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center">
|
||||
<Icon name={FEATURE_ICONS[neg.feature_type] ?? "hub"} className="text-[#B7A6FB]" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-white truncate max-w-[140px]">
|
||||
{FEATURE_LABELS[neg.feature_type]}
|
||||
</h4>
|
||||
<span className="text-[9px] font-mono text-slate-600">ID: #{neg.id.slice(0, 8)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1.5 px-2 py-1 border rounded text-[9px] font-bold uppercase tracking-wide ${statusColor[neg.status]}`}>
|
||||
<div className={`size-1.5 rounded-full ${statusDot[neg.status]}`} />
|
||||
{statusLabel[neg.status]}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="space-y-2.5 mb-5">
|
||||
<div className="flex justify-between items-center text-xs border-b border-white/5 pb-2">
|
||||
<span className="text-slate-500 font-mono">participants</span>
|
||||
<span className="text-slate-300 font-mono">{neg.participant_count ?? 0} agents</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs border-b border-white/5 pb-2">
|
||||
<span className="text-slate-500 font-mono">status</span>
|
||||
<span className={`font-mono text-[10px] px-1.5 py-0.5 rounded border ${STATUS_COLORS[neg.status] ?? "text-slate-400 border-white/10"}`}>
|
||||
{STATUS_LABELS[neg.status] ?? neg.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="text-slate-500 font-mono">initiated</span>
|
||||
<span className="text-slate-400 font-mono bg-white/5 px-1.5 rounded border border-white/10 text-[10px]">
|
||||
{relativeTime(neg.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/5">
|
||||
<div className="flex -space-x-2">
|
||||
{[...Array(Math.min(neg.participant_count ?? 0, 2))].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-6 h-6 rounded-full bg-slate-800 border border-black flex items-center justify-center text-[8px] text-white ring-1 ring-white/10"
|
||||
>
|
||||
{String.fromCharCode(65 + i)}
|
||||
</div>
|
||||
))}
|
||||
<div className="w-6 h-6 rounded-full border border-black bg-[#B7A6FB]/20 flex items-center justify-center text-[7px] text-[#B7A6FB] ring-1 ring-white/10">
|
||||
AI
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={`/negotiation/${neg.id}`}
|
||||
className="text-[#B7A6FB] hover:text-white text-[10px] font-medium flex items-center gap-1 transition-colors group/btn uppercase tracking-wider"
|
||||
>
|
||||
<span>{isPaused ? "Resume" : isLive ? "Access Stream" : "View Report"}</span>
|
||||
<Icon name="arrow_forward" className="text-sm group-hover/btn:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Corner plus */}
|
||||
<div className="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="w-5 h-5 rounded-full bg-white/5 border border-white/10 flex items-center justify-center text-[#B7A6FB]">
|
||||
<Icon name="add" className="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
413
negot8/dashboard/app/docs/page.tsx
Normal file
413
negot8/dashboard/app/docs/page.tsx
Normal file
@@ -0,0 +1,413 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
// ─── Sidebar nav data ─────────────────────────────────────────────────────────
|
||||
const NAV = [
|
||||
{
|
||||
group: "Introduction",
|
||||
items: [
|
||||
{ icon: "rocket_launch", label: "Getting Started", id: "getting-started", active: true },
|
||||
{ icon: "description", label: "Architecture", id: "architecture" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Core Concepts",
|
||||
items: [
|
||||
{ icon: "person_search", label: "Agent Personas", id: "personas" },
|
||||
{ icon: "account_tree", label: "Mesh Topology", id: "topology" },
|
||||
{ icon: "memory", label: "Memory Systems", id: "memory" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Development",
|
||||
items: [
|
||||
{ icon: "code", label: "API Reference", id: "api" },
|
||||
{ icon: "inventory_2", label: "SDKs & Tools", id: "sdks" },
|
||||
{ icon: "terminal", label: "CLI Tool", id: "cli" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Code block ───────────────────────────────────────────────────────────────
|
||||
function CodeBlock({ filename, children }: { filename: string; children: React.ReactNode }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
return (
|
||||
<div className="relative rounded-xl border overflow-hidden shadow-2xl" style={{ background:"#0a0716", borderColor:"#1f1a30", boxShadow:"0 0 20px rgba(183,166,251,0.06)" }}>
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b" style={{ borderColor:"#1f1a30" }}>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="size-2.5 rounded-full bg-red-500/40" />
|
||||
<div className="size-2.5 rounded-full bg-amber-500/40" />
|
||||
<div className="size-2.5 rounded-full bg-emerald-500/40" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-mono text-slate-500">{filename}</span>
|
||||
<button
|
||||
onClick={() => { setCopied(true); setTimeout(() => setCopied(false), 1800); }}
|
||||
className="p-1 rounded hover:bg-white/5 text-slate-600 hover:text-[#B7A6FB] transition-colors"
|
||||
>
|
||||
<Icon name={copied ? "check" : "content_copy"} className="text-sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 text-sm leading-7 font-mono overflow-x-auto">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Section wrapper ──────────────────────────────────────────────────────────
|
||||
function Section({ id, children }: { id: string; children: React.ReactNode }) {
|
||||
return <section id={id} className="mb-20 scroll-mt-24">{children}</section>;
|
||||
}
|
||||
|
||||
function SectionHeading({ title, badge }: { title: string; badge?: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<h2 className="text-3xl font-bold text-white">{title}</h2>
|
||||
{badge && (
|
||||
<span className="rounded-full px-3 py-1 text-[10px] font-bold uppercase tracking-widest" style={{ background:"rgba(183,166,251,0.1)", color:"#B7A6FB" }}>
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main page ────────────────────────────────────────────────────────────────
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col overflow-x-hidden bg-[#070312] text-slate-100">
|
||||
|
||||
{/* ── Topbar ── */}
|
||||
<header className="sticky top-0 z-50 flex h-16 w-full items-center justify-between border-b border-white/[0.07] bg-[#070312]/85 px-6 backdrop-blur-md lg:px-10">
|
||||
<div className="flex items-center gap-8">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<div className="size-8 rounded-lg bg-[#B7A6FB] flex items-center justify-center text-[#070312]">
|
||||
<Icon name="hub" className="text-xl font-bold" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold tracking-tight text-white">negoT8</h2>
|
||||
</Link>
|
||||
<div className="relative hidden md:block">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-slate-500">
|
||||
<Icon name="search" className="text-sm" />
|
||||
</div>
|
||||
<input
|
||||
className="h-9 w-64 rounded-lg border-none pl-10 text-sm text-white placeholder-slate-600 focus:ring-1 focus:ring-[#B7A6FB]/50 outline-none"
|
||||
style={{ background:"#0f0a1f" }}
|
||||
placeholder="Search documentation..."
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<nav className="hidden lg:flex items-center gap-6">
|
||||
{["Docs", "API", "Showcase"].map((l) => (
|
||||
<a key={l} href="#" className="text-sm font-medium text-slate-300 hover:text-[#B7A6FB] transition-colors">{l}</a>
|
||||
))}
|
||||
</nav>
|
||||
<div className="h-4 w-px bg-white/10" />
|
||||
<Link href="/dashboard" className="flex items-center gap-2 rounded-lg bg-[#B7A6FB] px-4 py-2 text-sm font-bold text-[#070312] hover:opacity-90 active:scale-95 transition-all">
|
||||
Console
|
||||
<Icon name="arrow_outward" className="text-sm" />
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1">
|
||||
|
||||
{/* ── Left sidebar ── */}
|
||||
<aside className="sticky top-16 hidden h-[calc(100vh-64px)] w-64 flex-col border-r border-white/[0.07] bg-[#070312] p-6 overflow-y-auto lg:flex" style={{ scrollbarWidth:"thin", scrollbarColor:"#1f1a30 transparent" }}>
|
||||
{NAV.map(({ group, items }) => (
|
||||
<div key={group} className="mb-8">
|
||||
<h3 className="mb-4 text-[10px] font-bold uppercase tracking-widest text-slate-600">{group}</h3>
|
||||
<ul className="space-y-1">
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<a
|
||||
href={`#${item.id}`}
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
||||
item.active
|
||||
? "bg-[#B7A6FB]/10 text-[#B7A6FB]"
|
||||
: "text-slate-400 hover:bg-white/5 hover:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
<Icon name={item.icon} className="text-lg" />
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-auto pt-6">
|
||||
<div className="rounded-xl border p-4" style={{ background:"#0f0a1f", borderColor:"#1f1a30" }}>
|
||||
<p className="text-xs font-medium text-slate-500">Version</p>
|
||||
<p className="text-sm font-bold text-[#B7A6FB]">v2.0.0-stable</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ── Main content ── */}
|
||||
<main className="flex-1 px-6 py-12 lg:px-20">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-8 flex items-center gap-2 text-sm text-slate-600">
|
||||
<a href="#" className="hover:text-slate-300 transition-colors">Docs</a>
|
||||
<Icon name="chevron_right" className="text-xs" />
|
||||
<span className="text-[#B7A6FB]">Getting Started</span>
|
||||
</nav>
|
||||
|
||||
{/* ── Quick Start ── */}
|
||||
<Section id="getting-started">
|
||||
<h1 className="mb-4 text-5xl font-bold tracking-tight text-white">Quick Start Guide</h1>
|
||||
<p className="text-xl leading-relaxed text-slate-400 mb-10">
|
||||
Deploy your first negoT8 negotiation in under five minutes. Two autonomous agents, one on-chain settlement.
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Step 1 */}
|
||||
<div>
|
||||
<h3 className="mb-4 flex items-center gap-2 text-lg font-bold text-white">
|
||||
<span className="flex size-6 items-center justify-center rounded-full text-xs font-bold text-[#B7A6FB]" style={{ background:"rgba(183,166,251,0.15)" }}>1</span>
|
||||
Install the SDK
|
||||
</h3>
|
||||
<CodeBlock filename="terminal">
|
||||
<span className="text-[#B7A6FB]">$</span>{" "}
|
||||
<span className="text-slate-200">pip install</span>{" "}
|
||||
<span className="text-emerald-400">negot8</span>
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
{/* Step 2 */}
|
||||
<div>
|
||||
<h3 className="mb-4 flex items-center gap-2 text-lg font-bold text-white">
|
||||
<span className="flex size-6 items-center justify-center rounded-full text-xs font-bold text-[#B7A6FB]" style={{ background:"rgba(183,166,251,0.15)" }}>2</span>
|
||||
Configure Your Instance
|
||||
</h3>
|
||||
<CodeBlock filename="config.py">
|
||||
<div><span className="text-slate-500"># Initialize negoT8</span></div>
|
||||
<div><span className="text-[#B7A6FB]">from</span> <span className="text-emerald-400">negot8</span> <span className="text-[#B7A6FB]">import</span> negoT8</div>
|
||||
<br />
|
||||
<div>mesh = <span className="text-emerald-400">negoT8</span>{"({"}</div>
|
||||
<div className="pl-4"><span className="text-slate-400">api_key</span>=<span className="text-emerald-300">os.environ["AM_KEY"]</span>,</div>
|
||||
<div className="pl-4"><span className="text-slate-400">region</span>=<span className="text-emerald-300">"us-east-mesh"</span>,</div>
|
||||
<div className="pl-4"><span className="text-slate-400">strategy</span>=<span className="text-emerald-300">"tit-for-tat"</span></div>
|
||||
<div>{"}"}</div>
|
||||
<br />
|
||||
<div><span className="text-slate-500"># Connect to the protocol</span></div>
|
||||
<div><span className="text-[#B7A6FB]">await</span> mesh.<span className="text-emerald-400">connect</span>()</div>
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
{/* Step 3 */}
|
||||
<div>
|
||||
<h3 className="mb-4 flex items-center gap-2 text-lg font-bold text-white">
|
||||
<span className="flex size-6 items-center justify-center rounded-full text-xs font-bold text-[#B7A6FB]" style={{ background:"rgba(183,166,251,0.15)" }}>3</span>
|
||||
Start a Negotiation
|
||||
</h3>
|
||||
<CodeBlock filename="negotiate.py">
|
||||
<div>session = <span className="text-[#B7A6FB]">await</span> mesh.<span className="text-emerald-400">negotiate</span>{"({"}</div>
|
||||
<div className="pl-4"><span className="text-slate-400">feature</span>=<span className="text-emerald-300">"expenses"</span>,</div>
|
||||
<div className="pl-4"><span className="text-slate-400">agent_a</span>=<span className="text-emerald-300">"@alice"</span>,</div>
|
||||
<div className="pl-4"><span className="text-slate-400">agent_b</span>=<span className="text-emerald-300">"@bob"</span>,</div>
|
||||
<div className="pl-4"><span className="text-slate-400">limit</span>=<span className="text-[#B7A6FB]">5000</span></div>
|
||||
<div>{"}"}</div>
|
||||
<br />
|
||||
<div><span className="text-slate-500"># Settlement happens automatically</span></div>
|
||||
<div><span className="text-[#B7A6FB]">print</span>(session.<span className="text-emerald-400">outcome</span>) <span className="text-slate-500"># AGREED: ...</span></div>
|
||||
</CodeBlock>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── Architecture ── */}
|
||||
<Section id="architecture">
|
||||
<SectionHeading title="Architecture" />
|
||||
<p className="text-slate-400 leading-relaxed mb-6">
|
||||
negoT8 runs a bilateral negotiation protocol over a persistent WebSocket mesh. Each agent maintains a preference vector and personality profile. Rounds are logged immutably, and final settlement is recorded on Polygon POS.
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
{[
|
||||
{ icon: "person", label: "Personal Agents", desc: "Telegram-native, personality-driven" },
|
||||
{ icon: "sync_alt", label: "Negotiation Engine", desc: "Game-theoretic, multi-round" },
|
||||
{ icon: "link", label: "Blockchain Layer", desc: "Polygon POS — immutable record" },
|
||||
].map(({ icon, label, desc }) => (
|
||||
<div key={label} className="rounded-xl p-5 border border-white/[0.06]" style={{ background:"#0f0a1f" }}>
|
||||
<div className="size-10 rounded-lg flex items-center justify-center mx-auto mb-3" style={{ background:"rgba(183,166,251,0.1)" }}>
|
||||
<Icon name={icon} className="text-[#B7A6FB]" />
|
||||
</div>
|
||||
<p className="text-white text-sm font-bold mb-1">{label}</p>
|
||||
<p className="text-slate-500 text-xs">{desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── Agent Personas ── */}
|
||||
<Section id="personas">
|
||||
<SectionHeading title="Agent Personas" badge="Core Concept" />
|
||||
<p className="mb-8 text-slate-400 leading-relaxed">
|
||||
Personas define the negotiation behaviour and cognitive boundaries of each agent. Assigning a persona controls how aggressively the agent bids, how empathetically it responds, and how analytically it evaluates proposals.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{[
|
||||
{ icon: "psychology", name: "Analytical", desc: "Pattern recognition, data-first decisions. Minimal emotional drift — best for financial negotiations." },
|
||||
{ icon: "terminal", name: "Aggressive", desc: "Opens high, concedes slow. Optimised for maximising individual outcome in competitive scenarios." },
|
||||
{ icon: "chat", name: "Empathetic", desc: "Human-centric, sentiment-aligned. Prioritises mutual satisfaction over raw gain." },
|
||||
{ icon: "balance", name: "Balanced", desc: "Default profile. Adapts strategy dynamically based on opponent behaviour and round history." },
|
||||
{ icon: "favorite", name: "People Pleaser", desc: "High concession rate, fast convergence. Ideal when relationship preservation matters most." },
|
||||
{ icon: "security", name: "Sentinel", desc: "Enforces fair-play policy. Flags anomalous proposals and prevents runaway concession spirals." },
|
||||
].map(({ icon, name, desc }) => (
|
||||
<div key={name} className="group rounded-xl border border-white/[0.06] p-6 transition-all hover:border-[#B7A6FB]/40" style={{ background:"#0f0a1f" }}>
|
||||
<div className="mb-4 size-10 rounded-lg flex items-center justify-center text-[#B7A6FB] transition-transform group-hover:scale-110" style={{ background:"rgba(183,166,251,0.1)" }}>
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
<h4 className="mb-2 font-bold text-white">{name}</h4>
|
||||
<p className="text-sm leading-relaxed text-slate-500">{desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── Mesh Topology ── */}
|
||||
<Section id="topology">
|
||||
<SectionHeading title="Mesh Topology" />
|
||||
<p className="text-slate-400 leading-relaxed mb-6">
|
||||
The negoT8 protocol supports 1:1 bilateral negotiations today, with n-party consensus rolling out in v2.1. Agents communicate over an encrypted Socket.IO channel. The backend orchestrator assigns feature handlers (expenses, scheduling, freelance, etc.) and routes messages to the correct agent pair.
|
||||
</p>
|
||||
<div className="p-5 rounded-xl border border-white/[0.06] font-mono text-xs leading-6 text-slate-400" style={{ background:"#08051a" }}>
|
||||
<div><span className="text-[#B7A6FB]">Agent A</span> → PROPOSE → <span className="text-[#22d3ee]">Orchestrator</span> → RELAY → <span className="text-[#B7A6FB]">Agent B</span></div>
|
||||
<div><span className="text-[#B7A6FB]">Agent B</span> → COUNTER → <span className="text-[#22d3ee]">Orchestrator</span> → RELAY → <span className="text-[#B7A6FB]">Agent A</span></div>
|
||||
<div className="text-slate-600">{'─'.repeat(52)}</div>
|
||||
<div><span className="text-emerald-400">CONSENSUS</span> → <span className="text-[#22d3ee]">Orchestrator</span> → SETTLE → <span className="text-amber-400">Blockchain</span></div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── API Reference ── */}
|
||||
<Section id="api">
|
||||
<SectionHeading title="Core API Endpoints" />
|
||||
<div className="overflow-hidden rounded-xl border border-white/[0.07]" style={{ background:"#0f0a1f" }}>
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b border-white/[0.07] uppercase tracking-wider text-slate-600 text-[11px]">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-bold">Endpoint</th>
|
||||
<th className="px-6 py-4 font-bold">Method</th>
|
||||
<th className="px-6 py-4 font-bold">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.04]">
|
||||
{[
|
||||
{ path:"/negotiations", method:"POST", color:"text-emerald-400", desc:"Create a new negotiation session between two agents." },
|
||||
{ path:"/negotiations", method:"GET", color:"text-blue-400", desc:"List all negotiations with status and fairness score." },
|
||||
{ path:"/negotiations/:id", method:"GET", color:"text-blue-400", desc:"Fetch full detail, rounds, and analytics for a session." },
|
||||
{ path:"/negotiations/:id/start", method:"POST", color:"text-emerald-400", desc:"Kick off the negotiation loop between the two agents." },
|
||||
{ path:"/users", method:"POST", color:"text-emerald-400", desc:"Register a new Telegram user with a personality profile." },
|
||||
{ path:"/users/:id", method:"GET", color:"text-blue-400", desc:"Retrieve user profile and personality settings." },
|
||||
{ path:"/stats", method:"GET", color:"text-blue-400", desc:"Global platform metrics: resolved, active, fairness avg." },
|
||||
].map(({ path, method, color, desc }) => (
|
||||
<tr key={path+method} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-6 py-4"><code className="text-[#B7A6FB] text-xs">{path}</code></td>
|
||||
<td className={`px-6 py-4 font-mono text-xs font-bold ${color}`}>{method}</td>
|
||||
<td className="px-6 py-4 text-slate-500 text-xs">{desc}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── SDKs & Tools ── */}
|
||||
<Section id="sdks">
|
||||
<SectionHeading title="SDKs & Tools" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ icon:"smart_toy", name:"Python SDK", tag:"Official", desc:"pip install negot8" },
|
||||
{ icon:"send", name:"Telegram Bot", tag:"Built-in", desc:"@negoT8Bot on Telegram" },
|
||||
{ icon:"record_voice_over", name:"Voice API", tag:"ElevenLabs", desc:"AI-generated settlement summaries" },
|
||||
].map(({ icon, name, tag, desc }) => (
|
||||
<div key={name} className="rounded-xl border border-white/[0.06] p-5" style={{ background:"#0f0a1f" }}>
|
||||
<div className="size-9 rounded-lg flex items-center justify-center mb-3" style={{ background:"rgba(183,166,251,0.1)" }}>
|
||||
<Icon name={icon} className="text-[#B7A6FB]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-sm font-bold text-white">{name}</p>
|
||||
<span className="text-[9px] px-1.5 py-0.5 rounded font-mono" style={{ background:"rgba(183,166,251,0.1)", color:"#B7A6FB" }}>{tag}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 font-mono">{desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── CLI ── */}
|
||||
<Section id="cli">
|
||||
<SectionHeading title="CLI Tool" />
|
||||
<p className="text-slate-400 text-sm leading-relaxed mb-6">Run and inspect negotiations directly from your terminal.</p>
|
||||
<CodeBlock filename="terminal">
|
||||
<div><span className="text-[#B7A6FB]">$</span> <span className="text-slate-200">negot8 negotiate</span> <span className="text-emerald-300">--feature expenses --agent-a @alice --agent-b @bob</span></div>
|
||||
<br />
|
||||
<div><span className="text-slate-600">▶ Round 1 · Agent A proposes $54.20</span></div>
|
||||
<div><span className="text-slate-600">▶ Round 2 · Agent B counters $58.00</span></div>
|
||||
<div><span className="text-slate-600">▶ Round 3 · Convergence at 98.4%</span></div>
|
||||
<div><span className="text-emerald-400">✓ SETTLED · $56.10 · TX: 0x8fb…442b</span></div>
|
||||
</CodeBlock>
|
||||
</Section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-16 flex flex-col items-center justify-between gap-6 border-t border-white/[0.07] py-10 md:flex-row">
|
||||
<div className="flex items-center gap-4 text-sm text-slate-600">
|
||||
<span>© 2026 negoT8 Protocol</span>
|
||||
<div className="size-1 rounded-full bg-slate-700" />
|
||||
<a href="#" className="hover:text-[#B7A6FB] transition-colors">Privacy</a>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Link href="/" className="flex items-center gap-1.5 text-xs text-slate-500 hover:text-[#B7A6FB] transition-colors font-medium">
|
||||
<Icon name="arrow_back" className="text-sm" /> Back to landing
|
||||
</Link>
|
||||
<Link href="/dashboard" className="flex items-center gap-1.5 text-xs text-[#B7A6FB] hover:text-white transition-colors font-medium">
|
||||
Open Dashboard <Icon name="arrow_forward" className="text-sm" />
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* ── Right TOC ── */}
|
||||
<aside className="sticky top-16 hidden h-[calc(100vh-64px)] w-64 p-10 xl:block">
|
||||
<h4 className="mb-4 text-[10px] font-bold uppercase tracking-widest text-slate-600">On this page</h4>
|
||||
<nav className="space-y-4">
|
||||
{[
|
||||
{ label:"Quick Start", id:"getting-started", active:true },
|
||||
{ label:"Architecture", id:"architecture" },
|
||||
{ label:"Agent Personas", id:"personas" },
|
||||
{ label:"Mesh Topology", id:"topology" },
|
||||
{ label:"API Reference", id:"api" },
|
||||
{ label:"SDKs & Tools", id:"sdks" },
|
||||
{ label:"CLI Tool", id:"cli" },
|
||||
].map(({ label, id, active }) => (
|
||||
<a key={id} href={`#${id}`}
|
||||
className={`block text-sm font-medium transition-colors ${active ? "text-[#B7A6FB]" : "text-slate-600 hover:text-slate-200"}`}>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<div className="mt-10 rounded-xl p-4 border" style={{ background:"linear-gradient(135deg,rgba(183,166,251,0.08),transparent)", borderColor:"rgba(183,166,251,0.15)" }}>
|
||||
<h5 className="mb-2 text-xs font-bold text-white">Need help?</h5>
|
||||
<p className="mb-4 text-xs leading-relaxed text-slate-400">Connect with the team on Telegram or file an issue on GitHub.</p>
|
||||
<a href="https://t.me/" target="_blank" rel="noopener noreferrer" className="block w-full rounded-lg py-2 text-center text-xs font-bold text-white border border-white/10 hover:border-[#B7A6FB]/40 transition-colors" style={{ background:"#0f0a1f" }}>
|
||||
Open Telegram
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
negot8/dashboard/app/favicon.ico
Normal file
BIN
negot8/dashboard/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
114
negot8/dashboard/app/globals.css
Normal file
114
negot8/dashboard/app/globals.css
Normal file
@@ -0,0 +1,114 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--primary: #B7A6FB;
|
||||
--bg-dark: #020105;
|
||||
--bg-card: rgba(7, 3, 18, 0.5);
|
||||
--glass-border: rgba(183, 166, 251, 0.15);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #020105;
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(183, 166, 251, 0.08) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, rgba(100, 50, 200, 0.08) 0px, transparent 50%);
|
||||
color: #cbd5e1;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(183, 166, 251, 0.2); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(183, 166, 251, 0.4); }
|
||||
|
||||
/* Glass card */
|
||||
.glass-card {
|
||||
background: rgba(7, 3, 18, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255,255,255,0.02);
|
||||
position: relative;
|
||||
}
|
||||
.glass-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.08), transparent 60%);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
.glass-card:hover {
|
||||
border-color: rgba(183, 166, 251, 0.25);
|
||||
box-shadow: 0 0 20px rgba(183, 166, 251, 0.08);
|
||||
}
|
||||
|
||||
/* Glow border */
|
||||
.glow-border {
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.glow-border::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 12px;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, rgba(183, 166, 251, 0.4), rgba(183, 166, 251, 0.05));
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Text glow */
|
||||
.text-glow { text-shadow: 0 0 20px rgba(183, 166, 251, 0.5); }
|
||||
|
||||
/* Data stream line */
|
||||
.data-stream-line {
|
||||
background: linear-gradient(180deg,
|
||||
rgba(183, 166, 251, 0) 0%,
|
||||
rgba(183, 166, 251, 0.3) 20%,
|
||||
rgba(183, 166, 251, 0.3) 80%,
|
||||
rgba(183, 166, 251, 0) 100%
|
||||
);
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
/* Subtle grid overlay */
|
||||
.bg-grid-subtle {
|
||||
background-image: linear-gradient(to right, rgba(255,255,255,0.02) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255,255,255,0.02) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
|
||||
/* Code snippet */
|
||||
.code-snippet {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Shimmer */
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
|
||||
/* Bar chart */
|
||||
.bar-chart { display: flex; align-items: flex-end; gap: 2px; height: 20px; }
|
||||
.bar { width: 3px; background: #B7A6FB; opacity: 0.4; border-radius: 1px; }
|
||||
.bar.active { opacity: 1; box-shadow: 0 0 5px #B7A6FB; }
|
||||
293
negot8/dashboard/app/history/page.tsx
Normal file
293
negot8/dashboard/app/history/page.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { api } from "@/lib/api";
|
||||
import type { Negotiation } from "@/lib/types";
|
||||
import { FEATURE_LABELS, STATUS_COLORS, STATUS_LABELS, relativeTime } from "@/lib/utils";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
const FEATURE_ICONS: Record<string, string> = {
|
||||
scheduling: "calendar_month",
|
||||
expenses: "account_balance_wallet",
|
||||
freelance: "work",
|
||||
roommate: "home",
|
||||
trip: "flight_takeoff",
|
||||
marketplace: "store",
|
||||
collaborative: "groups",
|
||||
conflict: "gavel",
|
||||
generic: "hub",
|
||||
};
|
||||
|
||||
const STATUS_FILTERS = [
|
||||
{ key: "all", label: "All Statuses", dot: "" },
|
||||
{ key: "resolved", label: "Resolved", dot: "bg-emerald-400" },
|
||||
{ key: "escalated", label: "Escalated", dot: "bg-rose-400" },
|
||||
{ key: "active", label: "In Progress", dot: "bg-amber-400" },
|
||||
{ key: "pending", label: "Pending", dot: "bg-slate-400" },
|
||||
];
|
||||
|
||||
const STATUS_BADGE: Record<string, string> = {
|
||||
resolved: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
|
||||
escalated: "text-rose-400 bg-rose-500/10 border-rose-500/20",
|
||||
active: "text-amber-400 bg-amber-500/10 border-amber-500/20",
|
||||
pending: "text-slate-400 bg-white/5 border-white/10",
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 8;
|
||||
|
||||
export default function HistoryPage() {
|
||||
const [negotiations, setNegotiations] = useState<Negotiation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await api.negotiations();
|
||||
setNegotiations(res.negotiations);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return negotiations.filter((n) => {
|
||||
const matchStatus = statusFilter === "all" || n.status === statusFilter;
|
||||
const matchSearch =
|
||||
!search ||
|
||||
n.id.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(FEATURE_LABELS[n.feature_type] ?? n.feature_type)
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
return matchStatus && matchSearch;
|
||||
});
|
||||
}, [negotiations, statusFilter, search]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||
const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||
|
||||
const getFairness = (n: Negotiation) => {
|
||||
const r = (n as any).result;
|
||||
if (r?.fairness_score != null) return Math.round(r.fairness_score * 100);
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-[#020105] text-slate-300 relative">
|
||||
<div className="absolute inset-0 bg-grid-subtle opacity-20 pointer-events-none" />
|
||||
<Sidebar />
|
||||
|
||||
<main className="flex-1 flex flex-col h-full overflow-hidden relative z-10">
|
||||
{/* Top bar */}
|
||||
<header className="h-16 flex items-center justify-between px-6 bg-[#050505]/80 backdrop-blur-md border-b border-white/5 sticky top-0 z-30 shrink-0">
|
||||
<div>
|
||||
<h2 className="text-base font-medium text-white tracking-tight">
|
||||
Negotiation History
|
||||
</h2>
|
||||
<p className="text-[10px] text-slate-600 mt-0.5">
|
||||
{filtered.length} record{filtered.length !== 1 ? "s" : ""} found
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[#B7A6FB] text-[#020105] text-xs font-bold hover:brightness-110 transition-all"
|
||||
>
|
||||
<Icon name="add" className="text-sm" />
|
||||
New Request
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{/* Search + Filters */}
|
||||
<div className="glass-card rounded-xl p-4 mb-6">
|
||||
<div className="relative mb-4">
|
||||
<Icon
|
||||
name="search"
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 text-lg"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||
placeholder="Search by ID or feature type..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-[#B7A6FB]/40 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{STATUS_FILTERS.map((f) => (
|
||||
<button
|
||||
key={f.key}
|
||||
onClick={() => { setStatusFilter(f.key); setPage(1); }}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium transition-all border ${
|
||||
statusFilter === f.key
|
||||
? "bg-[#B7A6FB] text-[#020105] border-[#B7A6FB]"
|
||||
: "bg-white/5 text-slate-400 border-white/10 hover:border-[#B7A6FB]/30 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{f.dot && (
|
||||
<span className={`size-1.5 rounded-full ${f.dot}`} />
|
||||
)}
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-48 text-slate-600">
|
||||
<Icon name="progress_activity" className="animate-spin text-3xl text-[#B7A6FB]" />
|
||||
</div>
|
||||
) : paginated.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-48 gap-3 text-slate-600">
|
||||
<Icon name="search_off" className="text-4xl" />
|
||||
<p className="text-sm">No negotiations match your filters.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="glass-card rounded-xl overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/5 text-[10px] text-slate-500 uppercase tracking-wider font-mono">
|
||||
<th className="text-left px-5 py-3">Feature</th>
|
||||
<th className="text-left px-5 py-3">Participants</th>
|
||||
<th className="text-left px-5 py-3">Status</th>
|
||||
<th className="text-left px-5 py-3">Fairness</th>
|
||||
<th className="text-left px-5 py-3">Date</th>
|
||||
<th className="text-right px-5 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginated.map((neg, i) => {
|
||||
const fairness = getFairness(neg);
|
||||
return (
|
||||
<tr
|
||||
key={neg.id}
|
||||
className={`border-b border-white/5 hover:bg-[#B7A6FB]/5 transition-colors group ${
|
||||
i === paginated.length - 1 ? "border-b-0" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Feature */}
|
||||
<td className="px-5 py-3.5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center shrink-0">
|
||||
<Icon
|
||||
name={FEATURE_ICONS[neg.feature_type] ?? "hub"}
|
||||
className="text-[#B7A6FB] text-base"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium text-xs">
|
||||
{FEATURE_LABELS[neg.feature_type] ?? neg.feature_type}
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-600 font-mono">
|
||||
#{neg.id.slice(0, 8)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Participants */}
|
||||
<td className="px-5 py-3.5">
|
||||
<div className="flex -space-x-2">
|
||||
{[...Array(Math.min(neg.participant_count ?? 0, 3))].map((_, j) => (
|
||||
<div
|
||||
key={j}
|
||||
className="w-6 h-6 rounded-full bg-slate-800 border border-black flex items-center justify-center text-[8px] text-white ring-1 ring-white/10"
|
||||
>
|
||||
{String.fromCharCode(65 + j)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Status */}
|
||||
<td className="px-5 py-3.5">
|
||||
<span
|
||||
className={`text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded border ${
|
||||
STATUS_BADGE[neg.status] ?? "text-slate-400 bg-white/5 border-white/10"
|
||||
}`}
|
||||
>
|
||||
{STATUS_LABELS[neg.status] ?? neg.status}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* Fairness */}
|
||||
<td className="px-5 py-3.5">
|
||||
{fairness != null ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-20 h-1 bg-white/10 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#B7A6FB] rounded-full"
|
||||
style={{ width: `${fairness}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-400 font-mono">
|
||||
{fairness}%
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[10px] text-slate-600 font-mono">—</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Date */}
|
||||
<td className="px-5 py-3.5">
|
||||
<span className="text-[10px] text-slate-500 font-mono">
|
||||
{relativeTime(neg.created_at)}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* Actions */}
|
||||
<td className="px-5 py-3.5 text-right">
|
||||
<Link
|
||||
href={`/negotiation/${neg.id}`}
|
||||
className="inline-flex items-center justify-center size-8 rounded-lg bg-white/5 border border-white/10 text-slate-500 hover:text-[#B7A6FB] hover:border-[#B7A6FB]/30 transition-all"
|
||||
title="View details"
|
||||
>
|
||||
<Icon name="visibility" className="text-base" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-t border-white/5 text-[10px] text-slate-500 font-mono">
|
||||
<span>
|
||||
Showing {Math.min((page - 1) * PAGE_SIZE + 1, filtered.length)}–
|
||||
{Math.min(page * PAGE_SIZE, filtered.length)} of {filtered.length} negotiations
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="size-7 rounded flex items-center justify-center bg-white/5 border border-white/10 hover:border-[#B7A6FB]/30 hover:text-[#B7A6FB] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<Icon name="chevron_left" className="text-sm" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="size-7 rounded flex items-center justify-center bg-white/5 border border-white/10 hover:border-[#B7A6FB]/30 hover:text-[#B7A6FB] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<Icon name="chevron_right" className="text-sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
negot8/dashboard/app/layout.tsx
Normal file
43
negot8/dashboard/app/layout.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Roboto, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const roboto = Roboto({
|
||||
variable: "--font-roboto",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "700"],
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-jetbrains-mono",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "negoT8 — Mission Control",
|
||||
description: "Real-time AI agent negotiation dashboard",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${roboto.variable} ${jetbrainsMono.variable} antialiased`}
|
||||
style={{ fontFamily: "var(--font-roboto), sans-serif" }}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
335
negot8/dashboard/app/negotiation/[id]/page.tsx
Normal file
335
negot8/dashboard/app/negotiation/[id]/page.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { api } from "@/lib/api";
|
||||
import { getSocket, joinNegotiation, leaveNegotiation } from "@/lib/socket";
|
||||
import type { Negotiation, Round, SatisfactionPoint, ConcessionEntry } from "@/lib/types";
|
||||
import {
|
||||
FEATURE_LABELS,
|
||||
STATUS_COLORS,
|
||||
STATUS_LABELS,
|
||||
PERSONALITY_LABELS,
|
||||
relativeTime,
|
||||
} from "@/lib/utils";
|
||||
import type { Personality } from "@/lib/types";
|
||||
import SatisfactionChart from "@/components/SatisfactionChart";
|
||||
import FairnessScore from "@/components/FairnessScore";
|
||||
import ConcessionTimeline from "@/components/ConcessionTimeline";
|
||||
import NegotiationTimeline from "@/components/NegotiationTimeline";
|
||||
import ResolutionCard from "@/components/ResolutionCard";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
const FEATURE_ICONS: Record<string, string> = {
|
||||
scheduling: "calendar_month",
|
||||
expenses: "account_balance_wallet",
|
||||
freelance: "work",
|
||||
roommate: "home",
|
||||
trip: "flight_takeoff",
|
||||
marketplace: "store",
|
||||
collaborative: "groups",
|
||||
conflict: "gavel",
|
||||
generic: "hub",
|
||||
};
|
||||
|
||||
export default function NegotiationDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const id = params.id;
|
||||
|
||||
const [negotiation, setNegotiation] = useState<Negotiation | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [liveRound, setLiveRound] = useState<number | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.negotiation(id);
|
||||
setNegotiation(data);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
const socket = getSocket();
|
||||
const onConnect = () => setConnected(true);
|
||||
const onDisconnect = () => setConnected(false);
|
||||
const onState = (data: Negotiation) => { setNegotiation(data); setLoading(false); };
|
||||
const onRound = (payload: { negotiation_id: string; round: Round }) => {
|
||||
if (payload.negotiation_id !== id) return;
|
||||
setLiveRound(payload.round.round_number);
|
||||
setNegotiation((prev) => {
|
||||
if (!prev) return prev;
|
||||
const rounds = [...(prev.rounds ?? [])];
|
||||
const idx = rounds.findIndex((r) => r.id === payload.round.id);
|
||||
if (idx >= 0) rounds[idx] = payload.round; else rounds.push(payload.round);
|
||||
return { ...prev, rounds, status: "active" };
|
||||
});
|
||||
};
|
||||
const onResolved = (payload: { negotiation_id: string }) => {
|
||||
if (payload.negotiation_id !== id) return;
|
||||
setLiveRound(null);
|
||||
load();
|
||||
};
|
||||
socket.on("connect", onConnect);
|
||||
socket.on("disconnect", onDisconnect);
|
||||
socket.on("negotiation_state", onState);
|
||||
socket.on("round_update", onRound);
|
||||
socket.on("negotiation_resolved", onResolved);
|
||||
setConnected(socket.connected);
|
||||
joinNegotiation(id);
|
||||
return () => {
|
||||
leaveNegotiation(id);
|
||||
socket.off("connect", onConnect);
|
||||
socket.off("disconnect", onDisconnect);
|
||||
socket.off("negotiation_state", onState);
|
||||
socket.off("round_update", onRound);
|
||||
socket.off("negotiation_resolved", onResolved);
|
||||
};
|
||||
}, [id, load]);
|
||||
|
||||
if (loading) return <Shell><LoadingState /></Shell>;
|
||||
if (error) return <Shell><ErrorState message={error} onRetry={load} /></Shell>;
|
||||
if (!negotiation) return <Shell><ErrorState message="Negotiation not found" onRetry={load} /></Shell>;
|
||||
|
||||
const rounds = negotiation.rounds ?? [];
|
||||
const participants = negotiation.participants ?? [];
|
||||
const analytics = negotiation.analytics;
|
||||
const isLive = negotiation.status === "active";
|
||||
|
||||
const satTimeline: SatisfactionPoint[] =
|
||||
analytics?.satisfaction_timeline?.length
|
||||
? analytics.satisfaction_timeline
|
||||
: rounds.map((r) => ({ round: r.round_number, score_a: r.satisfaction_a, score_b: r.satisfaction_b }));
|
||||
|
||||
const concessions: ConcessionEntry[] = analytics?.concession_log ?? [];
|
||||
const lastSat = satTimeline[satTimeline.length - 1];
|
||||
const fairness = analytics?.fairness_score ?? (lastSat ? 100 - Math.abs(lastSat.score_a - lastSat.score_b) : null);
|
||||
|
||||
const userA = participants[0];
|
||||
const userB = participants[1];
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
{/* ── Header ── */}
|
||||
<div className="flex items-start justify-between gap-4 mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="p-2 rounded-lg bg-white/5 border border-white/10 text-slate-400 hover:text-white hover:border-white/20 transition-all"
|
||||
>
|
||||
<Icon name="arrow_back" className="text-lg" />
|
||||
</Link>
|
||||
<div className="size-10 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center">
|
||||
<Icon name={FEATURE_ICONS[negotiation.feature_type] ?? "hub"} className="text-[#B7A6FB]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h1 className="text-base font-bold text-white font-mono">{id}</h1>
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded-full border font-medium ${STATUS_COLORS[negotiation.status]}`}>
|
||||
{STATUS_LABELS[negotiation.status]}
|
||||
</span>
|
||||
<span className="text-[10px] px-2 py-0.5 bg-white/5 text-slate-400 rounded-full border border-white/10">
|
||||
{FEATURE_LABELS[negotiation.feature_type]}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-600 mt-0.5 font-mono">
|
||||
Started {relativeTime(negotiation.created_at)}
|
||||
{liveRound && (
|
||||
<span className="ml-2 text-[#B7A6FB] animate-pulse">
|
||||
· Round {liveRound} processing…
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className={`flex items-center gap-1.5 text-[10px] px-2.5 py-1 rounded-full border font-mono ${connected ? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" : "bg-white/5 text-slate-500 border-white/10"}`}>
|
||||
<span className={`size-1.5 rounded-full ${connected ? "bg-emerald-400 animate-pulse" : "bg-slate-600"}`} />
|
||||
{connected ? "Live" : "Offline"}
|
||||
</div>
|
||||
{isLive && (
|
||||
<span className="flex items-center gap-1.5 text-[10px] px-2.5 py-1 rounded-full border bg-red-500/10 text-red-400 border-red-500/20 font-mono animate-pulse">
|
||||
<span className="size-1.5 rounded-full bg-red-500" />
|
||||
Negotiating · Rd {liveRound ?? rounds.length}
|
||||
</span>
|
||||
)}
|
||||
<button onClick={load} className="p-2 rounded-lg bg-white/5 border border-white/10 text-slate-400 hover:text-white hover:border-white/20 transition-all" title="Refresh">
|
||||
<Icon name="refresh" className="text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Participants ── */}
|
||||
{participants.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-3 mb-6">
|
||||
{[userA, userB].filter(Boolean).map((p, i) => (
|
||||
<ParticipantCard key={i} participant={p} label={i === 0 ? "A" : "B"} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Main 2-col grid ── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{/* Left */}
|
||||
<div className="space-y-5">
|
||||
<Section title="Convergence Timeline" icon="show_chart">
|
||||
<SatisfactionChart data={satTimeline} />
|
||||
</Section>
|
||||
|
||||
{fairness !== null && (
|
||||
<Section title="Fairness Score" icon="balance">
|
||||
<FairnessScore score={fairness} satA={lastSat?.score_a} satB={lastSat?.score_b} />
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{(negotiation.status === "resolved" || negotiation.status === "escalated") && (
|
||||
<Section title="Resolution" icon="check_circle">
|
||||
<ResolutionCard negotiation={negotiation} />
|
||||
<Link
|
||||
href={`/negotiation/${id}/resolved`}
|
||||
className="mt-4 flex items-center justify-between gap-3 w-full px-4 py-3.5 rounded-xl border border-[#B7A6FB]/25 bg-[#B7A6FB]/6 hover:bg-[#B7A6FB]/12 hover:border-[#B7A6FB]/50 transition-all group"
|
||||
style={{ boxShadow: "0 0 18px rgba(183,166,251,0.08)" }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-9 rounded-lg bg-[#B7A6FB]/15 flex items-center justify-center text-[#B7A6FB] shrink-0">
|
||||
<Icon name="verified" className="text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-white leading-none mb-0.5">View Settlement Details</p>
|
||||
<p className="text-[10px] text-slate-500 font-mono">Blockchain record · UPI · Full transcript</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon name="arrow_forward" className="text-[#B7A6FB] text-lg group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right */}
|
||||
<div className="space-y-5">
|
||||
<Section
|
||||
title={`Negotiation Stream (${rounds.length} round${rounds.length !== 1 ? "s" : ""})`}
|
||||
icon="receipt_long"
|
||||
>
|
||||
<NegotiationTimeline rounds={rounds} participants={participants} />
|
||||
</Section>
|
||||
|
||||
{concessions.length > 0 && (
|
||||
<Section title="Concession Log" icon="swap_horiz">
|
||||
<ConcessionTimeline concessions={concessions} />
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
function Shell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#020105] text-white relative">
|
||||
<div className="absolute inset-0 bg-grid-subtle opacity-20 pointer-events-none" />
|
||||
|
||||
{/* Top bar */}
|
||||
<header className="h-16 flex items-center justify-between px-6 bg-[#050505]/80 backdrop-blur-md border-b border-white/5 sticky top-0 z-30">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center size-8 rounded-lg bg-[#B7A6FB]/10 border border-[#B7A6FB]/20 text-[#B7A6FB]">
|
||||
<Icon name="hub" className="text-lg" />
|
||||
</div>
|
||||
<h1 className="text-sm font-bold tracking-tight text-white">
|
||||
Agent<span className="text-[#B7A6FB] font-light">Mesh</span>
|
||||
</h1>
|
||||
</div>
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
<Link href="/" className="text-slate-400 hover:text-white hover:bg-white/5 px-3 py-1.5 rounded-md transition-all text-xs font-medium">
|
||||
Dashboard
|
||||
</Link>
|
||||
<span className="text-white bg-[#B7A6FB]/10 border border-[#B7A6FB]/20 px-3 py-1.5 rounded-md text-xs font-medium">
|
||||
Active Agents
|
||||
</span>
|
||||
<a href="#" className="text-slate-400 hover:text-white hover:bg-white/5 px-3 py-1.5 rounded-md transition-all text-xs font-medium">History</a>
|
||||
<a href="#" className="text-slate-400 hover:text-white hover:bg-white/5 px-3 py-1.5 rounded-md transition-all text-xs font-medium">Settings</a>
|
||||
</nav>
|
||||
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-[#B7A6FB] text-black rounded-md text-xs font-bold hover:bg-white transition-all shadow-[0_0_10px_rgba(183,166,251,0.2)]">
|
||||
<Icon name="add" className="text-sm" /> New Session
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 py-6 relative z-10">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({ title, icon, children }: { title: string; icon: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="glass-card rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Icon name={icon} className="text-[#B7A6FB] text-lg" />
|
||||
<h2 className="text-xs font-bold text-white uppercase tracking-wider">{title}</h2>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ParticipantCard({
|
||||
participant,
|
||||
label,
|
||||
}: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
participant: any;
|
||||
label: string;
|
||||
}) {
|
||||
if (!participant) return null;
|
||||
const pers = (participant.personality_used ?? "balanced") as Personality;
|
||||
const name = participant.display_name || participant.username || `Agent ${label}`;
|
||||
const isA = label === "A";
|
||||
|
||||
return (
|
||||
<div className={`glow-border rounded-xl p-4 flex items-center gap-3 ${isA ? "bg-[#B7A6FB]/5" : "bg-cyan-900/5"}`}>
|
||||
<div className={`size-10 rounded-full flex items-center justify-center text-sm font-bold shrink-0 ${isA ? "bg-[#B7A6FB]/15 text-[#B7A6FB]" : "bg-cyan-900/20 text-cyan-400"}`}>
|
||||
{label}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{name}</p>
|
||||
<span className={`text-[9px] font-mono px-1.5 py-0.5 rounded border ${isA ? "text-[#B7A6FB] bg-[#B7A6FB]/10 border-[#B7A6FB]/20" : "text-cyan-400 bg-cyan-500/10 border-cyan-500/20"}`}>
|
||||
{PERSONALITY_LABELS[pers]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingState() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-32 gap-3 text-slate-600">
|
||||
<Icon name="refresh" className="text-4xl animate-spin text-[#B7A6FB]" />
|
||||
<span className="text-xs font-mono uppercase tracking-wider">Loading negotiation…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorState({ message, onRetry }: { message: string; onRetry: () => void }) {
|
||||
return (
|
||||
<div className="py-24 text-center space-y-4">
|
||||
<Icon name="error" className="text-5xl text-red-400 block mx-auto" />
|
||||
<p className="text-red-400 text-sm">{message}</p>
|
||||
<button onClick={onRetry} className="text-[10px] text-slate-400 hover:text-white underline font-mono">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
353
negot8/dashboard/app/negotiation/[id]/resolved/page.tsx
Normal file
353
negot8/dashboard/app/negotiation/[id]/resolved/page.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { api } from "@/lib/api";
|
||||
import type { Negotiation } from "@/lib/types";
|
||||
import { relativeTime } from "@/lib/utils";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
// ─── Animated waveform bars ───────────────────────────────────────────────────
|
||||
function WaveBars() {
|
||||
return (
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 opacity-20 flex items-center gap-0.5 h-8 pointer-events-none">
|
||||
{[3, 6, 4, 8, 5, 2].map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-1 rounded-full bg-[#B7A6FB] animate-pulse"
|
||||
style={{ height: `${h * 4}px`, animationDelay: `${i * 75}ms` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Copy button ──────────────────────────────────────────────────────────────
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1800);
|
||||
};
|
||||
return (
|
||||
<button
|
||||
onClick={copy}
|
||||
className="p-1.5 hover:bg-white/10 rounded-md text-[#B7A6FB]/70 hover:text-[#B7A6FB] transition-colors shrink-0"
|
||||
title="Copy"
|
||||
>
|
||||
<Icon name={copied ? "check" : "content_copy"} className="text-sm" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Outcome metric chip ──────────────────────────────────────────────────────
|
||||
function MetricChip({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-3 rounded-xl border border-white/5 text-center" style={{ background: "#0d0a1a" }}>
|
||||
<span className="text-slate-500 text-[10px] uppercase font-bold tracking-wider mb-1">{label}</span>
|
||||
<span className={`text-xl font-black ${accent ? "text-[#B7A6FB]" : "text-white"}`}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Action button ────────────────────────────────────────────────────────────
|
||||
function ActionBtn({
|
||||
icon, title, sub, accent, wave, full,
|
||||
}: { icon: string; title: string; sub: string; accent?: boolean; wave?: boolean; full?: boolean }) {
|
||||
return (
|
||||
<button
|
||||
className={`relative flex items-center gap-4 p-4 rounded-xl border text-left transition-all duration-300 group overflow-hidden ${
|
||||
accent
|
||||
? "border-[#B7A6FB]/30 bg-[#B7A6FB]/5 hover:bg-[#B7A6FB]/10 shadow-[0_0_10px_rgba(183,166,251,0.15)] hover:shadow-[0_0_18px_rgba(183,166,251,0.3)]"
|
||||
: "border-white/10 bg-white/5 hover:border-[#B7A6FB]/40 hover:bg-white/10"
|
||||
} ${full ? "col-span-2" : ""}`}
|
||||
>
|
||||
{wave && <WaveBars />}
|
||||
<div
|
||||
className={`size-10 rounded-full flex items-center justify-center shrink-0 transition-transform group-hover:scale-110 ${
|
||||
accent ? "bg-[#B7A6FB]/20 text-[#B7A6FB]" : "bg-white/10 text-slate-300 group-hover:text-[#B7A6FB]"
|
||||
}`}
|
||||
>
|
||||
<Icon name={icon} className="text-xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-bold text-sm">{title}</h3>
|
||||
<p className="text-slate-400 text-xs">{sub}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main page ────────────────────────────────────────────────────────────────
|
||||
export default function ResolvedPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const id = params.id;
|
||||
|
||||
const [neg, setNeg] = useState<Negotiation | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.negotiation(id);
|
||||
setNeg(data);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
// ── derived data ────────────────────────────────────────────────────────────
|
||||
const analytics = neg?.analytics;
|
||||
const rounds = neg?.rounds ?? [];
|
||||
const participants = neg?.participants ?? [];
|
||||
const userA = participants[0];
|
||||
const userB = participants[1];
|
||||
|
||||
const fairness = analytics?.fairness_score ?? null;
|
||||
const totalRounds = rounds.length;
|
||||
const duration = neg ? relativeTime(neg.created_at) : "";
|
||||
|
||||
// Pull settlement / blockchain data from resolution record or defaults
|
||||
const resolution = neg?.resolution ?? {};
|
||||
const outcomeText = (resolution as Record<string, string>)?.summary ?? (resolution as Record<string, string>)?.outcome ?? "";
|
||||
const txHash = (resolution as Record<string, string>)?.tx_hash ?? "0x8fbe3f766cd6055749e91558d066f1c5cf8feb0f58b45085c57785701fa442b8";
|
||||
const blockNum = (resolution as Record<string, string>)?.block_number ?? "34591307";
|
||||
const network = (resolution as Record<string, string>)?.network ?? "Polygon POS (Amoy Testnet)";
|
||||
const upiId = (resolution as Record<string, string>)?.upi_id ?? "negot8@upi";
|
||||
const timestamp = neg?.updated_at ? relativeTime(neg.updated_at) : "recently";
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#070312] flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-3 text-slate-600">
|
||||
<Icon name="refresh" className="text-5xl animate-spin text-[#B7A6FB]" />
|
||||
<span className="text-xs font-mono uppercase tracking-wider">Loading resolution…</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !neg) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#070312] flex items-center justify-center">
|
||||
<div className="text-center space-y-4">
|
||||
<Icon name="error" className="text-5xl text-red-400 block mx-auto" />
|
||||
<p className="text-red-400 text-sm">{error ?? "Negotiation not found"}</p>
|
||||
<button onClick={load} className="text-[10px] text-slate-400 hover:text-white underline font-mono">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes fadeUp { from{opacity:0;transform:translateY(12px)} to{opacity:1;transform:translateY(0)} }
|
||||
@keyframes shimmerLine { 0%{opacity:0;transform:translateX(-100%)} 50%{opacity:1} 100%{opacity:0;transform:translateX(100%)} }
|
||||
.fade-up { animation: fadeUp 0.5s ease forwards; }
|
||||
.fade-up-1 { animation: fadeUp 0.5s 0.1s ease both; }
|
||||
.fade-up-2 { animation: fadeUp 0.5s 0.2s ease both; }
|
||||
.fade-up-3 { animation: fadeUp 0.5s 0.3s ease both; }
|
||||
.fade-up-4 { animation: fadeUp 0.5s 0.4s ease both; }
|
||||
.shimmer-line {
|
||||
position:absolute; top:0; left:0; right:0; height:1px;
|
||||
background:linear-gradient(to right,transparent,#B7A6FB,transparent);
|
||||
animation: shimmerLine 3s ease-in-out infinite;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="min-h-screen bg-[#070312] text-slate-300 flex flex-col">
|
||||
{/* bg glows */}
|
||||
<div className="fixed inset-0 pointer-events-none overflow-hidden z-0">
|
||||
<div className="absolute top-[-20%] left-[-10%] w-[600px] h-[600px] rounded-full blur-[120px]" style={{ background: "rgba(183,166,251,0.07)" }} />
|
||||
<div className="absolute bottom-[-10%] right-[-5%] w-[500px] h-[500px] rounded-full blur-[100px]" style={{ background: "rgba(183,166,251,0.04)" }} />
|
||||
<div className="absolute inset-0" style={{ backgroundImage:"linear-gradient(rgba(183,166,251,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(183,166,251,0.03) 1px,transparent 1px)", backgroundSize:"40px 40px", opacity:0.4 }} />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center justify-center flex-grow p-4 md:p-8">
|
||||
|
||||
{/* ── Main card ── */}
|
||||
<div className="w-full max-w-5xl rounded-2xl overflow-hidden shadow-2xl relative fade-up" style={{ background:"rgba(13,10,26,0.8)", backdropFilter:"blur(14px)", border:"1px solid rgba(183,166,251,0.2)" }}>
|
||||
<div className="shimmer-line" />
|
||||
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
|
||||
{/* ── LEFT COLUMN ── */}
|
||||
<div className="flex-1 p-7 md:p-10 flex flex-col gap-7">
|
||||
|
||||
{/* Header */}
|
||||
<div className="fade-up-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="size-11 rounded-full flex items-center justify-center border" style={{ background:"rgba(74,222,128,0.08)", borderColor:"rgba(74,222,128,0.25)", boxShadow:"0 0 16px rgba(74,222,128,0.15)" }}>
|
||||
<Icon name="check_circle" className="text-2xl text-emerald-400" />
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-black tracking-tight bg-clip-text text-transparent" style={{ backgroundImage:"linear-gradient(to right,#ffffff,#B7A6FB)" }}>
|
||||
Negotiation Resolved
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm font-medium pl-1">
|
||||
Deal successfully closed via negoT8 AI protocol
|
||||
{userA && userB && (
|
||||
<> · <span className="text-[#B7A6FB]">{userA.display_name ?? userA.username ?? "Agent A"}</span> & <span className="text-cyan-400">{userB.display_name ?? userB.username ?? "Agent B"}</span></>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Deal summary */}
|
||||
<div className="relative p-6 rounded-xl overflow-hidden fade-up-2" style={{ background:"rgba(183,166,251,0.04)", border:"1px solid rgba(255,255,255,0.06)" }}>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#B7A6FB]/5 to-transparent opacity-50 pointer-events-none" />
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-[#B7A6FB] text-[10px] font-bold uppercase tracking-wider mb-2">Deal Summary</h2>
|
||||
<p className="text-slate-200 text-base md:text-lg font-light leading-relaxed">
|
||||
{outcomeText
|
||||
? outcomeText
|
||||
: <>Negotiation <span className="text-white font-bold">#{id.slice(0, 8)}</span> reached consensus after <span className="text-white font-bold">{totalRounds} round{totalRounds !== 1 ? "s" : ""}</span>. Settlement recorded on-chain {timestamp}.</>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blockchain verification */}
|
||||
<div className="p-6 rounded-xl flex flex-col gap-4 fade-up-3" style={{ background:"rgba(255,255,255,0.02)", border:"1px solid rgba(255,255,255,0.06)" }}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-[#B7A6FB] text-[10px] font-bold uppercase tracking-wider">Blockchain Verification</h2>
|
||||
<div className="flex items-center gap-2 px-2.5 py-1 rounded-md border text-[10px] font-bold uppercase" style={{ background:"rgba(74,222,128,0.08)", borderColor:"rgba(74,222,128,0.2)", color:"#4ade80" }}>
|
||||
<span className="size-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
Confirmed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TX Hash */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-slate-500 text-[10px] font-bold uppercase tracking-tight">Transaction Hash</span>
|
||||
<div className="flex items-center justify-between gap-2 p-2.5 rounded-lg" style={{ background:"rgba(0,0,0,0.3)" }}>
|
||||
<span className="text-slate-200 font-mono text-xs truncate">{txHash}</span>
|
||||
<CopyButton text={txHash} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-2 gap-4 pt-3 border-t border-white/[0.06]">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-slate-500 text-[10px] font-bold uppercase tracking-tight">Network</span>
|
||||
<span className="text-slate-300 text-xs">{network}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-slate-500 text-[10px] font-bold uppercase tracking-tight">Block</span>
|
||||
<span className="text-slate-200 font-mono text-xs font-bold">{blockNum}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-slate-500 text-[10px] font-bold uppercase tracking-tight">Timestamp</span>
|
||||
<span className="text-slate-300 text-xs capitalize">{timestamp}</span>
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<a href={`https://amoy.polygonscan.com/tx/${txHash}`} target="_blank" rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-[#B7A6FB] hover:text-white transition-colors text-[11px] font-bold">
|
||||
VIEW ON POLYGONSCAN
|
||||
<Icon name="open_in_new" className="text-sm" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 fade-up-4">
|
||||
<ActionBtn icon="payments" title="Pay via UPI" sub="Instant Transfer" accent />
|
||||
<ActionBtn icon="chat" title="Open Telegram" sub="View Chat History" />
|
||||
<ActionBtn icon="description" title="Download PDF" sub="Full Transcript" />
|
||||
<ActionBtn icon="graphic_eq" title="Play AI Summary" sub="Voice Note (0:45)" wave full />
|
||||
</div>
|
||||
|
||||
{/* Outcome metrics */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<MetricChip label="Fairness" value={fairness !== null ? `${Math.round(fairness)}%` : "—"} accent />
|
||||
<MetricChip label="Rounds" value={String(totalRounds)} />
|
||||
<MetricChip label="Duration" value={duration} />
|
||||
</div>
|
||||
|
||||
{/* Back link */}
|
||||
<div className="pt-2 border-t border-white/[0.06]">
|
||||
<Link href={`/negotiation/${id}`} className="inline-flex items-center gap-1.5 text-xs text-slate-500 hover:text-[#B7A6FB] transition-colors font-mono">
|
||||
<Icon name="arrow_back" className="text-sm" /> Back to negotiation detail
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── RIGHT COLUMN: QR / UPI ── */}
|
||||
<div className="lg:w-80 flex-shrink-0 flex flex-col items-center justify-center gap-6 p-8 relative border-t lg:border-t-0 lg:border-l border-white/[0.06]" style={{ background:"rgba(0,0,0,0.35)" }}>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-[#B7A6FB]/[0.03] to-transparent pointer-events-none" />
|
||||
|
||||
<div className="text-center relative z-10">
|
||||
<h3 className="text-white font-bold text-lg mb-1">Instant Settlement</h3>
|
||||
<p className="text-slate-400 text-sm">Scan to pay via UPI</p>
|
||||
</div>
|
||||
|
||||
{/* QR code frame */}
|
||||
<div className="relative z-10 p-1 rounded-xl" style={{ background:"linear-gradient(135deg,rgba(183,166,251,0.5),transparent)" }}>
|
||||
<div className="bg-white p-3 rounded-lg shadow-2xl relative">
|
||||
{/* Stylised QR placeholder */}
|
||||
<div className="w-48 h-48 rounded flex items-center justify-center overflow-hidden" style={{ background:"#0d0a1a" }}>
|
||||
<div className="grid grid-cols-7 gap-0.5 p-2 w-full h-full">
|
||||
{Array.from({ length: 49 }).map((_, i) => (
|
||||
<div key={i} className="rounded-[1px]"
|
||||
style={{ background: Math.random() > 0.45 ? "#B7A6FB" : "transparent",
|
||||
opacity: 0.85 + Math.random() * 0.15 }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Rupee badge */}
|
||||
<div className="absolute -bottom-3 -right-3 size-10 rounded-full flex items-center justify-center border-4" style={{ background:"#B7A6FB", borderColor:"#0d0a1a" }}>
|
||||
<Icon name="currency_rupee" className="text-sm text-[#070312] font-bold" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* UPI ID row */}
|
||||
<div className="w-full flex flex-col gap-3 relative z-10">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg border border-white/10 w-full" style={{ background:"rgba(255,255,255,0.04)" }}>
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<span className="text-[10px] text-slate-500 uppercase font-bold">UPI ID</span>
|
||||
<span className="text-sm text-slate-200 font-mono truncate">{upiId}</span>
|
||||
</div>
|
||||
<CopyButton text={upiId} />
|
||||
</div>
|
||||
|
||||
{/* Negotiation ID */}
|
||||
<div className="flex items-center justify-between p-3 rounded-lg border border-white/10 w-full" style={{ background:"rgba(255,255,255,0.04)" }}>
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<span className="text-[10px] text-slate-500 uppercase font-bold">Negotiation ID</span>
|
||||
<span className="text-xs text-slate-400 font-mono truncate">{id}</span>
|
||||
</div>
|
||||
<CopyButton text={id} />
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-center text-slate-600">
|
||||
By paying, you agree to the terms resolved by the autonomous agents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer pulse */}
|
||||
<div className="mt-8 flex items-center gap-2 opacity-40">
|
||||
<span className="size-2 rounded-full bg-[#B7A6FB] animate-pulse" />
|
||||
<span className="text-xs font-mono text-[#B7A6FB] uppercase tracking-[0.2em]">negoT8 Protocol Active</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
474
negot8/dashboard/app/page.tsx
Normal file
474
negot8/dashboard/app/page.tsx
Normal file
@@ -0,0 +1,474 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
// ─── Icon helper ─────────────────────────────────────────────────────────────
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
// ─── Animated typing text ─────────────────────────────────────────────────────
|
||||
function TypeWriter({ lines }: { lines: string[] }) {
|
||||
const [displayed, setDisplayed] = useState("");
|
||||
const [lineIdx, setLineIdx] = useState(0);
|
||||
const [charIdx, setCharIdx] = useState(0);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const current = lines[lineIdx];
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
if (!deleting && charIdx < current.length) {
|
||||
timeout = setTimeout(() => setCharIdx((c) => c + 1), 55);
|
||||
} else if (!deleting && charIdx === current.length) {
|
||||
timeout = setTimeout(() => setDeleting(true), 2200);
|
||||
} else if (deleting && charIdx > 0) {
|
||||
timeout = setTimeout(() => setCharIdx((c) => c - 1), 28);
|
||||
} else {
|
||||
setDeleting(false);
|
||||
setLineIdx((l) => (l + 1) % lines.length);
|
||||
}
|
||||
setDisplayed(current.slice(0, charIdx));
|
||||
return () => clearTimeout(timeout);
|
||||
}, [charIdx, deleting, lineIdx, lines]);
|
||||
|
||||
return (
|
||||
<span className="text-[#B7A6FB]">
|
||||
{displayed}
|
||||
<span className="animate-pulse">_</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Animated counter ─────────────────────────────────────────────────────────
|
||||
function Counter({ to, suffix = "" }: { to: number; suffix?: string }) {
|
||||
const [val, setVal] = useState(0);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const obs = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
obs.disconnect();
|
||||
const duration = 1800;
|
||||
const start = performance.now();
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min((now - start) / duration, 1);
|
||||
const ease = 1 - Math.pow(1 - t, 3);
|
||||
setVal(Math.round(ease * to));
|
||||
if (t < 1) requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
},
|
||||
{ threshold: 0.4 }
|
||||
);
|
||||
if (ref.current) obs.observe(ref.current);
|
||||
return () => obs.disconnect();
|
||||
}, [to]);
|
||||
|
||||
return (
|
||||
<span ref={ref}>
|
||||
{val.toLocaleString()}
|
||||
{suffix}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Particle canvas ──────────────────────────────────────────────────────────
|
||||
function ParticleField() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let raf: number;
|
||||
const particles: { x: number; y: number; vx: number; vy: number; r: number; o: number }[] = [];
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
};
|
||||
resize();
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
for (let i = 0; i < 90; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
vx: (Math.random() - 0.5) * 0.35,
|
||||
vy: (Math.random() - 0.5) * 0.35,
|
||||
r: Math.random() * 1.5 + 0.3,
|
||||
o: Math.random() * 0.5 + 0.1,
|
||||
});
|
||||
}
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
particles.forEach((p) => {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
|
||||
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(183,166,251,${p.o})`;
|
||||
ctx.fill();
|
||||
});
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < 110) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.strokeStyle = `rgba(183,166,251,${0.12 * (1 - dist / 110)})`;
|
||||
ctx.lineWidth = 0.6;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
raf = requestAnimationFrame(draw);
|
||||
};
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener("resize", resize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} className="absolute inset-0 w-full h-full pointer-events-none" />;
|
||||
}
|
||||
|
||||
// ─── Negotiation Log (animated) ───────────────────────────────────────────────
|
||||
const LOG_ENTRIES = [
|
||||
{ time: "10:02:41", text: "Agent A proposes ", highlight: "$54.20", color: "#B7A6FB" },
|
||||
{ time: "10:02:42", text: "Agent B rejects proposal", highlight: null, color: "#94a3b8" },
|
||||
{ time: "10:02:42", text: "Agent B counters ", highlight: "$58.00", color: "#22d3ee" },
|
||||
{ time: "10:02:43", text: "Calculating overlap…", highlight: null, color: "#fbbf24" },
|
||||
{ time: "10:02:44", text: "Convergence at ", highlight: "98.4%", color: "#34d399" },
|
||||
];
|
||||
|
||||
function NegotiationLog() {
|
||||
const [visible, setVisible] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible >= LOG_ENTRIES.length) return;
|
||||
const t = setTimeout(() => setVisible((v) => v + 1), 900);
|
||||
return () => clearTimeout(t);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div className="flex-grow space-y-3 overflow-hidden">
|
||||
{LOG_ENTRIES.slice(0, visible).map((e, i) => (
|
||||
<div key={i} className="flex gap-2 text-xs font-mono" style={{ animation: "fadeSlideIn 0.3s ease forwards" }}>
|
||||
<span className="text-slate-600 shrink-0">[{e.time}]</span>
|
||||
<span style={{ color: e.color ?? "#94a3b8" }}>
|
||||
{e.text}
|
||||
{e.highlight && <span style={{ color: e.color ?? "#B7A6FB" }} className="font-bold">{e.highlight}</span>}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{visible >= LOG_ENTRIES.length && (
|
||||
<div className="mt-2 p-2.5 rounded border text-xs font-mono" style={{ background: "rgba(52,211,153,0.08)", borderColor: "rgba(52,211,153,0.3)", color: "#34d399", animation: "fadeSlideIn 0.3s ease forwards" }}>
|
||||
<span className="font-bold block">✓ SETTLEMENT REACHED</span>
|
||||
<span className="opacity-60 text-[10px]">TX: 0x8a…9f2c · 12ms</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Feature card ─────────────────────────────────────────────────────────────
|
||||
function FeatureCard({ icon, title, desc, tag, accent }: { icon: string; title: string; desc: string; tag: string; accent: string }) {
|
||||
return (
|
||||
<div
|
||||
className="group relative p-7 rounded-2xl border border-white/5 transition-all duration-500 overflow-hidden flex flex-col"
|
||||
style={{ background: "#0f0b1a" }}
|
||||
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = `0 0 30px ${accent}26`; (e.currentTarget as HTMLElement).style.borderColor = `${accent}40`; }}
|
||||
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = "none"; (e.currentTarget as HTMLElement).style.borderColor = "rgba(255,255,255,0.05)"; }}
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-[0.12] transition-opacity duration-500 pointer-events-none select-none">
|
||||
<Icon name={icon} className="text-[7rem]" />
|
||||
</div>
|
||||
<div className="size-11 rounded-lg flex items-center justify-center mb-5" style={{ background: `${accent}18`, color: accent }}>
|
||||
<Icon name={icon} className="text-xl" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white mb-2">{title}</h3>
|
||||
<p className="text-slate-400 text-sm leading-relaxed flex-grow">{desc}</p>
|
||||
<div className="border-t border-white/5 pt-4 mt-6">
|
||||
<span className="text-[10px] font-mono uppercase tracking-widest" style={{ color: accent }}>{tag}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main landing page ────────────────────────────────────────────────────────
|
||||
export default function LandingPage() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 20);
|
||||
window.addEventListener("scroll", onScroll);
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes fadeSlideIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
|
||||
@keyframes gridPulse { from { opacity:0.25; } to { opacity:0.65; transform:perspective(900px) rotateX(58deg) translateY(-175px) translateZ(-475px); } }
|
||||
@keyframes orb1 { 0%,100%{transform:translate(0,0) scale(1);} 50%{transform:translate(30px,-20px) scale(1.08);} }
|
||||
@keyframes orb2 { 0%,100%{transform:translate(0,0) scale(1);} 50%{transform:translate(-25px,18px) scale(1.06);} }
|
||||
@keyframes floatA { 0%,100%{transform:rotate(-6deg) translateY(0);} 50%{transform:rotate(-6deg) translateY(-8px);} }
|
||||
@keyframes floatB { 0%,100%{transform:rotate(10deg) translateY(0);} 50%{transform:rotate(10deg) translateY(-6px);} }
|
||||
@keyframes marquee { 0%{transform:translateX(0);} 100%{transform:translateX(-50%);} }
|
||||
@keyframes spinSlow { from{transform:rotate(0deg);} to{transform:rotate(360deg);} }
|
||||
@keyframes spinRev { from{transform:rotate(0deg);} to{transform:rotate(-360deg);} }
|
||||
@keyframes scanLine { 0%{top:0%;opacity:0;} 10%{opacity:1;} 90%{opacity:1;} 100%{top:100%;opacity:0;} }
|
||||
.cyber-grid-bg {
|
||||
background-image: linear-gradient(rgba(183,166,251,0.06) 1px,transparent 1px), linear-gradient(90deg,rgba(183,166,251,0.06) 1px,transparent 1px);
|
||||
background-size:48px 48px;
|
||||
transform:perspective(900px) rotateX(58deg) translateY(-200px) translateZ(-500px);
|
||||
mask-image:linear-gradient(to bottom,transparent,black 40%,black 60%,transparent);
|
||||
height:200%; width:200%; position:absolute; top:-50%; left:-50%;
|
||||
pointer-events:none; animation:gridPulse 9s ease-in-out infinite alternate;
|
||||
}
|
||||
.float-a { animation:floatA 5s ease-in-out infinite; }
|
||||
.float-b { animation:floatB 6.5s ease-in-out infinite; }
|
||||
.marquee-track { animation:marquee 32s linear infinite; }
|
||||
.spin-s { animation:spinSlow 12s linear infinite; }
|
||||
.spin-r { animation:spinRev 18s linear infinite; }
|
||||
.scan-ln { position:absolute; left:0; right:0; height:2px; background:linear-gradient(to right,transparent,rgba(183,166,251,0.6),transparent); animation:scanLine 3s ease-in-out infinite; pointer-events:none; }
|
||||
`}</style>
|
||||
|
||||
<div className="min-h-screen bg-[#070312] text-slate-300 overflow-x-hidden">
|
||||
|
||||
{/* Navbar */}
|
||||
<header className="fixed top-0 w-full z-50 transition-all duration-300" style={{ borderBottom: scrolled ? "1px solid rgba(255,255,255,0.08)" : "1px solid transparent", background: scrolled ? "rgba(7,3,18,0.88)" : "transparent", backdropFilter: scrolled ? "blur(16px)" : "none" }}>
|
||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 rounded-lg bg-gradient-to-br from-[#B7A6FB] to-[#22d3ee] flex items-center justify-center text-[#070312]">
|
||||
<Icon name="hub" className="text-xl" />
|
||||
</div>
|
||||
<span className="text-white text-lg font-bold tracking-tight">Agent<span className="text-[#B7A6FB] font-light">Mesh</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero */}
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
<div className="cyber-grid-bg" />
|
||||
<ParticleField />
|
||||
<div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] rounded-full pointer-events-none" style={{ background: "radial-gradient(circle,rgba(183,166,251,0.18) 0%,transparent 70%)", animation: "orb1 8s ease-in-out infinite", filter: "blur(40px)" }} />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-[400px] h-[400px] rounded-full pointer-events-none" style={{ background: "radial-gradient(circle,rgba(34,211,238,0.14) 0%,transparent 70%)", animation: "orb2 10s ease-in-out infinite", filter: "blur(40px)" }} />
|
||||
|
||||
{/* Floating code fragments — anchored to section edges, clear of headline */}
|
||||
<div className="float-a absolute hidden lg:block z-20" style={{ left:"calc(50% - 420px)", top:"42%", transform:"translateY(-50%)", background:"rgba(20,16,35,0.85)", backdropFilter:"blur(10px)", border:"1px solid rgba(183,166,251,0.25)", borderRadius:"12px", padding:"12px 16px", fontSize:"10px", fontFamily:"monospace", color:"rgba(183,166,251,0.85)", lineHeight:"1.7" }}>
|
||||
<div className="flex gap-1.5 mb-2 pb-2 border-b border-white/10"><span className="size-2 rounded-full bg-red-500/60" /><span className="size-2 rounded-full bg-yellow-500/60" /></div>
|
||||
SEQ_INIT: 0x98A1<br />PROBABILITY: 0.99982
|
||||
</div>
|
||||
<div className="float-b absolute hidden lg:block z-20" style={{ right:"calc(50% - 420px)", top:"42%", transform:"translateY(-50%)", background:"rgba(20,16,35,0.85)", backdropFilter:"blur(10px)", border:"1px solid rgba(34,211,238,0.25)", borderRadius:"12px", padding:"12px 16px", fontSize:"10px", fontFamily:"monospace", color:"rgba(34,211,238,0.85)", lineHeight:"1.7" }}>
|
||||
LOGIC_STREAM >> ON<br />WEIGHT_BALANCER: ENABLED
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-4xl mx-auto px-6 text-center flex flex-col items-center gap-8 pt-24">
|
||||
{/* Status pill */}
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full border border-[#B7A6FB]/30 bg-[#B7A6FB]/8 backdrop-blur-sm">
|
||||
<span className="size-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span className="text-[11px] font-mono text-[#B7A6FB] uppercase tracking-widest">v2.0 Protocol Live</span>
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<h1 className="text-5xl md:text-7xl font-black text-white leading-[1.06] tracking-tighter" style={{ textShadow: "0 0 60px rgba(183,166,251,0.15)" }}>
|
||||
The Agent Economy<br />
|
||||
<span className="bg-clip-text text-transparent" style={{ backgroundImage: "linear-gradient(135deg,#B7A6FB 0%,#ffffff 50%,#22d3ee 100%)" }}>
|
||||
is Here.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{/* Typewriter */}
|
||||
<p className="text-lg md:text-xl text-slate-400 font-light max-w-2xl leading-relaxed min-h-[2rem]">
|
||||
<TypeWriter lines={["AI-to-AI negotiation at machine speed.", "Autonomous settlement. Zero friction.", "Game-theoretic equilibrium, on-chain.", "Secure. Transparent. Trustless."]} />
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 mt-2 w-full justify-center">
|
||||
<Link href="/dashboard" className="flex items-center justify-center gap-2 px-8 py-3.5 rounded-lg font-bold text-[#070312] transition-all hover:scale-[1.03] hover:brightness-110" style={{ background:"#B7A6FB", boxShadow:"0 0 28px rgba(183,166,251,0.4)" }}>
|
||||
<Icon name="rocket_launch" className="text-xl" />
|
||||
Open Dashboard
|
||||
</Link>
|
||||
<Link href="/docs" className="flex items-center justify-center gap-2 px-8 py-3.5 rounded-lg font-medium text-white border border-white/15 hover:border-white/40 hover:bg-white/5 transition-all">
|
||||
<Icon name="terminal" className="text-xl" />
|
||||
Read Documentation
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Code snippet */}
|
||||
<div className="mt-8 p-5 rounded-xl max-w-lg w-full text-left hover:rotate-0 transition-all duration-500 cursor-default" style={{ background:"rgba(0,0,0,0.5)", border:"1px solid rgba(255,255,255,0.08)", backdropFilter:"blur(12px)", transform:"rotate(1deg)" }}>
|
||||
<div className="flex gap-1.5 mb-4">
|
||||
<div className="size-3 rounded-full bg-red-500/50" /><div className="size-3 rounded-full bg-yellow-500/50" /><div className="size-3 rounded-full bg-green-500/50" />
|
||||
</div>
|
||||
<code className="font-mono text-xs md:text-sm leading-relaxed">
|
||||
<span className="text-[#B7A6FB]">const</span>
|
||||
<span className="text-slate-300"> negotiation = </span>
|
||||
<span className="text-[#22d3ee]">await</span>
|
||||
<span className="text-slate-300"> mesh.init({"{"}</span><br />
|
||||
<span className="text-slate-500">{" "}strategy: </span><span className="text-emerald-400">'tit-for-tat'</span><span className="text-slate-500">,</span><br />
|
||||
<span className="text-slate-500">{" "}limit: </span><span className="text-[#B7A6FB]">5000</span><span className="text-slate-500">,</span><br />
|
||||
<span className="text-slate-500">{" "}currency: </span><span className="text-[#22d3ee]">'USDC'</span><br />
|
||||
<span className="text-slate-300">{"}"});</span>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Marquee */}
|
||||
<div className="w-full overflow-hidden py-4 border-y" style={{ background:"#0f0b1c", borderColor:"rgba(255,255,255,0.05)" }}>
|
||||
<div className="flex whitespace-nowrap marquee-track">
|
||||
{[...Array(2)].map((_, di) => (
|
||||
<div key={di} className="flex gap-12 items-center px-6 shrink-0">
|
||||
{[["RESOLVED","$1.2M IN EXPENSES","text-white"],["FAIRNESS SCORE","98%","text-emerald-400"],["ACTIVE NODES","14,203","text-[#22d3ee]"],["AVG SETTLEMENT","400MS","text-[#B7A6FB]"],["PROTOCOL","V2.0 STABLE","text-white"],["NEGOTIATIONS TODAY","3,841","text-amber-400"]].map(([label,value,cls],i) => (
|
||||
<span key={i} className="text-slate-500 font-mono text-sm flex items-center gap-3">
|
||||
{label}: <span className={`font-bold ${cls}`}>{value}</span><span className="text-slate-700 ml-3">•</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<section className="py-16 px-6 border-b border-white/5">
|
||||
<div className="max-w-6xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{[{ label:"Negotiations Settled", to:48291, suffix:"+" },{ label:"Avg Fairness Score", to:97, suffix:"%" },{ label:"Active Nodes", to:14203, suffix:"" },{ label:"Avg Settlement", to:400, suffix:"ms" }].map(({ label, to, suffix }) => (
|
||||
<div key={label} className="text-center">
|
||||
<div className="text-4xl font-black mb-1 tabular-nums" style={{ color:"#B7A6FB" }}><Counter to={to} suffix={suffix} /></div>
|
||||
<p className="text-xs text-slate-500 font-mono uppercase tracking-wider">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Mesh in Action */}
|
||||
<section className="py-24 px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row justify-between items-end mb-12 gap-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">The Mesh in Action</h2>
|
||||
<p className="text-slate-500 font-mono text-sm">Real-time settlement · Advanced Convergence Graph</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2.5 rounded-full bg-red-500 animate-pulse" />
|
||||
<span className="text-xs text-red-400 font-mono uppercase tracking-wide">Live Feed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl overflow-hidden border border-white/[0.08] relative" style={{ background:"#0d0a1a", boxShadow:"0 0 60px rgba(0,0,0,0.6)" }}>
|
||||
<div className="absolute inset-0 pointer-events-none" style={{ background:"linear-gradient(135deg,rgba(183,166,251,0.03),rgba(34,211,238,0.03))" }} />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 min-h-[500px]">
|
||||
{/* Graph pane */}
|
||||
<div className="lg:col-span-2 relative p-6 flex flex-col justify-between border-b lg:border-b-0 lg:border-r border-white/[0.08]" style={{ background:"#08051a", backgroundImage:"radial-gradient(rgba(183,166,251,0.06) 1px,transparent 1px)", backgroundSize:"20px 20px" }}>
|
||||
<div className="scan-ln" />
|
||||
<div className="flex gap-2 z-10 relative flex-wrap">
|
||||
{[["GRAPH_ID: 8X92","#B7A6FB",true],["LATENCY: 12ms","#94a3b8",false],["ENTROPY: 0.041","#22d3ee",false]].map(([label,color,pulse]) => (
|
||||
<span key={label as string} className="px-2 py-1 rounded text-[10px] font-mono flex items-center gap-1.5" style={{ background:"rgba(255,255,255,0.04)", border:"1px solid rgba(255,255,255,0.08)", color:color as string }}>
|
||||
{pulse && <span className="size-1.5 rounded-full animate-ping" style={{ background:color as string }} />}
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* Agent visualizer */}
|
||||
<div className="flex-grow flex items-center justify-center relative py-8">
|
||||
<div className="spin-s absolute" style={{ width:260, height:260, borderRadius:"50%", border:"1px solid rgba(183,166,251,0.2)" }} />
|
||||
<div className="spin-r absolute" style={{ width:190, height:190, borderRadius:"50%", border:"1px dashed rgba(34,211,238,0.25)" }} />
|
||||
<div className="absolute h-[2px] w-40" style={{ background:"linear-gradient(to right,#B7A6FB,#ffffff,#22d3ee)", boxShadow:"0 0 12px #B7A6FB" }} />
|
||||
<div className="absolute flex items-center justify-center size-14 rounded-full font-bold z-10 -translate-x-20" style={{ background:"rgba(183,166,251,0.15)", border:"1.5px solid #B7A6FB", color:"#B7A6FB", boxShadow:"0 0 24px rgba(183,166,251,0.4)" }}>A</div>
|
||||
<div className="absolute px-3 py-1 rounded-full text-[10px] font-mono text-white z-20" style={{ background:"rgba(0,0,0,0.8)", border:"1px solid rgba(255,255,255,0.2)", backdropFilter:"blur(8px)", top:"calc(50% - 60px)" }}>Consensus: 98%</div>
|
||||
<div className="absolute flex items-center justify-center size-14 rounded-full font-bold z-10 translate-x-20" style={{ background:"rgba(34,211,238,0.15)", border:"1.5px solid #22d3ee", color:"#22d3ee", boxShadow:"0 0 24px rgba(34,211,238,0.4)" }}>B</div>
|
||||
</div>
|
||||
{/* Mini bars */}
|
||||
<div className="h-16 w-full border-t border-white/[0.08] flex items-end justify-between gap-1 pt-3">
|
||||
{[["#B7A6FB",0.2,16],["#B7A6FB",0.4,32],["#B7A6FB",0.3,24],["#B7A6FB",0.6,48],["#22d3ee",0.5,40],["#22d3ee",1.0,56],["#22d3ee",0.4,32],["#B7A6FB",1.0,48],["#B7A6FB",0.2,16]].map(([color,opacity,h],i) => (
|
||||
<div key={i} className="w-1.5 rounded-sm" style={{ height:h as number, background:color as string, opacity:Number(opacity), boxShadow:Number(opacity)===1.0?`0 0 12px ${color}`:"none" }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Log pane */}
|
||||
<div className="lg:col-span-1 p-6 flex flex-col" style={{ background:"#0c0919" }}>
|
||||
<div className="flex items-center justify-between pb-4 mb-4" style={{ borderBottom:"1px solid rgba(255,255,255,0.08)" }}>
|
||||
<h3 className="text-white font-bold text-sm">Negotiation Log</h3>
|
||||
<span className="size-2 rounded-full bg-emerald-500" />
|
||||
</div>
|
||||
<NegotiationLog />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Core Features */}
|
||||
<section className="py-24 px-6" style={{ background:"#070312" }}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-14">
|
||||
<p className="text-[11px] font-mono text-[#B7A6FB] uppercase tracking-widest mb-3">Why negoT8</p>
|
||||
<h2 className="text-4xl font-black text-white mb-4">Core Features</h2>
|
||||
<p className="text-slate-400 max-w-2xl">Designed for the next generation of autonomous commerce. Pure logic, zero friction.</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
<FeatureCard icon="psychology" title="Autonomous Negotiations" desc="Agents settle complex disputes without human intervention using game-theoretic equilibrium models and learned preference profiles." tag="Algorithmic · Trustless" accent="#B7A6FB" />
|
||||
<FeatureCard icon="groups" title="Multi-Party Resolution" desc="Scale beyond 1:1 interactions. Our mesh supports n-party consensus models for complex supply chain logistics and group expenses." tag="Scalable · N-Party" accent="#22d3ee" />
|
||||
<FeatureCard icon="visibility" title="Explainable Reasoning" desc="Black boxes are for amateurs. Every decision is traced, logged, and audited for fairness. Full auditability on every round." tag="Transparent · Auditable" accent="#a78bfa" />
|
||||
<FeatureCard icon="account_balance_wallet" title="Instant Settlement" desc="UPI and on-chain settlement in under 400ms. Funds move when consensus is reached — no waiting, no friction, no middlemen." tag="UPI · On-Chain" accent="#34d399" />
|
||||
<FeatureCard icon="record_voice_over" title="Voice Interface" desc="ElevenLabs-powered voice summaries for every negotiation outcome. Your agent communicates in natural language." tag="ElevenLabs · TTS" accent="#fbbf24" />
|
||||
<FeatureCard icon="send" title="Telegram Native" desc="Deploy agents directly from Telegram. Initiate negotiations, monitor live rounds, and receive settlements without leaving the app." tag="Telegram · Bot API" accent="#60a5fa" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-6 border-t border-white/5" style={{ background:"linear-gradient(to bottom,#070312,#0f0b1a)" }}>
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="relative inline-flex items-center justify-center mb-10">
|
||||
<div className="absolute size-40 rounded-full" style={{ border:"1px solid rgba(183,166,251,0.1)", animation:"spinSlow 20s linear infinite" }} />
|
||||
<div className="absolute size-24 rounded-full" style={{ border:"1px dashed rgba(34,211,238,0.15)", animation:"spinRev 15s linear infinite" }} />
|
||||
<div className="size-16 rounded-2xl flex items-center justify-center" style={{ background:"rgba(183,166,251,0.12)", border:"1px solid rgba(183,166,251,0.25)" }}>
|
||||
<Icon name="hub" className="text-3xl text-[#B7A6FB]" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-black text-white mb-5 leading-tight">Ready to join the mesh?</h2>
|
||||
<p className="text-slate-400 mb-10 max-w-xl mx-auto leading-relaxed">Start deploying agents in minutes. Join thousands of developers building the autonomous agent economy.</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<Link href="/dashboard" className="flex items-center gap-2 px-8 py-3.5 rounded-lg font-bold text-[#070312] transition-all hover:scale-[1.03] hover:-translate-y-0.5" style={{ background:"#B7A6FB", boxShadow:"0 0 28px rgba(183,166,251,0.35)" }}>
|
||||
<Icon name="dashboard" className="text-xl" />Open Dashboard
|
||||
</Link>
|
||||
<a href="https://t.me/" className="flex items-center gap-2 px-8 py-3.5 rounded-lg font-bold text-white border border-white/10 hover:border-white/30 hover:bg-white/5 transition-all" style={{ background:"rgba(34,159,217,0.12)" }}>
|
||||
<Icon name="send" className="text-xl" />Connect on Telegram
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="py-8 px-6 border-t border-white/[0.08]" style={{ background:"#070312" }}>
|
||||
<div className="max-w-7xl mx-auto flex flex-col items-center gap-4 text-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-6 rounded bg-gradient-to-br from-[#B7A6FB] to-[#22d3ee] flex items-center justify-center text-[#070312]"><Icon name="hub" className="text-xs" /></div>
|
||||
<span className="text-white font-bold text-sm">negoT8</span>
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
{["Privacy","Terms","Status"].map((l) => <a key={l} href="#" className="text-xs text-slate-600 hover:text-white transition-colors">{l}</a>)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-700 font-mono">© 2025 negoT8 Protocol. All systems nominal.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
351
negot8/dashboard/app/preferences/page.tsx
Normal file
351
negot8/dashboard/app/preferences/page.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
const PERSONALITIES = [
|
||||
{
|
||||
key: "aggressive",
|
||||
label: "Aggressive",
|
||||
desc: "Direct, concise, and prioritizes speed over nuance. Best for rapid execution.",
|
||||
icon: "bolt",
|
||||
color: "text-red-400",
|
||||
bg: "bg-red-500/10",
|
||||
},
|
||||
{
|
||||
key: "empathetic",
|
||||
label: "Empathetic",
|
||||
desc: "Prioritizes rapport, tone matching, and emotional intelligence. Human-centric.",
|
||||
icon: "favorite",
|
||||
color: "text-[#B7A6FB]",
|
||||
bg: "bg-[#B7A6FB]/20",
|
||||
},
|
||||
{
|
||||
key: "analytical",
|
||||
label: "Analytical",
|
||||
desc: "Data-driven, cites sources, and avoids assumptions. Highly logical.",
|
||||
icon: "query_stats",
|
||||
color: "text-blue-400",
|
||||
bg: "bg-blue-500/10",
|
||||
},
|
||||
{
|
||||
key: "balanced",
|
||||
label: "Balanced",
|
||||
desc: "The default setting. Adaptable tone that shifts based on the query complexity.",
|
||||
icon: "balance",
|
||||
color: "text-emerald-400",
|
||||
bg: "bg-green-500/10",
|
||||
},
|
||||
];
|
||||
|
||||
const VOICE_MODELS = [
|
||||
"Adam (Deep Narration)",
|
||||
"Bella (Soft & Professional)",
|
||||
"Charlie (Energetic Male)",
|
||||
"Dorothy (Warm & Friendly)",
|
||||
];
|
||||
|
||||
export default function PreferencesPage() {
|
||||
const [personality, setPersonality] = useState("empathetic");
|
||||
const [voiceModel, setVoiceModel] = useState("Bella (Soft & Professional)");
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [showWebhook, setShowWebhook] = useState(false);
|
||||
const [savedToast, setSavedToast] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
setSavedToast(true);
|
||||
setTimeout(() => setSavedToast(false), 2500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-[#020105] text-slate-300 relative">
|
||||
<div className="absolute inset-0 bg-grid-subtle opacity-20 pointer-events-none" />
|
||||
<Sidebar />
|
||||
|
||||
<main className="flex-1 flex flex-col h-full overflow-hidden relative z-10">
|
||||
{/* Top bar */}
|
||||
<header className="h-16 flex items-center justify-between px-6 bg-[#050505]/80 backdrop-blur-md border-b border-white/5 sticky top-0 z-30 shrink-0">
|
||||
<div>
|
||||
<h2 className="text-base font-medium text-white tracking-tight">Settings</h2>
|
||||
<p className="text-[10px] text-slate-600 mt-0.5">
|
||||
negoT8 <span className="text-[#B7A6FB]/60">| Preferences</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="size-9 rounded-lg bg-[#B7A6FB]/10 border border-[#B7A6FB]/20 text-[#B7A6FB] flex items-center justify-center hover:bg-[#B7A6FB]/20 transition-colors">
|
||||
<Icon name="notifications" className="text-lg" />
|
||||
</button>
|
||||
<div className="size-9 rounded-full bg-gradient-to-tr from-[#B7A6FB] to-purple-500 flex items-center justify-center font-bold text-[#020105] text-sm">
|
||||
JD
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-[1100px] mx-auto p-6 grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
{/* Settings sidebar */}
|
||||
<aside className="lg:col-span-3 flex flex-col gap-2">
|
||||
<nav className="flex flex-col gap-1">
|
||||
{[
|
||||
{ icon: "person_outline", label: "User Preferences", active: true },
|
||||
{ icon: "smart_toy", label: "Agent Clusters", active: false },
|
||||
{ icon: "account_balance_wallet", label: "Billing & Credits", active: false },
|
||||
{ icon: "shield", label: "Security", active: false },
|
||||
].map(({ icon, label, active }) => (
|
||||
<button
|
||||
key={label}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm transition-all text-left ${
|
||||
active
|
||||
? "bg-[#B7A6FB]/10 text-[#B7A6FB] font-medium"
|
||||
: "text-slate-400 hover:bg-[#B7A6FB]/5 hover:text-[#B7A6FB]"
|
||||
}`}
|
||||
>
|
||||
<Icon name={icon} className="text-lg" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className="mt-6 p-4 rounded-xl glass-card">
|
||||
<p className="text-[10px] uppercase tracking-widest text-[#B7A6FB]/50 font-bold mb-2">
|
||||
Plan Status
|
||||
</p>
|
||||
<p className="text-sm font-medium text-white">Pro Developer</p>
|
||||
<div className="w-full bg-white/10 h-1.5 rounded-full mt-3 overflow-hidden">
|
||||
<div className="bg-[#B7A6FB] h-full w-[75%] shadow-[0_0_8px_#B7A6FB]" />
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 mt-2">75% of monthly tokens used</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main settings content */}
|
||||
<div className="lg:col-span-9 flex flex-col gap-8">
|
||||
{/* Agent Personality */}
|
||||
<section className="flex flex-col gap-5">
|
||||
<div className="border-b border-white/5 pb-3">
|
||||
<h3 className="text-xl font-bold text-[#B7A6FB]">Agent Personality</h3>
|
||||
<p className="text-slate-400 text-sm mt-1">
|
||||
Define the behavioral tone for your primary AI interactions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{PERSONALITIES.map((p) => {
|
||||
const isActive = personality === p.key;
|
||||
return (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => setPersonality(p.key)}
|
||||
className={`rounded-xl p-5 text-left flex flex-col gap-3 transition-all relative overflow-hidden ${
|
||||
isActive
|
||||
? "border-2 border-[#B7A6FB] bg-[#B7A6FB]/10 shadow-[0_0_20px_rgba(183,166,251,0.15)]"
|
||||
: "glass-card hover:border-[#B7A6FB]/30"
|
||||
}`}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="absolute top-2 right-2 text-[#B7A6FB]">
|
||||
<Icon name="check_circle" className="text-sm" />
|
||||
</div>
|
||||
)}
|
||||
<div className={`size-11 rounded-lg flex items-center justify-center ${p.bg}`}>
|
||||
<Icon name={p.icon} className={`${p.color} text-xl`} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-white text-sm">{p.label}</h4>
|
||||
<p className="text-[11px] text-slate-400 mt-1 leading-relaxed">{p.desc}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Voice Synthesis */}
|
||||
<section className="flex flex-col gap-5">
|
||||
<div className="border-b border-white/5 pb-3 flex justify-between items-end">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-[#B7A6FB]">Voice Synthesis</h3>
|
||||
<p className="text-slate-400 text-sm mt-1">
|
||||
Configured via ElevenLabs integration for realistic speech.
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest px-2 py-1 bg-[#B7A6FB]/10 text-[#B7A6FB] rounded border border-[#B7A6FB]/20">
|
||||
API Connected
|
||||
</span>
|
||||
</div>
|
||||
<div className="glass-card rounded-xl p-5 flex flex-col md:flex-row gap-5 items-center">
|
||||
<div className="flex-1 w-full">
|
||||
<label className="block text-[10px] font-bold uppercase tracking-tighter text-slate-500 mb-2">
|
||||
Voice Model Selection
|
||||
</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={voiceModel}
|
||||
onChange={(e) => setVoiceModel(e.target.value)}
|
||||
className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-3 text-slate-200 focus:ring-2 focus:ring-[#B7A6FB]/30 focus:border-[#B7A6FB]/40 outline-none appearance-none cursor-pointer text-sm transition-all"
|
||||
>
|
||||
{VOICE_MODELS.map((m) => (
|
||||
<option key={m}>{m}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-3 flex items-center pointer-events-none text-slate-500">
|
||||
<Icon name="expand_more" className="text-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full md:w-auto">
|
||||
<button className="flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-3 bg-[#B7A6FB] text-[#020105] font-bold rounded-lg hover:brightness-110 transition-all text-sm">
|
||||
<Icon name="play_circle" className="text-xl" />
|
||||
Preview
|
||||
</button>
|
||||
{/* Waveform visualizer */}
|
||||
<div className="h-12 w-32 glass-card rounded-lg flex items-center justify-center px-3">
|
||||
<div className="flex items-center gap-1">
|
||||
{[3, 6, 4, 8, 5, 7, 3].map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-1 rounded-full ${i % 2 === 0 ? "bg-[#B7A6FB]/40 animate-pulse" : "bg-[#B7A6FB]"}`}
|
||||
style={{ height: `${h * 3}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Payments + Security */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Default UPI */}
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="border-b border-white/5 pb-3">
|
||||
<h3 className="text-lg font-bold text-[#B7A6FB]">Default Payments</h3>
|
||||
<p className="text-slate-400 text-sm mt-1">Linked UPI ID for automated settlement.</p>
|
||||
</div>
|
||||
<div className="glass-card rounded-xl p-4 flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3 p-3 bg-black/40 rounded-lg border border-white/10">
|
||||
<div className="text-[#B7A6FB] bg-[#B7A6FB]/10 p-2 rounded">
|
||||
<Icon name="account_balance" className="text-lg" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[9px] text-slate-500 font-bold uppercase">Linked UPI</p>
|
||||
<p className="text-sm font-medium text-slate-200 font-mono truncate">
|
||||
negot8-hq@okaxis
|
||||
</p>
|
||||
</div>
|
||||
<button className="text-slate-500 hover:text-[#B7A6FB] transition-colors">
|
||||
<Icon name="edit" className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
<button className="w-full py-2 border border-dashed border-[#B7A6FB]/20 rounded-lg text-xs font-bold text-[#B7A6FB]/60 hover:bg-[#B7A6FB]/5 hover:border-[#B7A6FB]/40 hover:text-[#B7A6FB] transition-all tracking-widest">
|
||||
+ ADD SECONDARY METHOD
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Security / API Keys */}
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="border-b border-white/5 pb-3 flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-[#B7A6FB]">Security</h3>
|
||||
<p className="text-slate-400 text-sm mt-1">Manage environment access.</p>
|
||||
</div>
|
||||
<button className="text-[#B7A6FB] text-xs font-bold hover:underline transition-all">
|
||||
Revoke All
|
||||
</button>
|
||||
</div>
|
||||
<div className="glass-card rounded-xl p-4 flex flex-col gap-4">
|
||||
{/* Production API Key */}
|
||||
<div>
|
||||
<label className="text-[9px] font-bold text-slate-500 uppercase tracking-tighter block mb-1.5">
|
||||
Production API Key
|
||||
</label>
|
||||
<div className="flex items-center gap-2 bg-black/40 border border-white/10 rounded-lg px-3 py-2">
|
||||
<input
|
||||
type={showApiKey ? "text" : "password"}
|
||||
readOnly
|
||||
value="sk_mesh_live_483299283749"
|
||||
className="bg-transparent border-none focus:ring-0 text-sm text-slate-300 flex-1 font-mono outline-none min-w-0"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowApiKey((v) => !v)}
|
||||
className="text-slate-500 hover:text-[#B7A6FB] transition-colors shrink-0"
|
||||
>
|
||||
<Icon name={showApiKey ? "visibility_off" : "visibility"} className="text-lg" />
|
||||
</button>
|
||||
<button className="text-slate-500 hover:text-[#B7A6FB] transition-colors shrink-0">
|
||||
<Icon name="content_copy" className="text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Webhook Secret */}
|
||||
<div>
|
||||
<label className="text-[9px] font-bold text-slate-500 uppercase tracking-tighter block mb-1.5">
|
||||
Webhook Secret
|
||||
</label>
|
||||
<div className="flex items-center gap-2 bg-black/40 border border-white/10 rounded-lg px-3 py-2">
|
||||
<input
|
||||
type={showWebhook ? "text" : "password"}
|
||||
readOnly
|
||||
value="wh_mesh_123456"
|
||||
className="bg-transparent border-none focus:ring-0 text-sm text-slate-300 flex-1 font-mono outline-none min-w-0"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowWebhook((v) => !v)}
|
||||
className="text-slate-500 hover:text-[#B7A6FB] transition-colors shrink-0"
|
||||
>
|
||||
<Icon name={showWebhook ? "visibility_off" : "visibility"} className="text-lg" />
|
||||
</button>
|
||||
<button className="text-slate-500 hover:text-[#B7A6FB] transition-colors shrink-0">
|
||||
<Icon name="content_copy" className="text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Footer actions */}
|
||||
<div className="flex items-center justify-end gap-4 pt-4 border-t border-white/5">
|
||||
<button className="px-6 py-2.5 rounded-lg font-bold text-slate-400 hover:text-slate-200 transition-all text-sm">
|
||||
Discard Changes
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-8 py-2.5 rounded-lg bg-[#B7A6FB] text-[#020105] font-bold shadow-[0_0_20px_rgba(183,166,251,0.2)] hover:scale-[1.02] transition-all text-sm"
|
||||
>
|
||||
Save Preferences
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-white/5 py-6 px-6 mt-4">
|
||||
<div className="max-w-[1100px] mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
<Icon name="verified_user" className="text-lg" />
|
||||
<span className="text-xs">End-to-end encryption active for all preference data.</span>
|
||||
</div>
|
||||
<div className="flex gap-6 text-xs text-slate-500">
|
||||
<button className="hover:text-[#B7A6FB] transition-colors">Privacy Policy</button>
|
||||
<button className="hover:text-[#B7A6FB] transition-colors">Terms of Mesh</button>
|
||||
<span className="text-emerald-400">Status: Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Save toast */}
|
||||
{savedToast && (
|
||||
<div className="fixed bottom-6 right-6 z-50 flex items-center gap-2 px-4 py-3 bg-[#B7A6FB] text-[#020105] rounded-xl font-bold text-sm shadow-[0_0_30px_rgba(183,166,251,0.4)] animate-pulse">
|
||||
<Icon name="check_circle" className="text-lg" />
|
||||
Preferences saved
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
188
negot8/dashboard/app/profile/page.tsx
Normal file
188
negot8/dashboard/app/profile/page.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
function StatChip({ label, value, icon }: { label: string; value: string; icon: string }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-4 rounded-xl border border-white/[0.06] text-center gap-1" style={{ background: "#0f0a1f" }}>
|
||||
<Icon name={icon} className="text-[#B7A6FB] text-xl mb-1" />
|
||||
<span className="text-xl font-black text-white">{value}</span>
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wider font-mono">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionBtn({ icon, label, sub }: { icon: string; label: string; sub: string }) {
|
||||
return (
|
||||
<button className="flex items-center gap-3 rounded-xl p-4 border border-white/[0.06] hover:border-[#B7A6FB]/30 hover:bg-[#B7A6FB]/5 transition-all text-left group" style={{ background: "#0f0a1f" }}>
|
||||
<Icon name={icon} className="text-[#B7A6FB] text-2xl group-hover:scale-110 transition-transform" />
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">{label}</p>
|
||||
<p className="text-slate-500 text-xs">{sub}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const displayName = "Anirban Basak";
|
||||
const initials = "AB";
|
||||
const username = "@anirbanbasak";
|
||||
const telegramId = "#7291048";
|
||||
const personality = "Balanced";
|
||||
const voiceId = "tHnMa72bKS";
|
||||
const joinedAt = "January 12, 2026";
|
||||
|
||||
const accountRows = [
|
||||
{ label: "Display Name", value: displayName },
|
||||
{ label: "Telegram Handle", value: username },
|
||||
{ label: "Telegram ID", value: telegramId },
|
||||
{ label: "Personality", value: personality },
|
||||
{ label: "Member Since", value: joinedAt },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-[#020105] text-slate-300 relative">
|
||||
{/* grid bg */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(rgba(183,166,251,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(183,166,251,0.03) 1px,transparent 1px)",
|
||||
backgroundSize: "40px 40px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Sidebar />
|
||||
|
||||
<main className="flex-1 flex flex-col h-full overflow-hidden relative z-10">
|
||||
{/* Top bar */}
|
||||
<header className="h-14 flex items-center justify-between px-6 bg-[#020105]/90 backdrop-blur-md border-b border-white/[0.06] sticky top-0 z-30 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/dashboard" className="p-1.5 rounded-lg text-slate-500 hover:text-white hover:bg-white/5 transition-all">
|
||||
<Icon name="arrow_back" className="text-lg" />
|
||||
</Link>
|
||||
<h2 className="text-sm font-semibold text-white tracking-tight">User Profile</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="size-8 flex items-center justify-center rounded-lg bg-white/[0.04] border border-white/[0.08] text-slate-500 hover:text-white hover:border-white/20 transition-all" title="Settings">
|
||||
<Icon name="settings" className="text-base" />
|
||||
</button>
|
||||
<button className="size-8 flex items-center justify-center rounded-lg bg-white/[0.04] border border-white/[0.08] text-slate-500 hover:text-red-400 hover:border-red-400/20 transition-all" title="Logout">
|
||||
<Icon name="logout" className="text-base" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="max-w-2xl mx-auto flex flex-col gap-6">
|
||||
|
||||
{/* Profile hero */}
|
||||
<div
|
||||
className="flex flex-col items-center gap-5 py-8 px-6 rounded-2xl border border-white/[0.06] relative overflow-hidden"
|
||||
style={{ background: "rgba(183,166,251,0.04)" }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#B7A6FB]/5 to-transparent pointer-events-none" />
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="relative z-10">
|
||||
<div
|
||||
className="size-28 rounded-full border-2 border-[#B7A6FB] p-1"
|
||||
style={{ boxShadow: "0 0 24px rgba(183,166,251,0.25)" }}
|
||||
>
|
||||
<div className="size-full rounded-full bg-gradient-to-br from-[#B7A6FB]/30 to-[#22d3ee]/20 flex items-center justify-center">
|
||||
<span className="text-3xl font-black text-white">{initials}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-1 size-5 bg-emerald-500 border-4 border-[#020105] rounded-full" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center text-center z-10">
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">{displayName}</h1>
|
||||
<p className="text-[#B7A6FB] font-medium mt-1">{username}</p>
|
||||
<p className="text-slate-500 text-sm mt-1">
|
||||
Telegram ID: <span className="text-slate-400 font-mono">{telegramId}</span>
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-1.5 px-3 py-1 rounded-full border border-emerald-500/20 bg-emerald-500/10 text-emerald-400 text-[11px] font-bold uppercase tracking-wider">
|
||||
<span className="size-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
Active
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<StatChip icon="smart_toy" label="Persona" value={personality} />
|
||||
<StatChip icon="record_voice_over" label="Voice ID" value={voiceId + "…"} />
|
||||
<StatChip icon="bolt" label="Status" value="Online" />
|
||||
</div>
|
||||
|
||||
{/* Account details */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-slate-100 text-sm font-semibold px-1">Account Details</h3>
|
||||
<div
|
||||
className="rounded-2xl border border-white/[0.06] p-5 flex flex-col gap-4"
|
||||
style={{ background: "rgba(183,166,251,0.03)" }}
|
||||
>
|
||||
{accountRows.map(({ label, value }, i) => (
|
||||
<div
|
||||
key={label}
|
||||
className={`flex justify-between items-center${i < accountRows.length - 1 ? " pb-4 border-b border-white/[0.05]" : ""}`}
|
||||
>
|
||||
<span className="text-slate-500 text-sm">{label}</span>
|
||||
<span className="text-slate-100 font-medium text-sm font-mono">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active agent */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-slate-100 text-sm font-semibold px-1">Active Agent Status</h3>
|
||||
<div
|
||||
className="rounded-2xl border border-white/[0.06] p-5 flex items-center justify-between"
|
||||
style={{ background: "rgba(183,166,251,0.03)" }}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="size-10 rounded-lg bg-[#B7A6FB]/15 flex items-center justify-center text-[#B7A6FB]">
|
||||
<Icon name="smart_toy" className="text-xl" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-100 font-medium text-sm">Mesh-Core-Alpha</p>
|
||||
<p className="text-slate-500 text-xs">Primary Computing Node</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-1 rounded-full border border-emerald-500/20 bg-emerald-500/10 text-emerald-400 text-[11px] font-bold uppercase tracking-wider">
|
||||
Operational
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick actions */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-slate-100 text-sm font-semibold px-1">Quick Actions</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<ActionBtn icon="edit_square" label="Edit Profile" sub="Update your information" />
|
||||
<ActionBtn icon="shield_lock" label="Security Settings" sub="Manage 2FA and keys" />
|
||||
<ActionBtn icon="notifications" label="Notifications" sub="Alert preferences" />
|
||||
<ActionBtn icon="link" label="Connected Apps" sub="Telegram, UPI & more" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-center py-6">
|
||||
<p className="text-slate-700 text-xs font-mono">negoT8 v2.0.0 © 2026</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
negot8/dashboard/components/ConcessionTimeline.tsx
Normal file
71
negot8/dashboard/components/ConcessionTimeline.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { ConcessionEntry } from "@/lib/types";
|
||||
|
||||
interface Props {
|
||||
concessions: ConcessionEntry[];
|
||||
}
|
||||
|
||||
export default function ConcessionTimeline({ concessions }: Props) {
|
||||
if (!concessions || concessions.length === 0) {
|
||||
return (
|
||||
<div className="text-xs text-slate-600 py-4 text-center font-mono">
|
||||
No concessions recorded yet
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-3">
|
||||
{/* Data stream line */}
|
||||
<div className="absolute left-[14px] top-4 bottom-4 data-stream-line" />
|
||||
|
||||
{concessions.map((c, i) => {
|
||||
const isA = c.by === "A";
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
{/* Node */}
|
||||
<div
|
||||
className={`relative z-10 size-7 rounded-full shrink-0 flex items-center justify-center text-[9px] font-mono font-bold border ${
|
||||
isA
|
||||
? "bg-[#B7A6FB]/10 border-[#B7A6FB]/30 text-[#B7A6FB]"
|
||||
: "bg-cyan-900/20 border-cyan-500/30 text-cyan-400"
|
||||
}`}
|
||||
>
|
||||
{c.by}
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div
|
||||
className={`flex-1 flex items-center justify-between gap-3 px-3 py-2 rounded-lg border backdrop-blur-sm text-xs ${
|
||||
isA
|
||||
? "bg-[#B7A6FB]/5 border-[#B7A6FB]/15"
|
||||
: "bg-cyan-900/5 border-cyan-500/15"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
className={`text-[9px] font-mono px-1.5 py-0.5 rounded border shrink-0 ${
|
||||
isA
|
||||
? "text-[#B7A6FB] bg-[#B7A6FB]/10 border-[#B7A6FB]/20"
|
||||
: "text-cyan-400 bg-cyan-500/10 border-cyan-500/20"
|
||||
}`}
|
||||
>
|
||||
Agent {c.by}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-slate-600 text-sm">arrow_forward</span>
|
||||
<span className="text-slate-300 truncate">{c.gave_up}</span>
|
||||
</div>
|
||||
<span className="text-[9px] text-slate-600 font-mono shrink-0">Rd {c.round}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface Props {
|
||||
concessions: ConcessionEntry[];
|
||||
}
|
||||
65
negot8/dashboard/components/FairnessScore.tsx
Normal file
65
negot8/dashboard/components/FairnessScore.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
interface Props {
|
||||
score: number;
|
||||
satA?: number;
|
||||
satB?: number;
|
||||
}
|
||||
|
||||
export default function FairnessScore({ score, satA, satB }: Props) {
|
||||
const pct = Math.min(100, Math.max(0, score));
|
||||
const color =
|
||||
pct >= 80 ? "text-[#B7A6FB]" : pct >= 60 ? "text-amber-400" : "text-red-400";
|
||||
const barColor =
|
||||
pct >= 80
|
||||
? "bg-[#B7A6FB] shadow-[0_0_8px_#B7A6FB]"
|
||||
: pct >= 60
|
||||
? "bg-amber-400"
|
||||
: "bg-red-400";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Big score */}
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] text-slate-500 font-mono uppercase tracking-wider mb-1">Fairness Score</p>
|
||||
<span className={`text-4xl font-light tabular-nums ${color} text-glow`}>
|
||||
{pct.toFixed(0)}
|
||||
</span>
|
||||
<span className="text-base font-light text-slate-600 ml-1">/100</span>
|
||||
</div>
|
||||
<div className={`text-[10px] font-bold font-mono px-2 py-1 rounded border ${
|
||||
pct >= 80
|
||||
? "text-[#B7A6FB] bg-[#B7A6FB]/10 border-[#B7A6FB]/20"
|
||||
: pct >= 60
|
||||
? "text-amber-400 bg-amber-500/10 border-amber-500/20"
|
||||
: "text-red-400 bg-red-500/10 border-red-500/20"
|
||||
}`}>
|
||||
{pct >= 80 ? "FAIR" : pct >= 60 ? "MODERATE" : "SKEWED"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="w-full h-1.5 bg-white/5 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-700 ${barColor}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Per-agent */}
|
||||
{satA !== undefined && satB !== undefined && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-[#B7A6FB]/5 border border-[#B7A6FB]/20 rounded-lg p-3 text-center hover:border-[#B7A6FB]/40 transition-colors">
|
||||
<div className="text-[10px] text-slate-500 font-mono mb-1">Agent A</div>
|
||||
<div className="text-xl font-light text-[#B7A6FB] tabular-nums">{satA.toFixed(0)}%</div>
|
||||
</div>
|
||||
<div className="bg-cyan-900/10 border border-cyan-500/20 rounded-lg p-3 text-center hover:border-cyan-500/40 transition-colors">
|
||||
<div className="text-[10px] text-slate-500 font-mono mb-1">Agent B</div>
|
||||
<div className="text-xl font-light text-cyan-400 tabular-nums">{satB.toFixed(0)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
161
negot8/dashboard/components/NegotiationTimeline.tsx
Normal file
161
negot8/dashboard/components/NegotiationTimeline.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { Round, Participant, Personality } from "@/lib/types";
|
||||
import { PERSONALITY_LABELS } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
rounds: Round[];
|
||||
participants: Participant[];
|
||||
}
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
const ACTION_ICON: Record<string, string> = {
|
||||
propose: "send",
|
||||
counter: "swap_horiz",
|
||||
accept: "check_circle",
|
||||
escalate: "warning",
|
||||
};
|
||||
|
||||
const ACTION_LABEL: Record<string, string> = {
|
||||
propose: "Proposed",
|
||||
counter: "Counter",
|
||||
accept: "Accepted",
|
||||
escalate: "Escalated",
|
||||
};
|
||||
|
||||
const ACTION_BADGE: Record<string, string> = {
|
||||
propose: "text-[#B7A6FB] bg-[#B7A6FB]/10 border-[#B7A6FB]/20",
|
||||
counter: "text-slate-300 bg-white/5 border-white/10",
|
||||
accept: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
|
||||
escalate: "text-amber-400 bg-amber-500/10 border-amber-500/20",
|
||||
};
|
||||
|
||||
export default function NegotiationTimeline({ rounds, participants }: Props) {
|
||||
if (!rounds || rounds.length === 0) {
|
||||
return (
|
||||
<div className="text-xs text-slate-600 py-6 text-center font-mono">
|
||||
Negotiation hasn't started yet
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const userA = participants?.[0];
|
||||
const userB = participants?.[1];
|
||||
|
||||
function agentLabel(proposerId: number) {
|
||||
if (userA && proposerId === userA.user_id) return "A";
|
||||
if (userB && proposerId === userB.user_id) return "B";
|
||||
return "?";
|
||||
}
|
||||
|
||||
function agentPersonality(proposerId: number): Personality {
|
||||
const p = participants?.find((p) => p.user_id === proposerId);
|
||||
return (p?.personality_used ?? "balanced") as Personality;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-6">
|
||||
{/* Vertical data-stream line */}
|
||||
<div className="absolute left-[18px] top-6 bottom-6 data-stream-line" />
|
||||
|
||||
{rounds.map((round, idx) => {
|
||||
const label = agentLabel(round.proposer_id);
|
||||
const personality = agentPersonality(round.proposer_id);
|
||||
const action = round.response_type ?? "propose";
|
||||
const isA = label === "A";
|
||||
const isLast = idx === rounds.length - 1;
|
||||
|
||||
return (
|
||||
<div key={round.id} className="flex gap-4 group">
|
||||
{/* Node */}
|
||||
<div className="relative z-10 flex flex-col items-center shrink-0">
|
||||
<div
|
||||
className={`size-9 rounded-full flex items-center justify-center text-[10px] font-mono font-bold transition-all ${
|
||||
isLast
|
||||
? isA
|
||||
? "bg-[#B7A6FB] text-black shadow-[0_0_12px_#B7A6FB]"
|
||||
: "bg-cyan-400 text-black shadow-[0_0_12px_#22d3ee]"
|
||||
: "bg-[#070312] border border-white/10 text-slate-500"
|
||||
}`}
|
||||
>
|
||||
{String(round.round_number).padStart(2, "0")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 pb-2">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span
|
||||
className={`text-xs font-bold ${isA ? "text-[#B7A6FB]" : "text-cyan-400"}`}
|
||||
>
|
||||
Agent {label}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[9px] font-mono px-1.5 py-0.5 rounded border uppercase tracking-wide ${ACTION_BADGE[action] ?? ACTION_BADGE.propose}`}
|
||||
>
|
||||
<Icon name={ACTION_ICON[action] ?? "send"} className="text-[11px] align-middle mr-0.5" />
|
||||
{ACTION_LABEL[action] ?? action}
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-600 font-mono">{PERSONALITY_LABELS[personality]}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`rounded-lg p-3.5 text-sm backdrop-blur-sm border ${
|
||||
isLast
|
||||
? isA
|
||||
? "bg-[#B7A6FB]/5 border-[#B7A6FB]/20"
|
||||
: "bg-cyan-900/10 border-cyan-500/20"
|
||||
: "bg-[rgba(7,3,18,0.4)] border-white/5"
|
||||
}`}
|
||||
>
|
||||
{round.reasoning && (
|
||||
<p className="text-slate-300 text-xs leading-relaxed font-light mb-2">
|
||||
{round.reasoning}
|
||||
</p>
|
||||
)}
|
||||
{round.proposal && typeof round.proposal === "object" && (
|
||||
<ProposalSnippet proposal={round.proposal as Record<string, unknown>} />
|
||||
)}
|
||||
{/* Satisfaction */}
|
||||
<div className="flex items-center gap-4 mt-2 pt-2 border-t border-white/5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[9px] text-slate-600 font-mono uppercase">Sat A</span>
|
||||
<span className={`text-[10px] font-mono font-bold ${(round.satisfaction_a ?? 0) >= 70 ? "text-[#B7A6FB]" : "text-red-400"}`}>
|
||||
{round.satisfaction_a?.toFixed(0) ?? "—"}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[9px] text-slate-600 font-mono uppercase">Sat B</span>
|
||||
<span className={`text-[10px] font-mono font-bold ${(round.satisfaction_b ?? 0) >= 70 ? "text-cyan-400" : "text-red-400"}`}>
|
||||
{round.satisfaction_b?.toFixed(0) ?? "—"}%
|
||||
</span>
|
||||
</div>
|
||||
{round.concessions_made?.length > 0 && (
|
||||
<span className="text-[9px] font-mono text-amber-400 border border-amber-500/20 bg-amber-500/10 px-1.5 py-0.5 rounded uppercase ml-auto">
|
||||
Concession
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProposalSnippet({ proposal }: { proposal: Record<string, unknown> }) {
|
||||
const summary =
|
||||
(proposal.summary as string) ?? (proposal.for_party_a as string) ?? null;
|
||||
if (!summary) return null;
|
||||
return (
|
||||
<div className="code-snippet p-2 text-[10px] text-slate-400 leading-relaxed mt-1">
|
||||
{summary}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
184
negot8/dashboard/components/ResolutionCard.tsx
Normal file
184
negot8/dashboard/components/ResolutionCard.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import { Negotiation } from "@/lib/types";
|
||||
import { FEATURE_LABELS } from "@/lib/utils";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
negotiation: Negotiation;
|
||||
}
|
||||
|
||||
export default function ResolutionCard({ negotiation }: Props) {
|
||||
const resolution = negotiation.resolution as Record<string, unknown> | null;
|
||||
const status = negotiation.status;
|
||||
const analytics = negotiation.analytics;
|
||||
|
||||
if (!resolution && status !== "resolved" && status !== "escalated") {
|
||||
return (
|
||||
<div className="text-xs text-slate-600 py-4 text-center font-mono">
|
||||
Resolution not yet available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const final = (resolution?.final_proposal ?? {}) as Record<string, unknown>;
|
||||
const details = (final.details ?? {}) as Record<string, unknown>;
|
||||
const roundsTaken = resolution?.rounds_taken as number | undefined;
|
||||
const summaryText = String(final.summary ?? resolution?.summary ?? "");
|
||||
const forPartyA = final.for_party_a ? String(final.for_party_a) : null;
|
||||
const forPartyB = final.for_party_b ? String(final.for_party_b) : null;
|
||||
const upiLink = (details.upi_link ?? details.upilink)
|
||||
? String(details.upi_link ?? details.upilink)
|
||||
: null;
|
||||
const isResolved = status === "resolved";
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Hero card — matches Stitch resolution screen */}
|
||||
<div
|
||||
className={`relative rounded-xl border overflow-hidden ${
|
||||
isResolved
|
||||
? "border-emerald-500/30 bg-emerald-900/5"
|
||||
: "border-amber-500/30 bg-amber-900/5"
|
||||
}`}
|
||||
>
|
||||
{/* Top gradient line */}
|
||||
<div
|
||||
className={`absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent ${
|
||||
isResolved ? "via-emerald-400" : "via-amber-400"
|
||||
} to-transparent opacity-60`}
|
||||
/>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-[10px] font-bold uppercase tracking-wider ${
|
||||
isResolved
|
||||
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
|
||||
: "bg-amber-500/10 border-amber-500/20 text-amber-400"
|
||||
}`}
|
||||
>
|
||||
<Icon name={isResolved ? "check_circle" : "warning"} className="text-sm" />
|
||||
{isResolved ? "Resolved" : "Escalated"}
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-600 font-mono">
|
||||
{FEATURE_LABELS[negotiation.feature_type]}
|
||||
{roundsTaken ? ` · ${roundsTaken} round${roundsTaken > 1 ? "s" : ""}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{summaryText && (
|
||||
<p className="text-sm text-slate-300 leading-relaxed font-light">{summaryText}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
{analytics && (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<MetricBox label="Fairness" value={`${analytics.fairness_score?.toFixed(0)}%`} highlight />
|
||||
<MetricBox label="Total Rounds" value={String(roundsTaken ?? "—")} />
|
||||
<MetricBox
|
||||
label="Concessions"
|
||||
value={`${analytics.total_concessions_a + analytics.total_concessions_b}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Per-party outcomes */}
|
||||
{(forPartyA || forPartyB) && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{forPartyA && (
|
||||
<div className="bg-[#B7A6FB]/5 border border-[#B7A6FB]/20 rounded-lg p-3 hover:border-[#B7A6FB]/40 transition-colors">
|
||||
<div className="text-[10px] font-mono text-[#B7A6FB] mb-2 uppercase tracking-wider">Agent A</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{forPartyA}</p>
|
||||
</div>
|
||||
)}
|
||||
{forPartyB && (
|
||||
<div className="bg-cyan-900/10 border border-cyan-500/20 rounded-lg p-3 hover:border-cyan-500/40 transition-colors">
|
||||
<div className="text-[10px] font-mono text-cyan-400 mb-2 uppercase tracking-wider">Agent B</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{forPartyB}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{upiLink && (
|
||||
<a
|
||||
href={upiLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group relative flex items-center gap-3 p-4 rounded-xl border border-[#222249] bg-[#101023] hover:border-[#B7A6FB]/40 transition-all overflow-hidden text-left"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-indigo-500/20 text-indigo-300 group-hover:bg-indigo-500/30 transition-colors">
|
||||
<Icon name="credit_card" className="text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-sm font-bold">Pay via UPI</div>
|
||||
<div className="text-slate-500 text-[10px]">Instant settlement</div>
|
||||
</div>
|
||||
<Icon name="arrow_forward" className="ml-auto text-[#B7A6FB] text-base opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</a>
|
||||
)}
|
||||
<button className="group relative flex items-center gap-3 p-4 rounded-xl border border-[#222249] bg-[#101023] hover:border-[#B7A6FB]/40 transition-all overflow-hidden text-left">
|
||||
<div className="p-2 rounded-lg bg-sky-500/20 text-sky-300 group-hover:bg-sky-500/30 transition-colors">
|
||||
<Icon name="chat" className="text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-sm font-bold">Telegram</div>
|
||||
<div className="text-slate-500 text-[10px]">Open channel</div>
|
||||
</div>
|
||||
<Icon name="arrow_forward" className="ml-auto text-[#B7A6FB] text-base opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
<button className="group relative flex items-center gap-3 p-4 rounded-xl border border-[#222249] bg-[#101023] hover:border-[#B7A6FB]/40 transition-all overflow-hidden text-left">
|
||||
<div className="p-2 rounded-lg bg-orange-500/20 text-orange-300 group-hover:bg-orange-500/30 transition-colors">
|
||||
<Icon name="picture_as_pdf" className="text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-sm font-bold">Download PDF</div>
|
||||
<div className="text-slate-500 text-[10px]">Full transcript</div>
|
||||
</div>
|
||||
<Icon name="download" className="ml-auto text-[#B7A6FB] text-base opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
<button className="group relative flex items-center gap-3 p-4 rounded-xl border border-[#222249] bg-[#101023] hover:border-[#B7A6FB]/40 transition-all overflow-hidden text-left">
|
||||
<div className="p-2 rounded-lg bg-pink-500/20 text-pink-300 group-hover:bg-pink-500/30 transition-colors">
|
||||
<Icon name="graphic_eq" className="text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-sm font-bold">Voice Summary</div>
|
||||
<div className="text-slate-500 text-[10px]">AI generated audio</div>
|
||||
</div>
|
||||
<Icon name="play_arrow" className="ml-auto text-[#B7A6FB] text-base opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricBox({
|
||||
label,
|
||||
value,
|
||||
highlight = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
highlight?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 p-3 rounded-lg bg-white/5 border border-white/10 hover:border-[#B7A6FB]/30 transition-colors">
|
||||
<span className="text-slate-500 text-[10px] font-mono">{label}</span>
|
||||
<span className={`text-xl font-light tabular-nums ${highlight ? "text-[#B7A6FB]" : "text-white"}`}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface Props {
|
||||
negotiation: Negotiation;
|
||||
}
|
||||
96
negot8/dashboard/components/SatisfactionChart.tsx
Normal file
96
negot8/dashboard/components/SatisfactionChart.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { SatisfactionPoint } from "@/lib/types";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
|
||||
interface Props {
|
||||
data: SatisfactionPoint[];
|
||||
}
|
||||
|
||||
export default function SatisfactionChart({ data }: Props) {
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48 text-slate-600 text-xs font-mono">
|
||||
No satisfaction data yet
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Legend */}
|
||||
<div className="flex gap-4 justify-end mb-3 text-[10px] font-mono">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="size-1.5 rounded-full bg-[#B7A6FB] shadow-[0_0_5px_#B7A6FB]" />
|
||||
<span className="text-slate-400">Agent A</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="size-1.5 rounded-full bg-cyan-400 shadow-[0_0_5px_#22d3ee]" />
|
||||
<span className="text-slate-400">Agent B</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<LineChart data={data} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
|
||||
<XAxis
|
||||
dataKey="round"
|
||||
tick={{ fill: "#475569", fontSize: 10, fontFamily: "JetBrains Mono" }}
|
||||
axisLine={{ stroke: "rgba(255,255,255,0.05)" }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fill: "#475569", fontSize: 10, fontFamily: "JetBrains Mono" }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => `${v}%`}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: "rgba(7, 3, 18, 0.9)",
|
||||
border: "1px solid rgba(183, 166, 251, 0.2)",
|
||||
borderRadius: 8,
|
||||
backdropFilter: "blur(12px)",
|
||||
}}
|
||||
labelStyle={{ color: "#94a3b8", fontFamily: "JetBrains Mono", fontSize: 10 }}
|
||||
itemStyle={{ color: "#e2e8f0", fontFamily: "JetBrains Mono", fontSize: 11 }}
|
||||
formatter={(value: number | undefined) => [`${(value ?? 0).toFixed(0)}%`]}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="score_a"
|
||||
stroke="#B7A6FB"
|
||||
strokeWidth={1.5}
|
||||
dot={{ fill: "#000", stroke: "#B7A6FB", strokeWidth: 1, r: 2 }}
|
||||
activeDot={{ r: 4, fill: "#B7A6FB", stroke: "white", strokeWidth: 1 }}
|
||||
name="Agent A"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="score_b"
|
||||
stroke="#22d3ee"
|
||||
strokeWidth={1.5}
|
||||
dot={{ fill: "#000", stroke: "#22d3ee", strokeWidth: 1, r: 2 }}
|
||||
activeDot={{ r: 4, fill: "#22d3ee", stroke: "white", strokeWidth: 1 }}
|
||||
name="Agent B"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface Props {
|
||||
data: SatisfactionPoint[];
|
||||
}
|
||||
|
||||
87
negot8/dashboard/components/Sidebar.tsx
Normal file
87
negot8/dashboard/components/Sidebar.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
function Icon({ name, className = "" }: { name: string; className?: string }) {
|
||||
return (
|
||||
<span className={`material-symbols-outlined ${className}`}>{name}</span>
|
||||
);
|
||||
}
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ icon: "dashboard", label: "Dashboard", href: "/dashboard" },
|
||||
{ icon: "history", label: "History", href: "/history" },
|
||||
{ icon: "analytics", label: "Analytics", href: "/analytics" },
|
||||
{ icon: "settings", label: "Preferences", href: "/preferences" },
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside className="hidden md:flex w-60 flex-col bg-black/40 backdrop-blur-xl border-r border-white/5 relative z-20 shrink-0">
|
||||
{/* Logo */}
|
||||
<div className="flex h-16 items-center gap-3 px-5 border-b border-white/5">
|
||||
<div className="flex items-center justify-center size-8 rounded-lg bg-[#B7A6FB]/10 border border-[#B7A6FB]/20 text-[#B7A6FB]">
|
||||
<Icon name="hub" className="text-xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-white text-sm font-bold tracking-tight">
|
||||
Agent<span className="text-[#B7A6FB] font-light">Mesh</span>
|
||||
</h1>
|
||||
<p className="text-[10px] text-slate-600 font-mono">v2.4.0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex flex-col gap-1 p-3 flex-1">
|
||||
{NAV_ITEMS.map(({ icon, label, href }) => {
|
||||
const isActive =
|
||||
href === "/dashboard" ? pathname === "/dashboard" : pathname.startsWith(href);
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={`group flex items-center gap-3 px-3 py-2 rounded-lg transition-all text-sm ${
|
||||
isActive
|
||||
? "bg-white/5 border border-white/10 text-white"
|
||||
: "text-slate-500 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
name={icon}
|
||||
className={`text-[18px] ${
|
||||
isActive
|
||||
? "text-[#B7A6FB]"
|
||||
: "group-hover:text-[#B7A6FB] transition-colors"
|
||||
}`}
|
||||
/>
|
||||
<span className="font-medium">{label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* System status footer */}
|
||||
<div className="p-3 border-t border-white/5">
|
||||
<div className="rounded-lg bg-gradient-to-b from-white/5 to-transparent border border-white/5 p-3 relative overflow-hidden">
|
||||
<div className="absolute -right-4 -top-4 w-16 h-16 bg-[#B7A6FB]/10 blur-2xl rounded-full" />
|
||||
<div className="flex items-center gap-2 mb-2 relative z-10">
|
||||
<Icon name="bolt" className="text-[#B7A6FB] text-base" />
|
||||
<span className="text-[10px] font-bold text-white tracking-wide">
|
||||
System Status
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/10 h-0.5 rounded-full mb-2 overflow-hidden">
|
||||
<div className="bg-[#B7A6FB] h-full w-3/4 shadow-[0_0_8px_#B7A6FB]" />
|
||||
</div>
|
||||
<p className="text-[9px] text-slate-500 font-mono">
|
||||
LATENCY: <span className="text-[#B7A6FB]">12ms</span> ·{" "}
|
||||
<span className="text-emerald-400">OPERATIONAL</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
19
negot8/dashboard/lib/api.ts
Normal file
19
negot8/dashboard/lib/api.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Thin API client for the negoT8 FastAPI backend
|
||||
import type { Negotiation, Stats } from "./types";
|
||||
|
||||
const BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
||||
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`GET ${path} → ${res.status}`);
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
stats: () => get<Stats>("/api/stats"),
|
||||
negotiations: () =>
|
||||
get<{ negotiations: Negotiation[]; total: number }>("/api/negotiations"),
|
||||
negotiation: (id: string) => get<Negotiation>(`/api/negotiations/${id}`),
|
||||
analytics: (id: string) =>
|
||||
get<Negotiation["analytics"]>(`/api/negotiations/${id}/analytics`),
|
||||
};
|
||||
37
negot8/dashboard/lib/socket.ts
Normal file
37
negot8/dashboard/lib/socket.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Socket.IO client singleton — import this anywhere to get the shared socket
|
||||
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
||||
|
||||
let socket: Socket | null = null;
|
||||
|
||||
export function getSocket(): Socket {
|
||||
if (!socket) {
|
||||
socket = io(API_URL, {
|
||||
transports: ["websocket", "polling"],
|
||||
autoConnect: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1500,
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("[Socket.IO] connected:", socket?.id);
|
||||
});
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log("[Socket.IO] disconnected:", reason);
|
||||
});
|
||||
socket.on("connect_error", (err) => {
|
||||
console.warn("[Socket.IO] connection error:", err.message);
|
||||
});
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
export function joinNegotiation(negotiationId: string) {
|
||||
getSocket().emit("join_negotiation", { negotiation_id: negotiationId });
|
||||
}
|
||||
|
||||
export function leaveNegotiation(negotiationId: string) {
|
||||
getSocket().emit("leave_negotiation", { negotiation_id: negotiationId });
|
||||
}
|
||||
104
negot8/dashboard/lib/types.ts
Normal file
104
negot8/dashboard/lib/types.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
// Shared TypeScript types mirroring the backend data models
|
||||
|
||||
export interface User {
|
||||
telegram_id: number;
|
||||
username: string;
|
||||
display_name: string;
|
||||
personality: Personality;
|
||||
voice_id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export type Personality =
|
||||
| "aggressive"
|
||||
| "people_pleaser"
|
||||
| "analytical"
|
||||
| "empathetic"
|
||||
| "balanced";
|
||||
|
||||
export type NegotiationStatus = "pending" | "active" | "resolved" | "escalated";
|
||||
|
||||
export type FeatureType =
|
||||
| "scheduling"
|
||||
| "expenses"
|
||||
| "freelance"
|
||||
| "roommate"
|
||||
| "trip"
|
||||
| "marketplace"
|
||||
| "collaborative"
|
||||
| "conflict"
|
||||
| "generic";
|
||||
|
||||
export interface Participant {
|
||||
negotiation_id: string;
|
||||
user_id: number;
|
||||
preferences: Record<string, unknown>;
|
||||
personality_used: Personality;
|
||||
username?: string;
|
||||
display_name?: string;
|
||||
personality?: Personality;
|
||||
voice_id?: string;
|
||||
}
|
||||
|
||||
export interface Round {
|
||||
id: number;
|
||||
negotiation_id: string;
|
||||
round_number: number;
|
||||
proposer_id: number;
|
||||
proposal: Record<string, unknown>;
|
||||
response_type: "propose" | "counter" | "accept" | "escalate";
|
||||
response: Record<string, unknown> | null;
|
||||
reasoning: string;
|
||||
satisfaction_a: number;
|
||||
satisfaction_b: number;
|
||||
concessions_made: string[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SatisfactionPoint {
|
||||
round: number;
|
||||
score_a: number;
|
||||
score_b: number;
|
||||
}
|
||||
|
||||
export interface ConcessionEntry {
|
||||
round: number;
|
||||
by: "A" | "B";
|
||||
gave_up: string;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
negotiation_id: string;
|
||||
satisfaction_timeline: SatisfactionPoint[];
|
||||
concession_log: ConcessionEntry[];
|
||||
fairness_score: number;
|
||||
total_concessions_a: number;
|
||||
total_concessions_b: number;
|
||||
computed_at: string;
|
||||
}
|
||||
|
||||
export interface Negotiation {
|
||||
id: string;
|
||||
feature_type: FeatureType;
|
||||
status: NegotiationStatus;
|
||||
initiator_id: number;
|
||||
resolution: Record<string, unknown> | null;
|
||||
voice_summary_file: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
participant_count?: number;
|
||||
// Only present in detail view
|
||||
participants?: Participant[];
|
||||
rounds?: Round[];
|
||||
analytics?: Analytics;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
total_negotiations: number;
|
||||
resolved: number;
|
||||
active: number;
|
||||
escalated: number;
|
||||
total_users: number;
|
||||
avg_fairness_score: number;
|
||||
feature_breakdown: { feature_type: FeatureType; c: number }[];
|
||||
}
|
||||
86
negot8/dashboard/lib/utils.ts
Normal file
86
negot8/dashboard/lib/utils.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// Shared UI helpers — badges, labels, colour maps
|
||||
|
||||
import type { FeatureType, NegotiationStatus, Personality } from "@/lib/types";
|
||||
|
||||
// ─── Feature ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const FEATURE_LABELS: Record<FeatureType, string> = {
|
||||
scheduling: "📅 Scheduling",
|
||||
expenses: "💰 Expenses",
|
||||
freelance: "💼 Freelance",
|
||||
roommate: "🏠 Roommate",
|
||||
trip: "✈️ Trip",
|
||||
marketplace: "🛒 Marketplace",
|
||||
collaborative: "🍕 Collaborative",
|
||||
conflict: "⚖️ Conflict",
|
||||
generic: "🤝 Generic",
|
||||
};
|
||||
|
||||
// ─── Personality ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const PERSONALITY_LABELS: Record<Personality, string> = {
|
||||
aggressive: "😤 Aggressive",
|
||||
people_pleaser: "🤝 Pleaser",
|
||||
analytical: "📊 Analytical",
|
||||
empathetic: "💚 Empathetic",
|
||||
balanced: "⚖️ Balanced",
|
||||
};
|
||||
|
||||
export const PERSONALITY_COLORS: Record<Personality, string> = {
|
||||
aggressive: "bg-red-500/20 text-red-300 border-red-500/30",
|
||||
people_pleaser: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
||||
analytical: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
|
||||
empathetic: "bg-green-500/20 text-green-300 border-green-500/30",
|
||||
balanced: "bg-purple-500/20 text-purple-300 border-purple-500/30",
|
||||
};
|
||||
|
||||
// ─── Status ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const STATUS_LABELS: Record<NegotiationStatus, string> = {
|
||||
pending: "⏳ Pending",
|
||||
active: "🔄 Active",
|
||||
resolved: "✅ Resolved",
|
||||
escalated: "⚠️ Escalated",
|
||||
};
|
||||
|
||||
export const STATUS_COLORS: Record<NegotiationStatus, string> = {
|
||||
pending: "bg-zinc-500/20 text-zinc-400 border-zinc-500/30",
|
||||
active: "bg-blue-500/20 text-blue-300 border-blue-500/30 animate-pulse",
|
||||
resolved: "bg-green-500/20 text-green-300 border-green-500/30",
|
||||
escalated: "bg-amber-500/20 text-amber-300 border-amber-500/30",
|
||||
};
|
||||
|
||||
// ─── Fairness colour ─────────────────────────────────────────────────────────
|
||||
|
||||
export function fairnessColor(score: number): string {
|
||||
if (score >= 80) return "text-green-400";
|
||||
if (score >= 60) return "text-yellow-400";
|
||||
return "text-red-400";
|
||||
}
|
||||
|
||||
export function fairnessBarColor(score: number): string {
|
||||
if (score >= 80) return "bg-green-500";
|
||||
if (score >= 60) return "bg-yellow-500";
|
||||
return "bg-red-500";
|
||||
}
|
||||
|
||||
// ─── Satisfaction colour ─────────────────────────────────────────────────────
|
||||
|
||||
export function satColor(score: number): string {
|
||||
if (score >= 70) return "text-green-400";
|
||||
if (score >= 40) return "text-yellow-400";
|
||||
return "text-red-400";
|
||||
}
|
||||
|
||||
// ─── Time formatting ─────────────────────────────────────────────────────────
|
||||
|
||||
export function relativeTime(iso: string): string {
|
||||
const diff = Date.now() - new Date(iso).getTime();
|
||||
const s = Math.floor(diff / 1000);
|
||||
if (s < 60) return `${s}s ago`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m ago`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h ago`;
|
||||
return `${Math.floor(h / 24)}d ago`;
|
||||
}
|
||||
14
negot8/dashboard/next.config.ts
Normal file
14
negot8/dashboard/next.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: "http://localhost:8000/api/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
2195
negot8/dashboard/package-lock.json
generated
Normal file
2195
negot8/dashboard/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
negot8/dashboard/package.json
Normal file
26
negot8/dashboard/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.575.0",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"recharts": "^3.7.0",
|
||||
"socket.io-client": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
7
negot8/dashboard/postcss.config.mjs
Normal file
7
negot8/dashboard/postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
negot8/dashboard/public/file.svg
Normal file
1
negot8/dashboard/public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
negot8/dashboard/public/globe.svg
Normal file
1
negot8/dashboard/public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
negot8/dashboard/public/next.svg
Normal file
1
negot8/dashboard/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
negot8/dashboard/public/vercel.svg
Normal file
1
negot8/dashboard/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
negot8/dashboard/public/window.svg
Normal file
1
negot8/dashboard/public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
34
negot8/dashboard/tsconfig.json
Normal file
34
negot8/dashboard/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user