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.
This commit is contained in:
1 parent
7540da04a8
commit
b3eec34158
57 files changed
+2832
-1354
No files matched your search
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Workspace — Nexus One AI</title>
|
||||
<link rel="stylesheet" href="style.css?v=4">
|
||||
<link rel="stylesheet" href="style.css?v=12">
|
||||
<style>
|
||||
/* ── Layout ─────────────────────────────────────────────────────────────── */
|
||||
.mm-layout { display: grid; grid-template-columns: 260px 1fr; gap: 0; height: calc(100vh - 64px); overflow: hidden; }
|
||||
@@ -44,13 +44,15 @@
|
||||
.mm-search-wrap input:focus { outline: none; border-color: var(--purple); }
|
||||
.mm-search-wrap .mm-search-icon {
|
||||
position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
|
||||
font-size: 13px; color: var(--lt); pointer-events: none;
|
||||
width: 13px; height: 13px; color: var(--lt); pointer-events: none;
|
||||
}
|
||||
.mm-search-wrap .mm-search-icon svg { width: 100%; height: 100%; }
|
||||
.mm-search-clear {
|
||||
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
|
||||
background: none; border: none; color: var(--lt); cursor: pointer; font-size: 14px;
|
||||
background: none; border: none; color: var(--lt); cursor: pointer; width: 14px; height: 14px;
|
||||
display: none; padding: 0; line-height: 1;
|
||||
}
|
||||
.mm-search-clear svg { width: 100%; height: 100%; }
|
||||
.mm-search-clear.show { display: block; }
|
||||
|
||||
/* Chat list */
|
||||
@@ -67,7 +69,8 @@
|
||||
}
|
||||
.mm-chat-item:hover { background: var(--bg); }
|
||||
.mm-chat-item.active { background: rgba(124,58,237,.1); color: var(--purple); font-weight: 600; }
|
||||
.mm-chat-item-icon { flex-shrink: 0; font-size: 14px; }
|
||||
.mm-chat-item-icon { flex-shrink: 0; width: 14px; height: 14px; color: var(--text-secondary); }
|
||||
.mm-chat-item-icon svg { width: 100%; height: 100%; }
|
||||
.mm-chat-item-body { flex: 1; min-width: 0; }
|
||||
.mm-chat-item-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
||||
.mm-chat-item-meta { font-size: 10px; color: var(--lt); margin-top: 1px; display: flex; gap: 6px; }
|
||||
@@ -75,8 +78,9 @@
|
||||
.mm-chat-item:hover .mm-chat-item-actions { opacity: 1; }
|
||||
.mm-chat-action-btn {
|
||||
background: none; border: none; cursor: pointer; padding: 3px 5px;
|
||||
border-radius: 5px; font-size: 13px; color: var(--lt); transition: .1s;
|
||||
border-radius: 5px; color: var(--lt); transition: .1s; display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
.mm-chat-action-btn .icon-svg { width: 13px; height: 13px; }
|
||||
.mm-chat-action-btn:hover { background: var(--bdr); color: var(--ink); }
|
||||
.mm-pin-icon { color: var(--purple); font-size: 11px; }
|
||||
|
||||
@@ -132,7 +136,8 @@
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
flex: 1; text-align: center; color: var(--lt); padding: 40px 20px;
|
||||
}
|
||||
.mm-empty-icon { font-size: 52px; margin-bottom: 14px; }
|
||||
.mm-empty-icon { width: 52px; height: 52px; margin-bottom: 14px; color: var(--purple); }
|
||||
.mm-empty-icon svg { width: 100%; height: 100%; }
|
||||
.mm-empty-title { font-size: 20px; font-weight: 700; color: var(--ink); margin-bottom: 8px; }
|
||||
.mm-empty-sub { font-size: 14px; max-width: 380px; line-height: 1.6; color: var(--med); }
|
||||
.mm-tip-grid { display: flex; gap: 8px; margin-top: 20px; flex-wrap: wrap; justify-content: center; max-width: 560px; }
|
||||
@@ -142,15 +147,17 @@
|
||||
text-align: left;
|
||||
}
|
||||
.mm-tip:hover { border-color: var(--purple); color: var(--purple); background: rgba(124,58,237,.04); }
|
||||
.mm-tip-icon { margin-right: 5px; }
|
||||
.mm-tip-icon { display:inline-flex; width:14px; height:14px; margin-right: 5px; vertical-align:middle; color:var(--text-secondary); }
|
||||
.mm-tip-icon svg { width:100%; height:100%; }
|
||||
|
||||
/* ── Message bubbles ─────────────────────────────────────────────────────── */
|
||||
.mm-msg { display: flex; gap: 10px; align-items: flex-start; position: relative; }
|
||||
.mm-msg.user { flex-direction: row-reverse; }
|
||||
.mm-msg-avatar {
|
||||
width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 13px; flex-shrink: 0; margin-top: 2px;
|
||||
flex-shrink: 0; margin-top: 2px;
|
||||
}
|
||||
.mm-msg-avatar svg { width: 14px; height: 14px; }
|
||||
.mm-msg.user .mm-msg-avatar { background: var(--purple); color: white; }
|
||||
.mm-msg.assistant .mm-msg-avatar { background: var(--ink); color: white; }
|
||||
.mm-msg-content { display: flex; flex-direction: column; gap: 4px; max-width: 78%; }
|
||||
@@ -219,7 +226,8 @@
|
||||
/* ── Typing indicator ────────────────────────────────────────────────────── */
|
||||
.mm-typing { display: none; align-items: center; gap: 10px; }
|
||||
.mm-typing.show { display: flex; }
|
||||
.mm-typing-avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--ink); color: white; display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; }
|
||||
.mm-typing-avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--ink); color: white; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.mm-typing-avatar svg { width: 14px; height: 14px; }
|
||||
.mm-dots span {
|
||||
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--lt); animation: bounce 1.2s infinite ease-in-out; margin: 0 2px;
|
||||
@@ -243,15 +251,17 @@
|
||||
.mm-image-preview .mm-img-remove {
|
||||
position: absolute; top: -6px; right: -6px;
|
||||
background: var(--ink); color: white; border: none; border-radius: 50%;
|
||||
width: 18px; height: 18px; font-size: 10px; cursor: pointer;
|
||||
width: 18px; height: 18px; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.mm-image-preview .mm-img-remove svg { width: 10px; height: 10px; }
|
||||
.mm-input-row { display: flex; gap: 8px; align-items: flex-end; }
|
||||
.mm-attach-btn {
|
||||
background: none; border: 1.5px solid var(--bdr); border-radius: 8px;
|
||||
padding: 8px 11px; cursor: pointer; font-size: 17px; transition: .15s;
|
||||
padding: 8px 11px; cursor: pointer; transition: .15s;
|
||||
flex-shrink: 0; height: 40px; display: flex; align-items: center;
|
||||
}
|
||||
.mm-attach-btn .icon-svg { width: 18px; height: 18px; }
|
||||
.mm-attach-btn:hover { border-color: var(--purple); }
|
||||
#mm-file-input { display: none; }
|
||||
.mm-text-wrap { flex: 1; position: relative; }
|
||||
@@ -265,9 +275,10 @@
|
||||
#mm-prompt:focus { outline: none; border-color: var(--purple); }
|
||||
.mm-send-btn {
|
||||
background: var(--purple); color: white; border: none; border-radius: 10px;
|
||||
padding: 0 16px; height: 40px; cursor: pointer; font-size: 17px;
|
||||
padding: 0 16px; height: 40px; cursor: pointer;
|
||||
flex-shrink: 0; transition: .15s;
|
||||
}
|
||||
.mm-send-btn .icon-svg { width: 16px; height: 16px; }
|
||||
.mm-send-btn:hover { filter: brightness(1.08); }
|
||||
.mm-send-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.mm-hint { font-size: 11px; color: var(--lt); margin-top: 5px; text-align: center; }
|
||||
@@ -345,6 +356,7 @@
|
||||
<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 & Ratings</a>
|
||||
<span class="nav-drop-cat">MANAGE /</span>
|
||||
<a href="users.html">Users</a>
|
||||
@@ -384,7 +396,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<a href="notifications.html" style="position:relative">🔔</a>
|
||||
<a href="notifications.html" class="nav-icon-btn" style="position:relative" aria-label="Notifications">
|
||||
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></span>
|
||||
</a>
|
||||
<span class="badge" data-brand="tier">Basic Tier</span>
|
||||
<div id="nav-org-logo" class="nav-org-logo"></div>
|
||||
</header>
|
||||
@@ -396,12 +410,12 @@
|
||||
<div class="mm-sidebar-header">
|
||||
<div class="mm-sidebar-top">
|
||||
<span class="mm-sidebar-title">Chats</span>
|
||||
<button class="mm-new-btn" onclick="newChat()">+ New</button>
|
||||
<button class="mm-new-btn" onclick="newChat()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span><span>New</span></span></button>
|
||||
</div>
|
||||
<div class="mm-search-wrap">
|
||||
<span class="mm-search-icon">🔍</span>
|
||||
<span class="mm-search-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="6"/><path d="m20 20-4.35-4.35"/></svg></span>
|
||||
<input type="text" id="chat-search" placeholder="Search chats…" oninput="onSearch(this.value)">
|
||||
<button class="mm-search-clear" id="search-clear" onclick="clearSearch()" title="Clear">✕</button>
|
||||
<button class="mm-search-clear" id="search-clear" onclick="clearSearch()" title="Clear" aria-label="Clear"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mm-chat-list" id="chat-list">
|
||||
@@ -431,35 +445,35 @@
|
||||
</div>
|
||||
|
||||
<button class="mm-toolbar-btn" id="pin-btn" onclick="togglePin()" title="Pin this chat" style="display:none">
|
||||
📌 Pin
|
||||
<span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 17v5"/><path d="m5 3 14 14"/><path d="m15 3 6 6-4 4-6-6"/></svg></span><span>Pin</span></span>
|
||||
</button>
|
||||
<button class="mm-toolbar-btn" id="rename-btn" onclick="openRenameModal()" title="Rename" style="display:none">
|
||||
✏️ Rename
|
||||
<span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></span><span>Rename</span></span>
|
||||
</button>
|
||||
<button class="mm-toolbar-btn" id="clear-btn" onclick="clearCurrentChat()" title="Clear chat" style="display:none">
|
||||
🗑️ Clear
|
||||
<span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></span><span>Clear</span></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="mm-messages" id="messages">
|
||||
<div class="mm-empty-state" id="empty-state">
|
||||
<div class="mm-empty-icon">💬</div>
|
||||
<div class="mm-empty-icon icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div>
|
||||
<div class="mm-empty-title">AI Workspace</div>
|
||||
<div class="mm-empty-sub">Ask questions, analyse images, and get structured answers — all running on-premises.</div>
|
||||
<div class="mm-tip-grid">
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon">🖼️</span>Analyse this circuit diagram</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon">📸</span>Describe the equipment in this photo</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon">📄</span>Extract text from this image</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon">🔬</span>What does this satellite image show?</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon">📊</span>Summarise this chart</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon">🧠</span>Compare these two diagrams</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon icon-svg"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg></span>Analyse this circuit diagram</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M5 7h14"/><path d="M7 3h10l1 4H6z"/><rect x="4" y="7" width="16" height="14" rx="2"/></svg></span>Describe the equipment in this photo</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg></span>Extract text from this image</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon icon-svg"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="6"/><path d="m20 20-4.35-4.35"/></svg></span>What does this satellite image show?</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M4 19h16"/><path d="M7 16V8"/><path d="M12 16V5"/><path d="M17 16v-3"/></svg></span>Summarise this chart</div>
|
||||
<div class="mm-tip" onclick="useTip(this)"><span class="mm-tip-icon icon-svg"><svg viewBox="0 0 24 24"><path d="M9.5 2a3.5 3.5 0 0 0-3.4 2.7A3.5 3.5 0 0 0 3 8.2a3.5 3.5 0 0 0 1.2 2.6A3.5 3.5 0 0 0 4 12a3.5 3.5 0 0 0 2.1 3.2A3.5 3.5 0 0 0 9.5 18H10v4"/><path d="M14.5 2a3.5 3.5 0 0 1 3.4 2.7A3.5 3.5 0 0 1 21 8.2a3.5 3.5 0 0 1-1.2 2.6A3.5 3.5 0 0 1 20 12a3.5 3.5 0 0 1-2.1 3.2A3.5 3.5 0 0 1 14.5 18H14v4"/><path d="M10 10h4"/><path d="M9 14h6"/></svg></span>Compare these two diagrams</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<div class="mm-typing" id="typing">
|
||||
<div class="mm-typing-avatar">🤖</div>
|
||||
<div class="mm-typing-avatar"><svg viewBox="0 0 24 24"><rect x="7" y="8" width="10" height="8" rx="2"/><path d="M12 4v4M9 16v2M15 16v2M6 10H4M20 10h-2"/><circle cx="10" cy="12" r="1"/><circle cx="14" cy="12" r="1"/></svg></div>
|
||||
<div class="mm-typing-bubble">
|
||||
<div class="mm-dots"><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
@@ -470,17 +484,17 @@
|
||||
<div class="mm-input-area">
|
||||
<div class="mm-image-preview" id="img-preview">
|
||||
<img id="img-preview-img" src="" alt="preview">
|
||||
<button class="mm-img-remove" onclick="clearImage()" title="Remove">✕</button>
|
||||
<button class="mm-img-remove" onclick="clearImage()" title="Remove" aria-label="Remove"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
|
||||
</div>
|
||||
<div class="mm-input-row">
|
||||
<label class="mm-attach-btn" title="Attach image (or paste Ctrl+V)">
|
||||
🖼️
|
||||
<span class="icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg></span>
|
||||
<input type="file" id="mm-file-input" accept="image/*">
|
||||
</label>
|
||||
<div class="mm-text-wrap">
|
||||
<textarea id="mm-prompt" placeholder="Message the AI…" rows="1"></textarea>
|
||||
</div>
|
||||
<button class="mm-send-btn" id="send-btn" onclick="sendMessage()">➤</button>
|
||||
<button class="mm-send-btn" id="send-btn" onclick="sendMessage()" aria-label="Send"><span class="icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m3 12 18-9-4 18-4-6-10-3z"/><path d="m13 15 8-12"/></svg></span></button>
|
||||
</div>
|
||||
<div class="mm-hint">Ctrl+V to paste image · Shift+Enter for new line · Enter to send</div>
|
||||
</div>
|
||||
@@ -515,6 +529,21 @@ let pendingImageB64 = null;
|
||||
let isStreaming = false;
|
||||
let availableModels = [];
|
||||
let searchQuery = '';
|
||||
function mmIconSvg(name, sizeClass = 'icon-sm') {
|
||||
const icons = {
|
||||
pin:'<svg viewBox="0 0 24 24"><path d="M12 17v5"/><path d="m5 3 14 14"/><path d="m15 3 6 6-4 4-6-6"/></svg>',
|
||||
pinFilled:'<svg viewBox="0 0 24 24"><path d="M12 17v5"/><path d="M7 3h10l-2 6 3 3-3 3H9l3-3-2-6z"/></svg>',
|
||||
chat:'<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
||||
image:'<svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>',
|
||||
trash:'<svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg>',
|
||||
edit:'<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>',
|
||||
user:'<svg viewBox="0 0 24 24"><path d="M20 21a8 8 0 0 0-16 0"/><circle cx="12" cy="7" r="4"/></svg>',
|
||||
robot:'<svg viewBox="0 0 24 24"><rect x="7" y="8" width="10" height="8" rx="2"/><path d="M12 4v4M9 16v2M15 16v2M6 10H4M20 10h-2"/><circle cx="10" cy="12" r="1"/><circle cx="14" cy="12" r="1"/></svg>',
|
||||
copy:'<svg viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>',
|
||||
check:'<svg viewBox="0 0 24 24"><path d="m20 6-11 11-5-5"/></svg>'
|
||||
};
|
||||
return `<span class="icon-svg ${sizeClass}" aria-hidden="true">${icons[name] || icons.chat}</span>`;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────────
|
||||
Persistence helpers
|
||||
@@ -624,7 +653,7 @@ function renderMarkdown(raw) {
|
||||
para = para.trim();
|
||||
if (!para) return '';
|
||||
if (/^<(h[1-6]|ul|ol|li|blockquote|hr|pre|div|table)/.test(para)) return para;
|
||||
// single newlines → <br>
|
||||
// single newlines become <br>
|
||||
return `<p>${para.replace(/\n/g, '<br>')}</p>`;
|
||||
}).join('\n');
|
||||
|
||||
@@ -739,7 +768,7 @@ function renderChatList() {
|
||||
|
||||
let html = '';
|
||||
if (pinned.length) {
|
||||
html += `<div class="mm-chat-section-label">📌 Pinned</div>`;
|
||||
html += `<div class="mm-chat-section-label">Pinned</div>`;
|
||||
html += pinned.map(c => chatItemHtml(c)).join('');
|
||||
}
|
||||
if (unpinned.length) {
|
||||
@@ -751,10 +780,10 @@ function renderChatList() {
|
||||
|
||||
function chatItemHtml(c) {
|
||||
const lastMsg = c.messages[c.messages.length - 1];
|
||||
const preview = lastMsg ? (lastMsg.content || (lastMsg.imageB64 ? '🖼️ Image' : '')).slice(0,40) : 'Empty chat';
|
||||
const preview = lastMsg ? (lastMsg.content || (lastMsg.imageB64 ? 'Image' : '')).slice(0,40) : 'Empty chat';
|
||||
const isActive = c.id === currentChatId;
|
||||
return `<div class="mm-chat-item ${isActive ? 'active' : ''}" data-id="${c.id}" onclick="loadChat('${c.id}')">
|
||||
<span class="mm-chat-item-icon">${c.pinned ? '📌' : '💬'}</span>
|
||||
<span class="mm-chat-item-icon">${c.pinned ? mmIconSvg('pinFilled') : mmIconSvg('chat')}</span>
|
||||
<div class="mm-chat-item-body">
|
||||
<div class="mm-chat-item-title">${escHtml((c.title||'New Chat').slice(0,38))}</div>
|
||||
<div class="mm-chat-item-meta">
|
||||
@@ -764,9 +793,9 @@ function chatItemHtml(c) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="mm-chat-item-actions">
|
||||
<button class="mm-chat-action-btn" title="${c.pinned?'Unpin':'Pin'}" onclick="event.stopPropagation();toggleChatPin('${c.id}')">${c.pinned?'📍':'📌'}</button>
|
||||
<button class="mm-chat-action-btn" title="Rename" onclick="event.stopPropagation();openRenameModalFor('${c.id}')">✏️</button>
|
||||
<button class="mm-chat-action-btn" title="Delete" onclick="event.stopPropagation();deleteChat('${c.id}')">🗑️</button>
|
||||
<button class="mm-chat-action-btn" title="${c.pinned?'Unpin':'Pin'}" onclick="event.stopPropagation();toggleChatPin('${c.id}')">${c.pinned?mmIconSvg('pinFilled'):mmIconSvg('pin')}</button>
|
||||
<button class="mm-chat-action-btn" title="Rename" onclick="event.stopPropagation();openRenameModalFor('${c.id}')">${mmIconSvg('edit')}</button>
|
||||
<button class="mm-chat-action-btn" title="Delete" onclick="event.stopPropagation();deleteChat('${c.id}')">${mmIconSvg('trash')}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -809,7 +838,7 @@ function loadChat(id) {
|
||||
document.getElementById('pin-btn').style.display = '';
|
||||
document.getElementById('rename-btn').style.display = '';
|
||||
document.getElementById('clear-btn').style.display = '';
|
||||
document.getElementById('pin-btn').textContent = chat.pinned ? '📌 Pinned' : '📌 Pin';
|
||||
document.getElementById('pin-btn').innerHTML = chat.pinned ? `<span class="icon-label">${mmIconSvg('pinFilled')}<span>Pinned</span></span>` : `<span class="icon-label">${mmIconSvg('pin')}<span>Pin</span></span>`;
|
||||
|
||||
// Model
|
||||
if (chat.model) setModel(chat.model);
|
||||
@@ -847,7 +876,7 @@ function toggleChatPin(id) {
|
||||
chat.pinned = !chat.pinned;
|
||||
saveChats();
|
||||
if (id === currentChatId) {
|
||||
document.getElementById('pin-btn').textContent = chat.pinned ? '📌 Pinned' : '📌 Pin';
|
||||
document.getElementById('pin-btn').innerHTML = chat.pinned ? `<span class="icon-label">${mmIconSvg('pinFilled')}<span>Pinned</span></span>` : `<span class="icon-label">${mmIconSvg('pin')}<span>Pin</span></span>`;
|
||||
}
|
||||
renderChatList();
|
||||
toast(chat.pinned ? 'Chat pinned' : 'Chat unpinned');
|
||||
@@ -944,7 +973,7 @@ function appendBubbleDom(role, text, imageB64, ts, scroll = true) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = `mm-msg ${role}`;
|
||||
|
||||
const avatar = role === 'user' ? '👤' : '🤖';
|
||||
const avatar = role === 'user' ? mmIconSvg('user') : mmIconSvg('robot');
|
||||
let imgHtml = '';
|
||||
if (imageB64) {
|
||||
imgHtml = `<img src="${imageB64}" alt="attached image">`;
|
||||
@@ -954,7 +983,7 @@ function appendBubbleDom(role, text, imageB64, ts, scroll = true) {
|
||||
const timeStr = fmtMsgTime(ts || Date.now());
|
||||
|
||||
const copyBtn = role === 'assistant'
|
||||
? `<button class="mm-msg-copy" onclick="copyMsg(this)" data-text="${escHtml(text||'')}">📋 Copy</button>`
|
||||
? `<button class="mm-msg-copy" onclick="copyMsg(this)" data-text="${escHtml(text||'')}"><span class="icon-label">${mmIconSvg('copy')}<span>Copy</span></span></button>`
|
||||
: '';
|
||||
|
||||
wrap.innerHTML = `
|
||||
@@ -987,15 +1016,15 @@ function scrollBottom() {
|
||||
───────────────────────────────────────────────────────────────────────────── */
|
||||
function copyMsg(btn) {
|
||||
navigator.clipboard.writeText(btn.dataset.text || '').then(() => {
|
||||
btn.textContent = '✅ Copied';
|
||||
setTimeout(() => { btn.innerHTML = '📋 Copy'; }, 1800);
|
||||
btn.innerHTML = `<span class="icon-label">${mmIconSvg('check')}<span>Copied</span></span>`;
|
||||
setTimeout(() => { btn.innerHTML = `<span class="icon-label">${mmIconSvg('copy')}<span>Copy</span></span>`; }, 1800);
|
||||
}).catch(() => toast('Copy failed'));
|
||||
}
|
||||
|
||||
function copyCode(btn) {
|
||||
const code = btn.previousElementSibling?.querySelector('code')?.textContent || '';
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
btn.textContent = '✅';
|
||||
btn.textContent = 'Sent';
|
||||
setTimeout(() => { btn.textContent = 'Copy'; }, 1800);
|
||||
}).catch(() => toast('Copy failed'));
|
||||
}
|
||||
@@ -1126,7 +1155,7 @@ async function sendMessage() {
|
||||
const MOCK_RESPONSES = [
|
||||
`## Summary\n\nThis is a **mock response** from the Nexus One AI portal running in offline mode.\n\nThe real model (\`${currentModel || 'llava'}\`) would process your input on-premises and return a detailed answer here.\n\n### What you can expect:\n- Image analysis and description\n- Code generation with syntax highlighting\n- Document understanding and extraction\n- Multi-turn conversation with context\n\n> Connect your Ollama backend at \`/api\` to get real responses.`,
|
||||
`I can see you've sent a message! In production, this would be processed by the **on-premises LLM** without any data leaving your infrastructure.\n\nHere's a quick example of what structured output looks like:\n\n\`\`\`python\n# Example extraction result\nresult = {\n "entities": ["Nexus One AI", "on-premises"],\n "sentiment": "positive",\n "confidence": 0.94\n}\n\`\`\`\n\nThe model supports **markdown**, tables, code blocks, and more.`,
|
||||
`Great question! Here's what I found:\n\n| Feature | Status |\n|---------|--------|\n| On-premises deployment | ✅ Ready |\n| Image analysis | ✅ Ready |\n| Multi-modal support | ✅ Ready |\n| Data privacy | ✅ Guaranteed |\n\nAll processing happens **locally** — no data is sent to external servers.\n\n*Note: This is a demo response. Connect the backend to enable real AI responses.*`,
|
||||
`Great question! Here's what I found:\n\n| Feature | Status |\n|---------|--------|\n| On-premises deployment | Ready |\n| Image analysis | Ready |\n| Multi-modal support | Ready |\n| Data privacy | Guaranteed |\n\nAll processing happens **locally** — no data is sent to external servers.\n\n*Note: This is a demo response. Connect the backend to enable real AI responses.*`,
|
||||
`I've analysed your input. Here are my findings:\n\n1. **First observation** — the content appears to be related to enterprise AI workflows\n2. **Second observation** — on-premises models can handle this type of query effectively\n3. **Third observation** — response quality improves with fine-tuned domain models\n\nWould you like me to go deeper on any of these points?`
|
||||
];
|
||||
let _mockIdx = 0;
|
||||
@@ -1187,6 +1216,6 @@ loadModels();
|
||||
</script>
|
||||
|
||||
<script src="auth.js"></script>
|
||||
<script src="branding.js"></script>
|
||||
<script src="branding.js?v=3"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user