722 lines
34 KiB
HTML
722 lines
34 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=4">
|
||
<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">Audit Log</a>
|
||
<a href="feedback.html">Feedback & 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 Manager</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">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">Multimodal Chat</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 Builder</a>
|
||
<a href="schedules.html">Scheduled Jobs</a>
|
||
<a href="workflows.html">Workflow Automation</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" style="position:relative">🔔</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 style="padding:16px;font-size:12px;color:var(--lt);text-align:center">No rooms yet</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">💬</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">🤖</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()">➤</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()">✕</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 style="padding:16px;font-size:12px;color:var(--lt);text-align:center">No rooms yet — create one!</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 ? '🤖' : '💬'} ${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">🤖</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})">✕</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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
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"></script>
|
||
</body>
|
||
</html>
|