#!/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 = """
Model
API Key
Save Provider
Cancel
💬
"""
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 = 'Select provider... ';
_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