|
| 1 | +"""First-run onboarding tour for CodingBuddy. |
| 2 | +
|
| 3 | +Detects first-run via ~/.codingbuddy/onboarded flag file and renders |
| 4 | +an interactive 3-step tour introducing core features. |
| 5 | +""" |
| 6 | +import os |
| 7 | +from pathlib import Path |
| 8 | +from typing import Any, Dict, Optional |
| 9 | + |
| 10 | +from buddy_renderer import ( |
| 11 | + ANSI_COLORS, |
| 12 | + BUDDY_FACE, |
| 13 | + DEFAULT_BUDDY_CONFIG, |
| 14 | + get_buddy_config, |
| 15 | +) |
| 16 | + |
| 17 | +# Flag file location |
| 18 | +ONBOARDED_DIR = os.path.join(os.path.expanduser("~"), ".codingbuddy") |
| 19 | +ONBOARDED_FLAG = os.path.join(ONBOARDED_DIR, "onboarded") |
| 20 | + |
| 21 | +# Environment variable to skip tour |
| 22 | +SKIP_ENV_VAR = "CODINGBUDDY_SKIP_TOUR" |
| 23 | + |
| 24 | + |
| 25 | +def is_first_run() -> bool: |
| 26 | + """Check if this is the user's first run. |
| 27 | +
|
| 28 | + Returns: |
| 29 | + True if onboarded flag does not exist and skip env var is not set. |
| 30 | + """ |
| 31 | + if os.environ.get(SKIP_ENV_VAR): |
| 32 | + return False |
| 33 | + return not os.path.isfile(ONBOARDED_FLAG) |
| 34 | + |
| 35 | + |
| 36 | +def mark_onboarded() -> None: |
| 37 | + """Create the onboarded flag file to prevent future tours.""" |
| 38 | + os.makedirs(ONBOARDED_DIR, exist_ok=True) |
| 39 | + Path(ONBOARDED_FLAG).touch() |
| 40 | + |
| 41 | + |
| 42 | +# ── i18n Tour Content ────────────────────────────────────────────── |
| 43 | + |
| 44 | +TOUR_WELCOME: Dict[str, str] = { |
| 45 | + "en": "Welcome to CodingBuddy! Here's a quick tour...", |
| 46 | + "ko": "CodingBuddy에 오신 걸 환영해요! 간단히 소개할게요...", |
| 47 | + "ja": "CodingBuddyへようこそ!簡単にご紹介します...", |
| 48 | + "zh": "欢迎使用CodingBuddy!快速介绍一下...", |
| 49 | + "es": "Bienvenido a CodingBuddy! Un tour rapido...", |
| 50 | +} |
| 51 | + |
| 52 | +TOUR_STEPS: Dict[int, Dict[str, Dict[str, str]]] = { |
| 53 | + 1: { |
| 54 | + "en": { |
| 55 | + "title": "PLAN/ACT/EVAL Workflow", |
| 56 | + "body": "Type PLAN to design, ACT to implement, EVAL to review — or AUTO for the full cycle.", |
| 57 | + "example": 'PLAN add user authentication', |
| 58 | + }, |
| 59 | + "ko": { |
| 60 | + "title": "PLAN/ACT/EVAL 워크플로우", |
| 61 | + "body": "PLAN으로 설계, ACT로 구현, EVAL로 검토 — 또는 AUTO로 전체 사이클을 실행하세요.", |
| 62 | + "example": 'PLAN 사용자 인증 추가', |
| 63 | + }, |
| 64 | + "ja": { |
| 65 | + "title": "PLAN/ACT/EVAL ワークフロー", |
| 66 | + "body": "PLANで設計、ACTで実装、EVALでレビュー — またはAUTOで全サイクル実行。", |
| 67 | + "example": 'PLAN ユーザー認証を追加', |
| 68 | + }, |
| 69 | + "zh": { |
| 70 | + "title": "PLAN/ACT/EVAL 工作流", |
| 71 | + "body": "用PLAN设计、ACT实现、EVAL审查 — 或AUTO执行完整周期。", |
| 72 | + "example": 'PLAN 添加用户认证', |
| 73 | + }, |
| 74 | + "es": { |
| 75 | + "title": "Flujo PLAN/ACT/EVAL", |
| 76 | + "body": "PLAN para disenar, ACT para implementar, EVAL para revisar — o AUTO para el ciclo completo.", |
| 77 | + "example": 'PLAN agregar autenticacion', |
| 78 | + }, |
| 79 | + }, |
| 80 | + 2: { |
| 81 | + "en": { |
| 82 | + "title": "38 Specialist Agents", |
| 83 | + "body": "Security, accessibility, performance... experts ready to analyze your code.", |
| 84 | + "example": 'AUTO implement login page', |
| 85 | + }, |
| 86 | + "ko": { |
| 87 | + "title": "38명의 전문가 에이전트", |
| 88 | + "body": "보안, 접근성, 성능... 전문가들이 코드 분석을 도와줍니다.", |
| 89 | + "example": 'AUTO 로그인 페이지 구현', |
| 90 | + }, |
| 91 | + "ja": { |
| 92 | + "title": "38人の専門エージェント", |
| 93 | + "body": "セキュリティ、アクセシビリティ、パフォーマンス...専門家がコード分析をサポート。", |
| 94 | + "example": 'AUTO ログインページを実装', |
| 95 | + }, |
| 96 | + "zh": { |
| 97 | + "title": "38位专家代理", |
| 98 | + "body": "安全、无障碍、性能...专家随时准备分析您的代码。", |
| 99 | + "example": 'AUTO 实现登录页面', |
| 100 | + }, |
| 101 | + "es": { |
| 102 | + "title": "38 Agentes Especialistas", |
| 103 | + "body": "Seguridad, accesibilidad, rendimiento... expertos listos para analizar tu codigo.", |
| 104 | + "example": 'AUTO implementar pagina de login', |
| 105 | + }, |
| 106 | + }, |
| 107 | + 3: { |
| 108 | + "en": { |
| 109 | + "title": "Checklists & Skills", |
| 110 | + "body": "Auto-generated quality checklists and specialized skills for every task.", |
| 111 | + "example": 'EVAL review my changes', |
| 112 | + }, |
| 113 | + "ko": { |
| 114 | + "title": "체크리스트 & 스킬", |
| 115 | + "body": "자동 생성되는 품질 체크리스트와 모든 작업을 위한 전문 스킬.", |
| 116 | + "example": 'EVAL 변경사항 검토', |
| 117 | + }, |
| 118 | + "ja": { |
| 119 | + "title": "チェックリスト & スキル", |
| 120 | + "body": "自動生成の品質チェックリストと各タスク向けの専門スキル。", |
| 121 | + "example": 'EVAL 変更をレビュー', |
| 122 | + }, |
| 123 | + "zh": { |
| 124 | + "title": "清单 & 技能", |
| 125 | + "body": "自动生成质量清单和每个任务的专业技能。", |
| 126 | + "example": 'EVAL 审查我的更改', |
| 127 | + }, |
| 128 | + "es": { |
| 129 | + "title": "Listas & Habilidades", |
| 130 | + "body": "Listas de calidad auto-generadas y habilidades especializadas para cada tarea.", |
| 131 | + "example": 'EVAL revisar mis cambios', |
| 132 | + }, |
| 133 | + }, |
| 134 | +} |
| 135 | + |
| 136 | +TOUR_SKIP: Dict[str, str] = { |
| 137 | + "en": "Skip future tours: touch ~/.codingbuddy/onboarded", |
| 138 | + "ko": "투어 건너뛰기: touch ~/.codingbuddy/onboarded", |
| 139 | + "ja": "ツアーをスキップ: touch ~/.codingbuddy/onboarded", |
| 140 | + "zh": "跳过教程: touch ~/.codingbuddy/onboarded", |
| 141 | + "es": "Saltar tour: touch ~/.codingbuddy/onboarded", |
| 142 | +} |
| 143 | + |
| 144 | +TOUR_HEADER: Dict[str, str] = { |
| 145 | + "en": "Quick Tour", |
| 146 | + "ko": "퀵 투어", |
| 147 | + "ja": "クイックツアー", |
| 148 | + "zh": "快速导览", |
| 149 | + "es": "Tour Rapido", |
| 150 | +} |
| 151 | + |
| 152 | +# Step number circled digits |
| 153 | +_STEP_NUMBERS = {1: "\u2460", 2: "\u2461", 3: "\u2462"} |
| 154 | + |
| 155 | + |
| 156 | +def _get_text(mapping: Dict[str, str], language: str) -> str: |
| 157 | + """Get localized text with English fallback.""" |
| 158 | + return mapping.get(language, mapping.get("en", "")) |
| 159 | + |
| 160 | + |
| 161 | +def _get_step(step_num: int, language: str) -> Dict[str, str]: |
| 162 | + """Get localized step content with English fallback.""" |
| 163 | + step = TOUR_STEPS.get(step_num, {}) |
| 164 | + return step.get(language, step.get("en", {})) |
| 165 | + |
| 166 | + |
| 167 | +def render_onboarding_tour( |
| 168 | + language: str = "en", |
| 169 | + buddy_config: Optional[Dict[str, str]] = None, |
| 170 | +) -> str: |
| 171 | + """Render the complete onboarding tour output. |
| 172 | +
|
| 173 | + Args: |
| 174 | + language: Language code (en, ko, ja, zh, es). |
| 175 | + buddy_config: Optional buddy customization from get_buddy_config(). |
| 176 | +
|
| 177 | + Returns: |
| 178 | + Formatted onboarding tour string. |
| 179 | + """ |
| 180 | + bc = buddy_config or DEFAULT_BUDDY_CONFIG |
| 181 | + face = bc.get("face", BUDDY_FACE) |
| 182 | + welcome = _get_text(TOUR_WELCOME, language) |
| 183 | + |
| 184 | + cyan = ANSI_COLORS["cyan"] |
| 185 | + yellow = ANSI_COLORS["yellow"] |
| 186 | + green = ANSI_COLORS["green"] |
| 187 | + magenta = ANSI_COLORS["magenta"] |
| 188 | + reset = ANSI_COLORS["reset"] |
| 189 | + |
| 190 | + lines = [ |
| 191 | + f"\u256d\u2501\u2501\u2501\u256e", |
| 192 | + f"\u2503 {face} \u2503 {cyan}{welcome}{reset}", |
| 193 | + f"\u2570\u2501\u2501\u2501\u256f", |
| 194 | + "", |
| 195 | + f"\u2501\u2501 {_get_text(TOUR_HEADER, language)} \u2501\u2501\u2501\u2501\u2501\u2501", |
| 196 | + ] |
| 197 | + |
| 198 | + for step_num in (1, 2, 3): |
| 199 | + step = _get_step(step_num, language) |
| 200 | + if not step: |
| 201 | + continue |
| 202 | + circled = _STEP_NUMBERS.get(step_num, str(step_num)) |
| 203 | + title = step.get("title", "") |
| 204 | + body = step.get("body", "") |
| 205 | + example = step.get("example", "") |
| 206 | + |
| 207 | + lines.append(f"") |
| 208 | + lines.append(f" {yellow}{circled}{reset} {green}{title}{reset}") |
| 209 | + lines.append(f" {body}") |
| 210 | + if example: |
| 211 | + lines.append(f" {magenta}\U0001f4a1 {example}{reset}") |
| 212 | + |
| 213 | + lines.append("") |
| 214 | + lines.append(f"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501") |
| 215 | + lines.append(f"\U0001f4ac {_get_text(TOUR_SKIP, language)}") |
| 216 | + |
| 217 | + return "\n".join(lines) |
0 commit comments