aipackage/cezen-portal/chatrooms.html
2026-06-30 10:51:41 +05:30

722 lines
34 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 &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 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,'&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"></script>
</body>
</html>