Просмотр исходного кода

feat(tui): ASCII art logo, input separator, styled LLM wizard

- ASCII block logo with gradient animation (bright→mid→dim cyan)
- Input separator line between responses
- Styled LLM setup wizard: numbered steps, masked API key, confirmations
- Consistent ◇/◈/◌ iconography throughout
Ma 7 месяцев назад
Родитель
Сommit
6ae937b20a
3 измененных файлов с 90 добавлено и 73 удалено
  1. 7 0
      packages/cli/src/tui/app.ts
  2. 39 39
      packages/cli/src/tui/effects.ts
  3. 44 34
      packages/cli/src/tui/setup.ts

+ 7 - 0
packages/cli/src/tui/app.ts

@@ -27,6 +27,7 @@ import {
   intentToTheme,
   printStyledHelp,
   printStyledStatus,
+  printInputSeparator,
 } from "./effects.js";
 
 /* ── Version ── */
@@ -243,6 +244,8 @@ export async function launchTui(
 
     if (/^\/help$/i.test(input) || /^(help|帮助)$/i.test(input)) {
       printStyledHelp();
+      printInputSeparator();
+      console.log();
       rl.prompt();
       continue;
     }
@@ -260,6 +263,8 @@ export async function launchTui(
       } catch {
         console.log(c("  Could not load session.", dim));
       }
+      printInputSeparator();
+      console.log();
       rl.prompt();
       continue;
     }
@@ -273,6 +278,8 @@ export async function launchTui(
     // Delegate to interaction layer with themed animation
     await processInput(projectRoot, input, tools);
     console.log();
+    printInputSeparator();
+    console.log();
 
     rl.prompt();
   }

+ 39 - 39
packages/cli/src/tui/effects.ts

@@ -150,67 +150,65 @@ export class ThemedSpinner {
   }
 }
 
