UNPKG

aura-ai

Version:

AI-powered marketing strategist CLI tool for developers

305 lines (275 loc) 9.62 kB
import React, { useState, useRef, useEffect } from 'react' import { Text, Box, useApp, Newline, Static } from 'ink' import { Alert } from '@inkjs/ui' import fs from 'fs/promises' import CommandInput from './components/CommandInput.jsx' import InitPage from './init/page.jsx' import SettingsPage from './settings/page.jsx' import SyncPage from './sync/page.jsx' import AddAuraAgentPage from './add-aura-agent/page.jsx' import EmojiSpinner from './init/components/EmojiSpinner.jsx' import chatService from './services/chatService.js' /** * Main App - Command Router */ const App = () => { const [inputValue, setInputValue] = useState('') const [exitPrompt, setExitPrompt] = useState(false) const [currentRoute, setCurrentRoute] = useState('home') const [hasAuraConfig, setHasAuraConfig] = useState(false) const [settings, setSettings] = useState({ avatars: { aura: '🌀', user: '>' } }) const lastCtrlC = useRef(0) const { exit } = useApp() // Chat state management const [messages, setMessages] = useState([]) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) // 檢查 aura.md 是否存在 & 載入設定 useEffect(() => { const checkAuraConfig = async () => { try { await fs.access('./aura.md') setHasAuraConfig(true) } catch { setHasAuraConfig(false) } } const loadSettings = async () => { try { const data = await fs.readFile('./.aura.json', 'utf-8') setSettings(JSON.parse(data)) } catch { // 使用預設值 } } checkAuraConfig() loadSettings() }, []) // Available commands (removed chat) const availableCommands = [ { name: 'init', description: 'Analyze my product and generate a marketing strategy', }, { name: 'sync', description: "Generate today's progress report from git commits", }, { name: 'today', description: "[DEMO] Check today's marketing report and recommended actions", }, { name: 'post', description: '[DEMO] Tweet, blog post, email, short video, etc' }, { name: 'settings', description: 'Manage your Aura settings' }, { name: 'add-aura-agent', description: 'Install Aura agent to Claude for @aura commands', }, ] // Handle Ctrl+C logic const handleClearInput = () => { setInputValue('') setExitPrompt(false) } const handleExit = () => { const now = Date.now() if (now - lastCtrlC.current < 2000) { exit() } else { setExitPrompt(true) lastCtrlC.current = now setTimeout(() => { setExitPrompt(false) }, 2000) } } const handleSubmit = value => { if (value.trim()) { const trimmedValue = value.trim() // 檢查是否為命令 if (trimmedValue.startsWith('/')) { const command = trimmedValue.substring(1).toLowerCase() if (command === 'init') { setCurrentRoute('init') } else if (command === 'settings') { setCurrentRoute('settings') } else if (command === 'sync') { setCurrentRoute('sync') } else if (command === 'add-aura-agent') { setCurrentRoute('add-aura-agent') } // 可以在這裡處理其他命令 setInputValue('') } else { // 非命令文字 - 發送到聊天 const userMessage = { id: Date.now().toString(), role: 'user', content: trimmedValue } setMessages(prev => [...prev, userMessage]) setError(null) // 呼叫真實 AI API setIsLoading(true) chatService.sendMessage([...messages, userMessage]) .then(async (stream) => { let fullContent = '' const assistantMessageId = (Date.now() + 1).toString() // 初始化助手訊息 setMessages(prev => [...prev, { id: assistantMessageId, role: 'assistant', content: '' }]) // 處理串流回應 for await (const chunk of stream.textStream) { fullContent += chunk // 更新助手訊息內容 setMessages(prev => prev.map(msg => msg.id === assistantMessageId ? { ...msg, content: fullContent } : msg )) } setIsLoading(false) }) .catch((err) => { console.error('Chat error:', err) setError(err.message) // 如果 API 失敗,使用備用回應 if (err.message.includes('API key not configured')) { setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'assistant', content: '⚠️ OpenAI API key not configured. Please set OPENAI_API_KEY in your .env file to enable AI chat.' }]) } else { setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'assistant', content: hasAuraConfig ? 'Based on your product analysis, I recommend focusing on content marketing to build authority in your niche. Start with educational blog posts that solve your target audience\'s pain points.' : 'I\'m using demo analysis results. Run /init to analyze your product for personalized marketing insights! Meanwhile, I can help with general marketing strategies.' }]) } setIsLoading(false) }) setInputValue('') } setExitPrompt(false) } } const handleBack = () => { setCurrentRoute('home') } // Route to different pages if (currentRoute === 'init') { return <InitPage onBack={handleBack} /> } if (currentRoute === 'settings') { return <SettingsPage onBack={() => { // 重新載入設定 const loadSettings = async () => { try { const data = await fs.readFile('./.aura.json', 'utf-8') setSettings(JSON.parse(data)) } catch { // 使用預設值 } } loadSettings() handleBack() }} /> } if (currentRoute === 'sync') { return <SyncPage onBack={handleBack} /> } if (currentRoute === 'add-aura-agent') { return <AddAuraAgentPage onBack={handleBack} /> } // Home page return ( <Box flexDirection='column' padding={1}> <Box paddingY={1} paddingX={2} borderColor='blue' borderStyle='round' width={'100%'}> {/* Header */} <Text> 🌀 Welcome to <Text bold>Aura!</Text> <Newline /> <Newline /> <Text color={'white'} dimColor> Your AI marketing companion </Text> </Text> </Box> {/* Tips for getting started */} <Box marginTop={1} paddingLeft={1} flexDirection='column'> <Text color='gray'>Tips for getting started:</Text> <Newline /> <Text color='gray'> 1. Run /init to analyze your product and create a marketing strategy</Text> <Text color='gray'> 2. Chat directly with Aura for marketing insights and recommendations</Text> <Text color='gray'> 3. Be as specific as you would with a marketing expert for best results</Text> <Text color='gray'> 4. ✓ View available commands by typing /</Text> </Box> {/* Demo 警告 - 使用 Static 和 Alert */} {!hasAuraConfig && messages.length > 0 && ( <Static items={[{id: 'demo-alert'}]}> {() => ( <Box marginTop={1} marginBottom={1}> <Alert variant="warning"> You're chatting with demo analysis results. Run /init for personalized insights. </Alert> </Box> )} </Static> )} {/* API 錯誤警告 */} {error && ( <Static items={[{id: 'error-alert'}]}> {() => ( <Box marginTop={1} marginBottom={1}> <Alert variant="error"> {error} </Alert> </Box> )} </Static> )} {/* 聊天歷史 */} {messages.length > 0 && ( <Box flexDirection='column' marginTop={2} marginBottom={1} paddingLeft={1}> {messages.filter(m => m.role !== 'system').map((msg) => ( <Box key={msg.id} marginBottom={1} flexDirection='row'> <Box width={2} flexShrink={0}> <Text>{msg.role === 'user' ? settings.avatars.user : settings.avatars.aura}</Text> </Box> <Box flexGrow={1} paddingLeft={1}> {msg.role === 'user' ? ( <Text>{msg.content}</Text> ) : ( <Text color='white'>{msg.content}</Text> )} </Box> </Box> ))} {isLoading && <EmojiSpinner label="Aura is thinking..." />} </Box> )} {/* Command Input with integrated Ctrl+C status */} <Box marginTop={messages.length === 0 ? 2 : 1}> <CommandInput value={inputValue} onChange={setInputValue} onSubmit={handleSubmit} placeholder='Type a command or message...' commands={availableCommands} exitPrompt={exitPrompt} onClearInput={handleClearInput} onExit={handleExit} /> </Box> </Box> ) } export default App