CHAT WITH MARTIN

the DVT data agent · your gateway · live federation
⌂ HUB
SESSIONS — THIS PROJECT
checking provider…

Martin writes the SQL, DVT guards it (SELECT-only, capped) and answers it live through federation. SQL is always shown.

⏵ EXECUTE ENTER sends · SHIFT+ENTER newline · SHIFT+TAB plan/execute · / palette · /sql raw SQL
'; m.appendChild(frame); } var canvas=null; if(r.columns&&r.columns.length){ if(r.chart){ canvas=el('canvas','chart'); m.appendChild(canvas); canvas._draw=function(){drawChart(canvas,r.chart,r.columns,r.rows);}; setTimeout(canvas._draw,0); } var wrapT=el('div','tablewrap'); var t=el('table','res'), tr=el('tr'); r.columns.forEach(function(c){tr.appendChild(el('th','',c));}); t.appendChild(tr); (r.rows||[]).slice(0,200).forEach(function(row){ var trr=el('tr'); row.forEach(function(v){trr.appendChild(el('td','', v===null?'∅':String(v)));}); t.appendChild(trr); }); wrapT.appendChild(t); m.appendChild(wrapT); m.appendChild(el('div','rowcount', (r.row_count||(r.rows||[]).length)+' row(s)'+ ((r.rows||[]).length>200?' — showing first 200':''))); var acts=el('div','actions'); var save=el('button','smallbtn','💾 SAVE ASSET'); save.onclick=function(){ var name=prompt('Save as (feeds DVT Dashboards later):', (question||'').slice(0,40).replace(/[^\w\- ]/g,'').trim()||'saved query'); if(!name)return; fetch('/chat/save',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:name,question:question,sql:r.sql,chart:r.chart})}) .then(function(resp){return resp.json();}) .then(function(d){ save.textContent=d.error?('✗ '+d.error):'✓ SAVED'; loadAssets(); }); }; acts.appendChild(save); var copy=el('button','smallbtn','COPY SQL'); copy.onclick=function(){ navigator.clipboard.writeText(r.sql||'').then(function(){ copy.textContent='✓ COPIED'; setTimeout(function(){copy.textContent='COPY SQL';},1500); }); }; acts.appendChild(copy); if(canvas){ var png=el('button','smallbtn','PNG'); png.onclick=function(){downloadURI(canvas.toDataURL('image/png'), ((question||'chart').slice(0,30).replace(/[^\w\-]/g,'_')||'chart')+'.png');}; acts.appendChild(png); } var csv=el('button','smallbtn','CSV'); csv.onclick=function(){ downloadURI('data:text/csv;charset=utf-8,'+ encodeURIComponent(tableToCsv(r.columns,r.rows||[])), ((question||'result').slice(0,30).replace(/[^\w\-]/g,'_')||'result')+'.csv'); }; acts.appendChild(csv); m.appendChild(acts); } thread.appendChild(m); if(scroll!==false)scrollDown(); } /* ---------------- the / palette ---------------- */ var CATALOG={models:[],sources:[]}, FILES=null; fetch('/chat/catalog').then(function(r){return r.json();}).then(function(c){ CATALOG=c||CATALOG; }); function ensureFiles(cb){ if(FILES){cb();return;} fetch('/chat/files').then(function(r){return r.json();}).then(function(d){ FILES=d.files||[]; cb(); }).catch(function(){FILES=[];cb();}); } var palette=document.getElementById('palette'); var input=document.getElementById('question'); var palItems=[], palSel=0; var chatMode=localStorage.getItem('dvt-chat-mode')||'execute'; function paintMode(){ var chip=document.getElementById('mode-chip'); var plan=chatMode==='plan'; chip.textContent=plan?'◔ PLAN':'⏵ EXECUTE'; chip.className='modechip'+(plan?' plan':''); input.classList.toggle('planmode', plan); } function toggleMode(){ chatMode=chatMode==='plan'?'execute':'plan'; localStorage.setItem('dvt-chat-mode',chatMode); paintMode(); } var aiProvider=localStorage.getItem('dvt-chat-provider')||''; var aiModel=localStorage.getItem('dvt-chat-model')||''; var PROVIDERS=[]; var COMMANDS=[ {name:'/sql', desc:'run raw SQL directly — guarded, no LLM', insert:'/sql '}, {name:'/model', desc:'pick the AI model your gateway serves', pick:'model'}, {name:'/generate-sources', desc:'introspect a connection live → propose its whole sources.yml', pick:'connection'}, {name:'/errors', desc:'diagnose the last run — failures + fix suggestions', action:function(){ input.value='Diagnose the last run: what failed, why, and propose concrete fixes.'; ask(); }}, {name:'/plan', desc:'switch to PLAN mode (investigate only)', action:function(){if(chatMode!=='plan')toggleMode();}}, {name:'/execute', desc:'switch to EXECUTE mode (full toolbox)', action:function(){if(chatMode!=='execute')toggleMode();}}, {name:'/clear', desc:'start a new chat', action:function(){startNew();}}, {name:'/assets', desc:'open the saved assets drawer', action:function(){document.getElementById('btn-assets').click();}}, {name:'/help', desc:'what can Martin do?', action:function(){ renderAnswer('', {answer:"**Commands**\n- `/` — models, sources, commands\n- `@path` — reference a project file (Martin reads it)\n- `/sql SELECT …` — raw SQL, guarded, no LLM\n- `/model` — pick the gateway's AI model\n- `/plan` · `/execute` — switch modes (or SHIFT+TAB)\n- `/clear` — new chat · `/assets` — saved assets\n\n**Martin** investigates with tools (columns, samples, files), develops models and yml as proposals you APPLY, and answers live through federation — SQL always shown.", provider:'', mode:chatMode}); }}, ]; function paintProviderNote(){ var note=document.getElementById('provider-note'); if(!PROVIDERS.length){ note.innerHTML='AI-Proxy not configured
'+ 'get AI-Proxy ⬇ · then hub → Settings → AI CREDENTIALS'; return; } note.innerHTML='gateway: AI-PROXY'+ (aiModel?('
model: '+aiModel+''):'
model: gateway default'); } function paletteQuery(){ var v=input.value, pos=input.selectionStart||v.length; var before=v.slice(0,pos); var slash=before.match(/(^|\s)\/([\w.]*)$/); if(slash)return {mode:'slash', term:slash[2].toLowerCase(), start:pos-slash[2].length-1, pos:pos}; var at=before.match(/(^|\s)@([\w\-./]*)$/); if(at)return {mode:'at', term:at[2].toLowerCase(), start:pos-at[2].length-1, pos:pos}; return null; } function openPalette(){ var q=paletteQuery(); if(!q){palette.style.display='none';return;} var t=q.term; palItems=[]; if(q.mode==='at'){ ensureFiles(function(){ var q2=paletteQuery(); if(!q2||q2.mode!=='at'){palette.style.display='none';return;} var t2=q2.term; palItems=FILES.filter(function(f){return f.toLowerCase().indexOf(t2)>=0;}) .slice(0,24) .map(function(f){return {label:'@'+f, desc:'project file', insert:'@'+f+' ', group:'PROJECT FILES'};}); if(!palItems.length){palette.style.display='none';return;} palSel=0; paintPalette(); }); return; } COMMANDS.forEach(function(c){ if(c.name.slice(1).indexOf(t)===0) palItems.push({label:c.name, desc:c.desc, insert:c.insert, pick:c.pick, action:c.action, cmd:true}); }); CATALOG.models.forEach(function(mo){ if(mo.name.toLowerCase().indexOf(t)>=0) palItems.push({label:mo.name, desc:'['+mo.materialized+'] '+(mo.columns||[]).slice(0,6).join(', '), insert:mo.name+' ', group:'MODELS'}); }); CATALOG.sources.forEach(function(sname){ if(sname.toLowerCase().indexOf(t)>=0) palItems.push({label:sname, desc:'source', insert:sname+' ', group:'SOURCES'}); }); palItems=palItems.slice(0,24); if(!palItems.length){palette.style.display='none';return;} palSel=0; paintPalette(); } function paintPalette(q){ palette.innerHTML=''; var lastGroup=null; palItems.forEach(function(it,i){ var g=it.cmd?'COMMANDS':(it.group||''); if(g!==lastGroup){palette.appendChild(el('div','group',g));lastGroup=g;} var row=el('div','item'+(i===palSel?' sel':'')); row.appendChild(el('b','',it.label)); row.appendChild(el('span','',it.desc||'')); row.onmousedown=function(ev){ev.preventDefault();pickPalette(i);}; palette.appendChild(row); }); palette.style.display='block'; } function clearSlash(){ var q=paletteQuery(); if(!q)return; var v=input.value; input.value=v.slice(0,q.start)+v.slice(q.pos); input.focus(); input.setSelectionRange(q.start,q.start); } function showPickList(title, entries){ palItems=entries.map(function(e){return {label:e.label, desc:e.desc||'', action:e.action, group:title};}); palSel=0; paintPalette(); } function modelFamily(mid){ if(mid.indexOf(':')>=0) return 'OLLAMA (LOCAL)'; if(mid.indexOf('claude')===0) return 'CLAUDE'; if(mid.indexOf('gemini')===0) return 'GEMINI'; return 'OTHER'; } function generateSources(conn){ renderUser('/generate-sources '+conn); messages.push({role:'user',content:'/generate-sources '+conn}); var pending=el('div','msg'); pending.appendChild(el('div','who','DVT · DIRECT')); pending.appendChild(el('div','body','introspecting '+conn+' live…')); thread.appendChild(pending); scrollDown(); fetch('/chat/generate-sources',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({connection:conn})}) .then(function(r){return r.json();}) .then(function(r){ thread.removeChild(pending); r.role='assistant'; r.question='/generate-sources '+conn; renderAnswer(r.question, r); messages.push(r); persistSession(); }) .catch(function(e){ thread.removeChild(pending); renderAnswer('', {error:'request failed: '+e, provider:'direct'}); }); } function openConnectionPicker(){ clearSlash(); showPickList('CONNECTIONS', [{label:'…', desc:'reading the profile…'}]); fetch('/chat/status').then(function(r){return r.json();}).then(function(st){ var conns=st.connections||[]; if(!conns.length){showPickList('CONNECTIONS', [{label:'none found', desc:'add connections in hub → Settings'}]);return;} var items=[{label:'ALL CONNECTIONS', desc:'sweep the whole profile — one proposal per connection', action:function(){generateSources('all');}}]; conns.forEach(function(cn){ items.push({label:cn, desc:'introspect live → sources.yml proposal', action:function(){generateSources(cn);}}); }); showPickList('GENERATE SOURCES FROM — pick a connection', items); }); } function openModelPicker(){ clearSlash(); showPickList('AI MODELS', [{label:'…', desc:'asking the gateway…'}]); fetch('/chat/models').then(function(r){return r.json();}).then(function(d){ if(d.error){showPickList('AI MODELS', [{label:'unavailable', desc:d.error}]);return;} var models=d.models||[]; var byFam={}; models.forEach(function(mid){ var fam=modelFamily(mid); (byFam[fam]=byFam[fam]||[]).push(mid); }); var famOrder=['CLAUDE','GEMINI','OLLAMA (LOCAL)','OTHER'].filter(function(f){return byFam[f];}); var summary=famOrder.map(function(f){return byFam[f].length+' '+f.toLowerCase().split(' ')[0];}).join(' · '); palItems=[{label:'gateway default', desc:'let AI-Proxy choose', group:models.length+' MODELS — '+summary+' — scroll ⌄', action:function(){aiModel='';localStorage.setItem('dvt-chat-model','');paintProviderNote();}}]; famOrder.forEach(function(fam){ byFam[fam].forEach(function(mid){ palItems.push({label:mid, desc:'', group:fam+' ('+byFam[fam].length+')', action:function(){aiModel=mid;localStorage.setItem('dvt-chat-model',mid);paintProviderNote();}}); }); }); palSel=0; paintPalette(); }).catch(function(e){showPickList('AI MODELS', [{label:'unavailable', desc:String(e)}]);}); } function pickPalette(i){ var it=palItems[i]; if(!it)return; if(it.action&&it.cmd){clearSlash();it.action();palette.style.display='none';input.focus();return;} if(it.action){it.action();palette.style.display='none';input.focus();return;} if(it.pick==='model'){openModelPicker();return;} if(it.pick==='connection'){openConnectionPicker();return;} var q=paletteQuery(); if(!q)return; var v=input.value; input.value=v.slice(0,q.start)+it.insert+v.slice(q.pos); var newPos=q.start+it.insert.length; input.focus(); input.setSelectionRange(newPos,newPos); palette.style.display='none'; sqlModePaint(); } function sqlModePaint(){ input.classList.toggle('sqlmode', /^\s*\/sql\s/i.test(input.value)); } input.addEventListener('input', function(){openPalette();sqlModePaint();autoGrow();}); input.addEventListener('keydown', function(e){ if(palette.style.display==='block'){ if(e.key==='ArrowDown'){e.preventDefault();palSel=Math.min(palSel+1,palItems.length-1);paintPalette();return;} if(e.key==='ArrowUp'){e.preventDefault();palSel=Math.max(palSel-1,0);paintPalette();return;} if(e.key==='Tab'||(e.key==='Enter'&&palItems.length)){e.preventDefault();pickPalette(palSel);return;} if(e.key==='Escape'){palette.style.display='none';return;} } if(e.key==='Tab'&&e.shiftKey){e.preventDefault();toggleMode();return;} if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();ask();} }); document.getElementById('mode-chip').onclick=toggleMode; paintMode(); input.addEventListener('blur', function(){setTimeout(function(){palette.style.display='none';},150);}); function autoGrow(){ input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,160)+'px'; } /* ---------------- ask ---------------- */ var asking=false; function ask(){ var q=input.value.trim(); if(!q||asking)return; asking=true; var btn=document.getElementById('btn-ask'); btn.disabled=true; btn.innerHTML=''; input.value=''; autoGrow(); sqlModePaint(); if(!sessionTitle){sessionTitle=q.replace(/^\/sql\s+/i,'').slice(0,60);} renderUser(q); messages.push({role:'user',content:q}); var isSql=/^\/sql\s+/i.test(q); var pending=el('div','msg'); pending.appendChild(el('div','who',isSql?'DVT · DIRECT':(chatMode==='plan'?'MARTIN · PLAN':'MARTIN'))); var pendingBody=el('div','body', isSql?'executing (guarded, live)…':'thinking → writing SQL → executing live…'); pending.appendChild(pendingBody); var workLive=el('div','hint'); pending.appendChild(workLive); thread.appendChild(pending); scrollDown(); function finish(r){ thread.removeChild(pending); r.role='assistant'; r.question=q; renderAnswer(q,r); messages.push(r); llmHistory.push({role:'user',content:q}); llmHistory.push({role:'assistant', content:(r.answer||r.error||'')+(r.sql?('\nSQL: '+r.sql):'')}); if(llmHistory.length>16)llmHistory=llmHistory.slice(-16); persistSession(); } function fail(e){ thread.removeChild(pending); renderAnswer(q,{role:'assistant',question:q,error:'request failed: '+e}); } function done(){ asking=false; btn.disabled=false; btn.textContent='ASK'; input.focus(); } if(isSql){ fetch('/chat/run',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({sql:q.replace(/^\/sql\s+/i,'')})}) .then(function(resp){return resp.json();}) .then(finish).catch(fail).finally(done); return; } // MARTIN'S WORK streams live: NDJSON trace events as each tool // finishes, then one final "done" carrying the whole answer. fetch('/chat/ask',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({question:q,history:llmHistory,model:aiModel, mode:chatMode,stream:true})}) .then(function(resp){ var reader=resp.body.getReader(); var dec=new TextDecoder(); var buf=''; var settled=false; function handle(line){ if(!line.trim())return; var ev; try{ev=JSON.parse(line);}catch(_){return;} if(ev.event==='trace'&&ev.entry){ pendingBody.textContent='investigating…'; var t=ev.entry; workLive.appendChild(el('div','', (t.ok?'✓ ':'✗ ')+t.tool+(t.detail?' · '+t.detail:''))); scrollDown(); }else if(ev.event==='done'){ settled=true; finish(ev.result||{}); }else if(ev.event==='error'){ settled=true; thread.removeChild(pending); renderAnswer(q,{role:'assistant',question:q,error:ev.error}); } } function pump(){ return reader.read().then(function(step){ if(step.done){ if(buf)handle(buf); if(!settled)fail('stream ended early'); return; } buf+=dec.decode(step.value,{stream:true}); var lines=buf.split('\n'); buf=lines.pop(); lines.forEach(handle); return pump(); }); } return pump(); }) .catch(fail).finally(done); } document.getElementById('btn-ask').onclick=ask; /* ---------------- assets ---------------- */ function loadAssets(){ fetch('/chat/saved').then(function(r){return r.json();}).then(function(d){ var list=document.getElementById('assets-list'); list.innerHTML=''; var items=d.items||[]; if(!items.length){ list.appendChild(el('span','hint','nothing saved yet — SAVE ASSET on any result')); return; } items.sort(function(a,b){return (b.created||0)-(a.created||0);}); items.forEach(function(it){ var row=el('div','asset-item'); row.appendChild(el('b','',it.name)); row.appendChild(el('span','mono', it.chart?('['+it.chart.type+']'):'[table]')); row.appendChild(el('span','hint',(it.question||'').slice(0,60))); var run=el('button','smallbtn','RERUN LIVE'); run.onclick=function(){ renderUser('(rerun asset) '+it.name); messages.push({role:'user',content:'(rerun asset) '+it.name}); var pending=el('div','msg'); pending.appendChild(el('div','who','DVT · DIRECT')); pending.appendChild(el('div','body','executing saved SQL live…')); thread.appendChild(pending); scrollDown(); fetch('/chat/run',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({sql:it.sql,chart:it.chart})}) .then(function(r){return r.json();}) .then(function(r){ thread.removeChild(pending); r.role='assistant'; r.question=it.question; renderAnswer(it.question,r); messages.push(r); persistSession(); }); }; var del=el('button','smallbtn danger','✕'); del.onclick=function(){ fetch('/chat/delete',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:it.name})}).then(loadAssets); }; row.appendChild(run); row.appendChild(del); list.appendChild(row); }); }); } document.getElementById('btn-assets').onclick=function(){ var p=document.getElementById('assets-panel'); p.style.display=p.style.display==='none'?'':'none'; if(p.style.display!=='none')loadAssets(); }; /* ---------------- status + boot ---------------- */ fetch('/chat/status').then(function(r){return r.json();}).then(function(s){ PROVIDERS=s.providers||[]; if(aiProvider&&PROVIDERS.indexOf(aiProvider)<0){aiProvider='';aiModel='';} if(!s.provider){document.getElementById('btn-ask').disabled=true;} paintProviderNote(); var cat=document.getElementById('catalog-note'); cat.textContent=s.models+' models · '+s.sources+' sources'+ (s.has_catalog?'':' · run dvt docs generate'); }); startNew();