Update Astra: v2.80.19 - Refactoring Sidebar, LM Studio integration, and new tests
This commit is contained in:
@@ -0,0 +1,729 @@
|
||||
const vscode = acquireVsCodeApi();
|
||||
const chat = document.getElementById('chat');
|
||||
const input = document.getElementById('input');
|
||||
|
||||
// [State Persistence - Tier 0] 즉시 복원 (Instant Restore from WebView State)
|
||||
const previousState = vscode.getState();
|
||||
if (previousState && previousState.history && previousState.history.length > 0) {
|
||||
console.log('[Astra] Restoring from Webview State...');
|
||||
renderHistory(previousState.history);
|
||||
}
|
||||
|
||||
function saveWebviewState(history) {
|
||||
const current = vscode.getState() || {};
|
||||
vscode.setState({ ...current, history });
|
||||
}
|
||||
|
||||
function saveUiState() {
|
||||
const current = vscode.getState() || {};
|
||||
vscode.setState({ ...current, secondBrainTraceEnabled, secondBrainTraceDebug });
|
||||
}
|
||||
|
||||
function renderHistory(history) {
|
||||
if (!history || history.length === 0) return;
|
||||
chat.innerHTML = '';
|
||||
history.forEach(m => {
|
||||
if (!m) return;
|
||||
// Only skip truly internal system messages, keep assistant thoughts
|
||||
if (m.role === 'system' && m.internal) return;
|
||||
addMsg(m.content, m.role === 'assistant' ? 'assistant' : 'user', m.rationale);
|
||||
});
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const toastNotif = document.getElementById('toastNotif');
|
||||
const thinkingBar = document.getElementById('thinkingBar');
|
||||
const statusLabel = document.getElementById('statusLabel');
|
||||
const stepper = document.getElementById('stepper');
|
||||
|
||||
// --- Draft State Management ---
|
||||
let isDraftActive = false;
|
||||
let _toastTimer = null;
|
||||
|
||||
function showToast(msg, type = 'info') {
|
||||
toastNotif.textContent = msg;
|
||||
toastNotif.className = 'toast-notif toast-' + type + ' toast-visible';
|
||||
if (_toastTimer) clearTimeout(_toastTimer);
|
||||
_toastTimer = setTimeout(() => {
|
||||
toastNotif.classList.remove('toast-visible');
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
function setDraftActive(active) {
|
||||
isDraftActive = active;
|
||||
cancelBtn.style.display = active ? 'inline-flex' : 'none';
|
||||
}
|
||||
|
||||
// 생성 중/완료 시 Send ⇔ Stop 전환
|
||||
function setGenerating(generating) {
|
||||
if (generating) {
|
||||
sendBtn.style.display = 'none';
|
||||
stopBtn.style.display = 'inline-flex';
|
||||
// 생성 중에는 Clear 버튼 숨김
|
||||
cancelBtn.style.display = 'none';
|
||||
} else {
|
||||
stopBtn.style.display = 'none';
|
||||
sendBtn.style.display = 'inline-flex';
|
||||
sendBtn.disabled = false;
|
||||
// Draft 상태에 따라 Clear 버튼 복원
|
||||
if (isDraftActive) cancelBtn.style.display = 'inline-flex';
|
||||
}
|
||||
}
|
||||
|
||||
function clearDraft() {
|
||||
// Step 1: 상태 초기화 (Draft State Reset)
|
||||
setDraftActive(false);
|
||||
// Step 2: UI 반영 (Input + Attachments 초기화)
|
||||
input.value = '';
|
||||
input.style.height = 'auto';
|
||||
pendingFiles = [];
|
||||
renderAttachments();
|
||||
input.focus();
|
||||
// Step 3: Toast 알림으로 즉각적 피드백
|
||||
showToast('✕ 작성 내용이 초기화되었습니다.', 'warn');
|
||||
Sound.warn();
|
||||
}
|
||||
|
||||
|
||||
// --- Sound Manager ---
|
||||
const Sound = {
|
||||
ctx: null,
|
||||
init() { if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)(); },
|
||||
play(freq, type, dur) {
|
||||
try {
|
||||
this.init();
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
|
||||
gain.gain.setValueAtTime(0.05, this.ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + dur);
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(this.ctx.currentTime + dur);
|
||||
} catch(e) {}
|
||||
},
|
||||
success() { this.play(880, 'sine', 0.1); setTimeout(() => this.play(1109, 'sine', 0.15), 80); },
|
||||
warn() { this.play(440, 'triangle', 0.3); }
|
||||
};
|
||||
|
||||
function setStep(stepId, state = 'active') {
|
||||
stepper.classList.add('active');
|
||||
const step = document.getElementById('step-' + stepId);
|
||||
if (step) {
|
||||
if (state === 'active') {
|
||||
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
|
||||
step.classList.add('active');
|
||||
} else if (state === 'complete') {
|
||||
step.classList.remove('active');
|
||||
step.classList.add('complete');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetStepper() {
|
||||
stepper.classList.remove('active');
|
||||
document.querySelectorAll('.step').forEach(s => {
|
||||
s.classList.remove('active');
|
||||
s.classList.remove('complete');
|
||||
});
|
||||
}
|
||||
const modelSel = document.getElementById('modelSel');
|
||||
const brainSel = document.getElementById('brainSel');
|
||||
const historyOverlay = document.getElementById('historyOverlay');
|
||||
const historyList = document.getElementById('historyList');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const engineStatusText = document.getElementById('engineStatusText');
|
||||
const attachBtn = document.getElementById('attachBtn');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const attachPreview = document.getElementById('attachPreview');
|
||||
const agentSel = document.getElementById('agentSel');
|
||||
const designerSel = document.getElementById('designerSel');
|
||||
const chronicleRecordSel = document.getElementById('chronicleRecordSel');
|
||||
const editAgentBtn = document.getElementById('editAgentBtn');
|
||||
const addAgentBtn = document.getElementById('addAgentBtn');
|
||||
const deleteAgentBtn = document.getElementById('deleteAgentBtn');
|
||||
const addBrainBtn = document.getElementById('addBrainBtn');
|
||||
const editBrainBtn = document.getElementById('editBrainBtn');
|
||||
const deleteBrainBtn = document.getElementById('deleteBrainBtn');
|
||||
const saveWikiRawBtn = document.getElementById('saveWikiRawBtn');
|
||||
const agentConfigPanel = document.getElementById('agentConfigPanel');
|
||||
const agentPrompt = document.getElementById('agentPrompt');
|
||||
const negativePrompt = document.getElementById('negativePrompt');
|
||||
const updateAgentBtn = document.getElementById('updateAgentBtn');
|
||||
|
||||
let streamBody = null;
|
||||
let internetEnabled = false;
|
||||
let secondBrainTraceEnabled = true;
|
||||
let secondBrainTraceDebug = false;
|
||||
let pendingFiles = [];
|
||||
let editMode = false;
|
||||
if (previousState && typeof previousState.secondBrainTraceEnabled === 'boolean') {
|
||||
secondBrainTraceEnabled = previousState.secondBrainTraceEnabled;
|
||||
}
|
||||
if (previousState && typeof previousState.secondBrainTraceDebug === 'boolean') {
|
||||
secondBrainTraceDebug = previousState.secondBrainTraceDebug;
|
||||
}
|
||||
const initialTraceBtn = document.getElementById('brainTraceBtn');
|
||||
initialTraceBtn.classList.toggle('active', secondBrainTraceEnabled);
|
||||
initialTraceBtn.setAttribute('data-tooltip', secondBrainTraceEnabled ? 'Second Brain Trace Mode: On' : 'Second Brain Trace Mode: Off');
|
||||
const initialTraceDebugBtn = document.getElementById('brainTraceDebugBtn');
|
||||
initialTraceDebugBtn.classList.toggle('active', secondBrainTraceDebug);
|
||||
initialTraceDebugBtn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');
|
||||
|
||||
function fmt(text) { return marked.parse(text || ''); }
|
||||
|
||||
function copyToClipboard(text, btn) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea); textarea.select();
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
btn.innerText = '✅ Copied!'; setTimeout(() => { btn.innerText = '📋 Copy'; }, 2000);
|
||||
}
|
||||
} catch (err) { console.error('Copy failed', err); }
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
window.approve = () => {
|
||||
const box = document.querySelector('.approval-box');
|
||||
if (box) box.remove();
|
||||
vscode.postMessage({ type: 'approveAction' });
|
||||
};
|
||||
window.reject = () => {
|
||||
const box = document.querySelector('.approval-box');
|
||||
if (box) box.remove();
|
||||
vscode.postMessage({ type: 'rejectAction' });
|
||||
};
|
||||
|
||||
function exportToMD(text) {
|
||||
vscode.postMessage({ type: 'exportResponse', text: text });
|
||||
}
|
||||
|
||||
function addMsg(text, role, rationale) {
|
||||
const isUser = role === 'user';
|
||||
const msgEl = document.createElement('div');
|
||||
msgEl.className = 'msg ' + (isUser ? 'msg-user' : 'msg-ai');
|
||||
msgEl._raw = text;
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'msg-head';
|
||||
head.innerHTML = isUser ? '<div class="av av-user">U</div> You' : '<div class="av av-ai">✦</div> Astra';
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'msg-body markdown-body';
|
||||
|
||||
if (isUser) {
|
||||
body.innerText = text;
|
||||
} else {
|
||||
body.innerHTML = fmt(text);
|
||||
}
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'msg-actions';
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn'; copyBtn.innerText = '📋 Copy';
|
||||
copyBtn.onclick = (e) => { e.stopPropagation(); copyToClipboard(msgEl._raw, copyBtn); };
|
||||
|
||||
const exportBtn = document.createElement('button');
|
||||
exportBtn.className = 'action-btn'; exportBtn.innerText = '💾 Export';
|
||||
exportBtn.onclick = (e) => { e.stopPropagation(); exportToMD(msgEl._raw); };
|
||||
|
||||
actions.appendChild(copyBtn);
|
||||
actions.appendChild(exportBtn);
|
||||
|
||||
msgEl.appendChild(head); msgEl.appendChild(body);
|
||||
msgEl.appendChild(actions);
|
||||
chat.appendChild(msgEl); chat.scrollTop = chat.scrollHeight;
|
||||
return { body, msgEl };
|
||||
}
|
||||
|
||||
window.addEventListener('message', e => {
|
||||
const msg = e.data;
|
||||
switch(msg.type) {
|
||||
case 'addMessage':
|
||||
addMsg(msg.value, msg.role, msg.rationale);
|
||||
// Update state for non-streamed messages
|
||||
const s = vscode.getState() || { history: [] };
|
||||
s.history.push({ role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.value, rationale: msg.rationale });
|
||||
saveWebviewState(s.history);
|
||||
break;
|
||||
case 'streamStart':
|
||||
thinkingBar.classList.remove('active');
|
||||
if (document.querySelector('.welcome')) document.querySelector('.welcome').remove();
|
||||
const res = addMsg('', 'assistant');
|
||||
streamBody = res.body; streamBody._parent = res.msgEl; streamBody._parent._raw = '';
|
||||
streamBody.classList.add('stream-active');
|
||||
break;
|
||||
case 'streamChunk':
|
||||
if (streamBody) {
|
||||
streamBody._parent._raw += msg.value;
|
||||
streamBody.innerHTML = fmt(streamBody._parent._raw);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
break;
|
||||
case 'streamEnd':
|
||||
if (streamBody) {
|
||||
streamBody.classList.remove('stream-active');
|
||||
// Update state after stream finishes
|
||||
const state = vscode.getState() || { history: [] };
|
||||
state.history.push({ role: 'assistant', content: streamBody._parent._raw });
|
||||
saveWebviewState(state.history);
|
||||
}
|
||||
streamBody = null;
|
||||
// 생성 완료 시 Stop 버튼 숨기고 Send 복구
|
||||
setGenerating(false);
|
||||
resetStepper();
|
||||
Sound.success();
|
||||
break;
|
||||
case 'restoreHistory':
|
||||
case 'sessionLoaded':
|
||||
const historyPayload = msg.type === 'sessionLoaded' ? msg.value : msg.value;
|
||||
const history = Array.isArray(historyPayload)
|
||||
? historyPayload
|
||||
: (Array.isArray(historyPayload?.history) ? historyPayload.history : []);
|
||||
|
||||
if (history && history.length > 0) {
|
||||
renderHistory(history);
|
||||
saveWebviewState(history);
|
||||
}
|
||||
if (historyPayload?.negativePrompt !== undefined) {
|
||||
negativePrompt.value = historyPayload.negativePrompt;
|
||||
}
|
||||
historyOverlay.classList.remove('visible');
|
||||
break;
|
||||
case 'clearChat':
|
||||
chat.innerHTML = '<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">Welcome to Astra</div><p>Your premium local AI assistant.<br>Ready to analyze projects and build reports.</p></div>';
|
||||
break;
|
||||
case 'focusInput':
|
||||
input.focus();
|
||||
break;
|
||||
case 'modelsList': {
|
||||
modelSel.innerHTML = '';
|
||||
// [State Persistence - Tier 2] LocalStorage에서 마지막 선택 모델 복원 시도
|
||||
const _savedModel = localStorage.getItem('g1nation_last_model');
|
||||
// 서버 추천 모델 vs 로컬 저장 모델 중 우선순위 결정
|
||||
// LocalStorage에 저장된 값이 현재 목록에 있으면 그것을 우선 사용 (Tier 2 우선)
|
||||
const _preferredModel = (_savedModel && msg.value.models.includes(_savedModel))
|
||||
? _savedModel
|
||||
: msg.value.selected;
|
||||
msg.value.models.forEach(m => {
|
||||
const o = document.createElement('option'); o.value = m; o.innerText = m;
|
||||
if (m === _preferredModel) o.selected = true;
|
||||
modelSel.appendChild(o);
|
||||
});
|
||||
// LocalStorage에 저장된 모델이 실제로 적용된 경우, 백엔드 설정도 동기화
|
||||
if (_savedModel && _savedModel !== msg.value.selected && msg.value.models.includes(_savedModel)) {
|
||||
vscode.postMessage({ type: 'model', value: _savedModel });
|
||||
}
|
||||
if (typeof updateInputPlaceholder === 'function') updateInputPlaceholder();
|
||||
statusLabel.innerText = `Model: ${_preferredModel}`;
|
||||
break;
|
||||
}
|
||||
case 'brainProfiles':
|
||||
brainSel.innerHTML = '';
|
||||
msg.value.profiles.forEach(p => {
|
||||
const o = document.createElement('option'); o.value = p.id; o.innerText = p.name;
|
||||
if (p.id === msg.value.activeBrainId) o.selected = true;
|
||||
brainSel.appendChild(o);
|
||||
});
|
||||
const addOpt = document.createElement('option');
|
||||
addOpt.value = 'new'; addOpt.innerText = '+ Add New Brain...';
|
||||
brainSel.appendChild(addOpt);
|
||||
break;
|
||||
case 'sessionList':
|
||||
historyList.innerHTML = '';
|
||||
msg.value.forEach(s => {
|
||||
const el = document.createElement('div'); el.className = 'history-item';
|
||||
el.setAttribute('role', 'button');
|
||||
el.tabIndex = 0;
|
||||
el.dataset.sessionId = s.id;
|
||||
el.innerHTML = `<div style="font-weight:600; color:var(--text-bright); margin-bottom:2px;">${s.title}</div><div style="font-size:10px; color:var(--text-dim)">${new Date(s.timestamp).toLocaleString()} · ${s.messageCount} msgs</div>`;
|
||||
const load = () => {
|
||||
if (!el.dataset.sessionId) return;
|
||||
vscode.postMessage({ type: 'loadSession', id: el.dataset.sessionId });
|
||||
};
|
||||
el.addEventListener('click', load);
|
||||
el.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
load();
|
||||
}
|
||||
});
|
||||
historyList.appendChild(el);
|
||||
});
|
||||
break;
|
||||
case 'engineStatus':
|
||||
statusDot.style.background = msg.value.online ? 'var(--success)' : 'var(--error)';
|
||||
engineStatusText.innerText = msg.value.online ? 'Online' : 'Offline';
|
||||
break;
|
||||
case 'autoContinue':
|
||||
statusLabel.innerText = msg.value; thinkingBar.classList.add('active');
|
||||
if (msg.value.includes('Analyzing')) setStep('analyze');
|
||||
if (msg.value.includes('Planning')) setStep('plan');
|
||||
if (msg.value.includes('Executing')) setStep('execute');
|
||||
setTimeout(() => { thinkingBar.classList.remove('active'); }, 3000);
|
||||
break;
|
||||
case 'agentsList':
|
||||
agentSel.innerHTML = '<option value="none">No Agent</option>';
|
||||
msg.value.forEach(a => {
|
||||
const o = document.createElement('option'); o.value = a.path; o.innerText = a.name;
|
||||
if (a.path === msg.selected) o.selected = true;
|
||||
agentSel.appendChild(o);
|
||||
});
|
||||
if (msg.selected && msg.selected !== 'none') {
|
||||
vscode.postMessage({ type: 'getAgentContent', path: msg.selected });
|
||||
}
|
||||
break;
|
||||
case 'chronicleProjects':
|
||||
designerSel.innerHTML = '';
|
||||
msg.value.projects.forEach(p => {
|
||||
const o = document.createElement('option');
|
||||
o.value = p.id;
|
||||
o.innerText = p.name;
|
||||
o.title = p.recordRoot;
|
||||
if (p.id === msg.value.activeProjectId) o.selected = true;
|
||||
designerSel.appendChild(o);
|
||||
});
|
||||
const newDesignerOpt = document.createElement('option');
|
||||
newDesignerOpt.value = 'new';
|
||||
newDesignerOpt.innerText = '+ Add Designer Project...';
|
||||
designerSel.appendChild(newDesignerOpt);
|
||||
vscode.postMessage({ type: 'getChronicleRecords' });
|
||||
break;
|
||||
case 'chronicleRecords':
|
||||
chronicleRecordSel.innerHTML = '';
|
||||
if (!msg.value || msg.value.length === 0) {
|
||||
const emptyRecordOpt = document.createElement('option');
|
||||
emptyRecordOpt.value = '';
|
||||
emptyRecordOpt.innerText = 'No records yet';
|
||||
chronicleRecordSel.appendChild(emptyRecordOpt);
|
||||
break;
|
||||
}
|
||||
msg.value.forEach(record => {
|
||||
const o = document.createElement('option');
|
||||
o.value = record.path;
|
||||
o.innerText = record.relativePath;
|
||||
o.title = record.path;
|
||||
chronicleRecordSel.appendChild(o);
|
||||
});
|
||||
break;
|
||||
case 'agentContent':
|
||||
agentPrompt.value = msg.value;
|
||||
negativePrompt.value = msg.negativePrompt || '';
|
||||
break;
|
||||
case 'agentDeleted':
|
||||
agentConfigPanel.style.display = 'none';
|
||||
editMode = false;
|
||||
editAgentBtn.classList.remove('active');
|
||||
agentPrompt.value = '';
|
||||
negativePrompt.value = '';
|
||||
break;
|
||||
case 'error':
|
||||
thinkingBar.classList.remove('active'); sendBtn.disabled = false;
|
||||
addMsg(msg.value, 'error');
|
||||
break;
|
||||
case 'lmStudioError':
|
||||
showToast('LM Studio: ' + msg.value, 'warn');
|
||||
break;
|
||||
case 'requiresApproval':
|
||||
const box = document.createElement('div');
|
||||
box.className = 'approval-box';
|
||||
box.innerHTML = '<div class="approval-title"><span>🛡️</span> 작업 승인 대기 중 (Action Approval Required)</div>' +
|
||||
'<div style="font-size: 11px; color: var(--text-dim); margin-bottom: 8px;">위의 변경 사항을 프로젝트에 반영할까요?</div>' +
|
||||
'<div class="approval-btns">' +
|
||||
' <button class="btn-approve" onclick="approve()">승인 (Approve)</button>' +
|
||||
' <button class="btn-reject" onclick="reject()">롤백 (Rollback)</button>' +
|
||||
'</div>';
|
||||
chat.appendChild(box);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function renderAttachments() {
|
||||
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 = 'file-chip';
|
||||
chip.innerHTML = `<span>📎</span> ${f.name} <span class="remove" onclick="removeFile(${i})">✕</span>`;
|
||||
attachPreview.appendChild(chip);
|
||||
});
|
||||
}
|
||||
window.removeFile = (i) => {
|
||||
pendingFiles.splice(i, 1);
|
||||
renderAttachments();
|
||||
// 파일 삭제 후 Draft State 재평가
|
||||
setDraftActive(input.value.trim().length > 0 || pendingFiles.length > 0);
|
||||
};
|
||||
function processFiles(files) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
Array.from(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 });
|
||||
renderAttachments();
|
||||
setDraftActive(true);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
showToast(`${files.length}개의 파일이 추가되었습니다.`, 'success');
|
||||
Sound.success();
|
||||
}
|
||||
|
||||
attachBtn.onclick = () => fileInput.click();
|
||||
fileInput.onchange = () => {
|
||||
processFiles(fileInput.files);
|
||||
fileInput.value = '';
|
||||
};
|
||||
|
||||
// --- Drag and Drop Implementation ---
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
document.body.addEventListener(eventName, e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, false);
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
document.body.addEventListener(eventName, () => {
|
||||
document.body.classList.add('drag-over');
|
||||
}, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
document.body.addEventListener(eventName, () => {
|
||||
document.body.classList.remove('drag-over');
|
||||
}, false);
|
||||
});
|
||||
|
||||
document.body.addEventListener('drop', e => {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
// ⭐ Kodari PD 가이드 반영: Input 요소의 상태를 드롭된 파일로 강제 동기화
|
||||
if (files && files.length > 0) {
|
||||
fileInput.files = files; // Input의 files 속성 업데이트
|
||||
console.log(`✅ [DnD] Input 상태 동기화 성공: ${files[0].name} 외 ${files.length - 1}개`);
|
||||
}
|
||||
|
||||
processFiles(files);
|
||||
}, false);
|
||||
|
||||
function send() {
|
||||
const val = input.value.trim();
|
||||
if (!val && pendingFiles.length === 0) return;
|
||||
addMsg(val || (pendingFiles.length > 0 ? `[Sent ${pendingFiles.length} files]` : ''), 'user');
|
||||
vscode.postMessage({
|
||||
type: 'prompt',
|
||||
value: val,
|
||||
model: modelSel.value,
|
||||
internet: internetEnabled,
|
||||
files: pendingFiles.length > 0 ? pendingFiles : undefined,
|
||||
agentFile: agentSel.value === 'none' ? undefined : agentSel.value,
|
||||
brainProfileId: brainSel.value && brainSel.value !== 'new' ? brainSel.value : undefined,
|
||||
negativePrompt: negativePrompt.value.trim() || undefined,
|
||||
secondBrainTrace: secondBrainTraceEnabled,
|
||||
secondBrainTraceDebug
|
||||
});
|
||||
input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
|
||||
// 전송 완료 후 Draft State 리셋 + Stop 버튼 표시
|
||||
setDraftActive(false);
|
||||
setGenerating(true);
|
||||
thinkingBar.classList.add('active');
|
||||
|
||||
// Save state after sending
|
||||
const currentState = vscode.getState() || { history: [] };
|
||||
currentState.history.push({ role: 'user', content: val });
|
||||
saveWebviewState(currentState.history);
|
||||
}
|
||||
|
||||
sendBtn.onclick = send;
|
||||
input.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (e.isComposing) return;
|
||||
e.preventDefault();
|
||||
send();
|
||||
}
|
||||
});
|
||||
let _lastActivityBump = 0;
|
||||
const ACTIVITY_BUMP_INTERVAL_MS = 5000;
|
||||
const bumpActivity = () => {
|
||||
const now = Date.now();
|
||||
if (now - _lastActivityBump < ACTIVITY_BUMP_INTERVAL_MS) return;
|
||||
_lastActivityBump = now;
|
||||
vscode.postMessage({ type: 'activity' });
|
||||
};
|
||||
input.addEventListener('input', () => {
|
||||
input.style.height = 'auto';
|
||||
input.style.height = input.scrollHeight + 'px';
|
||||
// Draft State: 내용이 있으면 cancelBtn 표시
|
||||
setDraftActive(input.value.trim().length > 0 || pendingFiles.length > 0);
|
||||
bumpActivity();
|
||||
});
|
||||
|
||||
cancelBtn.onclick = () => clearDraft();
|
||||
stopBtn.onclick = () => {
|
||||
vscode.postMessage({ type: 'stopGeneration' });
|
||||
setGenerating(false);
|
||||
thinkingBar.classList.remove('active');
|
||||
showToast('■ 생성이 중단되었습니다.', 'warn');
|
||||
Sound.warn();
|
||||
};
|
||||
|
||||
const startNewChat = () => { Sound.play(660, 'sine', 0.1); vscode.postMessage({ type: 'newChat' }); };
|
||||
document.getElementById('newChatBtn').onclick = startNewChat;
|
||||
document.getElementById('inputNewChatBtn').onclick = startNewChat;
|
||||
|
||||
document.getElementById('settingsBtn').onclick = () => vscode.postMessage({ type: 'openSettings' });
|
||||
document.getElementById('internetBtn').onclick = () => {
|
||||
internetEnabled = !internetEnabled; document.getElementById('internetBtn').classList.toggle('active', internetEnabled);
|
||||
};
|
||||
document.getElementById('brainTraceBtn').onclick = () => {
|
||||
secondBrainTraceEnabled = !secondBrainTraceEnabled;
|
||||
const btn = document.getElementById('brainTraceBtn');
|
||||
btn.classList.toggle('active', secondBrainTraceEnabled);
|
||||
btn.setAttribute('data-tooltip', secondBrainTraceEnabled ? 'Second Brain Trace Mode: On' : 'Second Brain Trace Mode: Off');
|
||||
saveUiState();
|
||||
};
|
||||
document.getElementById('brainTraceDebugBtn').onclick = () => {
|
||||
secondBrainTraceDebug = !secondBrainTraceDebug;
|
||||
const btn = document.getElementById('brainTraceDebugBtn');
|
||||
btn.classList.toggle('active', secondBrainTraceDebug);
|
||||
btn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');
|
||||
saveUiState();
|
||||
};
|
||||
|
||||
const syncBrain = () => { Sound.play(550, 'sine', 0.1); vscode.postMessage({ type: 'syncBrain' }); };
|
||||
document.getElementById('brainBtn').onclick = syncBrain;
|
||||
saveWikiRawBtn.onclick = () => vscode.postMessage({ type: 'saveWikiRaw' });
|
||||
addBrainBtn.onclick = () => vscode.postMessage({ type: 'addBrain' });
|
||||
editBrainBtn.onclick = () => {
|
||||
if (!brainSel.value || brainSel.value === 'new') return;
|
||||
vscode.postMessage({ type: 'editBrain', id: brainSel.value });
|
||||
};
|
||||
deleteBrainBtn.onclick = () => {
|
||||
if (!brainSel.value || brainSel.value === 'new') return;
|
||||
vscode.postMessage({ type: 'deleteBrain', id: brainSel.value });
|
||||
};
|
||||
document.getElementById('inputSyncBtn').onclick = syncBrain;
|
||||
document.getElementById('historyBtn').onclick = () => vscode.postMessage({ type: 'getSessions' });
|
||||
document.getElementById('historyBtn').addEventListener('click', () => historyOverlay.classList.add('visible'));
|
||||
document.getElementById('closeHistoryBtn').onclick = () => historyOverlay.classList.remove('visible');
|
||||
const updateInputPlaceholder = () => {
|
||||
if (typeof input !== 'undefined' && input) {
|
||||
input.placeholder = `Ask ${modelSel ? modelSel.value : 'AI'}...`;
|
||||
}
|
||||
};
|
||||
|
||||
modelSel.onchange = () => {
|
||||
const _selectedModel = modelSel.value;
|
||||
// [State Persistence - Tier 2] 모델 변경 시 LocalStorage에 즉시 저장 (클라이언트 측 지속성)
|
||||
try {
|
||||
localStorage.setItem('g1nation_last_model', _selectedModel);
|
||||
} catch(e) {
|
||||
console.warn('[Astra] LocalStorage 저장 실패:', e);
|
||||
}
|
||||
// [State Persistence - Tier 1] VS Code 전역 설정에 동기화 (영구 저장)
|
||||
vscode.postMessage({ type: 'model', value: _selectedModel });
|
||||
updateInputPlaceholder();
|
||||
// 상태 레이블 즉시 업데이트
|
||||
statusLabel.innerText = `Model: ${_selectedModel}`;
|
||||
};
|
||||
brainSel.onchange = () => {
|
||||
if (brainSel.value === 'new') {
|
||||
vscode.postMessage({ type: 'addBrain' });
|
||||
} else {
|
||||
vscode.postMessage({ type: 'setBrainProfile', id: brainSel.value });
|
||||
}
|
||||
};
|
||||
|
||||
designerSel.onchange = () => {
|
||||
if (designerSel.value === 'new') {
|
||||
vscode.postMessage({ type: 'createChronicleProject' });
|
||||
} else {
|
||||
vscode.postMessage({ type: 'setChronicleProject', id: designerSel.value });
|
||||
vscode.postMessage({ type: 'getChronicleRecords' });
|
||||
}
|
||||
};
|
||||
|
||||
agentSel.onchange = () => {
|
||||
if (agentSel.value !== 'none') {
|
||||
vscode.postMessage({ type: 'getAgentContent', path: agentSel.value });
|
||||
// [State Persistence Fix] 에이전트 선택값을 즉시 백엔드에 저장
|
||||
vscode.postMessage({ type: 'saveAgentSelection', path: agentSel.value });
|
||||
if (editMode) agentConfigPanel.style.display = 'flex';
|
||||
} else {
|
||||
agentConfigPanel.style.display = 'none';
|
||||
editMode = false;
|
||||
editAgentBtn.classList.remove('active');
|
||||
agentPrompt.value = '';
|
||||
negativePrompt.value = '';
|
||||
// [State Persistence Fix] 에이전트 해제도 즉시 저장
|
||||
vscode.postMessage({ type: 'saveAgentSelection', path: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
editAgentBtn.onclick = () => {
|
||||
if (agentSel.value === 'none') return;
|
||||
editMode = !editMode;
|
||||
editAgentBtn.classList.toggle('active', editMode);
|
||||
agentConfigPanel.style.display = editMode ? 'flex' : 'none';
|
||||
};
|
||||
|
||||
updateAgentBtn.onclick = () => {
|
||||
if (agentSel.value !== 'none') {
|
||||
vscode.postMessage({
|
||||
type: 'updateAgent',
|
||||
path: agentSel.value,
|
||||
content: agentPrompt.value,
|
||||
negativePrompt: negativePrompt.value.trim()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addAgentBtn.onclick = () => vscode.postMessage({ type: 'createAgent' });
|
||||
deleteAgentBtn.onclick = () => {
|
||||
if (agentSel.value === 'none') return;
|
||||
vscode.postMessage({ type: 'deleteAgent', path: agentSel.value });
|
||||
};
|
||||
|
||||
document.getElementById('addDesignerBtn').onclick = () => vscode.postMessage({ type: 'createChronicleProject' });
|
||||
document.getElementById('openDesignerBtn').onclick = () => vscode.postMessage({ type: 'openChronicleFolder' });
|
||||
document.getElementById('refreshChronicleRecordsBtn').onclick = () => vscode.postMessage({ type: 'getChronicleRecords' });
|
||||
document.getElementById('openChronicleRecordBtn').onclick = () => {
|
||||
if (!chronicleRecordSel.value) return;
|
||||
vscode.postMessage({ type: 'openChronicleRecord', path: chronicleRecordSel.value });
|
||||
};
|
||||
|
||||
vscode.postMessage({ type: 'getModels' });
|
||||
vscode.postMessage({ type: 'getAgents' });
|
||||
vscode.postMessage({ type: 'getChronicleProjects' });
|
||||
vscode.postMessage({ type: 'getChronicleRecords' });
|
||||
vscode.postMessage({ type: 'ready' });
|
||||
|
||||
// --- Proactive Behavioral Tracking ---
|
||||
let hoverTimer = null;
|
||||
const trackBehavior = (elementId, context) => {
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) return;
|
||||
el.addEventListener('mouseenter', () => {
|
||||
hoverTimer = setTimeout(() => {
|
||||
vscode.postMessage({ type: 'proactiveTrigger', context: context });
|
||||
}, 5000); // 5 seconds threshold
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
if (hoverTimer) clearTimeout(hoverTimer);
|
||||
});
|
||||
};
|
||||
|
||||
trackBehavior('settingsBtn', 'settings_exploration');
|
||||
trackBehavior('brainBtn', 'brain_sync_exploration');
|
||||
trackBehavior('agentSel', 'agent_selection_exploration');
|
||||
Reference in New Issue
Block a user