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:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user