+/* ── ASCII Art Logo ── */
+
+const ASCII_LOGO = [
+  " ██╗███╗   ██╗██╗  ██╗ ██████╗ ███████╗",
+  " ██║████╗  ██║██║ ██╔╝██╔═══██╗██╔════╝",
+  " ██║██╔██╗ ██║█████╔╝ ██║   ██║███████╗",
+  " ██║██║╚██╗██║██╔═██╗ ██║   ██║╚════██║",
+  " ██║██║ ╚████║██║  ██╗╚██████╔╝███████║",
+  " ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝ ╚═════╝ ╚══════╝",
+];
+
+/* ── Input separator ── */
+
+export function printInputSeparator(): void {
+  const w = Math.min(process.stdout.columns ?? 60, 60);
+  console.log(c("  " + "─".repeat(w - 4), gray));
+}
+
 /* ── Startup animation ── */
 
 export async function animateStartup(version: string, projectName: string, bookTitle?: string): Promise<void> {
   const isTTY = process.stdout.isTTY;
 
-  // Typing effect for logo
-  const logoText = "InkOS";
-  const tagline = "Autonomous Novel Writing AI Agent";
-
   if (isTTY) {
     console.log();
     process.stdout.write(hideCursor);
 
-    // Draw box top
-    process.stdout.write("  ╭" + "─".repeat(52) + "╮\n");
-
-    // Animate logo typing
-    process.stdout.write("  │  ");
-    for (const char of logoText) {
-      process.stdout.write(c(char, bold, brightCyan));
-      await sleep(60);
-    }
-    process.stdout.write(c(` v${version}`, dim));
-    const logoPad = 52 - 2 - logoText.length - ` v${version}`.length;
-    process.stdout.write(" ".repeat(Math.max(0, logoPad)) + "│\n");
-
-    // Animate tagline
-    process.stdout.write("  │  ");
-    for (let i = 0; i < tagline.length; i++) {
-      process.stdout.write(c(tagline[i]!, dim));
-      if (i % 3 === 0) await sleep(15);
+    // Animate ASCII logo line by line
+    for (let i = 0; i < ASCII_LOGO.length; i++) {
+      const line = ASCII_LOGO[i]!;
+      // Gradient: top lines brighter, bottom lines dimmer
+      const shade = i < 2 ? brightCyan : i < 4 ? cyan : dim + cyan;
+      process.stdout.write(`  ${c(line, shade)}\n`);
+      await sleep(40);
     }
-    const tagPad = 52 - 2 - tagline.length;
-    process.stdout.write(" ".repeat(Math.max(0, tagPad)) + "│\n");
 
-    // Box bottom
-    process.stdout.write("  ╰" + "─".repeat(52) + "╯\n");
-    process.stdout.write(showCursor);
+    // Version + tagline below logo
+    console.log(c(`  v${version}`, dim));
+    await sleep(150);
 
-    await sleep(200);
+    process.stdout.write(showCursor);
   } else {
-    // Non-TTY fallback
     console.log();
-    console.log(
-      box([
-        `  ${c("InkOS", bold, brightCyan)}${c(` v${version}`, dim)}`,
-        `  ${c(tagline, dim)}`,
-      ]),
-    );
+    for (const line of ASCII_LOGO) {
+      console.log(`  ${c(line, brightCyan)}`);
+    }
+    console.log(c(`  v${version}`, dim));
   }
 
-  // Project info with fade-in effect
+  // Project info
   console.log();
   if (isTTY) {
     await typewrite(`  ${c("◇", cyan)} ${c("Project", gray)}  ${c(projectName, brightWhite)}`, 8);
-    await sleep(100);
+    await sleep(80);
     const bookDisplay = bookTitle
       ? c(bookTitle, brightWhite)
       : c("no book yet", dim);
     await typewrite(`  ${c("◇", cyan)} ${c("Book", gray)}     ${bookDisplay}`, 8);
-    await sleep(100);
+    await sleep(80);
   } else {
     console.log(`  ${c("◇", cyan)} ${c("Project", gray)}  ${c(projectName, brightWhite)}`);
     const bookDisplay = bookTitle
@@ -220,7 +218,9 @@ export async function animateStartup(version: string, projectName: string, bookT
   }
 
   console.log();
-  console.log(c("  Type anything to start. /help for commands.", dim));
+  console.log(c("  /help for commands. Type anything to begin.", dim));
+  console.log();
+  printInputSeparator();
   console.log();
 }
 

+ 44 - 34
packages/cli/src/tui/setup.ts

@@ -3,7 +3,11 @@
 import { access, mkdir, readFile, writeFile } from "node:fs/promises";
 import { join, basename } from "node:path";
 import readline from "node:readline/promises";
-import { c, bold, cyan, green, yellow, gray, dim, red, reset } from "./ansi.js";
+import {
+  c, bold, dim, italic,
+  cyan, green, yellow, gray, red,
+  brightCyan, brightGreen, brightWhite,
+} from "./ansi.js";
 import { GLOBAL_ENV_PATH } from "../utils.js";
 
 const PROVIDERS = ["openai", "anthropic", "custom"] as const;
@@ -35,42 +39,47 @@ export async function interactiveLlmSetup(
 
   try {
     console.log();
-    console.log(c("  LLM 配置向导", bold, cyan));
-    console.log(c("  Configure your LLM provider to start writing.", dim));
+    console.log(`  ${c("◈", brightCyan)} ${c("LLM Setup", bold, brightWhite)}`);
+    console.log(c("  Configure your model provider to start writing.", dim));
     console.log();
 
-    const providerInput = await rl.question(
-      c("  Provider ", gray) +
-        c("(openai/anthropic/custom)", dim) +
-        c(": ", gray),
-    );
+    // Provider
+    console.log(`  ${c("1", cyan)}  ${c("Provider", gray)}`);
+    console.log(c("     openai / anthropic / custom (OpenAI-compatible proxy)", dim));
+    const providerInput = await rl.question(`     ${c("❯", cyan)} `);
     const provider = PROVIDERS.includes(providerInput.trim() as typeof PROVIDERS[number])
       ? providerInput.trim()
       : "openai";
+    console.log(`     ${c("✓", brightGreen)} ${provider}`);
+    console.log();
 
-    const baseUrl = await rl.question(
-      c("  Base URL ", gray) +
-        c("(API endpoint)", dim) +
-        c(": ", gray),
-    );
-
-    const apiKey = await rl.question(
-      c("  API Key ", gray) +
-        c(": ", gray),
-    );
+    // Base URL
+    console.log(`  ${c("2", cyan)}  ${c("Base URL", gray)}`);
+    console.log(c("     Your API endpoint", dim));
+    const baseUrl = await rl.question(`     ${c("❯", cyan)} `);
+    console.log(`     ${c("✓", brightGreen)} ${baseUrl.trim() || "(default)"}`);
+    console.log();
 
-    const model = await rl.question(
-      c("  Model ", gray) +
-        c("(e.g. gpt-4o, claude-sonnet-4-20250514)", dim) +
-        c(": ", gray),
-    );
+    // API Key
+    console.log(`  ${c("3", cyan)}  ${c("API Key", gray)}`);
+    const apiKey = await rl.question(`     ${c("❯", cyan)} `);
+    const maskedKey = apiKey.trim().length > 8
+      ? apiKey.trim().slice(0, 4) + "···" + apiKey.trim().slice(-4)
+      : "···";
+    console.log(`     ${c("✓", brightGreen)} ${maskedKey}`);
+    console.log();
 
-    const scope = await rl.question(
-      c("  Save to ", gray) +
-        c("(global/project)", dim) +
-        c(" [global]: ", gray),
-    );
+    // Model
+    console.log(`  ${c("4", cyan)}  ${c("Model", gray)}`);
+    console.log(c("     e.g. gpt-4o, claude-sonnet-4-20250514, deepseek-chat", dim));
+    const model = await rl.question(`     ${c("❯", cyan)} `);
+    console.log(`     ${c("✓", brightGreen)} ${model.trim()}`);
+    console.log();
 
+    // Scope
+    console.log(`  ${c("5", cyan)}  ${c("Save scope", gray)}`);
+    console.log(c("     global = all projects, project = this directory only", dim));
+    const scope = await rl.question(`     ${c("❯", cyan)} ${c("[global]", dim)} `);
     const useGlobal = scope.trim().toLowerCase() !== "project";
 
     const envContent = [
@@ -84,10 +93,12 @@ export async function interactiveLlmSetup(
       const globalDir = join(GLOBAL_ENV_PATH, "..");
       await mkdir(globalDir, { recursive: true });
       await writeFile(GLOBAL_ENV_PATH, envContent + "\n", "utf-8");
-      console.log(c(`  ✓ Saved to ${GLOBAL_ENV_PATH}`, green));
+      console.log();
+      console.log(`  ${c("✓", brightGreen, bold)} ${c("Saved to", dim)} ${c(GLOBAL_ENV_PATH, gray)}`);
     } else {
       await writeFile(join(projectRoot, ".env"), envContent + "\n", "utf-8");
-      console.log(c("  ✓ Saved to .env", green));
+      console.log();
+      console.log(`  ${c("✓", brightGreen, bold)} ${c("Saved to", dim)} ${c(".env", gray)}`);
     }
     console.log();
   } finally {
@@ -97,7 +108,8 @@ export async function interactiveLlmSetup(
 
 async function autoInit(cwd: string): Promise<void> {
   const projectName = basename(cwd);
-  console.log(c(`  Auto-initializing project in ${projectName}/ ...`, dim));
+  console.log();
+  console.log(`  ${c("◌", cyan)} ${c(`Initializing project in ${projectName}/ ...`, dim)}`);
 
   await mkdir(join(cwd, "books"), { recursive: true });
   await mkdir(join(cwd, "radar"), { recursive: true });
@@ -148,14 +160,12 @@ async function autoInit(cwd: string): Promise<void> {
     "utf-8",
   );
 
-  console.log(c("  ✓ Project initialized", green));
+  console.log(`  ${c("✓", brightGreen, bold)} ${c("Project initialized", dim)}`);
 }
 
 async function hasLlmConfig(projectRoot: string): Promise<boolean> {
-  // Check project .env first
   const projectEnv = join(projectRoot, ".env");
   if (await checkEnvForKey(projectEnv)) return true;
-  // Check global
   return checkEnvForKey(GLOBAL_ENV_PATH);
 }