research-cli
Version:
AI-powered research assistant with web search capabilities and beautiful terminal UI
313 lines • 12 kB
JavaScript
/**
* Interactive UI components using terminal-kit
* Provides beautiful, user-friendly prompts for CLI interactions
*/
import termkit from "terminal-kit";
const { terminal } = termkit;
// Configure terminal for better behavior
const term = terminal;
export class InteractiveUI {
/**
* Display a welcome banner for easymode setup
*/
static showWelcomeBanner() {
term.clear();
term.moveTo(1, 1);
// Beautiful header with colors
term.bold.cyan("╭─────────────────────────────────────────────────────────────╮\n");
term.bold.cyan("│ │\n");
term.bold.cyan("│ ");
term.bold.white("🚀 Research CLI - Easy Setup Mode");
term.bold.cyan(" │\n");
term.bold.cyan("│ │\n");
term.bold.cyan("│ ");
term.white("Let's get you set up with AI research in just a few steps!");
term.bold.cyan(" │\n");
term.bold.cyan("│ │\n");
term.bold.cyan("╰─────────────────────────────────────────────────────────────╯\n\n");
}
/**
* Show provider selection menu with descriptions
*/
static async selectProvider() {
const providers = [
{
name: "openai",
displayName: "OpenAI",
description: "GPT-4, GPT-4o, ChatGPT models",
url: "https://platform.openai.com/api-keys",
},
{
name: "claude",
displayName: "Anthropic Claude",
description: "Claude 3.5 Sonnet, Haiku models",
url: "https://console.anthropic.com/",
},
{
name: "perplexity",
displayName: "Perplexity",
description: "Research-focused with web search",
url: "https://www.perplexity.ai/settings/api",
},
{
name: "gemini",
displayName: "Google Gemini",
description: "Gemini Pro, Ultra models",
url: "https://makersuite.google.com/app/apikey",
},
];
term.bold.yellow("\n📋 Choose your AI provider:\n\n");
// Create menu items with rich descriptions
const items = providers.map((provider) => `${provider.displayName.padEnd(20)} ${term.str?.dim(provider.description) ?? term.dim(provider.description)}`);
try {
const response = await term.singleColumnMenu(items, {
style: term.cyan,
selectedStyle: term.bold.blue.bgWhite,
leftPadding: " ",
extraLines: 1,
}).promise;
if (!response || response.selectedIndex === undefined) {
return null;
}
return providers[response.selectedIndex]?.name || null;
}
catch (error) {
if (error &&
typeof error === "object" &&
"code" in error &&
error.code === "USER_ABORT") {
return null; // User pressed Ctrl+C
}
throw error;
}
}
/**
* Interactive API key input with validation hints
*/
static async getApiKey(provider, providerUrl) {
term.bold.green(`\n🔑 Setting up ${provider.toUpperCase()} API Key\n\n`);
// Show instructions
term.white("To get your API key:\n");
term.cyan(` 1. Visit: ${providerUrl}\n`);
term.cyan(" 2. Sign up or log in to your account\n");
term.cyan(" 3. Generate a new API key\n");
term.cyan(" 4. Copy it and paste it below\n\n");
// Show expected format hint
const formatHints = {
openai: 'Format: sk-... (starts with "sk-")',
claude: 'Format: sk-ant-... (starts with "sk-ant-")',
perplexity: 'Format: pplx-... (starts with "pplx-")',
gemini: "Format: 39 character string",
};
term.dim(`${formatHints[provider] || "Check provider documentation for format"}\n\n`);
try {
term.bold.white("Enter your API key: ");
const apiKey = await term.inputField({
echoChar: "*", // Hide the key for security
autoComplete: [],
autoCompleteMenu: false,
}).promise;
if (!apiKey || apiKey.trim().length === 0) {
term.red("\n❌ API key is required\n");
return null;
}
return apiKey.trim();
}
catch (error) {
if (error &&
typeof error === "object" &&
"code" in error &&
error.code === "USER_ABORT") {
return null; // User pressed Ctrl+C
}
throw error;
}
}
/**
* Ask if user wants to save the key securely
*/
static async confirmSaveKey() {
term.bold.yellow("\n🔐 Save API key securely?\n\n");
term.white("This will store your API key in your system keychain\n");
term.white("(recommended for security and convenience)\n\n");
try {
const result = await term.yesOrNo({
yes: ["y", "Y", "ENTER"],
no: ["n", "N"],
}).promise;
return result || false;
}
catch (error) {
if (error &&
typeof error === "object" &&
"code" in error &&
error.code === "USER_ABORT") {
return false; // User pressed Ctrl+C, default to not saving
}
throw error;
}
}
/**
* Show loading spinner while testing API key
*/
static async testApiKeyWithSpinner(testFunction) {
term.bold.yellow("\n🧪 Testing API connection...\n");
// Create and show spinner
const spinner = await term.spinner("unboxing-color");
term(" Testing...");
try {
const result = await testFunction();
// Stop spinner and clear line
spinner.animate(false);
term.eraseLine();
try {
// getCursorLocation() requires a callback in terminal-kit
term.getCursorLocation((error, _, y) => {
if (!error && y !== undefined) {
term.moveTo(1, y);
}
});
}
catch {
// Ignore cursor positioning errors
}
return result;
}
catch (error) {
// Stop spinner on error
spinner.animate(false);
term.eraseLine();
try {
// getCursorLocation() requires a callback in terminal-kit
term.getCursorLocation((error, _, y) => {
if (!error && y !== undefined) {
term.moveTo(1, y);
}
});
}
catch {
// Ignore cursor positioning errors
}
return {
valid: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
* Show success message with next steps
*/
static showSuccessMessage(provider) {
term.bold.green("\n✅ Setup Complete!\n\n");
term.white(`Your ${provider} API key has been configured successfully.\n\n`);
term.bold.cyan("🚀 Try it out:\n");
term.white(` research-cli "What is the latest in AI research?"\n`);
term.white(` research-cli "Explain quantum computing" --web\n\n`);
term.bold.cyan("💡 Useful commands:\n");
term.white(" research-cli auth list - View all configured providers\n");
term.white(" research-cli auth test - Test API connections\n");
term.white(" research-cli --help - See all available options\n\n");
}
/**
* Show error message with helpful suggestions
*/
static showErrorMessage(error, provider) {
term.bold.red("\n❌ Setup Failed\n\n");
term.white(`Error: ${error}\n\n`);
if (provider) {
term.bold.yellow("💡 Troubleshooting:\n");
term.white(` • Double-check your API key from the provider\n`);
term.white(` • Make sure you have sufficient credits/quota\n`);
term.white(` • Try running: research-cli auth login ${provider}\n\n`);
}
term.white("Need help? Check the documentation or try again.\n");
}
/**
* Simple yes/no prompt with custom message
*/
static async confirmAction(message, defaultYes = false) {
term.white(`${message} `);
term.dim(defaultYes ? "(Y/n): " : "(y/N): ");
try {
const result = await term.yesOrNo({
yes: defaultYes ? ["y", "Y", "ENTER"] : ["y", "Y"],
no: defaultYes ? ["n", "N"] : ["n", "N", "ENTER"],
}).promise;
return result || false;
}
catch (error) {
if (error &&
typeof error === "object" &&
"code" in error &&
error.code === "USER_ABORT") {
return false; // User pressed Ctrl+C
}
throw error;
}
}
/**
* Enhanced text input with history and autocomplete
*/
static async textInput(prompt, options = {}) {
term.white(prompt);
try {
const input = await term.inputField({
history: options.history || [],
autoComplete: options.autoComplete || [],
autoCompleteMenu: !!options.autoComplete?.length,
}).promise;
return input?.trim() || null;
}
catch (error) {
if (error &&
typeof error === "object" &&
"code" in error &&
error.code === "USER_ABORT") {
return null; // User pressed Ctrl+C
}
throw error;
}
}
/**
* Show a progress indicator for multi-step processes
*/
static async showProgress(currentStep, totalSteps, stepDescription) {
const progress = currentStep / totalSteps;
const barWidth = 40;
const filledWidth = Math.round(barWidth * progress);
// Simple approach - just erase line without cursor positioning
// to avoid complex terminal-kit API issues
term.eraseLine();
term.bold.cyan("[");
// Use string repeat instead of term.str.repeat
term("█".repeat(filledWidth));
term("░".repeat(barWidth - filledWidth));
term.bold.cyan("]");
term.white(` ${currentStep}/${totalSteps} - ${stepDescription}\n`);
}
/**
* Handle Ctrl+C gracefully with cleanup
*/
static setupGracefulExit() {
term.grabInput({ mouse: "button" });
term.on("key", (name) => {
if (name === "CTRL_C") {
term("\n\n");
term.bold.yellow("👋 Setup cancelled by user\n");
term.white("You can run ");
term.cyan("research-cli easymode");
term.white(" again anytime!\n\n");
term.grabInput(false);
process.exit(0);
}
});
}
/**
* Cleanup terminal state
*/
static cleanup() {
term.grabInput(false);
term.styleReset();
}
}
//# sourceMappingURL=interactive.js.map