#!/usr/bin/env python3 """Inject VectorDB Chat panel into wiki HTML files. Usage: python3 inject_wiki_chat.py [--file /path/to/wiki.html] If no --file, modifies both ambassador and support wikis in-place. """ import os import re import sys WIKI_FILES = [ "/opt/blog/zai-ambassador-team-wiki.html", "/opt/blog/zai-support-wiki.html", ] CHAT_CSS = """ /* ── VectorDB Chat Panel ── */ .vdb-fab{position:fixed;bottom:24px;right:24px;width:56px;height:56px;border-radius:28px; background:linear-gradient(135deg,#4a9eff,#a78bfa);border:none;color:#fff;font-size:22px; cursor:pointer;box-shadow:0 4px 20px rgba(74,158,255,.35);z-index:999;transition:all .2s;display:flex;align-items:center;justify-content:center} .vdb-fab:hover{transform:scale(1.08);box-shadow:0 6px 28px rgba(74,158,255,.45)} .vdb-chat-container{position:fixed;bottom:0;right:0;width:420px;height:560px;background:#131620; border:1px solid #252a3b;border-radius:16px 16px 0 0;z-index:998;display:flex; flex-direction:column;box-shadow:-4px 0 30px rgba(0,0,0,.4);font-family:'Inter',system-ui,sans-serif; transition:opacity .25s, transform .25s cubic-bezier(.175,.885,.32,1.275)} .vdb-chat-container.hidden{opacity:0;pointer-events:none;transform:translateY(20px)} .vdb-chat-header{display:flex;align-items:center;gap:10px;padding:14px 16px; background:#0c0e14;border-bottom:1px solid #252a3b;flex-shrink:0} .vdb-chat-title{font-size:13px;font-weight:600;color:#e8eaed;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .vdb-provider-select{font-size:11px;padding:4px 8px;border:1px solid #333;border-radius:6px; background:#1a1d26;color:#ccc;cursor:pointer;outline:none;color:#e8eaed;min-width:120px} .vdb-provider-select:focus{border-color:#4a9eff} .vdb-toggle{background:none;border:none;color:#888;font-size:18px;cursor:pointer;padding:4px 8px;line-height:1} .vdb-toggle:hover{color:#fff} .vdb-messages{flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:8px; scrollbar-width:4px;scrollbar-thumb:#333} .vdb-msg{max-width:85%;padding:10px 14px;border-radius:12px;font-size:13px;line-height:1.5; color:#b0b5bc;word-wrap:break-word} .vdb-msg-user{align-self:flex-end;background:#1e3a5f;border:1px solid #2a4070;margin-left:auto} .vdb-msg-assistant{align-self:flex-start;background:#1a1f2e;border:1px solid #252a3b} .vdb-msg-meta{font-size:10px;color:#666;margin-top:4px;display:flex;gap:8px;align-items:center} .vdb-msg-provider{background:rgba(74,158,255,.1);color:#4a9eff;padding:1px 6px;border-radius:3px;font-weight:600} .vdb-msg-error{background:rgba(248,113,113,.1);color:#f87171;border-color:rgba(248,113,113,.3)} .vdb-typing{font-size:11px;color:#7c8497;font-style:italic;padding:8px 16px 0;display:none} .vdb-typing.active{display:block} .vdb-input-row{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #252a3b; background:#0c0e14;flex-shrink:0} .vdb-input{flex:1;padding:10px 12px;border:1px solid #333;border-radius:8px; background:#1a1d26;color:#e8eaed;font-size:13px;font-family:inherit;resize:none; outline:none;min-height:20px;max-height:80px;line-height:1.4} .vdb-input:focus{border-color:#4a9eff} .vdb-send{padding:10px 18px;border:1px solid #333;border-radius:8px;background:rgba(74,158,255,.1); color:#4a9eff;font-weight:600;font-size:12px;cursor:pointer;white-space:nowrap; transition:all .15s} .vdb-send:hover{background:rgba(74,158,255,.2);color:#fff} .vdb-send:disabled{opacity:.4;cursor:not-allowed} .vdb-settings{border-top:1px solid #252a3b;padding:12px 16px;display:none} .vdb-settings-row{display:flex;gap:8px;margin-bottom:8px;align-items:center} .vdb-settings-row label{font-size:11px;color:#888;width:70px;flex-shrink:0} .vdb-settings-row input,.vdb-settings-row select{flex:1;padding:6px 8px;border:1px solid #333; border-radius:4px;background:#1a1d26;color:#e8eaed;font-size:11px} .vdb-settings-btn{padding:4px 12px;border-radius:4px;font-size:10px;cursor:pointer; border:1px solid #333;background:#1a1d26;color:#aaa;transition:all .15s} .vdb-settings-btn:hover{color:#fff;border-color:#444} .vdb-settings-btn.danger{color:#f87171;border-color:rgba(248,113,113,.3)} @media(max-width:600px){ .vdb-chat-container{width:100vw;height:100vh;border-radius:0;right:0;bottom:0} } """ CHAT_HTML = """
""" CHAT_JS = """ (function(){ const CHAT_API = (function(){ const p = location.pathname; return p.endsWith('/') ? p : p + '/'; })(); const $ = id => document.getElementById(id); let _providers = []; let _activeProvider = null; let _history = []; let _isStreaming = false; function esc(s){ if(!s) return ''; const d=document.createElement('div'); d.textContent=s; return d.innerHTML; } function providerIcon(p){ return p.icon || '\u2B99'; } function providerLabel(p){ return p.icon + ' ' + p.name; } function renderProviders(){ const sel = $('vdb-provider-select'); sel.innerHTML = ''; _providers.forEach(p => { const opt = document.createElement('option'); opt.value = p.id; opt.textContent = providerLabel(p); if(_activeProvider && p.id === _activeProvider.id) opt.selected = true; sel.appendChild(opt); }); } function addMsg(content, isUser, meta){ const msgs = $('vdb-chat-messages'); const div = document.createElement('div'); div.className = 'vdb-msg vdb-msg-' + (isUser ? 'user' : 'assistant'); let html = ''; if(meta && meta.provider) html += '
' + providerIcon(meta.provider) + ' ' + meta.provider.name + '
'; if(meta && meta.error) { div.className += ' vdb-msg-error'; html = content; } else { html += esc(content).replace(/\\n/g, '
'); } div.innerHTML = html; msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; return div; } function setStreaming(on){ _isStreaming = on; $('vdb-typing').className = 'vdb-typing' + (on ? ' active' : ''); $('vdb-send').disabled = on; } async function sendMessage(){ const input = $('vdb-input'); const text = (input.value || '').trim(); if(!text || !_activeProvider || _isStreaming) return; _history.push({role:'user', content:text}); addMsg(text, true, null); input.value = ''; input.style.height = 'auto'; setStreaming(true); try{ const resp = await fetch(CHAT_API + 'chat/message', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({message:text, provider_id:_activeProvider.id, history:_history}) }); const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buf = ''; let fullText = ''; let assistantDiv = null; while(true){ const {done, value} = await reader.read(); if(value) buf += decoder.decode(value, {stream:true}); if(done) break; const lines = buf.split('\\n'); buf = lines.pop(); // keep incomplete line for(const line of lines){ if(!line.startsWith('data: ') || !line.slice(5)) continue; try{ const chunk = JSON.parse(line.slice(5)); if(chunk.type === 'done'){ setStreaming(false); break; } if(chunk.type === 'error'){ if(!assistantDiv) assistantDiv = addMsg(chunk.delta, false, {error:true}); else assistantDiv.textContent += chunk.delta; continue; } if(chunk.type === 'delta'){ if(!assistantDiv) assistantDiv = addMsg('', false, {provider:_activeProvider}); fullText += chunk.delta; assistantDiv.innerHTML = esc(fullText.replace(/\\n/g, '
')); assistantDiv.scrollIntoView({block:'nearest', behavior:'smooth'}); } else if(chunk.type === 'tool' || chunk.type === 'raw'){ if(!assistantDiv) assistantDiv = addMsg('', false, {provider:_activeProvider}); assistantDiv.innerHTML += esc(chunk.delta).replace(/\\n/g, '
'); } }catch(e){ /* skip malformed */ } } } // Process remaining buffer if(buf){ for(const line of buf.split('\\n')){ if(!line.startsWith('data: ')) continue; try{ const chunk = JSON.parse(line.slice(5)); if(chunk.type === 'delta' && chunk.delta){ if(!assistantDiv) assistantDiv = addMsg('', false, {provider:_activeProvider}); fullText += chunk.delta; assistantDiv.innerHTML = esc(fullText.replace(/\\n/g, '
')); } }catch(e){} } } _history.push({role:'assistant', content:fullText || '(no response)'}); } catch(e){ addMsg('Error: ' + e.message, false, {error:true}); } setStreaming(false); } // Provider selection $('vdb-provider-select').addEventListener('change', function(){ const pid = this.value; _activeProvider = _providers.find(p => p.id === pid) || null; }); // Send button $('vdb-send').addEventListener('click', sendMessage); $('vdb-input').addEventListener('keydown', function(e){ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMessage(); }}); // Toggle chat panel let _chatOpen = false; $('vdb-chat-btn').addEventListener('click', function(){ _chatOpen = !_chatOpen; const chat = $('vdb-chat'); chat.classList.toggle('hidden', !_chatOpen); this.textContent = _chatOpen ? '\u2715' : '\u1F4AC'; this.title = _chatOpen ? 'Close Chat' : 'Open Wiki Chat'; }); $('vdb-toggle').addEventListener('click', function(){ _chatOpen = false; $('vdb-chat').classList.add('hidden'); this.textContent = '\u25B2'; $('vdb-chat-btn').textContent = '\u1F4AC'; $('vdb-chat-btn').title = 'Open Wiki Chat'; }); // Settings $('vdb-cust-save').addEventListener('click', async function(){ const name = $('vdb-cust-name').value.trim(); const url = $('vdb-cust-url').value.trim(); const model = $('vdb-cust-model').value.trim(); const key = $('vdb-cust-key').value.trim(); if(!name || !url || !model){ alert('Name, URL, and Model are required'); return; } const provider = {id:'custom-'+Date.now(), name:name, base_url:url, model:model, api_key:key, format:'openai', icon:'\u2699', description:'Custom'}; // Save via API try{ await fetch(CHAT_API + 'providers/save', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(provider) }); _providers = await (await fetch(CHAT_API + 'providers')).json(); renderProviders(); _activeProvider = provider; $('vdb-settings').style.display = 'none'; alert('Provider saved!'); } catch(e){ alert('Save failed: ' + e.message); } }); $('vdb-cust-cancel').addEventListener('click', function(){ $('vdb-settings').style.display = 'none'; }); // Auto-open settings if no providers loaded function checkProviders(){ if(!_providers.length){ $('vdb-settings').style.display = ''; } } // Init async function init(){ try{ const [presetsResp, customResp] = await Promise.all([ fetch(CHAT_API + 'providers/presets'), fetch(CHAT_API + 'providers') ]); _presets = await presetsResp.json(); _custom = await customResp.json(); _providers = [..._presets, ..._custom]; renderProviders(); // Auto-select first available provider if(_providers.length > 0 && !_activeProvider){ _activeProvider = _providers[0]; renderProviders(); } checkProviders(); } catch(e){ console.error('Chat init error:', e); $('vdb-chat-messages').innerHTML = '
Failed to load chat service.
'; } } init(); })(); """ def inject_chat(html_content: str) -> str: """Inject chat CSS, HTML, and JS into wiki HTML.""" # Inject CSS before if "" in html_content: html_content = html_content.replace("", CHAT_CSS + "", 1) # Inject HTML before if "" in html_content: html_content = html_content.replace("", CHAT_HTML + "\n", 1) return html_content def main(): files = WIKI_FILES # Check for --file argument if "--file" in sys.argv: idx = sys.argv.index("--file") + 1 if idx < len(sys.argv): files = [sys.argv[idx]] for fpath in files: if not os.path.exists(fpath): print(f"SKIP: {fpath} not found") continue with open(fpath, "r") as f: content = f.read() # Check if already injected if "vdb-chat-container" in content: print(f"SKIP: {fpath} already has chat injected") continue new_content = inject_chat(content) with open(fpath, "w") as f: f.write(new_content) print(f"OK: {fpath} ({len(new_content)} bytes)") if __name__ == "__main__": main()