aipackage/cezen-portal/chatrooms.html
Jino Jose b3eec34158 Sync cezen-portal/ from live working copy (V1.0 GA polish + search-icon ordering fix)
Brings the ISO-packaged portal copy current with the live cezen-portal/
working directory, which had drifted across the entire V1.0 GA polish
initiative (Phases 1-10) plus this session's topnav icon-order bugfix.

- 52 existing files updated, 2 new pages added (about.html, health.html)
- cgit/cezen-portal/ is now byte-identical to cezen-portal/
- Backend files (rag_ingest.py, cezen_license.py) already in sync;
  main.py intentionally left as-is (cgit is ahead there with
  workstation-category commercial-fit logic not yet backported to the
  live working copy)

Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
2026-07-09 11:11:38 +05:30

723 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Rooms — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=12">
<style>
/* ── Full-height layout ── */
body { overflow: hidden; }
.cr-layout { display: grid; grid-template-columns: 260px 1fr; height: calc(100vh - 64px); overflow: hidden; }
@media(max-width:768px){ .cr-layout { grid-template-columns: 1fr; } }
/* ── Sidebar ── */
.cr-sidebar { border-right: 1px solid var(--bdr); background:var(--navy2); display: flex; flex-direction: column; overflow: hidden; }
.cr-sidebar-header { padding: 16px 14px 10px; border-bottom: 1px solid var(--bdr); flex-shrink: 0; }
.cr-sidebar-header h3 { font-size: 12px; font-weight: 700; color: var(--lt); text-transform: uppercase; letter-spacing: .5px; margin: 0 0 10px; }
.cr-new-btn { display: flex; align-items: center; gap: 8px; width: 100%; padding: 9px 12px; border-radius: 8px; border: 1.5px dashed var(--bdr); background:var(--navy2); cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 600; color: var(--med); transition: .15s; }
.cr-new-btn:hover { border-color: var(--purple); color: var(--purple); }
.cr-room-list { flex: 1; overflow-y: auto; padding: 8px; }
.cr-room-item { padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: .1s; border: 1px solid transparent; margin-bottom: 4px; position: relative; }
.cr-room-item:hover { background: var(--bg); }
.cr-room-item.active { background:rgba(124,58,237,.12); border-color:rgba(124,58,237,.4); }
.cr-room-name { font-size: 13px; font-weight: 600; color: var(--ink); display: flex; align-items: center; gap: 6px; }
.cr-room-meta { font-size: 11px; color: var(--lt); margin-top: 3px; }
.cr-unread { background: var(--purple); color: white; border-radius: 20px; font-size: 10px; font-weight: 700; padding: 1px 6px; }
/* ── Main chat area ── */
.cr-main { display: flex; flex-direction: column; background: var(--bg); overflow: hidden; }
/* ── Chat header ── */
.cr-chat-header { background:var(--navy2); border-bottom: 1px solid var(--bdr); padding: 14px 20px; display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
.cr-chat-title { font-size: 15px; font-weight: 700; color: var(--ink); flex: 1; }
.cr-chat-desc { font-size: 12px; color: var(--lt); margin-top: 1px; }
.cr-header-btn { background: none; border: 1.5px solid var(--bdr); border-radius: 8px; padding: 6px 12px; font-size: 12px; font-weight: 600; color: var(--med); cursor: pointer; font-family: inherit; transition: .15s; }
.cr-header-btn:hover { border-color: var(--purple); color: var(--purple); }
/* ── Messages ── */
.cr-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
/* ── Empty state ── */
.cr-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; color: var(--lt); padding: 40px; }
.cr-empty-icon { font-size: 56px; margin-bottom: 16px; }
.cr-empty-title { font-size: 20px; font-weight: 700; color: var(--ink); margin-bottom: 8px; }
.cr-empty-sub { font-size: 14px; max-width: 400px; line-height: 1.6; }
/* ── Message bubbles ── */
.cr-msg { display: flex; gap: 10px; align-items: flex-end; max-width: 78%; }
.cr-msg.mine { margin-left: auto; flex-direction: row-reverse; }
.cr-msg.ai { max-width: 85%; }
.cr-msg.system { margin: 0 auto; max-width: 100%; }
.cr-avatar { width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; font-weight: 700; }
.cr-msg.mine .cr-avatar { background: var(--purple); color: white; }
.cr-msg.other .cr-avatar { background: #E2E8F0; color: var(--ink); }
.cr-msg.ai .cr-avatar { background: var(--navy); color: white; }
.cr-bubble-wrap { display: flex; flex-direction: column; gap: 3px; }
.cr-msg.mine .cr-bubble-wrap { align-items: flex-end; }
.cr-sender { font-size: 11px; font-weight: 600; color: var(--lt); padding: 0 4px; }
.cr-msg.mine .cr-sender { color: var(--purple); }
.cr-msg.ai .cr-sender { color: var(--navy); }
.cr-bubble { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.6; color: var(--ink); word-break: break-word; white-space: pre-wrap; }
.cr-msg.mine .cr-bubble { background: var(--purple); color: white; border-radius: 14px 4px 14px 14px; }
.cr-msg.other .cr-bubble { background:var(--navy2); border: 1px solid var(--bdr); border-radius: 4px 14px 14px 14px; }
.cr-msg.ai .cr-bubble { background:rgba(59,130,246,.1); border: 1px solid #BFDBFE; border-radius: 4px 14px 14px 14px; }
.cr-msg.system .cr-bubble { background: none; color: var(--lt); font-size: 12px; text-align: center; border: none; padding: 4px; }
.cr-time { font-size: 10px; color: var(--lt); padding: 0 4px; }
.cr-msg.mine .cr-time { text-align: right; }
/* ── Typing indicator ── */
.cr-typing { display: none; align-items: center; gap: 8px; padding: 4px 0; }
.cr-typing.show { display: flex; }
.cr-typing-dots span { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--lt); animation: bounce 1.2s infinite ease-in-out; }
.cr-typing-dots span:nth-child(2) { animation-delay: .2s; }
.cr-typing-dots span:nth-child(3) { animation-delay: .4s; }
@keyframes bounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-6px)} }
.cr-typing-label { font-size: 12px; color: var(--lt); }
/* ── Input area ── */
.cr-input-area { border-top: 1px solid var(--bdr); background:var(--navy2); padding: 14px 16px; flex-shrink: 0; }
.cr-input-row { display: flex; gap: 10px; align-items: flex-end; }
#cr-input {
flex: 1; padding: 10px 14px; border: 1.5px solid var(--bdr); border-radius: 10px;
font-family: inherit; font-size: 14px; color: var(--ink); resize: none;
min-height: 42px; max-height: 140px; overflow-y: auto; line-height: 1.5;
box-sizing: border-box;
}
#cr-input:focus { outline: none; border-color: var(--purple); }
.cr-send-btn { background: var(--purple); color: white; border: none; border-radius: 10px; padding: 0 18px; height: 42px; cursor: pointer; font-size: 18px; flex-shrink: 0; transition: .15s; }
.cr-send-btn:hover { filter: brightness(1.08); }
.cr-send-btn:disabled { opacity: .45; cursor: not-allowed; }
.cr-hint { font-size: 11px; color: var(--lt); margin-top: 6px; }
.cr-hint kbd { background: var(--bg); border: 1px solid var(--bdr); border-radius: 4px; padding: 1px 5px; font-size: 10px; }
/* ── Room panel (right panel) ── */
.cr-panel { position: fixed; top: 64px; right: 0; bottom: 0; width: 300px; background:var(--navy2); border-left: 1px solid var(--bdr); z-index: 200; transform: translateX(100%); transition: transform .25s ease; display: flex; flex-direction: column; }
.cr-panel.open { transform: translateX(0); }
.cr-panel-header { padding: 16px 18px; border-bottom: 1px solid var(--bdr); display: flex; align-items: center; justify-content: space-between; }
.cr-panel-title { font-size: 14px; font-weight: 700; color: var(--ink); }
.cr-panel-close { background: none; border: none; font-size: 18px; cursor: pointer; color: var(--lt); }
.cr-panel-body { flex: 1; overflow-y: auto; padding: 16px; }
.cr-panel-section { margin-bottom: 20px; }
.cr-panel-label { font-size: 11px; font-weight: 700; color: var(--lt); text-transform: uppercase; letter-spacing: .4px; margin-bottom: 8px; }
.cr-panel-field { padding: 9px 12px; border: 1.5px solid var(--bdr); border-radius: 8px; font-family: inherit; font-size: 13px; color: var(--ink); width: 100%; box-sizing: border-box; }
.cr-panel-field:focus { outline: none; border-color: var(--purple); }
textarea.cr-panel-field { resize: vertical; min-height: 72px; }
.cr-member-item { display: flex; align-items: center; gap: 8px; padding: 7px 0; border-bottom: 1px solid var(--bg); }
.cr-member-avatar { width: 26px; height: 26px; border-radius: 50%; background: var(--purple); color: white; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.cr-member-name { font-size: 13px; color: var(--ink); flex: 1; }
.cr-member-role { font-size: 10px; font-weight: 700; color: var(--lt); text-transform: uppercase; }
.cr-member-del { background: none; border: none; cursor: pointer; color: var(--lt); font-size: 14px; }
.cr-member-del:hover { color: #B91C1C; }
.cr-toggle-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.cr-toggle-label { font-size: 13px; color: var(--ink); }
.cr-toggle { position: relative; width: 40px; height: 22px; }
.cr-toggle input { opacity: 0; width: 0; height: 0; }
.cr-toggle-slider { position: absolute; inset: 0; background: #CBD5E1; border-radius: 22px; cursor: pointer; transition: .2s; }
.cr-toggle-slider:before { content: ''; position: absolute; width: 16px; height: 16px; border-radius: 50%; background:var(--navy2); top: 3px; left: 3px; transition: .2s; }
.cr-toggle input:checked + .cr-toggle-slider { background: var(--purple); }
.cr-toggle input:checked + .cr-toggle-slider:before { transform: translateX(18px); }
/* ── Create room modal ── */
.cr-modal-bg { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 500; }
.cr-modal-bg.open { display: flex; align-items: center; justify-content: center; }
.cr-modal { background:var(--navy2); border-radius: 16px; padding: 28px; width: 480px; max-width: 94vw; }
.cr-modal h2 { font-size: 18px; font-weight: 700; color: var(--ink); margin: 0 0 20px; }
.cr-modal-field { margin-bottom: 14px; }
.cr-modal-field label { display: block; font-size: 11px; font-weight: 700; color: var(--lt); text-transform: uppercase; letter-spacing: .4px; margin-bottom: 5px; }
.cr-modal-field input, .cr-modal-field textarea, .cr-modal-field select { width: 100%; box-sizing: border-box; padding: 9px 12px; border: 1.5px solid var(--bdr); border-radius: 8px; font-family: inherit; font-size: 13px; color: var(--ink); }
.cr-modal-field input:focus, .cr-modal-field textarea:focus, .cr-modal-field select:focus { outline: none; border-color: var(--purple); }
.cr-modal-field textarea { resize: vertical; min-height: 64px; }
.cr-modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
</style>
</head>
<body>
<header class="topnav">
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
<nav>
<a href="index.html">Home</a>
<a href="quickstart.html">Quick Start</a>
<a href="prompts.html">Prompt Library</a>
<a href="usecases.html">Use Cases</a>
<span class="nav-sep"></span>
<div class="nav-dropdown">
<button class="nav-drop-btn">Help ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">LEARN /</span>
<a href="quickstart.html">Quick Start</a>
<a href="models.html">Models</a>
<span class="nav-drop-cat">SUPPORT /</span>
<a href="troubleshooting.html">Troubleshoot</a>
<a href="faq.html">FAQ</a>
<span class="nav-drop-cat">MORE /</span>
<a href="glossary.html">Glossary</a>
<a href="whats-new.html">What's New</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn">Admin ▾</button>
<div class="nav-drop-menu nav-drop-menu-wide">
<span class="nav-drop-cat">DOCS /</span>
<a href="security.html">Security & Privacy</a>
<a href="admin.html">Admin Guide</a>
<span class="nav-drop-cat">MONITOR /</span>
<a href="dashboard.html">Dashboard</a>
<a href="analytics.html">Usage Analytics</a>
<a href="audit.html">Governance & Audit</a>
<a href="health.html">Health Center</a>
<a href="feedback.html">Feedback &amp; Ratings</a>
<span class="nav-drop-cat">MANAGE /</span>
<a href="users.html">Users</a>
<a href="teams.html">Teams</a>
<a href="models-admin.html">Model Catalog</a>
<a href="training.html">Training</a>
<a href="knowledge.html">Knowledge Base</a>
<span class="nav-drop-cat">TOOLS /</span>
<a href="apikeys.html">API Keys</a>
<a href="benchmark.html">Benchmarking</a>
<a href="model-compare.html">Model Compare</a>
<a href="api-playground.html">API Playground</a>
<a href="guardrails.html">Guardrails</a>
<a href="rag-quality.html">RAG Quality</a>
<a href="router.html">Model Router</a>
<a href="connectors.html">MCP & Enterprise Connectors</a>
<span class="nav-drop-cat">SYSTEM /</span>
<a href="console.html">Console</a>
<a href="settings.html">Settings</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn active">AI Tools ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">INTELLIGENCE /</span>
<a href="documents.html">Document Intelligence</a>
<a href="chat-multi.html">AI Workspace</a>
<a href="prompt-studio.html">Prompt Studio</a>
<a href="meeting.html">Meeting Assistant</a>
<span class="nav-drop-cat">AUTOMATION /</span>
<a href="agents.html">Agent Studio</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Designer</a>
<span class="nav-drop-cat">QUALITY /</span>
<a href="evals.html">AI Eval Suite</a>
<a href="chatrooms.html">Chat Rooms</a>
</div>
</div>
</nav>
<a href="notifications.html" class="nav-icon-btn" style="position:relative" aria-label="Notifications"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M15 17h5l-1.4-1.4A2 2 0 0 1 18 14.2V11a6 6 0 1 0-12 0v3.2a2 2 0 0 1-.6 1.4L4 17h5"/><path d="M10 17a2 2 0 0 0 4 0"/></svg></span></a>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
<div class="cr-layout">
<!-- Sidebar -->
<aside class="cr-sidebar">
<div class="cr-sidebar-header">
<h3>Chat Rooms</h3>
<button class="cr-new-btn" onclick="openCreateModal()">New Room</button>
</div>
<div class="cr-room-list" id="room-list">
<div class="cezen-list-empty">No rooms yet &mdash; <a onclick="openCreateModal()">create your first one</a>.</div>
</div>
</aside>
<!-- Main -->
<main class="cr-main" id="cr-main">
<!-- Empty state -->
<div class="cr-empty" id="cr-empty">
<div class="cr-empty-icon">Chat</div>
<div class="cr-empty-title">Secure Chat Rooms</div>
<div class="cr-empty-sub">
Collaborate with your team in topic-based rooms. Type <strong>@AI</strong> anywhere in a message to get an instant AI response. All conversations stay on-premises.
</div>
<div style="margin-top:24px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
<button class="btn btn-primary" onclick="openCreateModal('general')">General</button>
<button class="btn btn-ghost" onclick="openCreateModal('procurement')">Procurement</button>
<button class="btn btn-ghost" onclick="openCreateModal('legal')">Legal</button>
</div>
</div>
<!-- Chat area (hidden until room selected) -->
<div id="cr-chat" style="display:none;flex:1;display:none;flex-direction:column;overflow:hidden">
<div class="cr-chat-header">
<div style="flex:1">
<div class="cr-chat-title" id="chat-title">Room Name</div>
<div class="cr-chat-desc" id="chat-desc"></div>
</div>
<button class="cr-header-btn" onclick="togglePanel()">Settings</button>
</div>
<div class="cr-messages" id="cr-messages">
<div class="cr-typing" id="cr-typing">
<div style="width:30px;height:30px;border-radius:50%;background:var(--navy);color:var(--ink);display:flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0">AI</div>
<div>
<div style="font-size:11px;font-weight:600;color:var(--lt);margin-bottom:4px">AI Assistant</div>
<div class="cr-typing-dots"><span></span><span></span><span></span></div>
</div>
</div>
</div>
<div class="cr-input-area">
<div class="cr-input-row">
<textarea id="cr-input" placeholder="Message… type @AI to ask the assistant" rows="1"
onkeydown="onKeyDown(event)" oninput="autoResize(this)"></textarea>
<button class="cr-send-btn" onclick="sendMessage()">Send</button>
</div>
<div class="cr-hint">Press <kbd>Enter</kbd> to send · <kbd>Shift+Enter</kbd> for new line · type <kbd>@AI</kbd> to invoke AI</div>
</div>
</div>
</main>
</div>
<!-- Room settings panel -->
<div class="cr-panel" id="cr-panel">
<div class="cr-panel-header">
<span class="cr-panel-title">Room Settings</span>
<button class="cr-panel-close" onclick="togglePanel()">Close</button>
</div>
<div class="cr-panel-body">
<div class="cr-panel-section">
<div class="cr-panel-label">Room Name</div>
<input class="cr-panel-field" type="text" id="panel-name">
</div>
<div class="cr-panel-section">
<div class="cr-panel-label">Description</div>
<input class="cr-panel-field" type="text" id="panel-desc">
</div>
<div class="cr-panel-section">
<div class="cr-panel-label">AI Topic / System Prompt</div>
<textarea class="cr-panel-field" id="panel-topic" rows="4" placeholder="e.g. You are a procurement assistant helping the team review contracts and tenders…"></textarea>
</div>
<div class="cr-panel-section">
<div class="cr-panel-label">AI Model</div>
<select class="cr-panel-field" id="panel-model">
<option value="">Auto (default)</option>
</select>
</div>
<div class="cr-panel-section">
<div class="cr-toggle-row">
<span class="cr-toggle-label">Auto AI (respond to every message)</span>
<label class="cr-toggle">
<input type="checkbox" id="panel-ai-auto">
<span class="cr-toggle-slider"></span>
</label>
</div>
</div>
<button class="btn btn-primary" style="width:100%;margin-bottom:14px" onclick="saveRoomSettings()">Save Settings</button>
<button class="btn btn-ghost" style="width:100%;color:#B91C1C;border-color:#FCA5A5" onclick="deleteRoom()">Delete Room</button>
<div class="cr-panel-section" style="margin-top:24px">
<div class="cr-panel-label">Members</div>
<div id="panel-members"></div>
<div style="display:flex;gap:8px;margin-top:10px">
<input class="cr-panel-field" type="text" id="add-member-input" placeholder="Username to add" style="flex:1">
<button class="btn btn-ghost" style="font-size:12px" onclick="addMember()">Add</button>
</div>
</div>
</div>
</div>
<!-- Create room modal -->
<div class="cr-modal-bg" id="create-modal" onclick="closeModalBg(event)">
<div class="cr-modal">
<h2>Create Chat Room</h2>
<div class="cr-modal-field">
<label>Room Name</label>
<input type="text" id="new-name" placeholder="e.g. Procurement Team">
</div>
<div class="cr-modal-field">
<label>Description</label>
<input type="text" id="new-desc" placeholder="What this room is for">
</div>
<div class="cr-modal-field">
<label>AI Topic / System Prompt <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--lt)">(optional)</span></label>
<textarea id="new-topic" placeholder="Give the AI context for this room — e.g. 'You are a legal assistant helping review government contracts.'"></textarea>
</div>
<div class="cr-modal-field">
<label>AI Model</label>
<select id="new-model">
<option value="">Auto (default)</option>
</select>
</div>
<div class="cr-modal-field" style="display:flex;align-items:center;gap:10px">
<label class="cr-toggle" style="margin:0">
<input type="checkbox" id="new-ai-auto">
<span class="cr-toggle-slider"></span>
</label>
<span style="font-size:13px;color:var(--ink)">Auto AI — respond to every message</span>
</div>
<div class="cr-modal-actions">
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="createRoom()">Create Room</button>
</div>
</div>
</div>
<script>
const _API = '/api';
const MOCK_ROOMS = [
{id:1, name:'Executive AI Briefing', description:'Leadership strategy discussions', visibility:'private', model:'llama3:70b', member_count:5, message_count:142, created_at:'2026-05-01T09:00:00Z'},
{id:2, name:'HR Helpdesk', description:'HR queries and policy assistance', visibility:'team', model:'mistral:7b', member_count:18, message_count:389, created_at:'2026-05-10T10:00:00Z'},
{id:3, name:'Dev Support', description:'Code review and technical help', visibility:'team', model:'codellama:34b', member_count:9, message_count:217, created_at:'2026-05-15T11:00:00Z'},
{id:4, name:'All Staff', description:'Company-wide AI assistant', visibility:'public', model:'llama3:8b', member_count:47, message_count:1204, created_at:'2026-04-01T08:00:00Z'},
];
let rooms = [];
let currentRoom = null;
let currentUser = null;
let lastMsgId = 0;
let pollTimer = null;
let aiWaiting = false;
let models = [];
// ── Auth ──────────────────────────────────────────────────────────────────────
document.addEventListener('cezenAuthReady', e => {
currentUser = e.detail;
loadRooms();
loadModels();
});
// ── Rooms ─────────────────────────────────────────────────────────────────────
async function loadRooms() {
try {
const res = await fetch(`${_API}/rooms`, { credentials:'include' });
rooms = await res.json();
if(!rooms.length) throw new Error();
renderSidebar();
} catch(e) { rooms = MOCK_ROOMS; renderSidebar(); }
}
function renderSidebar() {
const el = document.getElementById('room-list');
if (!rooms.length) {
el.innerHTML = '<div class="cezen-list-empty">No rooms yet &mdash; <a onclick="openCreateModal()">create your first one</a>.</div>';
return;
}
el.innerHTML = rooms.map(r => `
<div class="cr-room-item${r.id === currentRoom?.id ? ' active':''}" onclick="selectRoom(${r.id})">
<div class="cr-room-name">
${r.ai_auto ? 'AI' : 'Chat'} ${esc(r.name)}
</div>
<div class="cr-room-meta">${r.member_count} member${r.member_count !== 1 ? 's':''}</div>
</div>
`).join('');
}
async function selectRoom(id) {
const room = rooms.find(r => r.id === id);
if (!room) return;
currentRoom = room;
lastMsgId = 0;
aiWaiting = false;
renderSidebar();
document.getElementById('cr-empty').style.display = 'none';
const chat = document.getElementById('cr-chat');
chat.style.display = 'flex';
chat.style.flexDirection = 'column';
chat.style.overflow = 'hidden';
chat.style.flex = '1';
document.getElementById('chat-title').textContent = room.name;
document.getElementById('chat-desc').textContent = room.description || '';
document.getElementById('cr-messages').innerHTML = '';
// Populate panel
document.getElementById('panel-name').value = room.name;
document.getElementById('panel-desc').value = room.description || '';
document.getElementById('panel-topic').value = room.topic || '';
document.getElementById('panel-model').value = room.ai_model || '';
document.getElementById('panel-ai-auto').checked = !!room.ai_auto;
renderPanelMembers(room.members || []);
if (pollTimer) clearInterval(pollTimer);
await fetchMessages();
pollTimer = setInterval(fetchMessages, 2500);
}
// ── Messages ──────────────────────────────────────────────────────────────────
async function fetchMessages() {
if (!currentRoom) return;
try {
const res = await fetch(`${_API}/rooms/${currentRoom.id}/messages?since_id=${lastMsgId}`, { credentials:'include' });
const msgs = await res.json();
if (!msgs.length) return;
const container = document.getElementById('cr-messages');
const wasAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 60;
for (const m of msgs) {
appendMessage(m);
lastMsgId = Math.max(lastMsgId, m.id);
}
// Hide typing if AI responded
const hadAssistant = msgs.some(m => m.sender_role === 'assistant');
if (hadAssistant) {
aiWaiting = false;
document.getElementById('cr-typing').classList.remove('show');
}
if (wasAtBottom) container.scrollTop = container.scrollHeight;
} catch(e) {}
}
function appendMessage(m) {
const container = document.getElementById('cr-messages');
const typingEl = document.getElementById('cr-typing');
const div = document.createElement('div');
const isMe = currentUser && m.user_id == currentUser.id;
if (m.sender_role === 'system') {
div.className = 'cr-msg system';
div.innerHTML = `<div class="cr-bubble">${esc(m.content)}</div>`;
} else if (m.sender_role === 'assistant') {
div.className = 'cr-msg ai';
div.innerHTML = `
<div class="cr-avatar">AI</div>
<div class="cr-bubble-wrap">
<div class="cr-sender">AI Assistant</div>
<div class="cr-bubble">${esc(m.content)}</div>
<div class="cr-time">${fmtTime(m.created_at)}</div>
</div>`;
} else {
div.className = `cr-msg ${isMe ? 'mine' : 'other'}`;
const initials = (m.username || '?')[0].toUpperCase();
div.innerHTML = `
${!isMe ? `<div class="cr-avatar">${initials}</div>` : ''}
<div class="cr-bubble-wrap">
${!isMe ? `<div class="cr-sender">${esc(m.username)}</div>` : ''}
<div class="cr-bubble">${esc(m.content)}</div>
<div class="cr-time">${fmtTime(m.created_at)}</div>
</div>
${isMe ? `<div class="cr-avatar">${initials}</div>` : ''}`;
}
// Insert before typing indicator
container.insertBefore(div, typingEl);
}
async function sendMessage() {
const input = document.getElementById('cr-input');
const content = input.value.trim();
if (!content || !currentRoom) return;
input.value = '';
autoResize(input);
const mentionsAI = content.toLowerCase().includes('@ai') || currentRoom.ai_auto;
if (mentionsAI) {
aiWaiting = true;
const typing = document.getElementById('cr-typing');
typing.classList.add('show');
const container = document.getElementById('cr-messages');
container.scrollTop = container.scrollHeight;
}
try {
await fetch(`${_API}/rooms/${currentRoom.id}/messages`, {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ content })
});
await fetchMessages();
} catch(e) {
aiWaiting = false;
document.getElementById('cr-typing').classList.remove('show');
}
}
function onKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 140) + 'px';
}
// ── Panel ─────────────────────────────────────────────────────────────────────
function togglePanel() {
document.getElementById('cr-panel').classList.toggle('open');
}
function renderPanelMembers(members) {
const el = document.getElementById('panel-members');
el.innerHTML = members.map(m => `
<div class="cr-member-item">
<div class="cr-member-avatar">${m.username[0].toUpperCase()}</div>
<div class="cr-member-name">${esc(m.username)}</div>
<div class="cr-member-role">${m.role}</div>
${m.role !== 'admin' ? `<button class="cr-member-del" onclick="removeMember(${m.user_id})">Remove</button>` : ''}
</div>
`).join('') || '<div style="font-size:12px;color:var(--lt)">No members</div>';
}
async function saveRoomSettings() {
if (!currentRoom) return;
const body = {
name: document.getElementById('panel-name').value.trim(),
description: document.getElementById('panel-desc').value.trim(),
topic: document.getElementById('panel-topic').value.trim(),
ai_model: document.getElementById('panel-model').value,
ai_auto: document.getElementById('panel-ai-auto').checked ? 1 : 0,
};
try {
const res = await fetch(`${_API}/rooms/${currentRoom.id}`, {
method:'PUT', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(body)
});
const updated = await res.json();
currentRoom = updated;
document.getElementById('chat-title').textContent = updated.name;
document.getElementById('chat-desc').textContent = updated.description || '';
await loadRooms();
togglePanel();
} catch(e) { alert('Save failed'); }
}
async function addMember() {
const username = document.getElementById('add-member-input').value.trim();
if (!username || !currentRoom) return;
try {
await fetch(`${_API}/rooms/${currentRoom.id}/members`, {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ username })
});
document.getElementById('add-member-input').value = '';
const res = await fetch(`${_API}/rooms/${currentRoom.id}`, { credentials:'include' });
currentRoom = await res.json();
renderPanelMembers(currentRoom.members || []);
loadRooms();
} catch(e) { alert('Could not add member — check the username.'); }
}
async function removeMember(uid) {
if (!currentRoom) return;
await fetch(`${_API}/rooms/${currentRoom.id}/members/${uid}`, { method:'DELETE', credentials:'include' });
const res = await fetch(`${_API}/rooms/${currentRoom.id}`, { credentials:'include' });
currentRoom = await res.json();
renderPanelMembers(currentRoom.members || []);
loadRooms();
}
async function deleteRoom() {
if (!currentRoom || !confirm('Delete this room and all its messages?')) return;
await fetch(`${_API}/rooms/${currentRoom.id}`, { method:'DELETE', credentials:'include' });
currentRoom = null;
lastMsgId = 0;
if (pollTimer) clearInterval(pollTimer);
document.getElementById('cr-empty').style.display = '';
document.getElementById('cr-chat').style.display = 'none';
document.getElementById('cr-panel').classList.remove('open');
await loadRooms();
}
// ── Create modal ──────────────────────────────────────────────────────────────
function openCreateModal(template) {
document.getElementById('new-name').value = '';
document.getElementById('new-desc').value = '';
document.getElementById('new-topic').value = '';
document.getElementById('new-model').value = '';
document.getElementById('new-ai-auto').checked = false;
if (template === 'general') {
document.getElementById('new-name').value = 'General';
document.getElementById('new-desc').value = 'General team discussion';
document.getElementById('new-topic').value = 'You are a helpful AI assistant for the team. Answer questions clearly and concisely.';
} else if (template === 'procurement') {
document.getElementById('new-name').value = 'Procurement';
document.getElementById('new-desc').value = 'Tender reviews, contracts, vendor evaluation';
document.getElementById('new-topic').value = 'You are a procurement assistant. Help the team review contracts, identify risks, summarise tenders, and evaluate vendor proposals.';
} else if (template === 'legal') {
document.getElementById('new-name').value = 'Legal';
document.getElementById('new-desc').value = 'Legal queries and document review';
document.getElementById('new-topic').value = 'You are a legal assistant. Help the team understand regulations, review contract clauses, and flag compliance issues. Always note that your responses are informational and not legal advice.';
}
document.getElementById('create-modal').classList.add('open');
}
function closeModal() {
document.getElementById('create-modal').classList.remove('open');
}
function closeModalBg(e) {
if (e.target.id === 'create-modal') closeModal();
}
async function createRoom() {
const name = document.getElementById('new-name').value.trim();
if (!name) { alert('Please enter a room name.'); return; }
const body = {
name,
description: document.getElementById('new-desc').value.trim(),
topic: document.getElementById('new-topic').value.trim(),
ai_model: document.getElementById('new-model').value,
ai_auto: document.getElementById('new-ai-auto').checked ? 1 : 0,
};
try {
const res = await fetch(`${_API}/rooms`, {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(body)
});
if (!res.ok) throw new Error((await res.json()).detail || 'Failed');
const room = await res.json();
closeModal();
await loadRooms();
selectRoom(room.id);
} catch(e) { alert('Failed: ' + e.message); }
}
// ── Models ────────────────────────────────────────────────────────────────────
async function loadModels() {
try {
const res = await fetch(`${_API}/models/list`, { credentials:'include' });
const data = await res.json();
models = (data.models || []).map(m => m.name);
const opts = '<option value="">Auto (default)</option>' +
models.map(m => `<option value="${esc(m)}">${esc(m)}</option>`).join('');
document.getElementById('new-model').innerHTML = opts;
document.getElementById('panel-model').innerHTML = opts;
} catch(e) {}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function fmtTime(iso) {
const d = new Date(iso);
return d.toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' });
}
</script>
<script src="auth.js"></script>
<script src="branding.js?v=3"></script>
</body>
</html>