aura-ai
Version:
AI-powered marketing strategist CLI tool for developers
305 lines (275 loc) • 9.62 kB
JSX
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