Files
connectai/media/settings-panel.html
T
koriweb 47b3b9f93a v2.2.299~307: 아키텍처 수렴 + 채팅 정리 + 벤치마킹 강화 + Claude 구독 엔진
- v2.2.299 아키텍처 수렴: coreChat 통일(엔진 휴리스틱 3벌 제거), 기업 모드
  검색 오케스트레이터 승격, lib/execUtil(실행 래퍼 6곳·Python 탐지 3벌 단일화),
  lib/kstSchedule(워처 4개 nowInKst 통합), estimateTokens 통합, 설정 접근 규칙 명문화
- v2.2.300 채팅 화면 정리: LiveReasoningFilter(스트리밍 중 <think>/Harmony 추론
  토큰 단위 차단), 확신도·검토요청 footer 기본 숨김(계산·Reflection 은 유지)
- v2.2.301 문맥·의도 이해: [답변 전 이해 원칙] 상시 주입, 워크플로우 의도 브리핑,
  Report QA 루프(규칙 레지스트리+실측치+회귀 게이트, 블로그_v3 개념 이식)
- v2.2.302 /benchmark 비즈니스 렌즈(가격·수익·운영)+빌드 프롬프트 모드+QA 연계
- v2.2.303 handoff 모드(측정치 무손실 인수인계 문서)+/claude(Claude Code 터미널 위임)
- v2.2.304 이식성: 지식 경로 두뇌-상대 규약(pickWikiDir 상대 해석), 이사 체크리스트
- v2.2.305 Claude 구독 엔진: claude: 프로바이더(CLI 위임, 모델 드롭다운 자동 노출,
  coreChat 지원 — 워크플로우·QA도 구독 모델 가능)
- v2.2.306 Tone Guard: AI 상투어 금지 레지스트리(상담사 화법 실사례 8종+대조 예시)
- v2.2.307 /benchmark 레이아웃 골격(sectionRoles 결정론 분석, 롤링 배너 즉답),
  파트별 실패 격리, 합성 타임아웃 120→300초

검증: tsc 무오류 + jest 888 통과 + esbuild 정상

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 21:02:15 +09:00

