Update ConnectAI codebase

This commit is contained in:
g1nation
2026-05-18 08:15:01 +09:00
parent 88664c7c6e
commit 86cacaeb03
38 changed files with 1043 additions and 99 deletions
+12
View File
@@ -284,6 +284,18 @@
<div id="providersError" class="error" hidden></div>
</section>
<!-- Devil Agent (도현) -->
<section class="section" data-section="devilAgent">
<h2>🎭 Devil's Advocate (도현)</h2>
<p class="hint">매 답변 직후 별도 LLM 호출로 *비판적 sparring partner* 가 한 문단 반박. 사용자의 사고를 능동적 방어로 전환. 같은 모델 재사용 (~10-15% 추가 비용).</p>
<div class="row toggle">
<label><input id="devilEnabled" type="checkbox"> 도현 활성화 — 답변마다 반박 카드</label>
</div>
<div class="row">
<small class="hint">규칙: 한 답변당 약점 1개만, 통계·수치 인용 금지 (환각 차단), 끝에 우려+검증 방법 명시. 명령 팔레트 <code>Astra: Toggle Devil Agent 🎭</code> 로도 토글.</small>
</div>
</section>
<!-- Advanced -->
<section class="section" data-section="advanced">
<h2>고급</h2>
+73
View File
@@ -57,6 +57,20 @@
const googleFeedback = $('googleFeedback');
const googleError = $('googleError');
// ---- Devil Agent ----
const devilEnabled = $('devilEnabled');
// ---- Cloud LLM Providers ----
const prOpenrouterEnabled = $('prOpenrouterEnabled');
const prOpenrouterKey = $('prOpenrouterKey');
const prOpenrouterDefault = $('prOpenrouterDefault');
const prAnthropicEnabled = $('prAnthropicEnabled');
const prAnthropicKey = $('prAnthropicKey');
const prAnthropicDefault = $('prAnthropicDefault');
const prGeminiEnabled = $('prGeminiEnabled');
const prGeminiKey = $('prGeminiKey');
const prGeminiDefault = $('prGeminiDefault');
// ---- Banner ----
const bannerError = $('bannerError');
@@ -157,6 +171,43 @@
googleDisconnectBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.disconnect' }));
googleIcalRefreshBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.icalRefresh' }));
// ---- Devil Agent listener ----
devilEnabled.addEventListener('change', (e) =>
vscode.postMessage({ type: 'devilAgent.toggle', enabled: e.target.checked })
);
// ---- Cloud LLM Providers listeners ----
prOpenrouterEnabled.addEventListener('change', (e) =>
vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', enabled: e.target.checked })
);
document.querySelector('[data-save="providers.openrouter.apiKey"]').addEventListener('click', () => {
vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', apiKey: prOpenrouterKey.value });
prOpenrouterKey.value = '';
});
document.querySelector('[data-save="providers.openrouter.defaultModel"]').addEventListener('click', () =>
vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', defaultModel: prOpenrouterDefault.value })
);
prAnthropicEnabled.addEventListener('change', (e) =>
vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', enabled: e.target.checked })
);
document.querySelector('[data-save="providers.anthropic.apiKey"]').addEventListener('click', () => {
vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', apiKey: prAnthropicKey.value });
prAnthropicKey.value = '';
});
document.querySelector('[data-save="providers.anthropic.defaultModel"]').addEventListener('click', () =>
vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', defaultModel: prAnthropicDefault.value })
);
prGeminiEnabled.addEventListener('change', (e) =>
vscode.postMessage({ type: 'providers.update', providerId: 'gemini', enabled: e.target.checked })
);
document.querySelector('[data-save="providers.gemini.apiKey"]').addEventListener('click', () => {
vscode.postMessage({ type: 'providers.update', providerId: 'gemini', apiKey: prGeminiKey.value });
prGeminiKey.value = '';
});
document.querySelector('[data-save="providers.gemini.defaultModel"]').addEventListener('click', () =>
vscode.postMessage({ type: 'providers.update', providerId: 'gemini', defaultModel: prGeminiDefault.value })
);
document.querySelector('[data-save="advanced.ctxSize"]').addEventListener('click', () =>
vscode.postMessage({ type: 'advanced.update', maxContextSize: Number(advCtxSize.value) })
);
@@ -332,6 +383,28 @@
? `마지막 새로고침: ${g.lastIcalFetchAt.slice(0, 16).replace('T', ' ')}`
: '';
}
// ---- Devil Agent ----
if (state.devilAgent) {
devilEnabled.checked = !!state.devilAgent.enabled;
}
// ---- Cloud LLM Providers ----
const pr = state.providers;
if (pr) {
// OpenRouter
prOpenrouterEnabled.checked = !!pr.openrouter.enabled;
prOpenrouterKey.placeholder = pr.openrouter.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'sk-or-...';
setIfNotFocused(prOpenrouterDefault, pr.openrouter.defaultModel);
// Anthropic
prAnthropicEnabled.checked = !!pr.anthropic.enabled;
prAnthropicKey.placeholder = pr.anthropic.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'sk-ant-...';
setIfNotFocused(prAnthropicDefault, pr.anthropic.defaultModel);
// Gemini
prGeminiEnabled.checked = !!pr.gemini.enabled;
prGeminiKey.placeholder = pr.gemini.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'AIzaSy...';
setIfNotFocused(prGeminiDefault, pr.gemini.defaultModel);
}
}
vscode.postMessage({ type: 'ready' });
+44
View File
@@ -953,6 +953,50 @@
border-top: 1px dashed var(--border);
}
/* Devil's Advocate (도현) 반박 카드 — main 답변 직후 한 장씩.
accent 와 구분되도록 보라색 톤 + 마스크 이모지 prefix 로 시선 끌기. */
.devil-rebuttal-card {
border: 1px solid rgba(167, 139, 250, 0.55);
background: rgba(167, 139, 250, 0.06);
border-radius: 10px;
padding: 10px 12px;
margin: 8px 0;
font-size: 12px;
line-height: 1.55;
color: var(--text);
}
.devil-rebuttal-head {
font-size: 11px;
color: #a78bfa;
margin-bottom: 6px;
letter-spacing: 0.02em;
}
.devil-rebuttal-body {
white-space: pre-wrap;
margin-bottom: 8px;
}
.devil-rebuttal-actions {
display: flex;
gap: 8px;
}
.devil-rebuttal-actions button {
background: rgba(167, 139, 250, 0.16);
border: 1px solid rgba(167, 139, 250, 0.4);
color: #c7b7ff;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
cursor: pointer;
}
.devil-rebuttal-actions button.ghost {
background: transparent;
border-color: var(--line);
color: var(--muted);
}
.devil-rebuttal-actions button:hover {
background: rgba(167, 139, 250, 0.28);
}
/* Intent Alignment 카드 — new_task 요청 직후 C-G-C-F 분석 결과를 보여주고
질문 / 확인 버튼을 띄움. 다른 phase 카드보다 살짝 무게감을 주려고
accent 테두리. */
+36
View File
@@ -1011,6 +1011,42 @@
});
break;
}
case 'devilRebuttal': {
// Devil Agent (도현) 카드. main assistant 답변 직후 chat 하단에 한 장 추가.
// 사용자가 '재반박' 누르면 그 텍스트가 다음 user prompt 로 가서 main turn 한 번 더 돌고,
// 재반박-입장 hint 가 prompt 에 prepend 됨.
const v = msg.value || {};
const chatEl = document.getElementById('chat');
if (!chatEl) break;
const card = document.createElement('div');
card.className = 'devil-rebuttal-card';
const persona = String(v.persona || '도현');
const text = String(v.text || '');
card.innerHTML = `
<div class="devil-rebuttal-head">🎭 <strong>${escAttr(persona)}</strong>이(가) 반박합니다</div>
<div class="devil-rebuttal-body"></div>
<div class="devil-rebuttal-actions">
<button class="devil-reply">재반박</button>
<button class="devil-dismiss ghost">넘기기</button>
</div>`;
// body 는 textContent 로 안전하게.
card.querySelector('.devil-rebuttal-body').textContent = text;
chatEl.appendChild(card);
chatEl.scrollTop = chatEl.scrollHeight;
card.querySelector('.devil-dismiss').addEventListener('click', () => card.remove());
card.querySelector('.devil-reply').addEventListener('click', () => {
// 입력창에 prefix 채워서 사용자가 자기 반박 입력하게.
const input = document.getElementById('input');
if (input) {
input.value = `[${persona}의 반박에 답변] `;
input.focus();
// 커서 끝으로.
try { input.setSelectionRange(input.value.length, input.value.length); } catch {}
}
card.remove();
});
break;
}
case 'companyAlignmentCard': {
// Intent Alignment 카드. kind에 따라 4가지 모드:
// - 'auto-proceed' : confidence high → 자동 진행 안내(읽기 전용)