mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
init
This commit is contained in:
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user