542 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Astra Settings</title>
<link rel="stylesheet" href="__STYLES_URI__">
</head>
<body>
<header class="hd">
<h1>Astra Settings <span class="ver" id="settingsVersion">v__VERSION__</span></h1>
<button id="openVscodeSettings" class="link">VS Code Settings 열기</button>
</header>
<div id="bannerError" class="banner" hidden></div>
<nav class="tabs" id="settingsTabs">
<button type="button" class="tab" data-tab="model">모델·연결</button>
<button type="button" class="tab" data-tab="knowledge">지식·기억</button>
<button type="button" class="tab" data-tab="integrations">연동</button>
<button type="button" class="tab" data-tab="messenger">메신저</button>
<button type="button" class="tab" data-tab="advanced">동작·고급</button>
</nav>
<main id="root">
<!-- Connection -->
<section class="section" data-section="connection" data-tab="model">
<h2>연결</h2>
<p class="hint">로컬 AI 엔진(Ollama 또는 LM Studio) 위치와 기본 모델을 설정합니다.</p>
<div class="row">
<label for="cnUrl">Engine URL</label>
<div class="input-group">
<input id="cnUrl" type="text" placeholder="http://127.0.0.1:11434" spellcheck="false" />
<button data-save="connection.url">저장</button>
</div>
<small class="hint">Ollama 기본 11434 / LM Studio 기본 1234.</small>
</div>
<div class="row">
<label for="cnModel">기본 모델</label>
<div class="input-group">
<select id="cnModel"></select>
<button data-save="connection.model">저장</button>
<button id="cnRefreshModels" class="ghost" title="모델 목록 새로고침"></button>
</div>
<small class="hint" id="cnModelHint">사이드바에서 선택한 모델이 여기에도 동기화됩니다.</small>
</div>
<div class="row">
<label for="cnTimeout">요청 타임아웃 (초)</label>
<div class="input-group narrow">
<input id="cnTimeout" type="number" min="1" step="1" />
<button data-save="connection.timeout">저장</button>
</div>
</div>
</section>
<!-- Datacollect -->
<section class="section" data-section="datacollect" data-tab="integrations">
<h2>Datacollect (slash 명령)</h2>
<p class="hint">채팅에서 <code>/research</code> · <code>/benchmark</code> · <code>/youtube</code> 를 입력하면 Datacollect Bridge로 위임됩니다. <strong>타깃</strong>으로 로컬(<code>npm run bridge</code>) 또는 NAS의 경량 Bridge 중 어디를 호출할지 선택합니다.</p>
<div class="row">
<label for="dcBridgeTarget">Bridge 타깃</label>
<div class="input-group narrow">
<select id="dcBridgeTarget">
<option value="local">로컬 (Local)</option>
<option value="nas">NAS</option>
</select>
<button data-save="datacollect.bridgeTarget">저장</button>
</div>
<small class="hint"><strong>local</strong> = 아래 로컬 Bridge URL 사용. <strong>nas</strong> = NAS Bridge URL(+토큰) 사용. nas인데 URL이 비어 있으면 안전하게 로컬로 폴백합니다.</small>
</div>
<div class="row">
<label for="dcBridgeUrl">로컬 Bridge URL</label>
<div class="input-group">
<input id="dcBridgeUrl" type="text" placeholder="http://127.0.0.1:3002" spellcheck="false" />
<button data-save="datacollect.bridgeUrl">저장</button>
</div>
</div>
<div class="row">
<label for="dcBridgeNasUrl">NAS Bridge URL</label>
<div class="input-group">
<input id="dcBridgeNasUrl" type="text" placeholder="https://your-nas-domain 또는 http://nas-ip:3002" spellcheck="false" />
<button data-save="datacollect.bridgeNasUrl">저장</button>
</div>
<small class="hint">타깃이 <strong>nas</strong>일 때 호출할 NAS 경량 Bridge 주소.</small>
</div>
<div class="row">
<label for="dcBridgeNasToken">NAS Bridge 토큰</label>
<div class="input-group">
<input id="dcBridgeNasToken" type="text" placeholder="(NAS의 BRIDGE_AUTH_TOKEN 값)" spellcheck="false" />
<button data-save="datacollect.bridgeNasToken">저장</button>
</div>
<small class="hint">NAS Bridge의 <code>x-bridge-token</code>. <strong>nas</strong> 타깃일 때만 요청 헤더에 실립니다.</small>
</div>
<div class="row">
<label for="dcCrawlDepth">크롤 깊이 기본값</label>
<div class="input-group narrow">
<input id="dcCrawlDepth" type="number" min="0" max="3" step="1" />
<button data-save="datacollect.crawlDepth">저장</button>
</div>
<small class="hint">/benchmark 사이트맵 크롤 깊이. 0=루트만, 1=직속 자식, 2=손자, 3=깊은 크롤. 명령어에서 <code>depth=N</code> 으로 그때그때 덮어쓸 수 있습니다.</small>
</div>
<div class="row">
<label for="dcMaxPages">최대 페이지 기본값</label>
<div class="input-group narrow">
<input id="dcMaxPages" type="number" min="1" max="20" step="1" />
<button data-save="datacollect.maxPages">저장</button>
</div>
<small class="hint">/benchmark 스캔 최대 페이지 수. 명령어에서 <code>pages=N</code> 으로 덮어쓸 수 있습니다 (Bridge 상한 20).</small>
</div>
<div class="row">
<label for="dcSynthTemp">합성 Temperature</label>
<div class="input-group narrow">
<input id="dcSynthTemp" type="number" min="0" max="2" step="0.05" />
<button data-save="datacollect.synthesisTemperature">저장</button>
</div>
<small class="hint">/benchmark LLM 4-렌즈 합성의 temperature. 낮을수록(0.1) 환각·깨진 문자가 줄고 결정적입니다. 기본 0.1 권장.</small>
</div>
</section>
<!-- Memory -->
<section class="section" data-section="memory" data-tab="knowledge">
<h2>메모리</h2>
<p class="hint">대화 응답 전에 주입되는 단기/중기/장기 메모리의 양을 조정합니다.</p>
<div class="row toggle">
<label><input id="memEnabled" type="checkbox"> 메모리 시스템 활성화</label>
</div>
<div class="row">
<label for="memShort">최근 대화 메시지 (단기)</label>
<div class="input-group narrow">
<input id="memShort" type="number" min="0" step="1" />
<button data-save="memory.short">저장</button>
</div>
</div>
<div class="row">
<label for="memMid">최근 세션 (중기)</label>
<div class="input-group narrow">
<input id="memMid" type="number" min="0" step="1" />
<button data-save="memory.mid">저장</button>
</div>
</div>
<div class="row">
<label for="memLong">관련 brain 문서 (장기)</label>
<div class="input-group narrow">
<input id="memLong" type="number" min="0" step="1" />
<button data-save="memory.long">저장</button>
</div>
</div>
</section>
<!-- Brain -->
<section class="section" data-section="brain" data-tab="knowledge">
<h2>두뇌 (지식 폴더)</h2>
<p class="hint">현재 활성 두뇌 프로필 정보입니다. 추가·수정은 사이드바의 [변경 ▾ → 두뇌] 또는 VS Code Settings에서 처리합니다.</p>
<div class="row">
<label>활성 프로필</label>
<div class="readout" id="brainName"></div>
<small class="hint" id="brainPath"></small>
</div>
<div class="row toggle">
<label><input id="brainAutoPush" type="checkbox"> 변경 시 GitHub 자동 push (autoPushBrain)</label>
</div>
<div class="row">
<button id="brainOpenSettings" class="link">brainProfiles 편집 (VS Code Settings)</button>
</div>
<div class="row">
<label for="dcSavePath">지식 문서 저장 폴더</label>
<div class="input-group">
<input id="dcSavePath" type="text" placeholder="(비우면 활성 두뇌의 Datacollect_Wiki)" spellcheck="false" />
<button data-save="datacollect.savePath">저장</button>
</div>
<small class="hint"><code>/wikify</code> · <code>/meet</code> · <code>/benchmark</code> · <code>/youtube</code> · <code>/review</code> 가 생성하는 지식 문서(markdown)의 저장 위치. <strong>PC 로컬에 직접 저장</strong>되며, 비워두면 활성 두뇌 폴더의 <code>Datacollect_Wiki/</code> 에 저장돼 brain 검색에 자동 포함됩니다. 다른 곳에 두려면 절대경로 입력 (예: <code>E:\Wiki\2nd\10_Wiki</code>).</small>
</div>
<div class="row">
<label for="emailWikiSavePath">이메일 지식 문서 저장 폴더</label>
<div class="input-group">
<input id="emailWikiSavePath" type="text" placeholder="(비우면 활성 두뇌의 Email_Wiki)" spellcheck="false" />
<button data-save="email.wikifySavePath">저장</button>
</div>
<small class="hint"><code>/email wikify</code> 로 이메일 스레드를 지식화한 문서는 다른 wikify 산출물과 <strong>별도 폴더에서 관리</strong>됩니다. 비워두면 활성 두뇌 폴더의 <code>Email_Wiki/</code> 에 저장. 다른 곳에 두려면 절대경로 입력.</small>
</div>
</section>
<section class="section" data-section="domainKnowledge" data-tab="knowledge">
<h2>단계별 지식 폴더 (소형 모델 정밀 검색)</h2>
<p class="hint">질문이 <strong>수학·논리 / 코딩·디버깅 / 일반 상식</strong>으로 분류되면 전체 두뇌 대신 <strong>General(공통) + 해당 Specialty 폴더</strong>만 검색해 무관한 문서를 배제합니다. 멀티 에이전트 지식 체계(<code>_Common</code> + <code>Domain_*</code>)와 짝을 이루는 기능으로, 경로는 활성 두뇌 기준 상대 경로(절대 경로도 가능)입니다. Specialty 가 비어 있거나 폴더가 없는 단계는 전체 두뇌를 검색하며, 에이전트별 지식 매핑(agent-knowledge-map)이 있으면 그쪽이 우선합니다.</p>
<div class="row">
<label for="dkGeneral">General Knowledge 폴더 (모든 단계 공통)</label>
<div class="input-group">
<input id="dkGeneral" type="text" placeholder="예: 10_Wiki/Topics/_Common" spellcheck="false" />
<button data-save="domainKnowledge.general">저장</button>
</div>
</div>
<div class="row">
<label for="dkMath">수학·논리 Specialty 폴더</label>
<div class="input-group">
<input id="dkMath" type="text" placeholder="예: 10_Wiki/Topics/_Common/Math" spellcheck="false" />
<button data-save="domainKnowledge.math">저장</button>
</div>
</div>
<div class="row">
<label for="dkCoding">코딩·디버깅 Specialty 폴더</label>
<div class="input-group">
<input id="dkCoding" type="text" placeholder="예: 10_Wiki/Topics/Domain_Programming" spellcheck="false" />
<button data-save="domainKnowledge.coding">저장</button>
</div>
</div>
<div class="row">
<label for="dkFacts">일반 상식·팩트 Specialty 폴더</label>
<div class="input-group">
<input id="dkFacts" type="text" placeholder="예: 10_Wiki/Topics/Domain_General" spellcheck="false" />
<button data-save="domainKnowledge.facts">저장</button>
</div>
</div>
</section>
<!-- Messenger — 텔레그램 발송 스케줄·알림 (v2.2.284 중앙화) -->
<section class="section" data-section="messenger" data-tab="messenger">
<h2>발송 스케줄</h2>
<p class="hint">텔레그램으로 나가는 정기 메시지의 시각을 여기서 관리합니다 (KST 기준).</p>
<div class="row">
<label for="msgStocksTimes">주식 정기 보고서 시각</label>
<div class="input-group">
<input id="msgStocksTimes" type="text" placeholder="09:00,15:00 (비우면 정기 실행 없음)" spellcheck="false" />
<button data-save="messenger.stocksReportTimes">저장</button>
</div>
<small class="hint">HH:MM 을 쉼표로 구분 (예: <code>09:00,12:30,15:00</code>). 이 시각마다 가격 갱신·매수시그널 감지·보고서 발송.</small>
</div>
<div class="row toggle">
<label><input id="msgBriefingEnabled" type="checkbox"> 데일리 브리핑 (일정·할 일·답할 이메일)</label>
</div>
<div class="row">
<label for="msgBriefingTime">데일리 브리핑 시각</label>
<div class="input-group narrow">
<input id="msgBriefingTime" type="text" placeholder="09:30" spellcheck="false" />
<button data-save="messenger.briefingTime">저장</button>
</div>
<small class="hint">평일(월~금) 이 시각에 1회 발송. 미회신 이메일 정리는 이 메시지에 포함됩니다.</small>
</div>
</section>
<section class="section" data-section="messengerAlerts" data-tab="messenger">
<h2>이벤트 알림</h2>
<p class="hint">상황이 발생했을 때 즉시 발송되는 알림들입니다.</p>
<div class="row toggle">
<label><input id="msgBuySignal" type="checkbox"> 🚨 매수 시그널 진입 알림</label>
<small class="hint">관망 종목이 매수사정권에 진입하는 순간 1회.</small>
</div>
<div class="row toggle">
<label><input id="msgDiscoverTop5" type="checkbox"> 🎯 발굴 Top 5 (/stocks discover 후)</label>
</div>
<div class="row">
<label for="msgEmailOverdue">📧 이메일 미회신 즉시 알림 임계(시간)</label>
<div class="input-group narrow">
<input id="msgEmailOverdue" type="number" min="0" step="1" />
<button data-save="messenger.emailOverdueHours">저장</button>
</div>
<small class="hint"><code>0</code> = 끔(기본) — 미회신 정리는 데일리 브리핑이 담당. 즉시 알림을 원하면 시간 입력 (예: 24).</small>
</div>
</section>
<!-- Email providers (/email) -->
<section class="section" data-section="email" data-tab="integrations">
<h2>이메일 (/email)</h2>
<p class="hint">끈 프로바이더는 수집 시도 자체를 하지 않아 채팅에 "제외/실패" 문구가 뜨지 않습니다.</p>
<div class="row toggle">
<label><input id="emailOutlookEnabled" type="checkbox"> Outlook(클래식) 사용</label>
<small class="hint">데스크톱 클래식 Outlook COM — 별도 인증 없음. "새 Outlook" 토글 ON 상태는 미지원.</small>
</div>
<div class="row toggle">
<label><input id="emailGmailEnabled" type="checkbox"> Gmail 사용</label>
<small class="hint">Google OAuth 재연결 1회 필요 (gmail.readonly 동의). 연결 전이거나 안 쓰면 꺼두세요 — /email 출력이 깔끔해집니다.</small>
</div>
</section>
<!-- Telegram -->
<section class="section" data-section="telegram" data-tab="integrations">
<h2>Telegram 봇</h2>
<p class="hint">텔레그램으로 Astra와 대화하고 싶다면 BotFather에서 봇을 만들고 토큰을 여기에 저장하세요. Astra의 다른 기능에는 영향이 없습니다.</p>
<div class="row">
<label for="tgToken">Bot Token</label>
<div class="input-group">
<input id="tgToken" type="password" placeholder="123456789:AA..." autocomplete="off" spellcheck="false" />
<button id="tgSaveToken">저장</button>
<button id="tgClearToken" class="ghost">삭제</button>
</div>
<small id="tgTokenStatus" class="status"></small>
</div>
<div class="row">
<button id="tgTest">연결 테스트</button>
<span id="tgBotName" class="status-inline"></span>
</div>
<div class="row toggle">
<label><input id="tgEnabled" type="checkbox"> 봇 활성화 (체크하면 폴링 시작)</label>
</div>
<div class="row">
<button id="tgEnroll">내 채널 자동 등록</button>
<button id="tgEnrollCancel" class="ghost" hidden>등록 취소</button>
<small id="tgEnrollStatus" class="status"></small>
</div>
<div class="row" id="tgChatList">
<label>허용된 채널 IDs</label>
<ul id="tgChatIds" class="chips"></ul>
<small class="hint">목록이 비어 있으면 누구나 봇에 메시지를 보낼 수 있습니다 (자동 등록을 한 번 하시는 것을 권장).</small>
</div>
<div id="tgFeedback" class="feedback" hidden></div>
<div id="tgError" class="error" hidden></div>
</section>
<!-- Google (Calendar + Sheets) -->
<section class="section" data-section="google" data-tab="integrations">
<h2>Google (Calendar · Sheets)</h2>
<p class="hint">회의록·할일을 Google Calendar 에 자동 등록하고 Sheets 를 읽고 쓰려면 OAuth 가 필요합니다. <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a> 에서 Desktop OAuth Client 만들고 Client ID/Secret 을 아래에 붙여넣으세요.</p>
<div class="row">
<label>현재 연결 상태</label>
<div class="readout" id="googleConnStatus"></div>
</div>
<div class="row">
<label for="gClientId">OAuth Client ID</label>
<div class="input-group">
<input id="gClientId" type="text" placeholder="xxxxxxxx.apps.googleusercontent.com" autocomplete="off" spellcheck="false" />
<button data-save="google.clientId">저장</button>
</div>
</div>
<div class="row">
<label for="gClientSecret">OAuth Client Secret</label>
<div class="input-group">
<input id="gClientSecret" type="password" placeholder="GOCSPX-..." autocomplete="off" spellcheck="false" />
<button data-save="google.clientSecret">저장</button>
</div>
<small class="hint">Desktop OAuth client 의 secret 은 Google 가이드상 비공개 아님. 그래도 Settings Sync 에는 포함되지 않습니다 (machine scope).</small>
</div>
<div class="row">
<button id="googleConnect">OAuth 연결 / 재연결</button>
<button id="googleDisconnect" class="ghost">연결 해제</button>
<span id="googleConnStatusInline" class="status-inline"></span>
</div>
<div class="row">
<label for="gCalendarId">Calendar ID</label>
<div class="input-group">
<input id="gCalendarId" type="text" placeholder="primary" autocomplete="off" spellcheck="false" />
<button data-save="google.calendarId">저장</button>
</div>
<small class="hint">기본 'primary' (본인 메인 캘린더). 다른 캘린더 ID 입력 가능.</small>
</div>
<div class="row">
<label for="gDefaultDur">기본 일정 길이 (분)</label>
<div class="input-group narrow">
<input id="gDefaultDur" type="number" min="5" max="720" step="5" />
<button data-save="google.defaultEventDurationMinutes">저장</button>
</div>
<small class="hint">duration / end 안 지정된 일정 생성 시 사용. Default 60.</small>
</div>
<h3 style="margin-top:18px;font-size:13px;color:var(--muted)">iCal 읽기 (선택 사항)</h3>
<p class="hint">OAuth 없이 비공개 iCal URL 로 일정만 읽고 싶을 때. OAuth 연결돼 있으면 비워둬도 됩니다.</p>
<div class="row">
<label for="gIcalUrl">iCal URL</label>
<div class="input-group">
<input id="gIcalUrl" type="password" placeholder="https://calendar.google.com/calendar/ical/.../basic.ics" autocomplete="off" spellcheck="false" />
<button data-save="google.icalUrl">저장</button>
</div>
<small class="hint">URL 자체가 capability 토큰이라 password 처리. Settings Sync 에 안 포함됨.</small>
</div>
<div class="row">
<label for="gIcalDays">iCal 미리 가져올 일수</label>
<div class="input-group narrow">
<input id="gIcalDays" type="number" min="1" max="90" step="1" />
<button data-save="google.icalDaysAhead">저장</button>
</div>
</div>
<div class="row">
<button id="googleIcalRefresh">지금 iCal 새로고침</button>
<span id="googleIcalStatus" class="status-inline"></span>
</div>
<div id="googleFeedback" class="feedback" hidden></div>
<div id="googleError" class="error" hidden></div>
</section>
<!-- Cloud LLM Providers -->
<section class="section" data-section="providers" data-tab="model">
<h2>Cloud LLM Providers</h2>
<p class="hint">Ollama / LM Studio 로컬 외에 cloud API 를 붙여서 모델 선택지를 확장. API key 는 모두 Secret Storage 에 저장 (settings.json 침범 X). 사이드바 모델 dropdown 에서 활성 provider 의 모델이 함께 표시됩니다.</p>
<!-- OpenRouter -->
<h3 style="margin-top:6px;font-size:13px;color:var(--text)">OpenRouter</h3>
<p class="hint">100+ 모델 (Claude / Gemini / GPT / Llama 전부) 을 단일 API 로. <a href="https://openrouter.ai/keys" target="_blank">openrouter.ai/keys</a> 에서 API key 발급.</p>
<div class="row toggle">
<label><input id="prOpenrouterEnabled" type="checkbox"> OpenRouter 활성화</label>
</div>
<div class="row">
<label for="prOpenrouterKey">API Key</label>
<div class="input-group">
<input id="prOpenrouterKey" type="password" placeholder="sk-or-..." autocomplete="off" spellcheck="false" />
<button data-save="providers.openrouter.apiKey">저장</button>
</div>
</div>
<div class="row">
<label for="prOpenrouterDefault">기본 모델 (선택)</label>
<div class="input-group">
<input id="prOpenrouterDefault" type="text" placeholder="anthropic/claude-3.5-sonnet" autocomplete="off" />
<button data-save="providers.openrouter.defaultModel">저장</button>
</div>
</div>
<!-- Anthropic -->
<h3 style="margin-top:18px;font-size:13px;color:var(--text)">Anthropic Claude (직통)</h3>
<p class="hint">Anthropic 직접 API — prompt caching 등 native 기능 활용 가능. <a href="https://console.anthropic.com/settings/keys" target="_blank">console.anthropic.com/settings/keys</a> 에서 API key 발급.</p>
<div class="row toggle">
<label><input id="prAnthropicEnabled" type="checkbox"> Anthropic 활성화</label>
</div>
<div class="row">
<label for="prAnthropicKey">API Key</label>
<div class="input-group">
<input id="prAnthropicKey" type="password" placeholder="sk-ant-..." autocomplete="off" spellcheck="false" />
<button data-save="providers.anthropic.apiKey">저장</button>
</div>
</div>
<div class="row">
<label for="prAnthropicDefault">기본 모델</label>
<div class="input-group">
<input id="prAnthropicDefault" type="text" placeholder="claude-3-5-sonnet-20241022" autocomplete="off" />
<button data-save="providers.anthropic.defaultModel">저장</button>
</div>
</div>
<!-- Gemini -->
<h3 style="margin-top:18px;font-size:13px;color:var(--text)">Google Gemini (직통)</h3>
<p class="hint">1M context (gemini-1.5-pro), 무료 tier 사용 가능. <a href="https://aistudio.google.com/app/apikey" target="_blank">aistudio.google.com/app/apikey</a> 에서 발급.</p>
<div class="row toggle">
<label><input id="prGeminiEnabled" type="checkbox"> Gemini 활성화</label>
</div>
<div class="row">
<label for="prGeminiKey">API Key</label>
<div class="input-group">
<input id="prGeminiKey" type="password" placeholder="AIzaSy..." autocomplete="off" spellcheck="false" />
<button data-save="providers.gemini.apiKey">저장</button>
</div>
</div>
<div class="row">
<label for="prGeminiDefault">기본 모델</label>
<div class="input-group">
<input id="prGeminiDefault" type="text" placeholder="gemini-2.0-flash-exp" autocomplete="off" />
<button data-save="providers.gemini.defaultModel">저장</button>
</div>
</div>
<div id="providersFeedback" class="feedback" hidden></div>
<div id="providersError" class="error" hidden></div>
</section>
<!-- Devil Agent (도현) -->
<section class="section" data-section="devilAgent" data-tab="advanced">
<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" data-tab="advanced">
<h2>고급</h2>
<p class="hint">대부분의 사용자는 건드릴 필요 없습니다.</p>
<div class="row toggle">
<label><input id="advDryRun" type="checkbox"> Dry Run (파일 변경 전 승인 요청)</label>
</div>
<div class="row toggle">
<label><input id="advMulti" type="checkbox"> 멀티 에이전트 워크플로우 (Planner → Researcher → Writer)</label>
</div>
<div class="row">
<label for="advAutoSteps">최대 자동 단계 (maxAutoSteps)</label>
<div class="input-group narrow">
<input id="advAutoSteps" type="number" min="1" step="1" />
<button data-save="advanced.autoSteps">저장</button>
</div>
</div>
<div class="row">
<label for="advCtxSize">최대 컨텍스트 (maxContextSize)</label>
<div class="input-group narrow">
<input id="advCtxSize" type="number" min="1000" step="1000" />
<button data-save="advanced.ctxSize">저장</button>
</div>
</div>
<div class="row">
<label for="advChatTemp">채팅 Temperature</label>
<div class="input-group narrow">
<input id="advChatTemp" type="number" min="0" max="2" step="0.05" />
<button data-save="advanced.chatTemperature">저장</button>
</div>
<small class="hint">채팅 응답 생성의 temperature. 낮을수록(0.2~0.3) 한국어 오타·깨진 토큰이 줄고 안정적입니다. 기본 0.3 권장.</small>
</div>
<div class="row">
<label for="advChunkedSwitch">Chunked 진입 토큰 임계값 (chunkedSwitchTokens)</label>
<div class="input-group narrow">
<input id="advChunkedSwitch" type="number" min="1000" step="1000" />
<button data-save="advanced.chunkedSwitchTokens">저장</button>
</div>
<small class="hint">입력 prompt 가 이 토큰 수 *미만* 이면 chunked 파이프라인 발동 안 함 — 단일 호출로 답변. 기본 50000 권장 (대부분의 모델에 적합). 매우 작은 모델로 큰 입력 처리 시 OOM 가능성 있으면 이 값을 낮추면 됨.</small>
</div>
<div class="row">
<label for="advChunkedMax">Chunked 최대 섹션 수 (chunkedMaxSections)</label>
<div class="input-group narrow">
<input id="advChunkedMax" type="number" min="1" max="10" step="1" />
<button data-save="advanced.chunkedMaxSections">저장</button>
</div>
<small class="hint">Chunked 가 답변을 쪼갤 수 있는 최대 섹션 수. 실제 LLM 호출 = `2 + N` 회 (outline 1 + section N + polish 1). 기본 3 (총 5회). 빨리 받고 싶으면 2 (총 4회), 답변을 더 세분화하려면 5 (총 7회).</small>
</div>
<div class="row">
<label for="advPolishPersona">Polish persona 커스텀 (polishPersonaOverride)</label>
<div class="input-group" style="flex-direction:column; align-items:stretch;">
<textarea id="advPolishPersona" rows="6" placeholder="비워두면 기본 polish persona 사용. 내용을 입력하면 그 텍스트가 그대로 polish 단계의 system prompt 로 들어갑니다.&#10;&#10;예: '당신은 한국 법률 문서 톤의 편집자입니다. 격식체로 작성하고...'"></textarea>
<button data-save="advanced.polishPersonaOverride" style="margin-top: 6px; align-self: flex-start;">저장</button>
</div>
<small class="hint">답변의 최종 다듬기 단계(polish) 톤·구조를 직접 정의합니다. 예: 격식체/반말/법률·마케팅 도메인 톤. 빈 값이면 기본 persona (한 줄 요약 + subheading + 5-check) 사용.</small>
</div>
</section>
</main>
<script src="__SCRIPT_URI__"></script>
</body>
</html>