Release v2.1.9: Immersive onboarding and UX transformation

This commit is contained in:
g1nation
2026-05-14 22:39:13 +09:00
parent 6b10d002fa
commit d9d89e6db7
15 changed files with 349 additions and 113 deletions
+156 -1
View File
@@ -210,7 +210,155 @@
if (ctxBrainName) ctxBrainName.textContent = selText(brainSel) || '—';
if (ctxAgentName) { const t = selText(agentSel); ctxAgentName.textContent = (!t || /no agent/i.test(t)) ? '기본' : t; }
if (ctxProjectName) ctxProjectName.textContent = selText(designerSel) || '—';
// welcome 패널이 떠 있으면 두뇌/프로젝트 변경이 즉시 반영되도록 같이 호출.
try { _renderWelcome(); } catch { /* 초기화 전 호출은 무시 */ }
}
// ──────────────────────────────────────────────────────────────────────
// Welcome panel — 빈 채팅 상태에서 보이는 동적 시작 가이드.
//
// 첫 사용자(두뇌 미설정·모델 미선택)에게는 "시작하기 3단계" 체크리스트를,
// 이미 준비가 끝난 사용자에게는 예시 질문 chip을 보여준다. 같은 슬롯에서
// 상태에 따라 내용만 바뀌므로 노이즈가 안 쌓인다. 첫 메시지가 가면
// sender(`if (document.querySelector('.welcome')) ... .remove()`)가
// 패널 자체를 제거하므로 dismiss 로직은 별도로 둘 필요 없음.
// ──────────────────────────────────────────────────────────────────────
const _SAMPLE_PROMPTS = [
{ emoji: '📋', text: '지금 열린 프로젝트의 구조를 분석하고 핵심 모듈을 알려줘' },
{ emoji: '🐞', text: '이 코드에서 잠재적인 버그·엣지 케이스가 있는지 검토해줘' },
{ emoji: '✍️', text: '이 함수에 사용자 입장에서 이해하기 쉬운 주석을 달아줘' },
{ emoji: '🧭', text: '오늘 무엇부터 하면 좋을지 우선순위를 짜줘 (1인 기업 모드 추천)' },
];
function _renderWelcome() {
const panel = document.getElementById('welcomePanel');
if (!panel) return;
// 패널이 한 번 제거된 상태(첫 메시지 이후)면 다시 만들지 않는다.
if (!panel.isConnected) return;
const brainName = (ctxBrainName && ctxBrainName.textContent || '').trim();
const hasBrain = brainName && brainName !== '—';
const modelVal = (modelSel && modelSel.value || '').trim()
|| (document.getElementById('modelInlineSel') && document.getElementById('modelInlineSel').value || '').trim();
const hasModel = !!modelVal;
const ready = hasBrain && hasModel;
panel.innerHTML = '';
const logo = document.createElement('div');
logo.className = 'welcome-logo';
logo.textContent = '✦';
const title = document.createElement('div');
title.className = 'welcome-title';
title.textContent = ready
? '준비 완료. 무엇을 도와드릴까요?'
: 'Astra에 오신 것을 환영합니다';
const lead = document.createElement('p');
lead.className = 'welcome-lead';
lead.textContent = ready
? '아래 예시 중 하나를 눌러 시작하거나, 입력창에 직접 적어보세요.'
: '로컬에서 동작하는 개인 AI 비서입니다. 시작하기 전에 두 가지만 확인해주세요.';
panel.appendChild(logo);
panel.appendChild(title);
panel.appendChild(lead);
if (!ready) {
// ── 시작 체크리스트 (3단계) ──
const steps = document.createElement('div');
steps.className = 'welcome-checklist';
const mkStep = (n, done, label, hint, actionLabel, onAction) => {
const row = document.createElement('div');
row.className = 'wc-step' + (done ? ' done' : '');
const bullet = document.createElement('div');
bullet.className = 'wc-bullet';
bullet.textContent = done ? '✓' : String(n);
const txt = document.createElement('div');
txt.className = 'wc-text';
const t1 = document.createElement('div');
t1.className = 'wc-label';
t1.textContent = label;
const t2 = document.createElement('div');
t2.className = 'wc-hint';
t2.textContent = hint;
txt.appendChild(t1);
txt.appendChild(t2);
row.appendChild(bullet);
row.appendChild(txt);
if (!done && actionLabel && onAction) {
const btn = document.createElement('button');
btn.className = 'wc-action';
btn.textContent = actionLabel;
btn.onclick = onAction;
row.appendChild(btn);
}
return row;
};
steps.appendChild(mkStep(
1, hasBrain,
'두뇌(지식 폴더) 연결',
'자주 쓰는 노트·문서를 모아둔 로컬 폴더입니다. Astra가 답변할 때 이 폴더의 내용을 참고합니다.',
'두뇌 추가',
() => {
const editBtn = document.getElementById('contextEditBtn');
if (editBtn) editBtn.click();
const addBrainBtn = document.getElementById('addBrainBtn');
if (addBrainBtn) setTimeout(() => addBrainBtn.click(), 120);
},
));
steps.appendChild(mkStep(
2, hasModel,
'사용할 모델 선택',
'LM Studio 또는 Ollama에 로드되어 있는 로컬 모델을 고릅니다.',
'모델 열기',
() => {
const editBtn = document.getElementById('contextEditBtn');
if (editBtn) editBtn.click();
setTimeout(() => { if (modelSel) modelSel.focus(); }, 120);
},
));
steps.appendChild(mkStep(
3, false,
'첫 질문 적어보기',
'아래 입력창에 자연어로 무엇이든 적어보세요. 코드·문서·아이디어 모두 가능합니다.',
'입력창으로',
() => { try { input && input.focus(); } catch {} },
));
panel.appendChild(steps);
return;
}
// ── 준비 완료 상태: 예시 질문 chip ──
const chips = document.createElement('div');
chips.className = 'welcome-chips';
for (const p of _SAMPLE_PROMPTS) {
const chip = document.createElement('button');
chip.className = 'welcome-chip';
chip.innerHTML = `<span class="welcome-chip-emoji">${p.emoji}</span><span class="welcome-chip-text"></span>`;
chip.querySelector('.welcome-chip-text').textContent = p.text;
chip.title = '클릭하면 입력창에 채워집니다 (자동 전송 안 함).';
chip.onclick = () => {
if (!input) return;
input.value = p.text;
input.style.height = 'auto';
input.style.height = input.scrollHeight + 'px';
input.focus();
};
chips.appendChild(chip);
}
panel.appendChild(chips);
// ── 하단 부드러운 안내 (1인 기업 모드, 단축키) ──
const tips = document.createElement('div');
tips.className = 'welcome-tips';
tips.innerHTML = '복잡한 작업은 헤더의 <b>[기업 모드]</b>로 여러 전문 에이전트에게 분배할 수 있습니다. · 입력 후 <b>Cmd/Ctrl + Enter</b>로 전송.';
panel.appendChild(tips);
}
// 모델 selector가 바뀌면 welcome도 즉시 갱신.
document.addEventListener('change', (e) => {
if (e.target && (e.target.id === 'modelSel' || e.target.id === 'modelInlineSel')) {
try { _renderWelcome(); } catch {}
}
});
function syncRecordsLine() {
if (!recordsLatest) return;
const opt = chronicleRecordSel && chronicleRecordSel.value ? selText(chronicleRecordSel) : '';
@@ -656,7 +804,11 @@
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>';
// welcomePanel을 빈 div로 다시 만들고 _renderWelcome으로 상태에 맞는
// 내용을 채워 넣는다 — 신규 사용자에게는 체크리스트, 준비된 사용자에게는
// 예시 질문 chip이 나옴.
chat.innerHTML = '<div class="welcome" id="welcomePanel"></div>';
try { _renderWelcome(); } catch {}
break;
case 'focusInput':
input.focus();
@@ -706,6 +858,9 @@
&& document.getElementById('companyOverlay')?.classList.contains('visible')) {
renderCompanyAgentCards(_lastCompanyAgentsPayload);
}
// 모델 목록이 채워졌고 default 선택이 정해졌으니 welcome 패널의
// "사용할 모델 선택" 체크 표시도 즉시 업데이트.
try { _renderWelcome(); } catch {}
break;
}
case 'brainProfiles':