Release v2.1.9: Immersive onboarding and UX transformation
This commit is contained in:
+156
-1
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user