orgOS / ui /index.html
srishtichugh's picture
animate ui
c06d0c3
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OrgOS β€” Enterprise RL Environment</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Mono:wght@400;500&display=swap"
rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--font-sans: 'DM Sans', sans-serif;
--font-mono: 'DM Mono', monospace;
--bg: #F5F6F8;
--surface: #FFFFFF;
--border: #E3E6EA;
--text-1: #111827;
--text-2: #6B7280;
--text-3: #9CA3AF;
--green: #10B981;
--red: #EF4444;
--amber: #F59E0B;
--jira: #0052CC;
--jira-light: #E9F0FF;
--jira-mid: #B3C8F0;
--zendesk: #03363D;
--zendesk-light: #E6F3F4;
--zendesk-mid: #B2D8DB;
--sf: #00A1E0;
--sf-light: #E5F6FD;
--sf-mid: #B3E3F8;
--wd: #FF6B35;
--wd-light: #FFF0EB;
--wd-mid: #FFD0C0;
}
body {
font-family: var(--font-sans);
background: var(--bg);
color: var(--text-1);
font-size: 13px;
line-height: 1.5;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-3);
}
/* ── Animations ── */
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.35;
}
}
@keyframes checkmark {
from {
transform: scale(0) rotate(-20deg);
opacity: 0;
}
to {
transform: scale(1) rotate(0);
opacity: 1;
}
}
@keyframes score-flash {
0%,
100% {
background: transparent;
}
40% {
background: rgba(16, 185, 129, 0.12);
}
}
@keyframes record-focus {
0% {
transform: scale(0.985);
}
45% {
transform: scale(1.018);
}
100% {
transform: scale(1);
}
}
@keyframes action-sweep {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(250%);
}
}
@keyframes banner-in {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes tab-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.fade-up {
animation: fadeUp 0.25s ease forwards;
}
.live-dot {
animation: pulse-dot 1.4s ease-in-out infinite;
}
.step-check {
animation: checkmark 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.score-flash {
animation: score-flash 0.5s ease;
}
.record-focus {
animation: record-focus 0.75s ease;
}
[x-cloak] {
display: none !important;
}
/* ── Topbar ── */
.topbar {
background: var(--surface);
border-bottom: 1px solid var(--border);
height: 52px;
display: flex;
align-items: center;
padding: 0 20px;
gap: 14px;
position: sticky;
top: 0;
z-index: 100;
}
.logo-mark {
width: 28px;
height: 28px;
background: linear-gradient(135deg, #111827 60%, #374151);
border-radius: 7px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
font-size: 13px;
letter-spacing: -0.5px;
flex-shrink: 0;
}
.logo-text {
font-weight: 600;
color: var(--text-1);
font-size: 14px;
letter-spacing: -0.3px;
}
.logo-sub {
font-size: 11px;
color: var(--text-3);
}
.divider {
width: 1px;
height: 20px;
background: var(--border);
flex-shrink: 0;
}
.wf-select {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 7px;
padding: 5px 28px 5px 10px;
font-family: var(--font-sans);
font-size: 12px;
color: var(--text-1);
font-weight: 500;
outline: none;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%236B7280' viewBox='0 0 16 16'%3E%3Cpath d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 9px center;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 7px;
font-family: var(--font-sans);
font-size: 12px;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.15s;
}
.btn-primary {
background: var(--text-1);
color: #fff;
}
.btn-primary:hover {
background: #1F2937;
}
.btn-danger {
background: #FEE2E2;
color: var(--red);
}
.btn-danger:hover {
background: #FECACA;
}
.btn-ghost {
background: var(--bg);
color: var(--text-2);
border: 1px solid var(--border);
}
.btn-ghost:hover {
background: var(--border);
color: var(--text-1);
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 20px;
font-size: 11px;
font-weight: 500;
}
/* ── Layout ── */
.layout {
display: grid;
grid-template-columns: 260px 1fr 240px;
height: calc(100vh - 52px);
overflow: hidden;
}
/* ── Left sidebar ── */
.sidebar-left {
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-section {
padding: 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.section-label {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-3);
margin-bottom: 10px;
}
.step-row {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 6px 0;
border-bottom: 1px solid #F9FAFB;
}
.step-row:last-child {
border-bottom: none;
}
.step-num {
width: 20px;
height: 20px;
border-radius: 50%;
border: 1.5px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 500;
color: var(--text-3);
flex-shrink: 0;
transition: all 0.2s;
}
.step-num.done {
background: var(--green);
border-color: var(--green);
color: #fff;
}
.step-desc {
font-size: 12px;
color: var(--text-2);
line-height: 1.4;
}
.step-desc.done {
color: var(--green);
font-weight: 500;
}
.drift-pill {
display: flex;
align-items: center;
gap: 6px;
background: var(--bg);
border-radius: 6px;
padding: 5px 8px;
margin-bottom: 4px;
}
.drift-old {
font-family: var(--font-mono);
font-size: 11px;
color: var(--red);
text-decoration: line-through;
}
.drift-arr {
color: var(--text-3);
font-size: 11px;
}
.drift-new {
font-family: var(--font-mono);
font-size: 11px;
color: var(--green);
font-weight: 500;
}
.drift-app {
font-size: 10px;
color: var(--text-3);
margin-left: auto;
}
.rule-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.rule-key {
font-size: 11px;
color: var(--text-2);
}
.rule-val {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
color: var(--text-1);
}
/* ── Center panel ── */
.center-panel {
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
}
/* ── Action banner (cinematic) ── */
.action-banner {
margin: 10px 14px 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 9px 14px;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
align-items: center;
position: relative;
overflow: hidden;
box-shadow: 0 4px 16px rgba(17, 24, 39, 0.07);
animation: banner-in 0.2s ease;
flex-shrink: 0;
}
.action-banner::after {
content: '';
position: absolute;
top: 0;
left: -60%;
width: 40%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.65), transparent);
animation: action-sweep 1.4s ease-in-out infinite;
pointer-events: none;
}
.banner-dot {
width: 36px;
height: 36px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 11px;
position: relative;
z-index: 1;
flex-shrink: 0;
}
.banner-copy {
position: relative;
z-index: 1;
min-width: 0;
}
.banner-eyebrow {
font-size: 10px;
color: var(--text-3);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 2px;
}
.banner-title {
font-size: 13px;
font-weight: 600;
color: var(--text-1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.banner-sub {
font-size: 11px;
color: var(--text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.banner-step {
position: relative;
z-index: 1;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-3);
background: var(--bg);
border-radius: 99px;
padding: 3px 9px;
flex-shrink: 0;
}
.banner-jira {
border-color: rgba(0, 82, 204, 0.25);
}
.banner-zendesk {
border-color: rgba(3, 54, 61, 0.25);
}
.banner-salesforce {
border-color: rgba(0, 161, 224, 0.25);
}
.banner-workday {
border-color: rgba(255, 107, 53, 0.25);
}
/* ── App tabs ── */
.app-tabs {
display: flex;
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 16px;
gap: 2px;
flex-shrink: 0;
}
.app-tab {
display: flex;
align-items: center;
gap: 7px;
padding: 12px 14px 10px;
border-bottom: 2px solid transparent;
cursor: pointer;
font-size: 12px;
font-weight: 500;
color: var(--text-2);
transition: all 0.15s;
white-space: nowrap;
}
.app-tab:hover {
color: var(--text-1);
}
.app-tab.active {
color: var(--text-1);
border-bottom-color: var(--text-1);
}
.app-tab.acting {
animation: tab-pulse 0.8s ease-in-out 2;
}
.app-tab .app-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.tab-count {
background: var(--bg);
border-radius: 10px;
padding: 1px 6px;
font-size: 10px;
font-weight: 600;
color: var(--text-3);
}
.app-tab.active .tab-count {
background: var(--text-1);
color: #fff;
}
/* ── App content ── */
.app-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.app-inner {
flex: 1;
overflow-y: auto;
}
/* ── JIRA ── */
.jira-header {
background: var(--jira);
padding: 10px 20px;
display: flex;
align-items: center;
gap: 12px;
}
.jira-logo {
font-weight: 700;
font-size: 14px;
color: #fff;
letter-spacing: -0.5px;
}
.jira-project {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
padding: 2px 8px;
}
.jira-nav {
display: flex;
gap: 4px;
margin-left: auto;
}
.jira-nav-item {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
}
.jira-nav-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.jira-board {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.jira-board-cols {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.jira-col-header {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-2);
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.jira-col-count {
background: var(--border);
border-radius: 10px;
padding: 1px 7px;
font-size: 11px;
font-weight: 600;
}
.jira-col-body {
background: #F4F5F7;
border-radius: 8px;
min-height: 200px;
padding: 8px;
}
.jira-card {
background: #fff;
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 6px;
border: 1px solid var(--border);
cursor: pointer;
transition: box-shadow 0.15s, border-color 0.2s, transform 0.2s;
}
.jira-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.jira-card.highlighted {
border-color: var(--jira);
box-shadow: 0 0 0 3px rgba(0, 82, 204, 0.18), 0 12px 30px rgba(0, 82, 204, 0.16);
animation: record-focus 0.75s ease;
}
.jira-card-title {
font-size: 12px;
font-weight: 500;
color: var(--text-1);
margin-bottom: 8px;
line-height: 1.4;
}
.jira-card-meta {
display: flex;
align-items: center;
gap: 6px;
}
.jira-id {
font-size: 10px;
font-family: var(--font-mono);
color: var(--text-3);
}
.priority-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.p0 {
background: var(--red);
}
.p1 {
background: var(--amber);
}
.p2 {
background: var(--green);
}
.jira-assignee {
margin-left: auto;
width: 20px;
height: 20px;
background: var(--jira-light);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: 600;
color: var(--jira);
}
/* ── ZENDESK ── */
.zd-header {
background: var(--zendesk);
padding: 10px 20px;
display: flex;
align-items: center;
gap: 12px;
}
.zd-logo {
font-weight: 700;
font-size: 14px;
color: #fff;
}
.zd-search {
flex: 1;
max-width: 320px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 5px 12px;
font-family: var(--font-sans);
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
outline: none;
}
.zd-search::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.zd-layout {
display: grid;
grid-template-columns: 200px 1fr;
height: 100%;
overflow: hidden;
}
.zd-sidebar {
background: #F8FAFB;
border-right: 1px solid var(--border);
padding: 12px 0;
overflow-y: auto;
}
.zd-nav-group {
margin-bottom: 16px;
}
.zd-nav-title {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-3);
padding: 4px 16px 6px;
}
.zd-nav-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 16px;
font-size: 12px;
color: var(--text-2);
cursor: pointer;
transition: background 0.1s;
}
.zd-nav-item:hover,
.zd-nav-item.active {
background: var(--zendesk-light);
color: var(--zendesk);
}
.zd-count {
font-size: 11px;
font-weight: 600;
background: var(--zendesk-mid);
color: var(--zendesk);
border-radius: 10px;
padding: 1px 6px;
}
.zd-tickets {
padding: 16px;
overflow-y: auto;
}
.zd-ticket-row {
display: grid;
grid-template-columns: 60px 1fr 80px 90px 80px;
gap: 12px;
align-items: center;
padding: 10px 14px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 7px;
margin-bottom: 4px;
font-size: 12px;
cursor: pointer;
transition: box-shadow 0.15s, border-color 0.2s, transform 0.2s;
}
.zd-ticket-row:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.zd-ticket-row.highlighted {
border-color: var(--zendesk);
box-shadow: 0 0 0 3px rgba(3, 54, 61, 0.16), 0 12px 30px rgba(3, 54, 61, 0.12);
animation: record-focus 0.75s ease;
}
.zd-tid {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-3);
}
.zd-subject {
font-weight: 500;
color: var(--text-1);
}
.zd-urgency,
.zd-status {
text-align: center;
}
.urgency-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.urg-high {
background: #FEE2E2;
color: var(--red);
}
.urg-medium {
background: #FEF3C7;
color: #92400E;
}
.urg-low {
background: #D1FAE5;
color: #065F46;
}
.zd-agent {
font-size: 11px;
color: var(--text-2);
}
.status-badge {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-open {
background: var(--amber);
}
.status-pending {
background: #818CF8;
}
.status-resolved {
background: var(--green);
}
.status-new {
background: var(--red);
}
/* ── SALESFORCE ── */
.sf-header {
background: var(--sf);
padding: 10px 20px;
display: flex;
align-items: center;
gap: 12px;
}
.sf-logo {
font-weight: 700;
font-size: 14px;
color: #fff;
letter-spacing: -0.3px;
}
.sf-tabs {
display: flex;
gap: 2px;
}
.sf-tab {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
}
.sf-tab.active,
.sf-tab:hover {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
.sf-body {
padding: 20px;
overflow-y: auto;
}
.sf-pipeline {
margin-bottom: 20px;
}
.sf-pipeline-title {
font-size: 12px;
font-weight: 600;
color: var(--text-1);
margin-bottom: 10px;
}
.sf-stages {
display: flex;
margin-bottom: 16px;
border-radius: 6px;
overflow: hidden;
}
.sf-stage {
flex: 1;
padding: 6px 8px;
background: var(--sf-light);
border-right: 1px solid #fff;
text-align: center;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--sf);
}
.sf-stage.active {
background: var(--sf);
color: #fff;
}
.sf-stage:last-child {
border-right: none;
}
.sf-account-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 8px;
display: grid;
grid-template-columns: 36px 1fr auto;
gap: 12px;
align-items: center;
cursor: pointer;
transition: box-shadow 0.15s, border-color 0.2s, transform 0.2s;
}
.sf-account-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.sf-account-card.highlighted {
border-color: var(--sf);
box-shadow: 0 0 0 3px rgba(0, 161, 224, 0.18), 0 12px 30px rgba(0, 161, 224, 0.16);
animation: record-focus 0.75s ease;
}
.sf-avatar {
width: 36px;
height: 36px;
background: var(--sf-light);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 700;
color: var(--sf);
}
.sf-company {
font-size: 13px;
font-weight: 600;
color: var(--text-1);
}
.sf-meta {
font-size: 11px;
color: var(--text-2);
margin-top: 2px;
}
.health-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 4px;
}
.health-green {
background: var(--green);
}
.health-yellow {
background: var(--amber);
}
.health-red {
background: var(--red);
}
.sf-arr {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
color: var(--text-1);
}
/* ── WORKDAY ── */
.wd-header {
background: var(--wd);
padding: 10px 20px;
display: flex;
align-items: center;
gap: 12px;
}
.wd-logo {
font-weight: 700;
font-size: 14px;
color: #fff;
}
.wd-tagline {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
}
.wd-body {
padding: 20px;
overflow-y: auto;
}
.wd-section-title {
font-size: 12px;
font-weight: 600;
color: var(--text-1);
margin-bottom: 10px;
}
.wd-tasks-header {
display: grid;
grid-template-columns: 1fr 100px 80px 80px;
gap: 12px;
padding: 6px 14px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-3);
}
.wd-task-row {
display: grid;
grid-template-columns: 1fr 100px 80px 80px;
gap: 12px;
padding: 10px 14px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 7px;
margin-bottom: 4px;
font-size: 12px;
align-items: center;
cursor: pointer;
transition: box-shadow 0.15s, border-color 0.2s, transform 0.2s;
}
.wd-task-row:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.wd-task-row.highlighted {
border-color: var(--wd);
box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.18), 0 12px 30px rgba(255, 107, 53, 0.14);
animation: record-focus 0.75s ease;
}
.wd-emp {
font-weight: 500;
color: var(--text-1);
}
.wd-dept {
font-size: 11px;
color: var(--text-2);
}
.wd-level {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-2);
}
.wd-stat-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.wd-stat {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
}
.wd-stat-val {
font-size: 22px;
font-weight: 600;
color: var(--text-1);
}
.wd-stat-label {
font-size: 11px;
color: var(--text-2);
margin-top: 2px;
}
/* ── Agent log ── */
.agent-log {
display: flex;
flex-direction: column;
background: var(--surface);
border-top: 1px solid var(--border);
overflow: hidden;
min-height: 180px;
max-height: 260px;
}
.log-header {
padding: 8px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.log-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-3);
}
.log-scroll {
flex: 1;
overflow-y: auto;
padding: 8px 16px;
}
.log-row {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 6px 0;
border-bottom: 1px solid #F9FAFB;
font-size: 11.5px;
animation: fadeUp 0.2s ease;
}
.log-row:last-child {
border-bottom: none;
}
.log-step {
font-family: var(--font-mono);
color: var(--text-3);
width: 28px;
text-align: right;
flex-shrink: 0;
padding-top: 1px;
}
.log-indicator {
width: 6px;
height: 6px;
border-radius: 50%;
margin-top: 4px;
flex-shrink: 0;
}
.ind-success {
background: var(--green);
}
.ind-error {
background: var(--red);
}
.ind-info {
background: #60A5FA;
}
.ind-reset {
background: var(--text-3);
}
.log-body {
flex: 1;
min-width: 0;
}
.log-tags {
display: flex;
gap: 5px;
flex-wrap: wrap;
margin-bottom: 2px;
}
.log-app-tag {
padding: 1px 7px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.tag-jira {
background: var(--jira-light);
color: var(--jira);
}
.tag-zendesk {
background: var(--zendesk-light);
color: var(--zendesk);
}
.tag-salesforce {
background: var(--sf-light);
color: #0070A0;
}
.tag-workday {
background: var(--wd-light);
color: #C04A1A;
}
.log-op {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-2);
}
.log-reward {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
}
.log-msg {
color: var(--text-2);
font-size: 11px;
line-height: 1.4;
}
/* ── Right sidebar ── */
.sidebar-right {
background: var(--surface);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.score-block {
padding: 20px 16px 16px;
border-bottom: 1px solid var(--border);
text-align: center;
}
.score-gauge-wrap {
position: relative;
width: 110px;
height: 110px;
margin: 0 auto 10px;
}
.score-gauge-wrap canvas {
position: absolute;
top: 0;
left: 0;
}
.score-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.score-val {
font-size: 22px;
font-weight: 600;
color: var(--text-1);
line-height: 1;
}
.score-sub {
font-size: 10px;
color: var(--text-3);
margin-top: 2px;
}
.breakdown-block {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.breakdown-row {
margin-bottom: 9px;
}
.breakdown-label-row {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
}
.breakdown-label {
font-size: 11px;
color: var(--text-2);
}
.breakdown-pct {
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-1);
font-weight: 500;
}
.bar-track {
height: 4px;
background: var(--bg);
border-radius: 2px;
}
.bar-fill {
height: 4px;
border-radius: 2px;
transition: width 0.4s ease;
}
.stats-block {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 0;
}
.stat-label {
font-size: 11px;
color: var(--text-2);
}
.stat-val {
font-size: 11px;
font-family: var(--font-mono);
font-weight: 500;
color: var(--text-1);
}
.violations-block {
padding: 14px 16px;
flex: 1;
overflow-y: auto;
}
.violation-item {
font-size: 11px;
color: var(--red);
padding: 4px 0;
border-bottom: 1px solid #FEE2E2;
line-height: 1.4;
}
.violation-item:last-child {
border-bottom: none;
}
/* ── Misc ── */
.drift-banner {
background: linear-gradient(90deg, #FFFBEB, #FEF3C7);
border: 1px solid #FDE68A;
border-radius: 7px;
padding: 8px 12px;
margin: 10px 14px 0;
display: flex;
align-items: center;
gap: 8px;
animation: banner-in 0.3s ease;
flex-shrink: 0;
}
.drift-banner-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--amber);
flex-shrink: 0;
}
.drift-banner-text {
font-size: 11px;
font-weight: 500;
color: #92400E;
}
.schema-pill {
background: rgba(255, 255, 255, 0.18);
border-radius: 4px;
padding: 2px 8px;
font-size: 10px;
color: rgba(255, 255, 255, 0.9);
font-family: var(--font-mono);
}
.live-badge {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--green);
font-weight: 500;
}
.live-badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--green);
}
</style>
</head>
<body x-data="orgos()" x-init="init()">
<!-- ═══ TOP BAR ═══ -->
<header class="topbar">
<div class="logo-mark">OS</div>
<div>
<div class="logo-text">OrgOS</div>
</div>
<div class="logo-sub">Enterprise RL Environment</div>
<div class="divider"></div>
<label style="font-size:11px;color:var(--text-3);font-weight:500;">Workflow</label>
<select class="wf-select" x-model="selectedWorkflow">
<option value="A">A β€” Customer Bug Fix</option>
<option value="B">B β€” Employee Onboarding</option>
<option value="C">C β€” Churn Risk Alert</option>
</select>
<button class="btn btn-ghost" @click="resetEpisode()" :disabled="isRunning">Reset</button>
<button class="btn" :class="isRunning ? 'btn-danger' : 'btn-primary'"
@click="isRunning ? stopAgent() : startAgent()">
<svg x-show="!isRunning" width="10" height="10" fill="currentColor" viewBox="0 0 16 16">
<path
d="M11.596 8.697l-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z" />
</svg>
<svg x-show="isRunning" width="10" height="10" fill="currentColor" viewBox="0 0 16 16">
<path
d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z" />
</svg>
<span x-text="isRunning ? 'Stop' : 'Run Agent'"></span>
</button>
<div class="divider"></div>
<div style="display:flex;align-items:baseline;gap:5px;">
<span style="font-size:11px;color:var(--text-3);font-weight:500;">Score</span>
<span style="font-size:18px;font-weight:600;color:var(--text-1);font-family:var(--font-mono);"
:class="scoreUpdated ? 'score-flash' : ''" x-text="currentScore.toFixed(3)"></span>
</div>
<div style="display:flex;align-items:baseline;gap:5px;">
<span style="font-size:11px;color:var(--text-3);font-weight:500;">Step</span>
<span style="font-size:18px;font-weight:600;color:var(--text-1);font-family:var(--font-mono);"
x-text="stepCount+'/'+maxSteps"></span>
</div>
<div x-show="policyDriftActive" class="badge" style="background:#FEF3C7;color:#92400E;border:1px solid #FDE68A;">
Policy Drift Active</div>
<div style="margin-left:auto;display:flex;align-items:center;gap:6px;">
<div class="live-badge-dot" :class="serverHealthy?'live-dot':''"
:style="serverHealthy?'':'background:var(--red)'"></div>
<span style="font-size:11px;" :class="serverHealthy?'live-badge':''"
:style="!serverHealthy?'color:var(--red);font-size:11px;':''"
x-text="serverHealthy?'Connected':'Offline'"></span>
</div>
</header>
<!-- ═══ THREE-COLUMN LAYOUT ═══ -->
<div class="layout">
<!-- ── LEFT SIDEBAR ── -->
<aside class="sidebar-left">
<div class="sidebar-section" style="flex:1;overflow-y:auto;">
<div class="section-label">
Workflow <span style="color:var(--text-1);font-weight:700;" x-text="workflowId||selectedWorkflow"></span>
Progress
</div>
<div style="margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--text-2);margin-bottom:5px;">
<span x-text="completedSteps.length+' of '+totalSteps+' steps done'"></span>
<span style="font-weight:600;color:var(--text-1);"
x-text="Math.round(completedSteps.length/Math.max(totalSteps,1)*100)+'%'"></span>
</div>
<div style="height:4px;background:var(--bg);border-radius:2px;">
<div style="height:4px;background:var(--green);border-radius:2px;transition:width 0.4s ease;"
:style="'width:'+(completedSteps.length/Math.max(totalSteps,1)*100)+'%'"></div>
</div>
</div>
<div x-show="workflowGoal"
style="font-size:11px;color:var(--text-2);line-height:1.5;margin-bottom:12px;padding:8px;background:var(--bg);border-radius:6px;"
x-text="workflowGoal"></div>
<template x-for="(step,i) in allSteps" :key="i">
<div class="step-row">
<div class="step-num" :class="completedSteps.includes(step.id)?'done':''">
<template x-if="completedSteps.includes(step.id)">
<svg class="step-check" width="10" height="10" fill="currentColor" viewBox="0 0 16 16">
<path
d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
</template>
<template x-if="!completedSteps.includes(step.id)">
<span x-text="step.id"></span>
</template>
</div>
<span class="step-desc" :class="completedSteps.includes(step.id)?'done':''"
x-text="step.description"></span>
</div>
</template>
</div>
<div class="sidebar-section">
<div class="section-label">Schema Drift</div>
<div x-show="Object.keys(schemaHints).length===0" style="font-size:11px;color:var(--text-3);">No drift β€” v1
canonical names active.</div>
<template x-for="[field,drifted] in Object.entries(schemaHints)" :key="field">
<div class="drift-pill">
<span class="drift-old" x-text="field.split('.')[1]??field"></span>
<span class="drift-arr">β†’</span>
<span class="drift-new" x-text="drifted"></span>
<span class="drift-app" x-text="field.split('.')[0]??''"></span>
</div>
</template>
</div>
<div class="sidebar-section">
<div class="section-label">Active Rules</div>
<template x-for="[key,val] in Object.entries(activeRules)" :key="key">
<div class="rule-row">
<span class="rule-key" x-text="key.replace(/_/g,' ')"></span>
<span class="rule-val" x-text="val"></span>
</div>
</template>
<div x-show="Object.keys(activeRules).length===0" style="font-size:11px;color:var(--text-3);">Reset to load
rules.</div>
</div>
<div class="sidebar-section" style="border-bottom:none;">
<div class="section-label">Schema Versions</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<template x-for="[app,ver] in Object.entries(schemaVersions)" :key="app">
<div
style="background:var(--bg);border:1px solid var(--border);border-radius:5px;padding:3px 8px;font-size:11px;">
<span style="color:var(--text-3);" x-text="app.charAt(0).toUpperCase()+app.slice(1,3)"></span>
<span style="font-family:var(--font-mono);font-weight:600;color:var(--text-1);margin-left:4px;"
x-text="ver"></span>
</div>
</template>
<div x-show="Object.keys(schemaVersions).length===0" style="font-size:11px;color:var(--text-3);">β€”</div>
</div>
</div>
</aside>
<!-- ── CENTER PANEL ── -->
<main class="center-panel">
<!-- Policy drift banner -->
<div x-show="policyDriftActive" class="drift-banner">
<div class="drift-banner-dot live-dot"></div>
<span class="drift-banner-text">Policy drift active β€” SLA and approval thresholds have tightened this
episode.</span>
</div>
<!-- β˜… CINEMATIC ACTION BANNER β€” shows while agent is acting on an app -->
<div x-show="cinematicAction" class="action-banner" :class="cinematicAction?'banner-'+cinematicAction.app:''">
<div class="banner-dot" :style="'background:'+appColor(cinematicAction?.app)"
x-text="appInitial(cinematicAction?.app)"></div>
<div class="banner-copy">
<div class="banner-eyebrow">Agent operating in <span x-text="appLabel(cinematicAction?.app)"></span></div>
<div class="banner-title" x-text="operationLabel(cinematicAction)"></div>
<div class="banner-sub" x-text="cinematicAction?.summary||'Applying workflow action...'"></div>
</div>
<div class="banner-step" x-text="'step '+(cinematicAction?.step||stepCount)"></div>
</div>
<!-- App tab strip -->
<div class="app-tabs">
<template x-for="tab in appTabs" :key="tab.id">
<div class="app-tab"
:class="[(activeAppTab===tab.id?'active':''), (cinematicAction?.app===tab.id&&activeAppTab===tab.id?'acting':'')]"
@click="activeAppTab=tab.id">
<div class="app-dot" :style="'background:'+tab.color"></div>
<span x-text="tab.label"></span>
<template x-if="appOpenCounts[tab.id]>0">
<span class="tab-count" x-text="appOpenCounts[tab.id]"></span>
</template>
</div>
</template>
</div>
<!-- App content views -->
<div class="app-content">
<div class="app-inner">
<!-- ════ JIRA ════ -->
<div x-show="activeAppTab==='jira'">
<div class="jira-header">
<span class="jira-logo">Jira</span>
<span class="jira-project">OrgOS / Engineering</span>
<template x-for="[app,ver] in Object.entries(schemaVersions)" :key="app">
<span x-show="app==='jira'" class="schema-pill" x-text="'schema '+ver"></span>
</template>
<div class="jira-nav" style="margin-left:auto;">
<div class="jira-nav-item">Board</div>
<div class="jira-nav-item">Backlog</div>
<div class="jira-nav-item">Reports</div>
</div>
</div>
<div class="jira-board">
<div style="font-size:12px;font-weight:600;color:var(--text-1);margin-bottom:14px;">Sprint Board</div>
<div class="jira-board-cols">
<div>
<div class="jira-col-header">To Do <span class="jira-col-count"
x-text="jiraCards.filter(c=>c.status==='open').length"></span></div>
<div class="jira-col-body">
<template x-for="card in jiraCards.filter(c=>c.status==='open')" :key="card.id">
<div class="jira-card" :class="card.highlighted?'highlighted':''">
<div class="jira-card-title" x-text="card.title"></div>
<div class="jira-card-meta">
<div class="priority-dot" :class="card.priority==='p0'?'p0':card.priority==='p1'?'p1':'p2'">
</div>
<span class="jira-id" x-text="card.id"></span>
<template x-if="card.linked_zendesk">
<span
style="font-size:10px;background:var(--zendesk-light);color:var(--zendesk);border-radius:3px;padding:1px 5px;font-weight:500;"
x-text="card.linked_zendesk"></span>
</template>
<div class="jira-assignee" x-text="card.assignee?card.assignee.charAt(0).toUpperCase():'?'"
:style="card.assignee?'':'color:var(--text-3);'"></div>
</div>
</div>
</template>
<div x-show="jiraCards.filter(c=>c.status==='open').length===0"
style="font-size:11px;color:var(--text-3);padding:12px;text-align:center;">No open issues</div>
</div>
</div>
<div>
<div class="jira-col-header">In Progress <span class="jira-col-count"
x-text="jiraCards.filter(c=>c.status==='in_progress').length"></span></div>
<div class="jira-col-body">
<template x-for="card in jiraCards.filter(c=>c.status==='in_progress')" :key="card.id">
<div class="jira-card" :class="card.highlighted?'highlighted':''">
<div class="jira-card-title" x-text="card.title"></div>
<div class="jira-card-meta">
<div class="priority-dot" :class="card.priority==='p0'?'p0':card.priority==='p1'?'p1':'p2'">
</div>
<span class="jira-id" x-text="card.id"></span>
<div class="jira-assignee" x-text="card.assignee?card.assignee.charAt(0).toUpperCase():'?'">
</div>
</div>
</div>
</template>
<div x-show="jiraCards.filter(c=>c.status==='in_progress').length===0"
style="font-size:11px;color:var(--text-3);padding:12px;text-align:center;">Nothing in progress
</div>
</div>
</div>
<div>
<div class="jira-col-header" style="color:var(--green);">Done <span class="jira-col-count"
x-text="jiraCards.filter(c=>c.status==='closed'||c.status==='done').length"></span></div>
<div class="jira-col-body">
<template x-for="card in jiraCards.filter(c=>c.status==='closed'||c.status==='done')"
:key="card.id">
<div class="jira-card" style="opacity:0.65;">
<div class="jira-card-title" style="text-decoration:line-through;" x-text="card.title"></div>
<div class="jira-card-meta"><span class="jira-id" x-text="card.id"></span></div>
</div>
</template>
<div x-show="jiraCards.filter(c=>c.status==='closed'||c.status==='done').length===0"
style="font-size:11px;color:var(--text-3);padding:12px;text-align:center;">Nothing done yet</div>
</div>
</div>
</div>
</div>
</div>
<!-- ════ ZENDESK ════ -->
<div x-show="activeAppTab==='zendesk'">
<div class="zd-header">
<span class="zd-logo">Zendesk</span>
<input class="zd-search" placeholder="Search tickets..." readonly />
<template x-for="[app,ver] in Object.entries(schemaVersions)" :key="app">
<span x-show="app==='zendesk'" class="schema-pill" x-text="'schema '+ver"></span>
</template>
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;">
<div class="badge" style="background:rgba(255,255,255,0.15);color:#fff;font-size:10px;"
x-text="zdTickets.filter(t=>t.state==='open'||t.state==='new').length+' open'"></div>
</div>
</div>
<div class="zd-layout" style="height:calc(100% - 42px);">
<div class="zd-sidebar">
<div class="zd-nav-group">
<div class="zd-nav-title">Views</div>
<div class="zd-nav-item active">All tickets <span class="zd-count" x-text="zdTickets.length"></span>
</div>
<div class="zd-nav-item">Open <span class="zd-count"
x-text="zdTickets.filter(t=>t.state==='open').length"></span></div>
<div class="zd-nav-item">New <span class="zd-count"
x-text="zdTickets.filter(t=>t.state==='new').length"></span></div>
<div class="zd-nav-item">Pending</div>
<div class="zd-nav-item">Resolved</div>
</div>
<div class="zd-nav-group">
<div class="zd-nav-title">Manage</div>
<div class="zd-nav-item">Agents</div>
<div class="zd-nav-item">Reports</div>
<div class="zd-nav-item">Settings</div>
</div>
</div>
<div class="zd-tickets">
<div
style="display:grid;grid-template-columns:60px 1fr 80px 90px 80px;gap:12px;padding:6px 14px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;color:var(--text-3);margin-bottom:4px;">
<span>ID</span><span>Subject</span><span
style="text-align:center;">Urgency</span><span>Agent</span><span
style="text-align:center;">Status</span>
</div>
<template x-for="ticket in zdTickets" :key="ticket.id">
<div class="zd-ticket-row" :class="ticket.highlighted?'highlighted':''">
<span class="zd-tid" x-text="ticket.id"></span>
<span class="zd-subject" x-text="ticket.subject"></span>
<div class="zd-urgency">
<span class="urgency-badge"
:class="ticket.urgency==='p0'||ticket.urgency==='high'?'urg-high':ticket.urgency==='p1'||ticket.urgency==='medium'?'urg-medium':'urg-low'"
x-text="ticket.urgency||'p2'"></span>
</div>
<span class="zd-agent" x-text="ticket.agent||'β€”'"></span>
<div class="zd-status">
<span class="status-badge"
:class="ticket.state==='open'?'status-open':ticket.state==='pending'?'status-pending':ticket.state==='resolved'?'status-resolved':'status-new'"></span>
<span style="font-size:10px;color:var(--text-2);margin-left:4px;"
x-text="ticket.state||'new'"></span>
</div>
</div>
</template>
<div x-show="zdTickets.length===0"
style="font-size:12px;color:var(--text-3);padding:24px;text-align:center;">No tickets loaded β€” reset
to start an episode.</div>
</div>
</div>
</div>
<!-- ════ SALESFORCE ════ -->
<div x-show="activeAppTab==='salesforce'">
<div class="sf-header">
<span class="sf-logo">Salesforce</span>
<div class="sf-tabs">
<div class="sf-tab active">Accounts</div>
<div class="sf-tab">Opportunities</div>
<div class="sf-tab">Pipeline</div>
</div>
<template x-for="[app,ver] in Object.entries(schemaVersions)" :key="app">
<span x-show="app==='salesforce'" class="schema-pill" x-text="'schema '+ver"></span>
</template>
</div>
<div class="sf-body">
<div class="sf-pipeline">
<div class="sf-pipeline-title">Deal Pipeline</div>
<div class="sf-stages">
<div class="sf-stage">Prospect</div>
<div class="sf-stage active">Qualified</div>
<div class="sf-stage">Proposal</div>
<div class="sf-stage">Negotiation</div>
<div class="sf-stage">Closed Won</div>
</div>
</div>
<div style="font-size:12px;font-weight:600;color:var(--text-1);margin-bottom:10px;">Accounts</div>
<template x-for="acct in sfAccounts" :key="acct.id">
<div class="sf-account-card" :class="acct.highlighted?'highlighted':''">
<div class="sf-avatar" x-text="acct.name.charAt(0)"></div>
<div>
<div class="sf-company" x-text="acct.name"></div>
<div class="sf-meta">
<span class="health-dot"
:class="acct.health==='green'?'health-green':acct.health==='yellow'?'health-yellow':'health-red'"></span>
<span x-text="acct.stage||'Qualified'"></span>
<span style="margin:0 6px;color:var(--border);">Β·</span>
<span x-text="acct.owner?'Owner: '+acct.owner:'Unassigned'"></span>
<template x-if="acct.churn_risk">
<span class="badge"
style="margin-left:6px;background:#FEE2E2;color:var(--red);font-size:10px;">Churn Risk</span>
</template>
</div>
</div>
<div style="text-align:right;">
<div class="sf-arr" x-text="acct.arr?'$'+(acct.arr/1000).toFixed(0)+'K':'β€”'"></div>
<div style="font-size:10px;color:var(--text-3);">ARR</div>
</div>
</div>
</template>
<div x-show="sfAccounts.length===0"
style="font-size:12px;color:var(--text-3);padding:24px;text-align:center;">No accounts loaded β€” reset to
start an episode.</div>
</div>
</div>
<!-- ════ WORKDAY ════ -->
<div x-show="activeAppTab==='workday'">
<div class="wd-header">
<span class="wd-logo">Workday</span>
<span class="wd-tagline">People &amp; HR Operations</span>
<template x-for="[app,ver] in Object.entries(schemaVersions)" :key="app">
<span x-show="app==='workday'" class="schema-pill" x-text="'schema '+ver"></span>
</template>
</div>
<div class="wd-body">
<div class="wd-stat-row">
<div class="wd-stat">
<div class="wd-stat-val" x-text="wdEmployees.length"></div>
<div class="wd-stat-label">Total Employees</div>
</div>
<div class="wd-stat">
<div class="wd-stat-val" x-text="wdEmployees.filter(e=>e.status==='pending').length"></div>
<div class="wd-stat-label">Pending Tasks</div>
</div>
<div class="wd-stat">
<div class="wd-stat-val" x-text="slaLogged?'1':'0'"></div>
<div class="wd-stat-label">SLA Events</div>
</div>
</div>
<div class="wd-section-title">Employee Records</div>
<div class="wd-tasks-header">
<span>Employee</span><span>Department</span><span>Level</span><span>Status</span>
</div>
<template x-for="emp in wdEmployees" :key="emp.id">
<div class="wd-task-row" :class="emp.highlighted?'highlighted':''">
<div>
<div class="wd-emp" x-text="emp.name"></div>
<div style="font-size:10px;font-family:var(--font-mono);color:var(--text-3);" x-text="emp.id"></div>
</div>
<span class="wd-dept" x-text="emp.department||'β€”'"></span>
<span class="wd-level" x-text="emp.level||'β€”'"></span>
<div>
<span class="badge"
:style="emp.status==='active'?'background:#D1FAE5;color:#065F46;':emp.status==='pending'?'background:#FEF3C7;color:#92400E;':'background:var(--bg);color:var(--text-2);'"
x-text="emp.status||'active'"></span>
</div>
</div>
</template>
<div x-show="wdEmployees.length===0"
style="font-size:12px;color:var(--text-3);padding:24px;text-align:center;">No employee records β€” reset
to start an episode.</div>
</div>
</div>
</div><!-- /app-inner -->
</div><!-- /app-content -->
<!-- ── Agent Log ── -->
<div class="agent-log">
<div class="log-header">
<span class="log-title">Agent Log</span>
<div style="display:flex;align-items:center;gap:10px;">
<div x-show="isRunning" class="live-badge">
<div class="live-badge-dot live-dot"></div>
Live
</div>
<button @click="actionLog=[]"
style="font-size:11px;color:var(--text-3);cursor:pointer;background:none;border:none;padding:0;">Clear</button>
</div>
</div>
<div class="log-scroll" id="log-scroll">
<div x-show="actionLog.length===0" style="font-size:11px;color:var(--text-3);padding:12px 0;">Waiting for
episode to start…</div>
<template x-for="(entry,i) in actionLog" :key="i">
<div class="log-row">
<span class="log-step" x-text="'#'+entry.step"></span>
<div class="log-indicator"
:class="entry.type==='success'?'ind-success':entry.type==='error'?'ind-error':entry.type==='reset'?'ind-reset':'ind-info'">
</div>
<div class="log-body">
<div class="log-tags">
<template x-if="entry.app">
<span class="log-app-tag" :class="'tag-'+entry.app" x-text="entry.app"></span>
</template>
<template x-if="entry.operation">
<span class="log-op" x-text="entry.operation+'()'"></span>
</template>
<template x-if="entry.reward!==undefined">
<span class="log-reward" :style="entry.reward>=0?'color:var(--green)':'color:var(--red)'"
x-text="(entry.reward>=0?'+':'')+entry.reward.toFixed(4)"></span>
</template>
</div>
<div class="log-msg" x-text="entry.message"></div>
</div>
</div>
</template>
</div>
</div>
</main>
<!-- ── RIGHT SIDEBAR ── -->
<aside class="sidebar-right">
<div class="score-block">
<div class="section-label" style="text-align:left;">Episode Score</div>
<div class="score-gauge-wrap">
<canvas id="gaugeChart" width="110" height="110"></canvas>
<div class="score-center">
<div class="score-val" x-text="(currentScore*100).toFixed(0)"></div>
<div class="score-sub">/ 100</div>
</div>
</div>
<div style="font-size:11px;color:var(--text-2);">
<span x-text="stepCount+' steps'"></span>
<span style="margin:0 6px;color:var(--border);">Β·</span>
<span x-text="maxSteps+' max'"></span>
</div>
</div>
<div class="breakdown-block">
<div class="section-label">Score Breakdown</div>
<template x-for="comp in rewardComponents" :key="comp.key">
<div class="breakdown-row">
<div class="breakdown-label-row">
<span class="breakdown-label" x-text="comp.label"></span>
<span class="breakdown-pct" x-text="(comp.value*100).toFixed(0)+'%'"></span>
</div>
<div class="bar-track">
<div class="bar-fill" :style="'width:'+(comp.value*100)+'%;background:'+comp.color"></div>
</div>
</div>
</template>
</div>
<div style="padding:14px 16px;border-bottom:1px solid var(--border);flex-shrink:0;">
<div class="section-label">Reward Per Step</div>
<canvas id="rewardChart" style="width:100%;max-height:90px;"></canvas>
</div>
<div class="stats-block">
<div class="section-label">Episode Stats</div>
<div class="stat-row">
<span class="stat-label">Violations</span>
<span class="stat-val" :style="violationCount>0?'color:var(--red)':'color:var(--green)'"
x-text="violationCount"></span>
</div>
<div class="stat-row">
<span class="stat-label">Schema adaptations</span>
<span class="stat-val" style="color:var(--green);" x-text="schemaAdaptCount"></span>
</div>
<div class="stat-row">
<span class="stat-label">Schema errors</span>
<span class="stat-val" :style="schemaErrorCount>0?'color:var(--red)':''" x-text="schemaErrorCount"></span>
</div>
<div class="stat-row">
<span class="stat-label">Workflow</span>
<span class="stat-val" x-text="workflowId||'β€”'"></span>
</div>
</div>
<div class="violations-block">
<div class="section-label">Rule Violations</div>
<div x-show="violations.length===0" style="font-size:11px;color:var(--text-3);">None this episode.</div>
<template x-for="(v,i) in violations.slice(-10)" :key="i">
<div class="violation-item" x-text="v"></div>
</template>
</div>
</aside>
</div><!-- /layout -->
<script>
function orgos() {
let _chartInst = null;
let _gaugeInst = null;
let _sseInst = null;
return {
envUrl: window.location.origin,
selectedWorkflow: 'A',
workflowId: '',
workflowGoal: '',
currentScore: 0.001,
stepCount: 0,
maxSteps: 15,
isRunning: false,
policyDriftActive: false,
serverHealthy: false,
scoreUpdated: false,
allSteps: [],
completedSteps: [],
totalSteps: 0,
appTabs: [
{ id: 'zendesk', label: 'Zendesk', color: '#03363D' },
{ id: 'jira', label: 'Jira', color: '#0052CC' },
{ id: 'salesforce', label: 'Salesforce', color: '#00A1E0' },
{ id: 'workday', label: 'Workday', color: '#FF6B35' },
],
activeAppTab: 'zendesk',
// β˜… cinematicAction drives the action banner and tab switching
// Set synchronously on every step event β€” no async queue needed
cinematicAction: null,
appOpenCounts: { zendesk: 0, jira: 0, salesforce: 0, workday: 0 },
jiraCards: [],
zdTickets: [],
sfAccounts: [],
wdEmployees: [],
slaLogged: false,
schemaHints: {},
schemaVersions: {},
activeRules: {},
rewardHistory: [],
rewardComponents: [
{ key: 'workflow_completion', label: 'Workflow', value: 0, color: '#10B981' },
{ key: 'rule_compliance', label: 'Compliance', value: 0, color: '#3B82F6' },
{ key: 'schema_adaptation', label: 'Schema', value: 0, color: '#8B5CF6' },
{ key: 'efficiency', label: 'Efficiency', value: 0, color: '#F59E0B' },
{ key: 'policy_drift_handling', label: 'Policy Drift', value: 0, color: '#EC4899' },
],
violationCount: 0,
schemaAdaptCount: 0,
schemaErrorCount: 0,
violations: [],
actionLog: [],
// ─────────────────────────────────────────
async init() {
await this.checkHealth();
_chartInst = this._initRewardChart();
_gaugeInst = this._initGauge();
setInterval(() => this.checkHealth(), 10000);
},
async checkHealth() {
try {
const r = await fetch(this.envUrl + '/health');
this.serverHealthy = r.ok;
} catch { this.serverHealthy = false; }
},
async resetEpisode() {
this.stopAgent();
try {
const r = await fetch(this.envUrl + '/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workflow_id: this.selectedWorkflow }),
});
const data = await r.json();
this._clearEpisodeState();
this._applyObservation(data.observation, null, 0);
this._updateChart();
this._updateGauge();
this._pushLog({ type: 'reset', step: 0, message: 'Episode reset. Ready to run agent.' });
const st = await fetch(this.envUrl + '/state').then(r => r.json());
this.schemaVersions = st.schema_versions || {};
this.policyDriftActive = st.policy_drift_active || false;
} catch (e) {
this._pushLog({ type: 'error', step: 0, message: 'Reset failed: ' + e });
}
},
_clearEpisodeState() {
this.actionLog = [];
this.rewardHistory = [];
this.violationCount = 0;
this.schemaAdaptCount = 0;
this.schemaErrorCount = 0;
this.violations = [];
this.slaLogged = false;
this.cinematicAction = null;
},
// ─────────────────────────────────────────
// startAgent β€” plain EventSource, no queue, no async handler
// The cinematic tab switch and banner update happen synchronously
// inside _handleSSEEvent, which is always called from onmessage.
// Pacing between steps comes from asyncio.sleep(0.3) on the server.
startAgent() {
if (this.isRunning) return;
this.isRunning = true;
this.cinematicAction = null;
const url = `${this.envUrl}/ui/run-agent?workflow_id=${this.selectedWorkflow}`;
_sseInst = new EventSource(url);
_sseInst.onmessage = (e) => {
try {
const evt = JSON.parse(e.data);
this._handleSSEEvent(evt);
} catch { }
};
_sseInst.onerror = () => {
// Ignore error noise after we've already closed the stream ourselves
if (!_sseInst || !this.isRunning) return;
this.isRunning = false;
this.cinematicAction = null;
_sseInst.close(); _sseInst = null;
this._pushLog({ type: 'error', step: this.stepCount, message: 'SSE connection error.' });
};
},
// Delay close so any in-flight 'done' event still arrives
stopAgent() {
this.isRunning = false;
this.cinematicAction = null;
const inst = _sseInst; _sseInst = null;
if (inst) setTimeout(() => inst.close(), 1500);
},
// ─────────────────────────────────────────
// _handleSSEEvent β€” SYNCHRONOUS (no await, no sleep)
// This is the key fix: removing the async queue that caused the deadlock.
_handleSSEEvent(evt) {
if (evt.type === 'reset') {
this._clearEpisodeState();
this._applyObservation(evt.observation, null, 0);
this._pushLog({ type: 'reset', step: 0, message: `Episode started β€” Workflow ${evt.workflow_id}` });
} else if (evt.type === 'step') {
const obs = evt.observation;
// β˜… CINEMATIC: switch tab immediately + show action banner
// This is the "movie" effect β€” happens the instant the event arrives
if (evt.action) {
this.activeAppTab = evt.action.app;
this.cinematicAction = {
app: evt.action.app,
operation: evt.action.operation,
args: evt.action.args || {},
step: evt.step,
summary: this.actionSummary(evt.action),
};
}
this._applyObservation(obs, evt.action, evt.reward);
// Track schema events
if (obs.message && (obs.message.includes('Stale schema') || obs.message.includes('Schema error') || obs.message.includes('schema_error'))) {
this.schemaErrorCount++;
}
if (obs.message && (obs.message.includes('adapted') || obs.message.includes('schema_adapted'))) {
this.schemaAdaptCount++;
}
if (obs.message && obs.message.includes('SLA event logged')) {
this.slaLogged = true;
}
this.rewardHistory.push(evt.reward);
this._updateChart();
this._updateGauge();
if (obs.rule_violations && obs.rule_violations.length > 0) {
this.violations.push(...obs.rule_violations);
this.violationCount += obs.rule_violations.length;
}
// β˜… Update the card in the active app UI
this._highlightAppCard(evt.action, obs.message);
this._pushLog({
type: evt.reward < 0 ? 'error' : (evt.reward > 0.05 ? 'success' : 'info'),
step: evt.step,
app: evt.action?.app,
operation: evt.action?.operation,
reward: evt.reward,
message: obs.message,
});
// Max steps hit
if (evt.done) {
this.isRunning = false;
this.cinematicAction = null;
this._pushLog({
type: 'info', step: evt.step,
message: `Max steps reached. Score: ${(obs.current_score || 0).toFixed(3)} | ${obs.pending_steps?.length ? obs.pending_steps.length + ' step(s) remaining' : 'Workflow complete'}`,
});
}
} else if (evt.type === 'done') {
const score = evt.final_score || this.currentScore;
this.cinematicAction = null;
this.currentScore = score;
this._updateGauge();
this.isRunning = false;
this._pushLog({
type: evt.completed ? 'success' : 'info',
step: evt.steps,
message: `Episode ended. Final score: ${score.toFixed(3)} | Workflow ${evt.completed ? 'completed' : 'incomplete'} | ${evt.steps} steps`,
});
const inst = _sseInst; _sseInst = null;
if (inst) inst.close();
} else if (evt.type === 'error') {
this.cinematicAction = null;
this.isRunning = false;
this._pushLog({ type: 'error', step: evt.step || this.stepCount, message: evt.message || 'Unknown error' });
const inst = _sseInst; _sseInst = null;
if (inst) inst.close();
}
},
// ─────────────────────────────────────────
// Helper labels for the action banner
appLabel(app) { return this.appTabs.find(t => t.id === app)?.label || 'App'; },
appInitial(app) { return this.appLabel(app).slice(0, 2).toUpperCase(); },
appColor(app) { return this.appTabs.find(t => t.id === app)?.color || '#111827'; },
operationLabel(action) {
if (!action) return 'Waiting for agent action';
return `${this.appLabel(action.app)}: ${String(action.operation || '').replace(/_/g, ' ')}()`;
},
actionSummary(action) {
const args = action?.args || {};
const id = args.ticket_number || args.issue_id || args.account_id ||
args.employee_id || args.ticket_id || args.customer_id;
const target = id ? `Target: ${id}` : 'Inspecting records';
const argStr = JSON.stringify(args).slice(0, 80);
return `${target} Β· ${argStr}`;
},
// ─────────────────────────────────────────
// β˜… _highlightAppCard β€” updates the card that the agent just touched
// Also handles create / update mutations so the UI reflects state changes
_highlightAppCard(action, message) {
if (!action) return;
const app = action.app;
const args = action.args || {};
// Clear all highlights first
this.jiraCards = this.jiraCards.map(c => ({ ...c, highlighted: false }));
this.zdTickets = this.zdTickets.map(t => ({ ...t, highlighted: false }));
this.sfAccounts = this.sfAccounts.map(a => ({ ...a, highlighted: false }));
this.wdEmployees = this.wdEmployees.map(e => ({ ...e, highlighted: false }));
if (app === 'jira') {
const id = args.issue_id || 'JIRA-001';
this.jiraCards = this.jiraCards.map(c => ({ ...c, highlighted: c.id === id }));
if (action.operation === 'create_issue' && message) {
const m = message.match(/Created (JIRA-\d+)/);
if (m && !this.jiraCards.find(c => c.id === m[1])) {
this.jiraCards = [{
id: m[1], title: args.title || 'New Issue',
priority: args.priority || args.severity || args.urgency_level || 'p1',
assignee: args.assignee || args.owner || args.assigned_to || null,
status: 'open',
linked_zendesk: args.linked_zendesk || args.zendesk_ticket || null,
highlighted: true,
}, ...this.jiraCards];
}
}
if (action.operation === 'assign_owner' || action.operation === 'update_status') {
this.jiraCards = this.jiraCards.map(c => c.id !== id ? c : {
...c,
assignee: args.assignee || args.owner || args.assigned_to || c.assignee,
status: args.status || args.state || args.current_state || c.status,
highlighted: true,
});
}
if (action.operation === 'link_zendesk_ticket') {
this.jiraCards = this.jiraCards.map(c => c.id !== id ? c : { ...c, linked_zendesk: args.zendesk_ticket_number, highlighted: true });
}
}
if (app === 'zendesk') {
const id = args.ticket_number || 'ZD-001';
this.zdTickets = this.zdTickets.map(t => ({ ...t, highlighted: t.id === id }));
if (action.operation === 'acknowledge_ticket') {
this.zdTickets = this.zdTickets.map(t => t.id !== id ? t : { ...t, state: 'open', highlighted: true });
}
if (action.operation === 'assign_agent') {
const agent = args.agent_email || args.handler || args.assigned_agent;
this.zdTickets = this.zdTickets.map(t => t.id !== id ? t : { ...t, agent, highlighted: true });
}
if (action.operation === 'resolve_ticket') {
this.zdTickets = this.zdTickets.map(t => t.id !== id ? t : { ...t, state: 'resolved', highlighted: true });
}
}
if (app === 'salesforce') {
const id = args.account_id || 'ACME-001';
this.sfAccounts = this.sfAccounts.map(a => ({ ...a, highlighted: a.id === id }));
if (action.operation === 'flag_churn_risk') {
this.sfAccounts = this.sfAccounts.map(a => a.id !== id ? a : { ...a, health: 'red', churn_risk: true, highlighted: true });
}
if (action.operation === 'assign_account_owner') {
const owner = args.owner || args.owner_name || args.account_owner || args.rep_email;
this.sfAccounts = this.sfAccounts.map(a => a.id !== id ? a : { ...a, owner, highlighted: true });
}
}
if (app === 'workday') {
const id = args.employee_id || 'EMP-001';
this.wdEmployees = this.wdEmployees.map(e => ({ ...e, highlighted: e.id === id }));
if (action.operation === 'create_onboarding_task' && !this.wdEmployees.find(e => e.id === id)) {
this.wdEmployees = [{
id, name: args.name || id,
department: args.department || 'support',
level: args.level || args.job_level || args.seniority || 'IC1',
status: 'pending', highlighted: true,
}, ...this.wdEmployees];
}
if (action.operation === 'log_sla_event') this.slaLogged = true;
}
},
// ─────────────────────────────────────────
_applyObservation(obs, action, reward) {
this.workflowId = obs.workflow_id || '';
this.workflowGoal = obs.workflow_goal || '';
this.schemaHints = obs.schema_hints || {};
this.activeRules = obs.active_rules || {};
this.stepCount = obs.step_count || 0;
this.completedSteps = (obs.completed_steps || []).slice();
this.policyDriftActive = obs.policy_drift_active || false;
const newScore = obs.current_score || 0.001;
if (newScore !== this.currentScore) {
this.currentScore = newScore;
this.scoreUpdated = true;
setTimeout(() => { this.scoreUpdated = false; }, 600);
}
const wfStepDefs = {
A: [
{ id: 'A1', description: 'Acknowledge ZD-001 in Zendesk' },
{ id: 'A2', description: 'Create linked Jira issue' },
{ id: 'A3', description: 'Verify ACME-001 in Salesforce' },
{ id: 'A4', description: 'Assign Jira issue to engineer' },
{ id: 'A5', description: 'Log SLA event in Workday' },
],
B: [
{ id: 'B1', description: 'Create Workday onboarding record' },
{ id: 'B2', description: 'Provision Jira access in Workday' },
{ id: 'B3', description: 'Assign to Salesforce territory' },
{ id: 'B4', description: 'Create Zendesk agent profile' },
],
C: [
{ id: 'C1', description: 'Flag ACME-003 as churn risk' },
{ id: 'C2', description: 'Query Zendesk support volume' },
{ id: 'C3', description: 'Check Jira open bugs' },
{ id: 'C4', description: 'Assign intervention owner' },
],
};
const wfId = obs.workflow_id || this.selectedWorkflow;
this.allSteps = wfStepDefs[wfId] || [];
this.totalSteps = this.allSteps.length;
this.maxSteps = { A: 15, B: 20, C: 18 }[wfId] || 15;
const rb = obs.reward_breakdown || {};
this.rewardComponents.forEach(c => { c.value = rb[c.key] ?? 0; });
if (obs.app_states) this._parseAppStates(obs.app_states);
},
// ─────────────────────────────────────────
_parseAppStates(states) {
const parseDictStr = (str) => {
if (!str) return [];
return str.split('\n').filter(Boolean).map(line => {
try {
return JSON.parse(
line.replace(/'/g, '"')
.replace(/\bTrue\b/g, 'true')
.replace(/\bFalse\b/g, 'false')
.replace(/\bNone\b/g, 'null')
);
} catch { return null; }
}).filter(Boolean);
};
const jiraRaw = parseDictStr(states.jira || '');
if (jiraRaw.length > 0 || this.jiraCards.length === 0) {
const m = new Map(this.jiraCards.map(c => [c.id, c]));
jiraRaw.forEach(r => {
const id = r.issue_id || r.id; if (!id) return;
const ex = m.get(id) || {};
m.set(id, {
...ex, id,
title: r.title || ex.title || 'Untitled',
priority: r.priority || r.severity || r.urgency_level || ex.priority || 'p2',
assignee: r.assignee || r.owner || r.assigned_to || ex.assignee || null,
status: r.status || r.state || r.current_state || ex.status || 'open',
linked_zendesk: r.linked_zendesk || ex.linked_zendesk || null,
highlighted: ex.highlighted || false,
});
});
this.jiraCards = Array.from(m.values());
}
const zdRaw = parseDictStr(states.zendesk || '');
if (zdRaw.length > 0 || this.zdTickets.length === 0) {
const m = new Map(this.zdTickets.map(t => [t.id, t]));
zdRaw.forEach(r => {
const id = r.ticket_number || r.id; if (!id) return;
const ex = m.get(id) || {};
m.set(id, {
...ex, id,
subject: r.title || ex.subject || 'Support ticket',
urgency: r.urgency || r.priority || r.impact_level || ex.urgency || 'p2',
agent: r.agent_email || r.handler || r.assigned_agent || ex.agent || null,
state: r.state || r.ticket_state || r.resolution_status || ex.state || 'new',
customer_id: r.customer_id || ex.customer_id,
highlighted: ex.highlighted || false,
});
});
this.zdTickets = Array.from(m.values());
}
const sfRaw = parseDictStr(states.salesforce || '');
if (sfRaw.length > 0 || this.sfAccounts.length === 0) {
const m = new Map(this.sfAccounts.map(a => [a.id, a]));
sfRaw.forEach(r => {
const id = r.account_id || r.id; if (!id) return;
const ex = m.get(id) || {};
m.set(id, {
...ex, id,
name: r.company_name || ex.name || id,
health: r.health || r.account_health || r.risk_score || ex.health || 'green',
stage: r.deal_stage || r.pipeline_stage || r.stage || ex.stage || 'Qualified',
owner: r.owner_name || r.account_owner || r.rep_email || r.owner || ex.owner || null,
arr: r.arr || r.annual_recurring_revenue || ex.arr || null,
churn_risk: ex.churn_risk || false,
highlighted: ex.highlighted || false,
});
});
this.sfAccounts = Array.from(m.values());
}
const wdRaw = parseDictStr(states.workday || '');
if (wdRaw.length > 0 || this.wdEmployees.length === 0) {
const m = new Map(this.wdEmployees.map(e => [e.id, e]));
wdRaw.forEach(r => {
const id = r.employee_id || r.id; if (!id) return;
const ex = m.get(id) || {};
m.set(id, {
...ex, id,
name: r.name || ex.name || id,
department: r.department || ex.department || 'β€”',
level: r.level || r.job_level || r.seniority || ex.level || 'β€”',
status: r.status || r.request_status || r.approval_state || ex.status || 'active',
highlighted: ex.highlighted || false,
});
});
this.wdEmployees = Array.from(m.values());
}
},
// ─────────────────────────────────────────
_pushLog(entry) {
this.actionLog.push(entry);
this.$nextTick(() => {
const el = document.getElementById('log-scroll');
if (el) el.scrollTop = el.scrollHeight;
});
},
_initRewardChart() {
const ctx = document.getElementById('rewardChart').getContext('2d');
return new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
data: [],
borderColor: '#10B981',
backgroundColor: 'rgba(16,185,129,0.08)',
borderWidth: 1.5,
pointRadius: 0,
tension: 0.4,
fill: true,
}],
},
options: {
animation: false,
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false }, tooltip: { enabled: false } },
scales: {
x: { display: false },
y: {
display: true,
grid: { color: 'rgba(0,0,0,0.04)' },
ticks: { color: '#9CA3AF', font: { size: 9, family: 'DM Mono' }, maxTicksLimit: 4 },
},
},
},
});
},
_initGauge() {
const ctx = document.getElementById('gaugeChart').getContext('2d');
return new Chart(ctx, {
type: 'doughnut',
data: {
datasets: [{
data: [0.001, 0.999],
backgroundColor: ['#111827', '#F3F4F6'],
borderWidth: 0,
circumference: 240,
rotation: 240,
}],
},
options: {
animation: { duration: 400 },
responsive: false,
cutout: '78%',
plugins: { legend: { display: false }, tooltip: { enabled: false } },
},
});
},
_updateChart() {
if (!_chartInst) return;
_chartInst.data.labels = this.rewardHistory.map((_, i) => i + 1);
_chartInst.data.datasets[0].data = this.rewardHistory;
_chartInst.update('none');
},
_updateGauge() {
if (!_gaugeInst) return;
const v = Math.max(0.001, Math.min(0.999, this.currentScore));
_gaugeInst.data.datasets[0].data = [v, 1 - v];
_gaugeInst.update();
},
};
}
</script>
</body>
</html>