192 lines
13 KiB
JavaScript
192 lines
13 KiB
JavaScript
|
|
window.onerror = function(msg, url, line, col, error) {
|
|
document.body.innerHTML += '<div style="position:absolute;z-index:9999;background:red;color:white;padding:10px;top:0;left:0;right:0">ERROR: ' + msg + ' at line ' + line + '</div>';
|
|
};
|
|
window.addEventListener('unhandledrejection', function(event) {
|
|
document.body.innerHTML += '<div style="position:absolute;z-index:9999;background:red;color:white;padding:10px;bottom:0;left:0;right:0">PROMISE REJECTION: ' + event.reason + '</div>';
|
|
});
|
|
try {
|
|
const vscode=acquireVsCodeApi(),chat=document.getElementById('chat'),input=document.getElementById('input'),
|
|
sendBtn=document.getElementById('sendBtn'),stopBtn=document.getElementById('stopBtn'),
|
|
modelSel=document.getElementById('modelSel'),newChatBtn=document.getElementById('newChatBtn'),settingsBtn=document.getElementById('settingsBtn'),brainBtn=document.getElementById('brainBtn'),
|
|
attachBtn=document.getElementById('attachBtn'),fileInput=document.getElementById('fileInput'),attachPreview=document.getElementById('attachPreview'),
|
|
thinkingBar=document.getElementById('thinkingBar');
|
|
let loader=null,sending=false,pendingFiles=[];
|
|
|
|
/* Syntax Highlighting (lightweight) */
|
|
function highlight(code,lang){
|
|
let h=esc(code);
|
|
h=h.replace(new RegExp("(\\\\/\\\\/[^\\\\n]*)", "g"),'<span class="cm">$1</span>');
|
|
h=h.replace(new RegExp("(#[^\\\\n]*)", "g"),'<span class="cm">$1</span>');
|
|
h=h.replace(new RegExp("(\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\/)", "g"),'<span class="cm">$1</span>');
|
|
h=h.replace(/("[^&]*?"|'[^&]*?')/g,'<span class="str">$1</span>');
|
|
h=h.replace(new RegExp("\\\\b(function|const|let|var|return|if|else|for|while|class|import|export|from|default|async|await|try|catch|throw|new|this|def|self|print|lambda|yield|with|as|raise|except|finally)\\\\b", "g"),'<span class="kw">$1</span>');
|
|
h=h.replace(new RegExp("\\\\b(\\\\d+\\\\.?\\\\d*)\\\\b", "g"),'<span class="num">$1</span>');
|
|
h=h.replace(new RegExp("\\\\b(True|False|None|true|false|null|undefined|NaN)\\\\b", "g"),'<span class="num">$1</span>');
|
|
h=h.replace(new RegExp("\\\\b(String|Number|Boolean|Array|Object|Map|Set|Promise|void|int|float|str|list|dict|tuple)\\\\b", "g"),'<span class="type">$1</span>');
|
|
h=h.replace(/([=!+*/%|&^~?:-]+)/g,'<span class="op">$1</span>');
|
|
return h;
|
|
}
|
|
|
|
/* Clipboard Paste (Ctrl+V images) */
|
|
input.addEventListener('paste',(e)=>{
|
|
const items=e.clipboardData&&e.clipboardData.items;
|
|
if(!items)return;
|
|
for(const item of items){
|
|
if(item.type.startsWith('image/')){
|
|
e.preventDefault();
|
|
const file=item.getAsFile();
|
|
if(!file)return;
|
|
const reader=new FileReader();
|
|
reader.onload=()=>{
|
|
const base64=reader.result.split(',')[1];
|
|
pendingFiles.push({name:'clipboard-image.png',type:file.type,data:base64});
|
|
renderPreview();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
vscode.postMessage({type:'getModels'});
|
|
setTimeout(()=>vscode.postMessage({type:'ready'}),300);
|
|
input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,150)+'px'});
|
|
function getTime(){return new Date().toLocaleTimeString('ko-KR',{hour:'2-digit',minute:'2-digit'})}
|
|
function esc(s){const d=document.createElement('div');d.innerText=s;return d.innerHTML}
|
|
function fmt(t){
|
|
if(t.lastIndexOf('<create_file') > t.lastIndexOf('</create_file>')) t += '</create_file>';
|
|
if(t.lastIndexOf('<edit_file') > t.lastIndexOf('</edit_file>')) t += '</edit_file>';
|
|
if(t.lastIndexOf('<run_command') > t.lastIndexOf('</run_command>')) t += '</run_command>';
|
|
if((t.match(/\`\`\`/g)||[]).length % 2 !== 0) t += '\\\\n\`\`\`';
|
|
|
|
const blocks = [];
|
|
function pushB(h){ blocks.push(h); return '__B' + (blocks.length-1) + '__'; }
|
|
t=t.replace(/<create_file\\s+path="([^"]+)">([\\s\\S]*?)<\\/create_file>/g,(_,p,c)=>pushB('<div class="file-badge">\u{1F4C1} '+esc(p)+' \u2014 \uC790\uB3D9 \uC0DD\uC131\uB428</div><div class="code-wrap"><pre><code>'+esc(c)+'</code></pre><button class="copy-btn" onclick="copyCode(this)">Copy</button></div>'));
|
|
t=t.replace(/<edit_file\\s+path="([^"]+)">([\\s\\S]*?)<\\/edit_file>/g,(_,p,c)=>pushB('<div class="edit-badge">\u270F\uFE0F '+esc(p)+' \u2014 \uD3B8\uC9D1\uB428</div><div class="code-wrap"><pre><code>'+esc(c)+'</code></pre><button class="copy-btn" onclick="copyCode(this)">Copy</button></div>'));
|
|
t=t.replace(/<run_command>([\\s\\S]*?)<\\/run_command>/g,(_,c)=>pushB('<div class="cmd-badge">\u25B6 '+esc(c)+'</div>'));
|
|
t=t.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g,(_,lang,c)=>{const l=lang||'code';return pushB('<div class="code-wrap"><span class="code-lang">'+esc(l)+'</span><pre><code>'+highlight(c,l)+'</code></pre><button class="copy-btn" onclick="copyCode(this)">Copy</button></div>');});
|
|
t=t.replace(/\`([^\`]+)\`/g,(_,c)=>pushB('<code>'+esc(c)+'</code>'));
|
|
t=esc(t);
|
|
t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');
|
|
t=t.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
t=t.replace(/__B(\\d+)__/g, (_,i)=>blocks[i]);
|
|
return t;
|
|
}
|
|
function copyCode(btn){const code=btn.parentElement.querySelector('code');if(!code)return;navigator.clipboard.writeText(code.innerText).then(()=>{btn.textContent='\u2713 Copied';btn.classList.add('copied');setTimeout(()=>{btn.textContent='Copy';btn.classList.remove('copied')},1500)})}
|
|
function addMsg(text,role){
|
|
const isUser=role==='user',isErr=role==='error';
|
|
const el=document.createElement('div');el.className='msg'+(isUser?' msg-user':'')+(isErr?' msg-error':'');
|
|
const head=document.createElement('div');head.className='msg-head';
|
|
head.innerHTML=(isUser?'<div class="av av-user">\u{1F464}</div><span>You</span>':'<div class="av av-ai">\u2726</div><span>Connect AI</span>')+'<span class="msg-time">'+getTime()+'</span>';
|
|
const body=document.createElement('div');body.className='msg-body';
|
|
if(isUser){body.innerText=text}else{body.innerHTML=fmt(text)}
|
|
el.appendChild(head);el.appendChild(body);chat.appendChild(el);chat.scrollTop=chat.scrollHeight;
|
|
}
|
|
function showLoader(){loader=document.createElement('div');loader.className='msg';loader.innerHTML='<div class="msg-head"><div class="av av-ai">\u2726</div><span>Connect AI</span><span class="msg-time">'+getTime()+'</span></div><div class="loading-wrap"><div class="loading-dots"><span></span><span></span><span></span></div><span class="loading-text">\uC0DD\uAC01\uD558\uB294 \uC911...</span></div>';chat.appendChild(loader);chat.scrollTop=chat.scrollHeight;thinkingBar.classList.add('active')}
|
|
function hideLoader(){if(loader&&loader.parentNode)loader.parentNode.removeChild(loader);loader=null;thinkingBar.classList.remove('active')}
|
|
function setSending(v){sending=v;sendBtn.disabled=v;stopBtn.classList.toggle('visible',v);input.disabled=v;if(!v){input.focus();thinkingBar.classList.remove('active')}}
|
|
function send(){
|
|
const text=input.value.trim();
|
|
if((!text&&pendingFiles.length===0)||sending)return;
|
|
document.body.classList.remove('init');
|
|
const w=document.querySelector('.welcome');if(w)w.remove();
|
|
document.querySelectorAll('.quick-actions').forEach(e=>e.remove());
|
|
const displayText=text+(pendingFiles.length>0?'\\\\n\\ud83d\\udcce '+pendingFiles.map(f=>f.name).join(', '):'');
|
|
addMsg(displayText,'user');
|
|
input.value='';input.style.height='auto';setSending(true);showLoader();
|
|
if(pendingFiles.length>0){
|
|
vscode.postMessage({type:'promptWithFile',value:text||'\uC774 \uD30C\uC77C\uC744 \uBD84\uC11D\uD574\uC8FC\uC138\uC694.',model:modelSel.value,files:pendingFiles});
|
|
pendingFiles=[];attachPreview.innerHTML='';attachPreview.classList.remove('visible');
|
|
} else {
|
|
vscode.postMessage({type:'prompt',value:text,model:modelSel.value});
|
|
}
|
|
}
|
|
|
|
/* Attachment Logic */
|
|
attachBtn.addEventListener('click',()=>fileInput.click());
|
|
fileInput.addEventListener('change',()=>{
|
|
const files=Array.from(fileInput.files);
|
|
files.forEach(file=>{
|
|
const reader=new FileReader();
|
|
reader.onload=()=>{
|
|
const base64=reader.result.split(',')[1];
|
|
pendingFiles.push({name:file.name,type:file.type,data:base64});
|
|
renderPreview();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
fileInput.value='';
|
|
});
|
|
function renderPreview(){
|
|
attachPreview.innerHTML='';
|
|
if(pendingFiles.length===0){attachPreview.classList.remove('visible');return;}
|
|
attachPreview.classList.add('visible');
|
|
pendingFiles.forEach((f,i)=>{
|
|
const chip=document.createElement('div');chip.className='attach-chip';
|
|
const isImg=f.type.startsWith('image/');
|
|
if(isImg){
|
|
const thumb=document.createElement('img');thumb.className='attach-thumb';thumb.src='data:'+f.type+';base64,'+f.data;chip.appendChild(thumb);
|
|
} else {
|
|
const icon=document.createElement('span');icon.className='chip-icon';icon.textContent=f.type.startsWith('audio/')?'\u{1F3A7}':'\u{1F4C4}';chip.appendChild(icon);
|
|
}
|
|
const nm=document.createElement('span');nm.className='chip-name';nm.textContent=f.name;chip.appendChild(nm);
|
|
const rm=document.createElement('span');rm.className='chip-remove';rm.textContent='\u2715';
|
|
rm.addEventListener('click',()=>{pendingFiles.splice(i,1);renderPreview();});
|
|
chip.appendChild(rm);
|
|
attachPreview.appendChild(chip);
|
|
});
|
|
}
|
|
document.addEventListener('click',e=>{if(e.target.classList.contains('qa-btn')){const p=e.target.getAttribute('data-prompt');if(p){input.value=p;send()}}});
|
|
sendBtn.addEventListener('click',send);
|
|
input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
|
|
newChatBtn.addEventListener('click',()=>vscode.postMessage({type:'newChat'}));
|
|
settingsBtn.addEventListener('click',()=>vscode.postMessage({type:'openSettings'}));
|
|
brainBtn.addEventListener('click',()=>vscode.postMessage({type:'syncBrain'}));
|
|
stopBtn.addEventListener('click',()=>{vscode.postMessage({type:'stopGeneration'});hideLoader();setSending(false);if(streamBody){streamBody.classList.remove('stream-active')}streamEl=null;streamBody=null;});
|
|
let streamEl=null,streamBody=null;
|
|
window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
|
|
case 'response':hideLoader();setSending(false);addMsg(msg.value,'ai');break;
|
|
case 'error':hideLoader();setSending(false);addMsg(msg.value,'error');break;
|
|
case 'streamStart':{
|
|
hideLoader();
|
|
streamEl=document.createElement('div');streamEl.className='msg';
|
|
const h=document.createElement('div');h.className='msg-head';
|
|
h.innerHTML='<div class="av av-ai">\u2726</div><span>Connect AI</span><span class="msg-time">'+getTime()+'</span>';
|
|
streamBody=document.createElement('div');streamBody.className='msg-body stream-active';
|
|
streamEl.appendChild(h);streamEl.appendChild(streamBody);chat.appendChild(streamEl);chat.scrollTop=chat.scrollHeight;
|
|
break;}
|
|
case 'streamChunk':{
|
|
if(streamBody){streamBody.innerHTML=fmt(streamBody._raw=(streamBody._raw||'')+msg.value);chat.scrollTop=chat.scrollHeight;}
|
|
break;}
|
|
case 'streamEnd':{
|
|
if(streamBody)streamBody.classList.remove('stream-active');
|
|
/* Add regenerate button */
|
|
if(streamEl){
|
|
const rb=document.createElement('button');rb.className='regen-btn';rb.innerHTML='\u{1F504} Regenerate';
|
|
rb.addEventListener('click',()=>{rb.remove();vscode.postMessage({type:'regenerate'});showLoader();setSending(true);});
|
|
streamEl.appendChild(rb);
|
|
}
|
|
setSending(false);streamEl=null;streamBody=null;
|
|
break;}
|
|
case 'modelsList':modelSel.innerHTML='';msg.value.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});break;
|
|
case 'clearChat':
|
|
document.body.classList.add('init');
|
|
chat.innerHTML='<div class="welcome"><div class="welcome-logo">\u2726</div><div class="welcome-title">Connect AI</div><div class="welcome-sub">\uBCF4\uC548 \xB7 \uBE44\uC6A9\uCD5C\uC801\uD654 \xB7 \uC9C0\uC2DD\uC5F0\uACB0<br>\uD504\uB85C\uC81D\uD2B8\uB97C \uC774\uD574\uD558\uACE0, \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uACE0, \uC2E4\uD589\uD569\uB2C8\uB2E4.</div></div>';
|
|
break;
|
|
case 'restoreMessages':
|
|
chat.innerHTML='';
|
|
if(msg.value&&msg.value.length>0){
|
|
document.body.classList.remove('init');
|
|
msg.value.forEach(m=>addMsg(m.text,m.role));
|
|
} else {
|
|
document.body.classList.add('init');
|
|
chat.innerHTML='<div class="welcome"><div class="welcome-logo">\u2726</div><div class="welcome-title">Connect AI</div><div class="welcome-sub">\uBCF4\uC548 \xB7 \uBE44\uC6A9\uCD5C\uC801\uD654 \xB7 \uC9C0\uC2DD\uC5F0\uACB0<br>\uD504\uB85C\uC81D\uD2B8\uB97C \uC774\uD574\uD558\uACE0, \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uACE0, \uC2E4\uD589\uD569\uB2C8\uB2E4.</div></div>';
|
|
}
|
|
break;
|
|
case 'focusInput':input.focus();break;
|
|
case 'injectPrompt':input.value=msg.value;input.style.height='auto';input.style.height=Math.min(input.scrollHeight,150)+'px';send();break;
|
|
} });
|
|
} catch(err) {
|
|
document.body.innerHTML = '<div style="color:#ff4444;padding:20px;background:#111;height:100%;font-size:14px;overflow:auto;"><h2>\u26A0\uFE0F WEBVIEW JS CRASH</h2><pre>' + err.name + ': ' + err.message + '\\n' + err.stack + '</pre></div>';
|
|
}
|