refactor: redesign AI Negotiation Simulator with Warm Editorial aesthetic

- extensive UI overhaul replacing neon/dark theme with stone/cream editorial design
- rewrite page.tsx for cleaner component structure and fix JSX errors
- update globals.css with new CSS variables and typography
- remove outdated assets
This commit is contained in:
Shubhamsaboo
2026-02-08 11:22:07 -08:00
parent 107dd2aaa5
commit 4819eb1321
6 changed files with 13568 additions and 339 deletions

View File

@@ -46,16 +46,16 @@ Watch two AI agents battle it out in an epic used car negotiation! Built with **
│ ADK Middleware │
└───────────┬───────────┘
┌───────────▼───────────┐
│ ADK Negotiation Agent │
│ (Battle Master) │
│ │
│ Tools: │
┌───────────▼─────────────
│ ADK Negotiation Agent
│ (Battle Master)
│ Tools:
│ • configure_negotiation│
│ • start_negotiation │
│ • buyer_make_offer │
│ • seller_respond │
└────────────────────────┘
│ • start_negotiation
│ • buyer_make_offer
│ • seller_respond
└────────────────────────
```
## 🚀 Quick Start

View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,102 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Warm Editorial Palette - Stone & Paper */
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 15, 23, 42;
--background-end-rgb: 30, 41, 59;
/* Core Colors */
--bg-app: #FAF8F5;
/* Warm cream paper background */
--bg-card: #FFFFFF;
/* Clean white card surface */
--bg-subtle: #F3F0EB;
/* Subtle separation background */
--text-primary: #1C1917;
/* Warm black (Stone 900) */
--text-secondary: #44403C;
/* Dark stone (Stone 700) */
--text-tertiary: #78716C;
/* Medium stone (Stone 500) */
--border-light: #E7E5E4;
/* Light stone border */
--border-medium: #D6D3D1;
/* Medium stone border */
/* Accents - Muted & Sophisticated */
--accent-gold: #D97706;
/* Editorial Gold */
--accent-emerald: #059669;
/* Editorial Green */
--accent-rose: #E11D48;
/* Editorial Rose */
}
/* Base Styles */
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
135deg,
rgb(var(--background-start-rgb)),
rgb(var(--background-end-rgb))
);
background-color: var(--bg-app);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
/* Battle Arena Styles */
.battle-arena {
background: radial-gradient(ellipse at center, rgba(255,215,0,0.1) 0%, transparent 70%);
/* Typography Utilities */
.font-serif {
font-family: 'Playfair Display', serif;
}
.buyer-glow {
box-shadow: 0 0 30px rgba(59, 130, 246, 0.5);
.font-sans {
font-family: 'Inter', sans-serif;
}
.seller-glow {
box-shadow: 0 0 30px rgba(239, 68, 68, 0.5);
/* Component Utilities */
@layer components {
/* Minimal Editorial Card */
.editorial-card {
background-color: var(--bg-card);
border: 1px solid var(--border-light);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
@apply rounded-xl transition-all duration-300;
}
.editorial-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
/* Chat Bubble Base */
.chat-bubble {
@apply flex items-start gap-4 max-w-2xl p-4 rounded-2xl relative mb-2;
}
.chat-bubble.buyer {
@apply ml-auto bg-white border border-stone-200 text-stone-900 rounded-tr-sm;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.chat-bubble.seller {
@apply mr-auto bg-stone-50 border border-stone-200 text-stone-900 rounded-tl-sm;
}
}
.offer-pulse {
animation: offerPulse 2s ease-in-out infinite;
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes offerPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
/* VS Badge */
.vs-badge {
background: linear-gradient(135deg, #ffd700 0%, #ff8c00 100%);
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
/* Deal Animation */
.deal-celebration {
animation: celebrate 0.5s ease-out;
}
@keyframes celebrate {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
/* Negotiation Timeline */
.timeline-connector {
background: linear-gradient(180deg, #3b82f6 0%, #ffd700 50%, #ef4444 100%);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255,255,255,0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.5);
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}

View File

@@ -1,20 +1,20 @@
"use client";
import { useState } from "react";
import {
CopilotSidebar,
CopilotKitCSSProperties
} from "@copilotkit/react-ui";
import { useState, useEffect } from "react";
import { useCoAgent, useCopilotAction } from "@copilotkit/react-core";
import { motion, AnimatePresence } from "framer-motion";
import {
Swords,
DollarSign,
TrendingDown,
import {
Swords,
DollarSign,
TrendingDown,
TrendingUp,
Handshake,
XCircle,
Trophy
Trophy,
Play,
RotateCcw,
Sparkles,
Zap
} from "lucide-react";
// Types for agent state
@@ -31,6 +31,22 @@ type NegotiationRound = {
seller_emoji?: string;
};
type Scenario = {
id: string;
title: string;
emoji: string;
item: string;
asking_price: number;
description: string;
};
type Personality = {
id: string;
name: string;
emoji: string;
description: string;
};
type AgentState = {
status: "setup" | "ready" | "negotiating" | "deal" | "no_deal";
scenario?: {
@@ -53,128 +69,205 @@ type AgentState = {
final_price?: number;
};
// Offer Card Component
// Offer Card Component - Editorial Style
function OfferCard({ round, isLatest }: { round: NegotiationRound; isLatest: boolean }) {
const isBuyer = round.type === "buyer_offer";
return (
<motion.div
initial={{ opacity: 0, x: isBuyer ? -50 : 50, scale: 0.9 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
transition={{ duration: 0.4 }}
initial={{ opacity: 0, x: isBuyer ? -20 : 20, y: 10 }}
animate={{ opacity: 1, x: 0, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={`
relative p-4 rounded-xl mb-4
${isBuyer
? "bg-gradient-to-r from-blue-900/80 to-blue-800/60 border-l-4 border-blue-400 mr-8"
: "bg-gradient-to-l from-red-900/80 to-red-800/60 border-r-4 border-red-400 ml-8"
relative p-6 rounded-lg mb-6 max-w-lg
${isBuyer
? "bg-white border border-border-light ml-auto shadow-sm"
: "bg-bg-subtle border border-border-light mr-auto shadow-sm"
}
${isLatest ? "offer-pulse" : ""}
${isLatest ? "ring-1 ring-text-tertiary" : ""}
`}
>
{/* Round Badge */}
{/* Round Indicator */}
<div className={`
absolute -top-2 ${isBuyer ? "-left-2" : "-right-2"}
bg-gray-800 px-2 py-0.5 rounded-full text-xs font-bold
absolute -top-3 ${isBuyer ? "-right-3" : "-left-3"}
bg-text-primary text-bg-app px-2 py-1 rounded text-[10px] font-bold tracking-widest uppercase shadow-md
`}>
R{round.round}
Round {round.round}
</div>
{/* Agent Info */}
<div className={`flex items-center gap-2 mb-2 ${!isBuyer && "flex-row-reverse"}`}>
<span className="text-2xl">
{/* Header */}
<div className={`flex items-center gap-3 mb-4 ${isBuyer ? "flex-row-reverse" : ""}`}>
<span className="text-xl filter grayscale opacity-80">
{isBuyer ? round.buyer_emoji : round.seller_emoji}
</span>
<span className="font-bold text-sm">
<span className="font-serif font-bold text-sm text-text-primary tracking-wide">
{isBuyer ? round.buyer_name : round.seller_name}
</span>
</div>
{/* Offer/Response */}
<div className={`${!isBuyer && "text-right"}`}>
{/* Offer Content */}
<div className={`${isBuyer ? "text-right" : "text-left"}`}>
{isBuyer && round.offer_amount && (
<div className="flex items-center gap-2 mb-2">
<DollarSign className="w-5 h-5 text-green-400" />
<span className="text-2xl font-bold text-green-400">
<div className="mb-3">
<span className="block text-xs font-bold text-text-tertiary uppercase tracking-widest mb-1">Offer</span>
<span className="font-serif text-3xl font-medium text-text-primary block">
${round.offer_amount.toLocaleString()}
</span>
</div>
)}
{!isBuyer && round.action && (
<div className={`flex items-center gap-2 mb-2 ${!isBuyer && "justify-end"}`}>
<div className={`mb-3 ${isBuyer ? "text-right" : "text-left"}`}>
<span className="block text-xs font-bold text-text-tertiary uppercase tracking-widest mb-1">Response</span>
{round.action === "accept" && (
<>
<Handshake className="w-5 h-5 text-green-400" />
<span className="text-xl font-bold text-green-400">ACCEPTED!</span>
</>
<span className="text-lg font-serif font-medium text-emerald-700 flex items-center gap-2">
<Handshake className="w-4 h-4" /> Deal Accepted
</span>
)}
{round.action === "counter" && (
<>
<TrendingDown className="w-5 h-5 text-yellow-400" />
<span className="text-xl font-bold text-yellow-400">
${round.counter_amount?.toLocaleString()}
</span>
</>
<span className="font-serif text-3xl font-medium text-text-primary block">
${round.counter_amount?.toLocaleString()}
</span>
)}
{round.action === "reject" && (
<>
<XCircle className="w-5 h-5 text-red-400" />
<span className="text-xl font-bold text-red-400">REJECTED</span>
</>
<span className="text-lg font-serif font-medium text-red-700 flex items-center gap-2">
<XCircle className="w-4 h-4" /> Offer Rejected
</span>
)}
{round.action === "walk" && (
<>
<XCircle className="w-5 h-5 text-red-500" />
<span className="text-xl font-bold text-red-500">WALKED AWAY</span>
</>
<span className="text-lg font-serif font-medium text-text-tertiary flex items-center gap-2">
<XCircle className="w-4 h-4" /> Ended Negotiation
</span>
)}
</div>
)}
{/* Message */}
<p className="text-gray-300 italic text-sm">"{round.message}"</p>
{/* Message Body */}
<p className="text-text-secondary text-sm leading-relaxed font-sans border-t border-border-light pt-3 mt-2">
{round.message}
</p>
</div>
</motion.div>
);
}
// Deal Banner Component
// Deal Banner Component - Editorial Style
function DealBanner({ finalPrice, askingPrice }: { finalPrice: number; askingPrice: number }) {
const savings = askingPrice - finalPrice;
const percentOff = ((savings / askingPrice) * 100).toFixed(1);
return (
<motion.div
initial={{ scale: 0, rotate: -10 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: "spring", bounce: 0.5 }}
className="deal-celebration bg-gradient-to-r from-green-600 to-emerald-500 rounded-2xl p-8 text-center shadow-2xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-bg-card rounded-2xl p-12 text-center shadow-lg border border-border-medium max-w-xl mx-auto"
>
<Trophy className="w-16 h-16 mx-auto mb-4 text-yellow-300" />
<h2 className="text-4xl font-black mb-2">🎉 DEAL CLOSED! 🎉</h2>
<p className="text-5xl font-black text-yellow-300 mb-4">
<div className="mb-6 flex justify-center">
<Trophy className="w-12 h-12 text-text-primary" strokeWidth={1} />
</div>
<h2 className="text-sm font-bold tracking-widest text-text-tertiary uppercase mb-4">Agreement Reached</h2>
<p className="text-6xl font-serif text-text-primary mb-8 font-medium">
${finalPrice.toLocaleString()}
</p>
<div className="flex justify-center gap-8 text-lg">
<div className="flex justify-center gap-12 border-t border-border-light pt-8">
<div>
<p className="text-green-200">Savings</p>
<p className="font-bold text-2xl">${savings.toLocaleString()}</p>
<p className="text-text-tertiary text-xs uppercase tracking-wider mb-1">Savings</p>
<p className="font-serif text-xl text-text-secondary">${savings.toLocaleString()}</p>
</div>
<div>
<p className="text-green-200">Off Asking</p>
<p className="font-bold text-2xl">{percentOff}%</p>
<p className="text-text-tertiary text-xs uppercase tracking-wider mb-1">Discount</p>
<p className="font-serif text-xl text-text-secondary">{percentOff}%</p>
</div>
</div>
</motion.div>
);
}
// ==================== CHAT UI COMPONENTS ====================
// Chat Bubble Component
const ChatBubble = ({ round, type }: { round: NegotiationRound; type: "buyer" | "seller" }) => {
const name = type === "buyer" ? round.buyer_name : round.seller_name;
const emoji = type === "buyer" ? round.buyer_emoji : round.seller_emoji;
return (
<motion.div
initial={{ opacity: 0, x: type === "buyer" ? -50 : 50 }}
animate={{ opacity: 1, x: 0 }}
className={`chat-bubble ${type}`}
>
<div className="avatar">{emoji}</div>
<div className="bubble-content">
<div className="name">{name}</div>
<div className="message">{round.message}</div>
{round.offer_amount && (
<div className="offer-badge">${round.offer_amount.toLocaleString()}</div>
)}
</div>
</motion.div>
);
};
// Typing Indicator Component
const TypingIndicator = ({ emoji, name }: { emoji: string; name: string }) => (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="typing-indicator"
>
<span className="avatar" style={{ fontSize: '20px' }}>{emoji}</span>
<div className="dots">
<span></span>
<span></span>
<span></span>
</div>
<span className="typing-text">{name} is thinking...</span>
</motion.div>
);
// Price Tracker Component
const PriceTracker = ({
currentOffer,
askingPrice,
minimum,
budget
}: {
currentOffer: number;
askingPrice: number;
minimum: number;
budget: number;
}) => {
const range = budget - minimum;
const position = ((currentOffer - minimum) / range) * 100;
return (
<div className="price-tracker">
<h4 style={{ fontSize: '14px', fontWeight: 600, marginBottom: '12px', color: 'var(--text-dark)' }}>
💰 Price Convergence
</h4>
<div className="track">
<div className="sweet-spot" style={{ left: `${Math.max(10, Math.min(90, position))}%` }} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div className="range-marker seller">Min: ${minimum.toLocaleString()}</div>
<div className="range-marker buyer">Max: ${budget.toLocaleString()}</div>
</div>
</div>
);
};
// Main Page Component
export default function NegotiationBattle() {
const [themeColor] = useState("#6366f1");
const [selectedScenario, setSelectedScenario] = useState<string | null>(null);
const [selectedBuyer, setSelectedBuyer] = useState<string | null>(null);
const [selectedSeller, setSelectedSeller] = useState<string | null>(null);
const [scenarios, setScenarios] = useState<Scenario[]>([]);
const [buyers, setBuyers] = useState<Personality[]>([]);
const [sellers, setSellers] = useState<Personality[]>([]);
// Connect to agent state
const { state } = useCoAgent<AgentState>({
const { state, setState } = useCoAgent<AgentState>({
name: "negotiation_agent",
initialState: {
status: "setup",
@@ -183,221 +276,444 @@ export default function NegotiationBattle() {
},
});
// Register tool renderers for Generative UI
useCopilotAction({
name: "buyer_make_offer",
description: "Buyer makes an offer",
available: "disabled",
parameters: [
{ name: "offer_amount", type: "number", required: true },
{ name: "message", type: "string", required: true },
],
render: ({ args }) => (
<div className="bg-blue-900/50 p-3 rounded-lg border border-blue-500">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-blue-400" />
<span className="font-bold">Buyer Offers:</span>
<span className="text-green-400 font-bold">
${(args.offer_amount as number)?.toLocaleString()}
</span>
</div>
</div>
),
});
// Fetch available scenarios and personalities on mount
useEffect(() => {
const fetchData = async () => {
try {
console.log('[DEBUG] Fetching scenarios and personalities...');
// Fetch scenarios
const scenariosRes = await fetch('http://localhost:8000/get_available_scenarios');
const scenariosData = await scenariosRes.json();
console.log('[DEBUG] Scenarios fetched:', scenariosData);
if (scenariosData.scenarios) {
setScenarios(scenariosData.scenarios);
console.log('[DEBUG] Scenarios state updated:', scenariosData.scenarios.length, 'scenarios');
}
useCopilotAction({
name: "seller_respond",
description: "Seller responds to offer",
available: "disabled",
parameters: [
{ name: "action", type: "string", required: true },
{ name: "counter_amount", type: "number", required: false },
{ name: "message", type: "string", required: true },
],
render: ({ args }) => (
<div className="bg-red-900/50 p-3 rounded-lg border border-red-500">
<div className="flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-red-400" />
<span className="font-bold">Seller {args.action}s:</span>
{args.counter_amount && (
<span className="text-yellow-400 font-bold">
${(args.counter_amount as number)?.toLocaleString()}
</span>
)}
</div>
</div>
),
});
// Fetch personalities
const personalitiesRes = await fetch('http://localhost:8000/get_available_personalities');
const personalitiesData = await personalitiesRes.json();
console.log('[DEBUG] Personalities fetched:', personalitiesData);
if (personalitiesData.buyers && personalitiesData.sellers) {
setBuyers(personalitiesData.buyers);
setSellers(personalitiesData.sellers);
console.log('[DEBUG] Personalities state updated:', personalitiesData.buyers.length, 'buyers,', personalitiesData.sellers.length, 'sellers');
}
} catch (error) {
console.error('[DEBUG] Error fetching data:', error);
}
};
fetchData();
}, []);
// Debug logging for state changes
useEffect(() => {
console.log('[DEBUG] Current agent state:', state);
console.log('[DEBUG] Scenarios count:', scenarios.length);
console.log('[DEBUG] Buyers count:', buyers.length);
console.log('[DEBUG] Sellers count:', sellers.length);
}, [state, scenarios, buyers, sellers]);
const handleStartBattle = async () => {
if (!selectedScenario || !selectedBuyer || !selectedSeller) return;
try {
// Configure the negotiation
const configRes = await fetch('http://localhost:8000/configure_negotiation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
scenario_id: selectedScenario,
buyer_personality: selectedBuyer,
seller_personality: selectedSeller,
}),
});
const configData = await configRes.json();
// Update state with configuration
setState({
...state,
status: "ready",
scenario: configData.scenario,
buyer: configData.buyer,
seller: configData.seller,
});
// Start the negotiation
const startRes = await fetch('http://localhost:8000/start_negotiation', {
method: 'POST',
});
const startData = await startRes.json();
if (startData.status === "started") {
setState({
...state,
status: "negotiating",
scenario: configData.scenario,
buyer: configData.buyer,
seller: configData.seller,
});
// Start the automated negotiation
runNegotiation();
}
} catch (error) {
console.error('Error starting battle:', error);
}
};
const runNegotiation = async () => {
// This will be handled by the agent automatically
// Just need to poll for state updates
const pollInterval = setInterval(async () => {
try {
const stateRes = await fetch('http://localhost:8000/get_negotiation_state');
const stateData = await stateRes.json();
setState({
status: stateData.status,
scenario: {
title: stateData.scenario,
item: stateData.item,
asking_price: stateData.asking_price,
},
buyer: stateData.buyer,
seller: stateData.seller,
rounds: stateData.rounds,
current_round: stateData.current_round,
final_price: stateData.final_price,
});
if (stateData.status === "deal" || stateData.status === "no_deal") {
clearInterval(pollInterval);
}
} catch (error) {
console.error('Error polling state:', error);
}
}, 1000);
};
const handleReset = () => {
setSelectedScenario(null);
setSelectedBuyer(null);
setSelectedSeller(null);
setState({
status: "setup",
rounds: [],
current_round: 0,
});
};
return (
<main
className="min-h-screen"
style={{ "--copilot-kit-primary-color": themeColor } as CopilotKitCSSProperties}
>
{/* Header */}
<header className="bg-gradient-to-r from-gray-900 via-gray-800 to-gray-900 border-b border-gray-700">
<div className="container mx-auto px-4 py-6">
<div className="flex items-center justify-center gap-4">
<Swords className="w-10 h-10 text-yellow-400" />
<h1 className="text-4xl font-black bg-gradient-to-r from-yellow-400 via-red-500 to-blue-500 bg-clip-text text-transparent">
AI NEGOTIATION BATTLE
<main className="min-h-screen bg-bg-app">
{/* Header - Editorial Style */}
<header className="border-b border-border-light bg-card/50 backdrop-blur-sm sticky top-0 z-50">
<div className="container mx-auto px-6 py-8">
<motion.div
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="flex flex-col items-center justify-center gap-2"
>
<h1 className="text-4xl md:text-5xl font-serif tracking-tight text-text-primary">
The Negotiation
</h1>
<Swords className="w-10 h-10 text-yellow-400 transform scale-x-[-1]" />
</div>
<p className="text-center text-gray-400 mt-2">
Watch AI agents battle it out in epic negotiations!
</p>
<p
className="text-center text-sm font-sans tracking-wide uppercase text-text-tertiary"
>
Autonomous Agent Simulation
</p>
</motion.div>
</div>
</header>
{/* Battle Arena */}
<div className="container mx-auto px-4 py-8 battle-arena">
{/* Scenario Header */}
{state.scenario && (
<div className="relative container mx-auto px-4 py-12">
{/* Setup Phase - Editorial Style */}
{(state.status === "setup" || (!state.status && scenarios.length > 0)) && (
<motion.div
initial={{ opacity: 0, y: -20 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-8"
className="max-w-5xl mx-auto space-y-16"
>
<h2 className="text-2xl font-bold text-yellow-400">
{state.scenario.title}
</h2>
<p className="text-xl text-gray-300">{state.scenario.item}</p>
<p className="text-lg">
Asking Price: <span className="text-green-400 font-bold">
${state.scenario.asking_price?.toLocaleString()}
</span>
</p>
</motion.div>
)}
{/* VS Display */}
{state.buyer && state.seller && (
<div className="flex items-center justify-center gap-8 mb-8">
{/* Buyer */}
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
className="buyer-glow bg-gradient-to-br from-blue-900 to-blue-800 rounded-2xl p-6 text-center min-w-[200px]"
>
<div className="text-5xl mb-2">{state.buyer.emoji}</div>
<h3 className="text-xl font-bold text-blue-300">{state.buyer.name}</h3>
<p className="text-sm text-gray-400">BUYER</p>
<p className="text-green-400 font-bold mt-2">
Budget: ${state.buyer.budget?.toLocaleString()}
</p>
</motion.div>
{/* VS Badge */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className="vs-badge rounded-full w-20 h-20 flex items-center justify-center text-3xl font-black text-gray-900"
>
VS
</motion.div>
{/* Seller */}
<motion.div
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
className="seller-glow bg-gradient-to-bl from-red-900 to-red-800 rounded-2xl p-6 text-center min-w-[200px]"
>
<div className="text-5xl mb-2">{state.seller.emoji}</div>
<h3 className="text-xl font-bold text-red-300">{state.seller.name}</h3>
<p className="text-sm text-gray-400">SELLER</p>
<p className="text-yellow-400 font-bold mt-2">
Minimum: ${state.seller.minimum?.toLocaleString()}
</p>
</motion.div>
</div>
)}
{/* Negotiation Timeline */}
{state.rounds && state.rounds.length > 0 && (
<div className="max-w-2xl mx-auto">
<h3 className="text-xl font-bold text-center mb-4 text-gray-300">
NEGOTIATION TIMELINE
</h3>
<div className="relative">
{/* Center line */}
<div className="absolute left-1/2 top-0 bottom-0 w-1 timeline-connector transform -translate-x-1/2" />
{/* Rounds */}
<AnimatePresence>
{state.rounds.map((round, index) => (
<OfferCard
key={`${round.type}-${round.round}-${index}`}
round={round}
isLatest={index === state.rounds.length - 1}
/>
{/* Scenario Selection */}
<section>
<div className="text-center mb-10">
<span className="text-xs font-bold tracking-widest text-text-tertiary uppercase mb-2 block">Step 1</span>
<h2 className="text-3xl font-serif text-text-primary">
Select Context
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{scenarios.map((scenario) => (
<motion.div
key={scenario.id}
whileHover={{ y: -4 }}
whileTap={{ scale: 0.98 }}
onClick={() => setSelectedScenario(scenario.id)}
className={`
cursor-pointer p-8 rounded-xl border transition-all duration-200
${selectedScenario === scenario.id
? 'bg-bg-card border-text-primary shadow-md ring-1 ring-text-primary'
: 'bg-bg-subtle border-transparent hover:border-border-medium hover:bg-bg-card'
}
`}
>
<div className="text-4xl mb-6 text-center filter grayscale opacity-90">{scenario.emoji}</div>
<h3 className="text-lg font-serif font-medium text-center mb-2 text-text-primary">{scenario.title}</h3>
<p className="text-text-secondary text-sm text-center mb-4 line-clamp-2">{scenario.item}</p>
<p className="text-center text-text-primary font-bold font-serif">
${scenario.asking_price.toLocaleString()}
</p>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
)}
</div>
</section>
{/* Deal Banner */}
{state.status === "deal" && state.final_price && state.scenario && (
<div className="max-w-xl mx-auto mt-8">
<DealBanner
finalPrice={state.final_price}
askingPrice={state.scenario.asking_price}
/>
</div>
)}
{/* Character Selection */}
{selectedScenario && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-16"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
{/* Buyer Selection */}
<section>
<div className="text-center mb-8">
<span className="text-xs font-bold tracking-widest text-text-tertiary uppercase mb-2 block">Step 2</span>
<h2 className="text-2xl font-serif text-text-primary">
Choose Buyer
</h2>
</div>
<div className="grid grid-cols-2 gap-4">
{buyers.map((buyer) => (
<motion.div
key={buyer.id}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => setSelectedBuyer(buyer.id)}
className={`
cursor-pointer p-5 rounded-lg border transition-all
${selectedBuyer === buyer.id
? 'bg-bg-card border-text-primary shadow-sm'
: 'bg-bg-subtle border-transparent hover:border-border-medium'
}
`}
>
<div className="text-3xl mb-3 text-center filter grayscale opacity-90">{buyer.emoji}</div>
<h4 className="text-sm font-medium text-center text-text-primary">{buyer.name}</h4>
</motion.div>
))}
</div>
</section>
{/* No Deal Banner */}
{state.status === "no_deal" && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="max-w-xl mx-auto mt-8 bg-gradient-to-r from-red-800 to-red-600 rounded-2xl p-8 text-center"
>
<XCircle className="w-16 h-16 mx-auto mb-4 text-red-300" />
<h2 className="text-3xl font-black mb-2">NO DEAL 💔</h2>
<p className="text-gray-200">The negotiation has ended without an agreement.</p>
{/* Seller Selection */}
<section>
<div className="text-center mb-8">
<span className="text-xs font-bold tracking-widest text-text-tertiary uppercase mb-2 block">Step 3</span>
<h2 className="text-2xl font-serif text-text-primary">
Choose Seller
</h2>
</div>
<div className="grid grid-cols-2 gap-4">
{sellers.map((seller) => (
<motion.div
key={seller.id}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => setSelectedSeller(seller.id)}
className={`
cursor-pointer p-5 rounded-lg border transition-all
${selectedSeller === seller.id
? 'bg-bg-card border-text-primary shadow-sm'
: 'bg-bg-subtle border-transparent hover:border-border-medium'
}
`}
>
<div className="text-3xl mb-3 text-center filter grayscale opacity-90">{seller.emoji}</div>
<h4 className="text-sm font-medium text-center text-text-primary">{seller.name}</h4>
</motion.div>
))}
</div>
</section>
</div>
{/* Start Button */}
{selectedBuyer && selectedSeller && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex justify-center pt-8"
>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleStartBattle}
className="px-10 py-4 bg-text-primary text-bg-app rounded-full font-serif font-medium text-xl shadow-lg hover:shadow-xl transition-all flex items-center gap-3 tracking-wide"
>
<Play className="w-5 h-5 fill-current" />
Begin Negotiation
</motion.button>
</motion.div>
)}
</motion.div>
)}
</motion.div>
)}
{/* Instructions when no negotiation */}
{state.status === "setup" && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-16"
>
<div className="text-6xl mb-4">🎮</div>
<h2 className="text-2xl font-bold text-gray-300 mb-4">
Ready to Start a Battle?
</h2>
<p className="text-gray-400 max-w-md mx-auto">
Open the chat sidebar and tell the Battle Master to start a negotiation!
Try: "Start a negotiation for a used car"
</p>
</motion.div>
{/* Battle Phase */}
{(state.status === "ready" || state.status === "negotiating" || state.status === "deal" || state.status === "no_deal") && (
<div className="max-w-5xl mx-auto">
{/* Scenario Header - Editorial Style */}
{state.scenario && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-16 bg-bg-card border border-border-light rounded-2xl p-8 shadow-sm"
>
<div className="inline-block px-3 py-1 mb-4 border border-border-medium rounded-full text-xs font-semibold tracking-wider text-text-tertiary uppercase">
Negotiation Item
</div>
<h2 className="text-4xl font-serif text-text-primary mb-2">
{state.scenario.title}
</h2>
<p className="text-xl text-text-secondary mb-6 font-light">{state.scenario.item}</p>
<div className="inline-flex items-center gap-2 text-lg border-t border-b border-border-light py-2 px-6">
<span className="text-text-tertiary uppercase text-xs font-bold tracking-widest">Asking Price</span>
<span className="text-text-primary font-serif font-bold text-xl">
${state.scenario.asking_price?.toLocaleString()}
</span>
</div>
</motion.div>
)}
{/* Comparison Display - Editorial Two-Column */}
{state.buyer && state.seller && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16 items-start">
{/* Buyer Column */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="text-right pr-6 border-r border-border-light"
>
<span className="text-xs font-bold tracking-widest text-text-tertiary uppercase mb-4 block">Buyer</span>
<div className="text-5xl mb-6 grayscale hover:grayscale-0 transition-all duration-500 opacity-90 hover:opacity-100">{state.buyer.emoji}</div>
<h3 className="text-3xl font-serif text-text-primary mb-2">{state.buyer.name}</h3>
<p className="font-sans text-text-secondary mb-1">Budget: <span className="font-semibold text-text-primary">${state.buyer.budget?.toLocaleString()}</span></p>
</motion.div>
{/* Seller Column */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="text-left pl-6"
>
<span className="text-xs font-bold tracking-widest text-text-tertiary uppercase mb-4 block">Seller</span>
<div className="text-5xl mb-6 grayscale hover:grayscale-0 transition-all duration-500 opacity-90 hover:opacity-100">{state.seller.emoji}</div>
<h3 className="text-3xl font-serif text-text-primary mb-2">{state.seller.name}</h3>
<p className="font-sans text-text-secondary mb-1">Minimum: <span className="font-semibold text-text-primary">${state.seller.minimum?.toLocaleString()}</span></p>
</motion.div>
</div>
)}
{/* Negotiation Chat */}
{state.rounds && state.rounds.length > 0 && (
<div className="max-w-3xl mx-auto mb-12">
<div className="flex items-center justify-center gap-2 mb-8">
<span className="h-px w-8 bg-border-medium"></span>
<span className="text-xs font-bold tracking-widest text-text-tertiary uppercase">Live Transcript</span>
<span className="h-px w-8 bg-border-medium"></span>
</div>
{/* Price Tracker */}
{state.buyer && state.seller && state.rounds.length > 0 && state.rounds[state.rounds.length - 1].offer_amount && (
<div className="mb-10 px-8">
<PriceTracker
currentOffer={state.rounds[state.rounds.length - 1].offer_amount || state.scenario?.asking_price || 0}
askingPrice={state.scenario?.asking_price || 0}
minimum={state.seller.minimum || 0}
budget={state.buyer.budget || 0}
/>
</div>
)}
{/* Chat Bubbles */}
<div className="space-y-6">
<AnimatePresence>
{state.rounds.map((round, index) => {
const isBuyer = round.type === "buyer_offer" || round.buyer_name;
return (
<OfferCard
key={`${round.type}-${round.round}-${index}`}
round={round}
isLatest={index === state.rounds.length - 1}
/>
);
})}
</AnimatePresence>
{/* Typing Indicator */}
{(state.status === "negotiating" || state.status === "ready") && (
<div className="flex justify-center py-4">
<TypingIndicator
emoji={state.current_round % 2 === 0 ? state.buyer?.emoji || "🔵" : state.seller?.emoji || "🔴"}
name={state.current_round % 2 === 0 ? state.buyer?.name || "Buyer" : state.seller?.name || "Seller"}
/>
</div>
)}
</div>
</div>
)}
{/* Deal Banner */}
{state.status === "deal" && state.final_price && state.scenario && (
<div className="max-w-2xl mx-auto mb-8">
<DealBanner
finalPrice={state.final_price}
askingPrice={state.scenario.asking_price}
/>
</div>
)}
{/* No Deal Banner */}
{state.status === "no_deal" && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="max-w-xl mx-auto mb-12 bg-bg-card border border-border-medium rounded-xl p-10 text-center shadow-lg"
>
<div className="mb-6 flex justify-center">
<div className="h-16 w-16 bg-bg-subtle rounded-full flex items-center justify-center">
<XCircle className="w-8 h-8 text-text-tertiary" />
</div>
</div>
<h2 className="text-3xl font-serif text-text-primary mb-3">Negotiation Ended</h2>
<p className="text-text-secondary font-sans leading-relaxed">No agreement could be reached between the parties.</p>
</motion.div>
)}
{/* Reset Button */}
{(state.status === "deal" || state.status === "no_deal") && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex justify-center mt-12 mb-12"
>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleReset}
className="px-8 py-3 bg-text-primary text-bg-app rounded-full font-serif font-medium text-lg shadow-lg hover:shadow-xl transition-all flex items-center gap-3 tracking-wide"
>
<RotateCcw className="w-5 h-5" />
Start New Negotiation
</motion.button>
</motion.div>
)}
</div>
)}
</div>
{/* CopilotKit Sidebar */}
<CopilotSidebar
clickOutsideToClose={false}
defaultOpen={true}
labels={{
title: "🎮 Battle Master",
initial: `👋 Welcome to the AI Negotiation Battle Simulator!
I'm your Battle Master. I'll orchestrate an epic negotiation between AI agents.
**Try saying:**
- "Show me the available scenarios"
- "Start a negotiation for a used car"
- "Use Desperate Dan as the buyer and Shark Steve as the seller"
Ready to watch some AI agents battle it out? 🔥`
}}
/>
</main>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB