<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Argentor — Agent Playground</title>
<style>
:root {
--bg-primary: #0d0d1a;
--bg-secondary: #1a1a2e;
--bg-card: #16213e;
--bg-card-hover: #1a2745;
--bg-input: #0f1b30;
--accent: #0f3460;
--accent-light: #1a4a8a;
--accent-bright: #2979ff;
--danger: #e94560;
--danger-hover: #ff5a75;
--success: #00c853;
--success-dim: #1b5e20;
--warning: #ffc107;
--info: #29b6f6;
--text-primary: #e0e0e0;
--text-secondary: #9e9e9e;
--text-muted: #616161;
--border: #2a2a4a;
--border-light: #3a3a5a;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
--radius: 8px;
--radius-sm: 4px;
--radius-lg: 12px;
--radius-xl: 16px;
--transition: 0.2s ease;
--font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
--font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--user-bubble: #1565c0;
--agent-bubble: #1b5e20;
--sidebar-width: 280px;
--trace-width: 320px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow: hidden;
height: 100vh;
}
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 52px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 20px;
z-index: 100;
gap: 16px;
}
.topnav-brand {
font-size: 1.1rem;
font-weight: 700;
color: var(--accent-bright);
letter-spacing: 0.5px;
}
.topnav-brand span {
color: var(--text-secondary);
font-weight: 400;
font-size: 0.9rem;
margin-left: 8px;
}
.topnav-actions {
margin-left: auto;
display: flex;
gap: 8px;
align-items: center;
}
.topnav-status {
font-size: 0.75rem;
color: var(--text-muted);
padding: 4px 10px;
background: var(--bg-card);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
}
.topnav-status.connected {
color: var(--success);
border-color: var(--success-dim);
}
.layout {
display: flex;
height: calc(100vh - 52px);
margin-top: 52px;
}
.sidebar-left {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 16px;
gap: 16px;
}
.sidebar-section {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px;
}
.sidebar-section h3 {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-secondary);
margin-bottom: 10px;
}
.form-group {
margin-bottom: 10px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: block;
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 4px;
font-weight: 500;
}
.form-group select,
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px 10px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 0.85rem;
font-family: var(--font-sans);
transition: border-color var(--transition);
outline: none;
}
.form-group select:focus,
.form-group input:focus,
.form-group textarea:focus {
border-color: var(--accent-bright);
}
.form-group select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239e9e9e' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 30px;
}
.form-group textarea {
resize: vertical;
min-height: 60px;
font-family: var(--font-mono);
font-size: 0.78rem;
line-height: 1.4;
}
.system-prompt-preview {
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 10px;
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--text-muted);
max-height: 100px;
overflow-y: auto;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
}
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
background: var(--bg-primary);
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
min-height: 46px;
}
.chat-header-info {
font-size: 0.85rem;
color: var(--text-secondary);
}
.chat-header-actions {
display: flex;
gap: 6px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
scroll-behavior: smooth;
}
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
.message {
display: flex;
flex-direction: column;
max-width: 75%;
animation: fadeInUp 0.25s ease;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
align-self: flex-end;
}
.message.agent {
align-self: flex-start;
}
.message-sender {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
padding: 0 4px;
}
.message.user .message-sender {
color: var(--info);
text-align: right;
}
.message.agent .message-sender {
color: var(--success);
}
.message-bubble {
padding: 10px 14px;
border-radius: var(--radius-lg);
font-size: 0.9rem;
line-height: 1.5;
word-break: break-word;
white-space: pre-wrap;
box-shadow: var(--shadow-sm);
}
.message.user .message-bubble {
background: var(--user-bubble);
border-bottom-right-radius: var(--radius-sm);
color: #fff;
}
.message.agent .message-bubble {
background: var(--agent-bubble);
border-bottom-left-radius: var(--radius-sm);
color: #e0e0e0;
}
.message-bubble code {
font-family: var(--font-mono);
background: rgba(0, 0, 0, 0.3);
padding: 1px 5px;
border-radius: 3px;
font-size: 0.82rem;
}
.message-bubble pre {
background: rgba(0, 0, 0, 0.35);
padding: 10px;
border-radius: var(--radius-sm);
margin: 8px 0;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 0.8rem;
line-height: 1.4;
}
.message-meta {
font-size: 0.68rem;
color: var(--text-muted);
margin-top: 4px;
padding: 0 4px;
}
.message.user .message-meta {
text-align: right;
}
.tool-call-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--accent);
border: 1px solid var(--accent-light);
border-radius: var(--radius-sm);
font-size: 0.75rem;
color: var(--info);
margin: 6px 0;
font-family: var(--font-mono);
}
.tool-call-indicator .tool-icon {
font-size: 0.85rem;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 12px 16px;
align-items: center;
}
.typing-dot {
width: 7px;
height: 7px;
background: var(--text-muted);
border-radius: 50%;
animation: typingBounce 1.2s infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingBounce {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-6px); opacity: 1; }
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--accent-bright);
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.chat-input-area {
padding: 12px 20px;
border-top: 1px solid var(--border);
background: var(--bg-secondary);
display: flex;
gap: 10px;
align-items: flex-end;
}
.chat-input-wrapper {
flex: 1;
position: relative;
}
#chat-input {
width: 100%;
padding: 10px 14px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 0.9rem;
font-family: var(--font-sans);
outline: none;
resize: none;
min-height: 42px;
max-height: 120px;
line-height: 1.4;
transition: border-color var(--transition);
}
#chat-input:focus {
border-color: var(--accent-bright);
}
#chat-input::placeholder {
color: var(--text-muted);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-card);
color: var(--text-secondary);
font-size: 0.8rem;
font-family: var(--font-sans);
cursor: pointer;
transition: all var(--transition);
white-space: nowrap;
}
.btn:hover {
background: var(--bg-card-hover);
color: var(--text-primary);
border-color: var(--border-light);
}
.btn:active {
transform: scale(0.97);
}
.btn-primary {
background: var(--accent-bright);
color: #fff;
border-color: var(--accent-bright);
}
.btn-primary:hover {
background: #448aff;
border-color: #448aff;
color: #fff;
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-danger {
color: var(--danger);
border-color: rgba(233, 69, 96, 0.3);
}
.btn-danger:hover {
background: rgba(233, 69, 96, 0.15);
border-color: var(--danger);
color: var(--danger-hover);
}
.btn-sm {
padding: 5px 10px;
font-size: 0.75rem;
}
#send-btn {
min-width: 80px;
height: 42px;
}
.sidebar-right {
width: var(--trace-width);
min-width: var(--trace-width);
background: var(--bg-secondary);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.trace-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: var(--bg-card);
}
.trace-header h3 {
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-secondary);
}
.trace-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.trace-stat {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 10px;
text-align: center;
}
.trace-stat-value {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
font-family: var(--font-mono);
}
.trace-stat-label {
font-size: 0.65rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 2px;
}
.trace-timeline {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 0;
}
.trace-timeline::-webkit-scrollbar {
width: 5px;
}
.trace-timeline::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
.trace-step {
position: relative;
padding: 10px 12px 10px 28px;
border-left: 2px solid var(--border);
margin-left: 6px;
font-size: 0.78rem;
animation: fadeInUp 0.2s ease;
}
.trace-step::before {
content: '';
position: absolute;
left: -6px;
top: 14px;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--border-light);
background: var(--bg-secondary);
}
.trace-step.thinking::before {
border-color: var(--warning);
background: rgba(255, 193, 7, 0.2);
}
.trace-step.tool_call::before {
border-color: var(--info);
background: rgba(41, 182, 246, 0.2);
}
.trace-step.tool_result::before {
border-color: var(--accent-bright);
background: rgba(41, 121, 255, 0.2);
}
.trace-step.output::before {
border-color: var(--success);
background: rgba(0, 200, 83, 0.2);
}
.trace-step.error::before {
border-color: var(--danger);
background: rgba(233, 69, 96, 0.2);
}
.trace-step-type {
font-weight: 600;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.trace-step.thinking .trace-step-type { color: var(--warning); }
.trace-step.tool_call .trace-step-type { color: var(--info); }
.trace-step.tool_result .trace-step-type { color: var(--accent-bright); }
.trace-step.output .trace-step-type { color: var(--success); }
.trace-step.error .trace-step-type { color: var(--danger); }
.trace-step-content {
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 0.72rem;
line-height: 1.4;
word-break: break-word;
white-space: pre-wrap;
max-height: 80px;
overflow-y: auto;
}
.trace-step-time {
font-size: 0.65rem;
color: var(--text-muted);
margin-top: 2px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
text-align: center;
padding: 40px;
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 12px;
opacity: 0.4;
}
.empty-state h3 {
font-size: 1rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 6px;
}
.empty-state p {
font-size: 0.82rem;
max-width: 280px;
line-height: 1.5;
}
.error-banner {
display: none;
padding: 10px 16px;
background: rgba(233, 69, 96, 0.12);
border: 1px solid rgba(233, 69, 96, 0.3);
border-radius: var(--radius-sm);
color: var(--danger);
font-size: 0.82rem;
margin: 10px 20px 0;
align-items: center;
gap: 8px;
}
.error-banner.visible {
display: flex;
}
.error-banner-close {
margin-left: auto;
cursor: pointer;
background: none;
border: none;
color: var(--danger);
font-size: 1.1rem;
padding: 0 4px;
}
@media (max-width: 1200px) {
.sidebar-right {
width: 260px;
min-width: 260px;
}
}
@media (max-width: 960px) {
.sidebar-left {
width: 220px;
min-width: 220px;
}
.sidebar-right {
width: 220px;
min-width: 220px;
}
}
@media (max-width: 768px) {
.layout {
flex-direction: column;
height: auto;
min-height: calc(100vh - 52px);
}
.sidebar-left {
width: 100%;
min-width: 100%;
max-height: 200px;
overflow-y: auto;
border-right: none;
border-bottom: 1px solid var(--border);
flex-direction: row;
flex-wrap: wrap;
padding: 10px;
gap: 8px;
}
.sidebar-left .sidebar-section {
flex: 1;
min-width: 200px;
}
.chat-panel {
min-height: 50vh;
}
.sidebar-right {
width: 100%;
min-width: 100%;
max-height: 250px;
border-left: none;
border-top: 1px solid var(--border);
}
.message {
max-width: 90%;
}
}
@media (max-width: 480px) {
.topnav-brand span {
display: none;
}
.sidebar-left {
max-height: 160px;
}
}
</style>
</head>
<body>
<nav class="topnav">
<div class="topnav-brand">
ARGENTOR <span>Agent Playground</span>
</div>
<div class="topnav-actions">
<span id="connection-status" class="topnav-status">Disconnected</span>
<a href="/dashboard" class="btn btn-sm">Dashboard</a>
</div>
</nav>
<div class="layout">
<aside class="sidebar-left">
<div class="sidebar-section">
<h3>Agent Role</h3>
<div class="form-group">
<label for="agent-selector">Select Agent</label>
<select id="agent-selector">
<option value="sales_qualifier">Sales Qualifier</option>
<option value="outreach_composer">Outreach Composer</option>
<option value="support_responder">Support Responder</option>
<option value="ticket_router">Ticket Router</option>
</select>
</div>
<div class="form-group">
<label>System Prompt</label>
<div id="system-prompt-preview" class="system-prompt-preview">Loading...</div>
</div>
</div>
<div class="sidebar-section">
<h3>Persona</h3>
<div class="form-group">
<label for="persona-name">Name</label>
<input type="text" id="persona-name" placeholder="Agent name" value="Argentor">
</div>
<div class="form-group">
<label for="persona-tone">Tone</label>
<select id="persona-tone">
<option value="professional">Professional</option>
<option value="friendly">Friendly</option>
<option value="concise">Concise</option>
<option value="technical">Technical</option>
<option value="casual">Casual</option>
</select>
</div>
</div>
<div class="sidebar-section">
<h3>Routing</h3>
<div class="form-group">
<label for="routing-hint">Model Strategy</label>
<select id="routing-hint">
<option value="fast_cheap">Fast & Cheap</option>
<option value="balanced" selected>Balanced</option>
<option value="quality_max">Quality Max</option>
</select>
</div>
<div class="form-group">
<label for="tenant-id">Tenant ID</label>
<input type="text" id="tenant-id" placeholder="Optional tenant ID">
</div>
</div>
</aside>
<main class="chat-panel">
<div class="chat-header">
<div class="chat-header-info" id="chat-header-info">
No messages yet
</div>
<div class="chat-header-actions">
<button class="btn btn-sm" id="export-btn" title="Export conversation as JSON">Export</button>
<button class="btn btn-sm btn-danger" id="clear-btn" title="Clear chat history">Clear</button>
</div>
</div>
<div id="error-banner" class="error-banner">
<span id="error-message">Error</span>
<button class="error-banner-close" id="error-close">×</button>
</div>
<div class="chat-messages" id="chat-container">
<div class="empty-state" id="empty-state">
<div class="empty-state-icon">⚙</div>
<h3>Agent Playground</h3>
<p>Select an agent role, configure the persona, and start chatting to test agent behavior in real time.</p>
</div>
</div>
<div class="chat-input-area">
<div class="chat-input-wrapper">
<textarea id="chat-input" placeholder="Type a message and press Enter..." rows="1"></textarea>
</div>
<button class="btn btn-primary" id="send-btn">Send</button>
</div>
</main>
<aside class="sidebar-right" id="trace-panel">
<div class="trace-header">
<h3>Execution Trace</h3>
<button class="btn btn-sm" id="clear-trace-btn">Clear</button>
</div>
<div class="trace-stats">
<div class="trace-stat">
<div class="trace-stat-value" id="total-tokens">0</div>
<div class="trace-stat-label">Tokens</div>
</div>
<div class="trace-stat">
<div class="trace-stat-value" id="total-cost">$0.00</div>
<div class="trace-stat-label">Est. Cost</div>
</div>
<div class="trace-stat">
<div class="trace-stat-value" id="total-duration">0ms</div>
<div class="trace-stat-label">Duration</div>
</div>
<div class="trace-stat">
<div class="trace-stat-value" id="message-count">0</div>
<div class="trace-stat-label">Messages</div>
</div>
</div>
<div class="trace-timeline" id="trace-timeline">
<div class="empty-state" id="trace-empty">
<div class="empty-state-icon">🔍</div>
<h3>No Trace Data</h3>
<p>Send a message to see the step-by-step execution trace here.</p>
</div>
</div>
</aside>
</div>
<script>
(function() {
'use strict';
const STATE_KEY = 'argentor_playground_state';
const SYSTEM_PROMPTS = {
sales_qualifier:
'You are a Sales Qualifier agent. Your goal is to assess incoming leads, ' +
'qualify them based on BANT criteria (Budget, Authority, Need, Timeline), ' +
'and provide a qualification score with reasoning. Ask focused questions ' +
'to determine if the lead is a good fit for the product.',
outreach_composer:
'You are an Outreach Composer agent. You craft personalized outreach ' +
'messages (emails, LinkedIn messages, follow-ups) based on the target ' +
'persona and context. Adapt your tone and content to maximize engagement ' +
'and response rates.',
support_responder:
'You are a Support Responder agent. You help resolve customer issues by ' +
'providing accurate, helpful responses. Use available tools to look up ' +
'documentation, check account status, and suggest solutions. Escalate ' +
'complex issues when necessary.',
ticket_router:
'You are a Ticket Router agent. You analyze incoming support tickets, ' +
'categorize them by type (bug, feature request, question, billing), ' +
'assess priority (P0-P3), and route them to the appropriate team. ' +
'Provide a brief summary of the issue.'
};
const COST_PER_1K = {
fast_cheap: { input: 0.00015, output: 0.0006 },
balanced: { input: 0.003, output: 0.015 },
quality_max: { input: 0.015, output: 0.075 }
};
let state = {
messages: [],
traceSteps: [],
totalTokens: 0,
totalCost: 0,
messageCount: 0,
isLoading: false
};
const elements = {
agentSelector: document.getElementById('agent-selector'),
systemPrompt: document.getElementById('system-prompt-preview'),
personaName: document.getElementById('persona-name'),
personaTone: document.getElementById('persona-tone'),
routingHint: document.getElementById('routing-hint'),
tenantId: document.getElementById('tenant-id'),
chatContainer: document.getElementById('chat-container'),
chatInput: document.getElementById('chat-input'),
sendBtn: document.getElementById('send-btn'),
clearBtn: document.getElementById('clear-btn'),
exportBtn: document.getElementById('export-btn'),
clearTraceBtn: document.getElementById('clear-trace-btn'),
errorBanner: document.getElementById('error-banner'),
errorMessage: document.getElementById('error-message'),
errorClose: document.getElementById('error-close'),
emptyState: document.getElementById('empty-state'),
traceEmpty: document.getElementById('trace-empty'),
traceTimeline: document.getElementById('trace-timeline'),
totalTokens: document.getElementById('total-tokens'),
totalCost: document.getElementById('total-cost'),
totalDuration: document.getElementById('total-duration'),
messageCount: document.getElementById('message-count'),
chatHeaderInfo: document.getElementById('chat-header-info'),
connectionStatus: document.getElementById('connection-status')
};
function init() {
loadState();
updateSystemPrompt();
renderMessages();
renderTrace();
updateStats();
checkConnection();
bindEvents();
}
function bindEvents() {
elements.agentSelector.addEventListener('change', updateSystemPrompt);
elements.sendBtn.addEventListener('click', sendMessage);
elements.clearBtn.addEventListener('click', clearChat);
elements.exportBtn.addEventListener('click', exportConversation);
elements.clearTraceBtn.addEventListener('click', clearTrace);
elements.errorClose.addEventListener('click', hideError);
elements.chatInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
elements.chatInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
}
function loadState() {
try {
var saved = localStorage.getItem(STATE_KEY);
if (saved) {
var parsed = JSON.parse(saved);
state.messages = parsed.messages || [];
state.traceSteps = parsed.traceSteps || [];
state.totalTokens = parsed.totalTokens || 0;
state.totalCost = parsed.totalCost || 0;
state.messageCount = parsed.messageCount || 0;
}
} catch (e) {
console.warn('Failed to load playground state:', e);
}
}
function saveState() {
try {
localStorage.setItem(STATE_KEY, JSON.stringify({
messages: state.messages,
traceSteps: state.traceSteps,
totalTokens: state.totalTokens,
totalCost: state.totalCost,
messageCount: state.messageCount
}));
} catch (e) {
console.warn('Failed to save playground state:', e);
}
}
function updateSystemPrompt() {
var role = elements.agentSelector.value;
elements.systemPrompt.textContent = SYSTEM_PROMPTS[role] || 'Unknown role';
}
function checkConnection() {
fetch('/health')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data && data.status === 'ok') {
elements.connectionStatus.textContent = 'Connected';
elements.connectionStatus.classList.add('connected');
} else {
setDisconnected();
}
})
.catch(function() {
setDisconnected();
});
}
function setDisconnected() {
elements.connectionStatus.textContent = 'Disconnected';
elements.connectionStatus.classList.remove('connected');
}
function sendMessage() {
var text = elements.chatInput.value.trim();
if (!text || state.isLoading) return;
if (elements.emptyState) {
elements.emptyState.style.display = 'none';
}
var userMsg = {
role: 'user',
content: text,
timestamp: new Date().toISOString()
};
state.messages.push(userMsg);
state.messageCount++;
appendMessageBubble(userMsg);
elements.chatInput.value = '';
elements.chatInput.style.height = 'auto';
state.isLoading = true;
elements.sendBtn.disabled = true;
var typingEl = showTypingIndicator();
var startTime = Date.now();
addTraceStep('thinking', 'Processing user input...', startTime);
var role = elements.agentSelector.value;
var requestBody = {
task: text,
role: role,
persona: {
name: elements.personaName.value || 'Argentor',
tone: elements.personaTone.value
},
routing_hint: elements.routingHint.value,
system_prompt: SYSTEM_PROMPTS[role],
context: {
conversation_history: state.messages.slice(-10)
}
};
var tenantId = elements.tenantId.value.trim();
if (tenantId) {
requestBody.tenant_id = tenantId;
}
addTraceStep('tool_call', 'POST /api/v1/agent/run-task\n' + JSON.stringify(requestBody, null, 2).slice(0, 300) + '...', Date.now());
fetch('/api/v1/agent/run-task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
})
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
return response.json();
})
.then(function(data) {
var endTime = Date.now();
var duration = endTime - startTime;
removeTypingIndicator(typingEl);
addTraceStep('tool_result', 'Response received (' + duration + 'ms)', endTime);
var agentContent = data.response || data.result || data.output || JSON.stringify(data);
var agentMsg = {
role: 'agent',
content: agentContent,
timestamp: new Date().toISOString(),
duration: duration
};
state.messages.push(agentMsg);
state.messageCount++;
appendMessageBubbleAnimated(agentMsg);
var inputTokens = Math.ceil(text.length / 4);
var outputTokens = Math.ceil(agentContent.length / 4);
var totalTurnTokens = inputTokens + outputTokens;
var routing = elements.routingHint.value;
var costRate = COST_PER_1K[routing] || COST_PER_1K.balanced;
var turnCost = (inputTokens / 1000) * costRate.input + (outputTokens / 1000) * costRate.output;
state.totalTokens += totalTurnTokens;
state.totalCost += turnCost;
addTraceStep('output', 'Tokens: ' + totalTurnTokens + ' (in: ' + inputTokens + ', out: ' + outputTokens + ')\nCost: $' + turnCost.toFixed(4) + '\nDuration: ' + duration + 'ms', endTime);
if (data.tool_calls && Array.isArray(data.tool_calls)) {
data.tool_calls.forEach(function(tc) {
addTraceStep('tool_call', tc.name + '(' + JSON.stringify(tc.arguments || {}) + ')', endTime);
if (tc.result) {
addTraceStep('tool_result', JSON.stringify(tc.result).slice(0, 200), endTime);
}
});
}
updateStats();
elements.totalDuration.textContent = duration + 'ms';
saveState();
hideError();
})
.catch(function(err) {
var endTime = Date.now();
removeTypingIndicator(typingEl);
addTraceStep('error', err.message, endTime);
var errorMsg = {
role: 'agent',
content: 'Error: ' + err.message + '\n\nMake sure the Argentor gateway is running and the /api/v1/agent/run-task endpoint is available.',
timestamp: new Date().toISOString(),
isError: true
};
state.messages.push(errorMsg);
appendMessageBubble(errorMsg);
showError(err.message);
updateStats();
saveState();
})
.finally(function() {
state.isLoading = false;
elements.sendBtn.disabled = false;
elements.chatInput.focus();
});
}
function appendMessageBubble(msg) {
var div = document.createElement('div');
div.className = 'message ' + msg.role;
var sender = document.createElement('div');
sender.className = 'message-sender';
sender.textContent = msg.role === 'user' ? 'You' : (elements.personaName.value || 'Agent');
var bubble = document.createElement('div');
bubble.className = 'message-bubble';
if (msg.isError) {
bubble.style.background = 'rgba(233, 69, 96, 0.2)';
bubble.style.borderColor = 'rgba(233, 69, 96, 0.3)';
}
bubble.textContent = msg.content;
var meta = document.createElement('div');
meta.className = 'message-meta';
var timeStr = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '';
meta.textContent = timeStr;
if (msg.duration) {
meta.textContent += ' (' + msg.duration + 'ms)';
}
div.appendChild(sender);
div.appendChild(bubble);
div.appendChild(meta);
elements.chatContainer.appendChild(div);
scrollToBottom();
updateChatHeader();
}
function appendMessageBubbleAnimated(msg) {
var div = document.createElement('div');
div.className = 'message ' + msg.role;
var sender = document.createElement('div');
sender.className = 'message-sender';
sender.textContent = elements.personaName.value || 'Agent';
var bubble = document.createElement('div');
bubble.className = 'message-bubble';
var meta = document.createElement('div');
meta.className = 'message-meta';
var timeStr = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '';
meta.textContent = timeStr;
if (msg.duration) {
meta.textContent += ' (' + msg.duration + 'ms)';
}
div.appendChild(sender);
div.appendChild(bubble);
div.appendChild(meta);
elements.chatContainer.appendChild(div);
var fullText = msg.content;
var idx = 0;
var chunkSize = 3;
function typeNextChunk() {
if (idx < fullText.length) {
var end = Math.min(idx + chunkSize, fullText.length);
bubble.textContent = fullText.slice(0, end);
idx = end;
scrollToBottom();
requestAnimationFrame(typeNextChunk);
}
}
requestAnimationFrame(typeNextChunk);
updateChatHeader();
}
function showTypingIndicator() {
var div = document.createElement('div');
div.className = 'message agent';
div.id = 'typing-indicator';
var sender = document.createElement('div');
sender.className = 'message-sender';
sender.textContent = elements.personaName.value || 'Agent';
var bubble = document.createElement('div');
bubble.className = 'message-bubble';
var typing = document.createElement('div');
typing.className = 'typing-indicator';
for (var i = 0; i < 3; i++) {
var dot = document.createElement('div');
dot.className = 'typing-dot';
typing.appendChild(dot);
}
bubble.appendChild(typing);
div.appendChild(sender);
div.appendChild(bubble);
elements.chatContainer.appendChild(div);
scrollToBottom();
return div;
}
function removeTypingIndicator(el) {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
}
function renderMessages() {
var children = Array.from(elements.chatContainer.children);
children.forEach(function(child) {
if (child.id !== 'empty-state') {
elements.chatContainer.removeChild(child);
}
});
if (state.messages.length > 0 && elements.emptyState) {
elements.emptyState.style.display = 'none';
}
state.messages.forEach(function(msg) {
appendMessageBubble(msg);
});
}
function scrollToBottom() {
elements.chatContainer.scrollTop = elements.chatContainer.scrollHeight;
}
function updateChatHeader() {
var count = state.messages.length;
if (count === 0) {
elements.chatHeaderInfo.textContent = 'No messages yet';
} else {
var role = elements.agentSelector.options[elements.agentSelector.selectedIndex].text;
elements.chatHeaderInfo.textContent = count + ' message' + (count !== 1 ? 's' : '') + ' with ' + role;
}
}
function addTraceStep(type, content, timestamp) {
var step = {
type: type,
content: content,
timestamp: timestamp || Date.now()
};
state.traceSteps.push(step);
appendTraceStepEl(step);
}
function appendTraceStepEl(step) {
if (elements.traceEmpty) {
elements.traceEmpty.style.display = 'none';
}
var div = document.createElement('div');
div.className = 'trace-step ' + step.type;
var typeEl = document.createElement('div');
typeEl.className = 'trace-step-type';
typeEl.textContent = step.type.replace('_', ' ');
var contentEl = document.createElement('div');
contentEl.className = 'trace-step-content';
contentEl.textContent = step.content;
var timeEl = document.createElement('div');
timeEl.className = 'trace-step-time';
timeEl.textContent = new Date(step.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 });
div.appendChild(typeEl);
div.appendChild(contentEl);
div.appendChild(timeEl);
elements.traceTimeline.appendChild(div);
elements.traceTimeline.scrollTop = elements.traceTimeline.scrollHeight;
}
function renderTrace() {
var children = Array.from(elements.traceTimeline.children);
children.forEach(function(child) {
if (child.id !== 'trace-empty') {
elements.traceTimeline.removeChild(child);
}
});
if (state.traceSteps.length > 0 && elements.traceEmpty) {
elements.traceEmpty.style.display = 'none';
}
state.traceSteps.forEach(function(step) {
appendTraceStepEl(step);
});
}
function updateStats() {
elements.totalTokens.textContent = formatNumber(state.totalTokens);
elements.totalCost.textContent = '$' + state.totalCost.toFixed(4);
elements.messageCount.textContent = state.messageCount;
}
function formatNumber(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return n.toString();
}
function showError(msg) {
elements.errorMessage.textContent = msg;
elements.errorBanner.classList.add('visible');
}
function hideError() {
elements.errorBanner.classList.remove('visible');
}
function clearChat() {
state.messages = [];
state.messageCount = 0;
state.totalTokens = 0;
state.totalCost = 0;
var children = Array.from(elements.chatContainer.children);
children.forEach(function(child) {
if (child.id !== 'empty-state') {
elements.chatContainer.removeChild(child);
}
});
if (elements.emptyState) {
elements.emptyState.style.display = '';
}
updateStats();
updateChatHeader();
clearTrace();
saveState();
hideError();
}
function clearTrace() {
state.traceSteps = [];
var children = Array.from(elements.traceTimeline.children);
children.forEach(function(child) {
if (child.id !== 'trace-empty') {
elements.traceTimeline.removeChild(child);
}
});
if (elements.traceEmpty) {
elements.traceEmpty.style.display = '';
}
elements.totalDuration.textContent = '0ms';
saveState();
}
function exportConversation() {
var exportData = {
exported_at: new Date().toISOString(),
agent_role: elements.agentSelector.value,
persona: {
name: elements.personaName.value,
tone: elements.personaTone.value
},
routing_hint: elements.routingHint.value,
tenant_id: elements.tenantId.value || null,
stats: {
total_tokens: state.totalTokens,
total_cost: state.totalCost,
message_count: state.messageCount
},
messages: state.messages,
trace: state.traceSteps
};
var json = JSON.stringify(exportData, null, 2);
var blob = new Blob([json], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'argentor-conversation-' + new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-') + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
</body>
</html>