refactor(api): split monolithic handlers.go into focused modules
All checks were successful
Beta Release / beta (push) Successful in 44s
All checks were successful
Beta Release / beta (push) Successful in 44s
Break down the 627-line handlers.go into specialized modules: - handlers_chat.go: chat and streaming endpoints - handlers_config.go: configuration endpoints - handlers_common.go: shared utilities - handlers_info.go: info and status endpoints - handlers_terminal.go: terminal/shell endpoints - handlers_tools.go: tool-related endpoints Also includes config improvements, orchestrator enhancements, and web component updates. 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
@@ -14,6 +14,7 @@ export default function App() {
|
||||
const [clock, setClock] = useState(new Date())
|
||||
const [updates, setUpdates] = useState([])
|
||||
const [tools, setTools] = useState([])
|
||||
const [config, setConfig] = useState(null)
|
||||
const { t, layout } = useI18n()
|
||||
|
||||
const TABS = useMemo(() => [
|
||||
@@ -27,7 +28,13 @@ export default function App() {
|
||||
api.getInfo().then(setInfo).catch(() => {})
|
||||
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
||||
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
||||
applyTheme(getTheme('cyberpunk-red'))
|
||||
api.getConfig().then(d => {
|
||||
setConfig(d)
|
||||
const theme = d.profile?.preferences?.theme || 'cyberpunk-red'
|
||||
applyTheme(getTheme(theme))
|
||||
}).catch(() => {
|
||||
applyTheme(getTheme('cyberpunk-red'))
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,7 +64,7 @@ export default function App() {
|
||||
const switchTab = useCallback((tabId) => setActiveTab(tabId), [])
|
||||
|
||||
const hasUpdates = updates.some(u => u.needsUpdate)
|
||||
const installed = tools.filter(t => t.installed).length
|
||||
const installed = tools.filter(tool => tool.installed).length
|
||||
|
||||
const WINDOW_SHORTCUTS = useMemo(() => ({
|
||||
dash: [
|
||||
@@ -80,10 +87,10 @@ export default function App() {
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'dash': return <Dashboard tools={tools} updates={updates} api={api} onRescan={t => setTools(t)} />
|
||||
case 'dash': return <Dashboard api={api} />
|
||||
case 'studio': return <Studio api={api} />
|
||||
case 'shell': return <Shell api={api} />
|
||||
case 'config': return <Config api={api} onThemeChange={() => {}} />
|
||||
case 'config': return <Config api={api} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { User, Brain, RefreshCw, Globe, Wrench } from 'lucide-react'
|
||||
import { User, Brain, RefreshCw, Globe, Wrench, Monitor } from 'lucide-react'
|
||||
import { useI18n, LANGUAGES } from '../i18n'
|
||||
import { getLayoutList } from '../i18n/keyboards'
|
||||
|
||||
const PANELS = [
|
||||
{ id: 'profile', icon: User },
|
||||
{ id: 'providers', icon: Brain },
|
||||
{ id: 'terminal', icon: Monitor },
|
||||
{ id: 'updates', icon: RefreshCw },
|
||||
{ id: 'locale', icon: Globe },
|
||||
{ id: 'skills', icon: Wrench },
|
||||
@@ -26,6 +27,9 @@ export default function Config({ api }) {
|
||||
const [profileForm, setProfileForm] = useState({})
|
||||
const [providerForm, setProviderForm] = useState({})
|
||||
const [toast, setToast] = useState(null)
|
||||
const [terminalThemes, setTerminalThemes] = useState([])
|
||||
const [terminalSettings, setTerminalSettings] = useState({ font_size: 14, font_family: '', theme: 'default' })
|
||||
const [savingTerminal, setSavingTerminal] = useState(false)
|
||||
|
||||
const layouts = getLayoutList()
|
||||
|
||||
@@ -39,11 +43,19 @@ export default function Config({ api }) {
|
||||
editor: d.profile?.preferences?.editor || '',
|
||||
shell: d.profile?.preferences?.shell || '',
|
||||
})
|
||||
if (d.terminal) {
|
||||
setTerminalSettings({
|
||||
font_size: d.terminal.font_size || 14,
|
||||
font_family: d.terminal.font_family || '',
|
||||
theme: d.terminal.theme || 'default',
|
||||
})
|
||||
}
|
||||
}).catch(() => {})
|
||||
api.getProviders().then(d => setProviders(d.providers || [])).catch(() => {})
|
||||
api.getSkills().then(d => setSkillList(d.skills || [])).catch(() => {})
|
||||
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
||||
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
||||
api.getTerminalThemes().then(d => setTerminalThemes(d.themes || [])).catch(() => {})
|
||||
}, [api])
|
||||
|
||||
useEffect(() => { loadData() }, [loadData])
|
||||
@@ -114,6 +126,18 @@ export default function Config({ api }) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveTerminalSettings = async () => {
|
||||
setSavingTerminal(true)
|
||||
try {
|
||||
await api.saveTerminalSettings(terminalSettings)
|
||||
showToast(t('config.saved'))
|
||||
window.location.reload()
|
||||
} catch (err) {
|
||||
showToast(`${t('config.error')}: ${err.message}`)
|
||||
}
|
||||
setSavingTerminal(false)
|
||||
}
|
||||
|
||||
const openProviderEdit = (p) => {
|
||||
setProviderForm({
|
||||
name: p.name,
|
||||
@@ -125,8 +149,8 @@ export default function Config({ api }) {
|
||||
}
|
||||
|
||||
const needsUpdateCount = updates.filter(u => u.needsUpdate).length
|
||||
const installedCount = tools.filter(t => t.installed).length
|
||||
const missingCount = tools.filter(t => !t.installed).length
|
||||
const installedCount = tools.filter(tool => tool.installed).length
|
||||
const missingCount = tools.filter(tool => !tool.installed).length
|
||||
|
||||
return (
|
||||
<div className="config-window">
|
||||
@@ -189,6 +213,13 @@ export default function Config({ api }) {
|
||||
{activePanel === 'skills' && (
|
||||
<PanelSkills skillList={skillList} t={t} />
|
||||
)}
|
||||
{activePanel === 'terminal' && (
|
||||
<PanelTerminal
|
||||
settings={terminalSettings} setSettings={setTerminalSettings}
|
||||
themes={terminalThemes} saving={savingTerminal}
|
||||
onSave={handleSaveTerminalSettings} t={t}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,8 +312,8 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
||||
<span className="provider-card-name">{p.name}</span>
|
||||
{p.apiKey && <span className="badge ok">{t('config.keyConfigured')}</span>}
|
||||
{!p.apiKey && <span className="badge error">{t('config.noKey')}</span>}
|
||||
{isValidationTarget && validationStatus.valid && <span className="badge ok">{t('config.keyValid')}</span>}
|
||||
{isValidationTarget && !validationStatus.valid && <span className="badge error">{validationStatus.error}</span>}
|
||||
{isValidationTarget && validationStatus?.valid && <span className="badge ok">{t('config.keyValid')}</span>}
|
||||
{isValidationTarget && !validationStatus?.valid && <span className="badge error">{validationStatus?.error}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -309,7 +340,7 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
||||
>
|
||||
{validating === p.name ? t('config.validating') : t('config.validateKey')}
|
||||
</button>
|
||||
{isValidationTarget && validationStatus.valid && (
|
||||
{isValidationTarget && validationStatus?.valid && (
|
||||
<button className="sm" onClick={handleSaveProvider}>{t('config.save')}</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -439,6 +470,102 @@ function PanelSkills({ skillList, t }) {
|
||||
)
|
||||
}
|
||||
|
||||
function PanelTerminal({ settings, setSettings, themes, saving, onSave, t }) {
|
||||
const previewTheme = {
|
||||
background: settings.theme === 'default' ? '#0A0A0C' :
|
||||
settings.theme === 'monokai' ? '#272822' :
|
||||
settings.theme === 'gruvbox' ? '#282828' :
|
||||
settings.theme === 'nord' ? '#2E3440' :
|
||||
settings.theme === 'solarized-dark' ? '#002B36' :
|
||||
settings.theme === 'dracula' ? '#282A36' : '#0A0A0C',
|
||||
foreground: settings.theme === 'default' ? '#EAE0E2' :
|
||||
settings.theme === 'monokai' ? '#F8F8F2' :
|
||||
settings.theme === 'gruvbox' ? '#EBDBB2' :
|
||||
settings.theme === 'nord' ? '#D8DEE9' :
|
||||
settings.theme === 'solarized-dark' ? '#839496' :
|
||||
settings.theme === 'dracula' ? '#F8F8F2' : '#EAE0E2',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="config-card">
|
||||
<div className="config-card-group">
|
||||
<span className="config-card-group-label">{t('config.terminalTheme')}</span>
|
||||
<div className="chip-row">
|
||||
{themes.map(th => (
|
||||
<div
|
||||
key={th.id}
|
||||
className={`chip ${settings.theme === th.id ? 'active' : ''}`}
|
||||
onClick={() => setSettings(s => ({ ...s, theme: th.id }))}
|
||||
>
|
||||
{th.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-card-group">
|
||||
<span className="config-card-group-label">{t('config.fontSize')}</span>
|
||||
<div className="chip-row">
|
||||
{[12, 14, 16, 18, 20, 24].map(size => (
|
||||
<div
|
||||
key={size}
|
||||
className={`chip ${settings.font_size === size ? 'active' : ''}`}
|
||||
onClick={() => setSettings(s => ({ ...s, font_size: size }))}
|
||||
>
|
||||
{size}px
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-card-group">
|
||||
<span className="config-card-group-label">{t('config.fontFamily')}</span>
|
||||
<select
|
||||
className="config-form-input"
|
||||
value={settings.font_family}
|
||||
onChange={e => setSettings(s => ({ ...s, font_family: e.target.value }))}
|
||||
style={{ maxWidth: 300 }}
|
||||
>
|
||||
<option value="">Default (JetBrains Mono)</option>
|
||||
<option value="'Fira Code', monospace">Fira Code</option>
|
||||
<option value="'Cascadia Code', 'SF Mono', monospace">Cascadia Code</option>
|
||||
<option value="'SF Mono', 'Menlo', monospace">SF Mono</option>
|
||||
<option value="'Source Code Pro', monospace">Source Code Pro</option>
|
||||
<option value="monospace">System Monospace</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="config-card-group">
|
||||
<span className="config-card-group-label">{t('config.preview')}</span>
|
||||
<div style={{
|
||||
background: previewTheme.background,
|
||||
color: previewTheme.foreground,
|
||||
padding: '16px 20px',
|
||||
borderRadius: 'var(--radius)',
|
||||
fontFamily: settings.font_family || "'JetBrains Mono', monospace",
|
||||
fontSize: settings.font_size || 14,
|
||||
border: '1px solid var(--border)',
|
||||
}}>
|
||||
<span style={{ color: '#00E676' }}>➜</span> <span>~/projects</span>
|
||||
<span style={{ color: '#448AFF' }}> git status</span>
|
||||
<br />
|
||||
<span>On branch </span>
|
||||
<span style={{ color: '#FFD740' }}>main</span>
|
||||
<br />
|
||||
<span style={{ opacity: 0.6 }}>Type a command...</span>
|
||||
<span style={{ animation: 'blink 1s step-end infinite' }}> ▋</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-card-actions" style={{ marginTop: 16 }}>
|
||||
<button className="primary sm" onClick={onSave} disabled={saving}>
|
||||
{saving ? t('config.saving') : t('config.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FormInput({ label, value, onChange, type = 'text' }) {
|
||||
return (
|
||||
<div className="config-form-field">
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function Dashboard({ api, onRescan }) {
|
||||
const { t, layout } = useI18n()
|
||||
export default function Dashboard({ api }) {
|
||||
const { t } = useI18n()
|
||||
const [notifications, setNotifications] = useState([])
|
||||
|
||||
const addNotif = (text, type) => {
|
||||
setNotifications(prev => [{ text, type, id: Date.now(), time: new Date() }, ...prev])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard-layout">
|
||||
<div className="dashboard-content">
|
||||
@@ -47,7 +43,7 @@ export default function Dashboard({ api, onRescan }) {
|
||||
{notifications.map(n => (
|
||||
<div key={n.id} className={`notif-row notif-${n.type}`}>
|
||||
<span className="notif-time">
|
||||
{n.time.toLocaleTimeString(layout.locale, { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
{n.time.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
</span>
|
||||
<span className="notif-text">{n.text}</span>
|
||||
</div>
|
||||
|
||||
@@ -8,37 +8,74 @@ import { useI18n } from '../i18n'
|
||||
|
||||
const MAX_TABS = 7
|
||||
|
||||
const XTERM_THEME = {
|
||||
background: '#0A0A0C',
|
||||
foreground: '#EAE0E2',
|
||||
cursor: '#FF0033',
|
||||
cursorAccent: '#0A0A0C',
|
||||
selectionBackground: '#FF003344',
|
||||
selectionForeground: '#ffffff',
|
||||
black: '#0A0A0C',
|
||||
red: '#FF0033',
|
||||
green: '#00E676',
|
||||
yellow: '#FFD740',
|
||||
blue: '#448AFF',
|
||||
magenta: '#FF1A5E',
|
||||
cyan: '#00BCD4',
|
||||
white: '#EAE0E2',
|
||||
brightBlack: '#5A4F52',
|
||||
brightRed: '#FF5252',
|
||||
brightGreen: '#69F0AE',
|
||||
brightYellow: '#FFFF00',
|
||||
brightBlue: '#82B1FF',
|
||||
brightMagenta: '#FF80AB',
|
||||
brightCyan: '#84FFFF',
|
||||
brightWhite: '#FFFFFF',
|
||||
const THEMES = {
|
||||
default: {
|
||||
background: '#0A0A0C', foreground: '#EAE0E2', cursor: '#FF0033',
|
||||
cursorAccent: '#0A0A0C', selectionBackground: '#FF003344', selectionForeground: '#ffffff',
|
||||
black: '#0A0A0C', red: '#FF0033', green: '#00E676', yellow: '#FFD740',
|
||||
blue: '#448AFF', magenta: '#FF1A5E', cyan: '#00BCD4', white: '#EAE0E2',
|
||||
brightBlack: '#5A4F52', brightRed: '#FF5252', brightGreen: '#69F0AE',
|
||||
brightYellow: '#FFFF00', brightBlue: '#82B1FF', brightMagenta: '#FF80AB',
|
||||
brightCyan: '#84FFFF', brightWhite: '#FFFFFF',
|
||||
},
|
||||
monokai: {
|
||||
background: '#272822', foreground: '#F8F8F2', cursor: '#F8F8F0',
|
||||
cursorAccent: '#272822', selectionBackground: '#F8F8F244', selectionForeground: '#ffffff',
|
||||
black: '#272822', red: '#F92672', green: '#A6E22E', yellow: '#E6DB74',
|
||||
blue: '#66D9EF', magenta: '#AE81FF', cyan: '#A1EFE4', white: '#F8F8F2',
|
||||
brightBlack: '#75715E', brightRed: '#F92672', brightGreen: '#A6E22E',
|
||||
brightYellow: '#E6DB74', brightBlue: '#66D9EF', brightMagenta: '#AE81FF',
|
||||
brightCyan: '#A1EFE4', brightWhite: '#F8F8F2',
|
||||
},
|
||||
gruvbox: {
|
||||
background: '#282828', foreground: '#EBDBB2', cursor: '#FB4934',
|
||||
cursorAccent: '#282828', selectionBackground: '#EBDBB244', selectionForeground: '#ffffff',
|
||||
black: '#282828', red: '#CC241D', green: '#98971A', yellow: '#D79921',
|
||||
blue: '#458588', magenta: '#B16286', cyan: '#689D6A', white: '#EBDBB2',
|
||||
brightBlack: '#928374', brightRed: '#FB4934', brightGreen: '#B8BB26',
|
||||
brightYellow: '#FABC2A', brightBlue: '#83A598', brightMagenta: '#D3869B',
|
||||
brightCyan: '#8EC07C', brightWhite: '#EBDBB2',
|
||||
},
|
||||
nord: {
|
||||
background: '#2E3440', foreground: '#D8DEE9', cursor: '#D8DEE9',
|
||||
cursorAccent: '#2E3440', selectionBackground: '#D8DEE944', selectionForeground: '#ffffff',
|
||||
black: '#2E3440', red: '#BF616A', green: '#A3BE8C', yellow: '#EBCB8B',
|
||||
blue: '#81A1C1', magenta: '#B48EAD', cyan: '#88C0D0', white: '#D8DEE9',
|
||||
brightBlack: '#4C566A', brightRed: '#BF616A', brightGreen: '#A3BE8C',
|
||||
brightYellow: '#EBCB8B', brightBlue: '#81A1C1', brightMagenta: '#B48EAD',
|
||||
brightCyan: '#8FBCBB', brightWhite: '#ECEFF4',
|
||||
},
|
||||
'solarized-dark': {
|
||||
background: '#002B36', foreground: '#839496', cursor: '#D33682',
|
||||
cursorAccent: '#002B36', selectionBackground: '#83949644', selectionForeground: '#ffffff',
|
||||
black: '#002B36', red: '#DC322F', green: '#859900', yellow: '#B58900',
|
||||
blue: '#268BD2', magenta: '#D33682', cyan: '#2AA198', white: '#FDF6E3',
|
||||
brightBlack: '#073642', brightRed: '#CB4B16', brightGreen: '#586E75',
|
||||
brightYellow: '#657B83', brightBlue: '#6C71C4', brightMagenta: '#6C71C4',
|
||||
brightCyan: '#93A1A1', brightWhite: '#FDF6E3',
|
||||
},
|
||||
dracula: {
|
||||
background: '#282A36', foreground: '#F8F8F2', cursor: '#F8F8F2',
|
||||
cursorAccent: '#282A36', selectionBackground: '#F8F8F244', selectionForeground: '#ffffff',
|
||||
black: '#282A36', red: '#FF5555', green: '#50FA7B', yellow: '#F1FA8C',
|
||||
blue: '#BD93F9', magenta: '#FF79C6', cyan: '#8BE9FD', white: '#F8F8F2',
|
||||
brightBlack: '#6272A4', brightRed: '#FF6E6E', brightGreen: '#69FF94',
|
||||
brightYellow: '#FFFFA5', brightBlue: '#D6ACFF', brightMagenta: '#FF92DF',
|
||||
brightCyan: '#A4FFFF', brightWhite: '#FFFFFF',
|
||||
},
|
||||
}
|
||||
|
||||
function createTerminal(container) {
|
||||
function getTheme(themeName) {
|
||||
return THEMES[themeName] || THEMES.default
|
||||
}
|
||||
|
||||
function createTerminal(container, settings = {}) {
|
||||
const theme = getTheme(settings.theme || 'default')
|
||||
const term = new XTerm({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||
theme: XTERM_THEME,
|
||||
fontSize: settings.fontSize || 14,
|
||||
fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||
theme,
|
||||
allowTransparency: false,
|
||||
scrollback: 5000,
|
||||
})
|
||||
@@ -116,27 +153,30 @@ export default function Shell({ api }) {
|
||||
const [showSshModal, setShowSshModal] = useState(false)
|
||||
const [editingTab, setEditingTab] = useState(null)
|
||||
const [editName, setEditName] = useState('')
|
||||
const [terminalSettings, setTerminalSettings] = useState({
|
||||
fontSize: 14,
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||
theme: 'default',
|
||||
})
|
||||
|
||||
const [sshForm, setSshForm] = useState({
|
||||
name: '', host: '', port: 22, user: '', key_path: '',
|
||||
})
|
||||
|
||||
const [aiMessages, setAiMessages] = useState([
|
||||
{ role: 'ai', content: t('shell.aiWelcome') }
|
||||
])
|
||||
const [aiInput, setAiInput] = useState('')
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const aiMessagesRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
aiMessagesRef.current?.scrollTo(0, aiMessagesRef.current.scrollHeight)
|
||||
}, [aiMessages])
|
||||
|
||||
useEffect(() => {
|
||||
api.getTerminalSessions().then(d => {
|
||||
setSshConnections(d.ssh || [])
|
||||
setSystemTerminals(d.system || [])
|
||||
}).catch(() => {})
|
||||
api.getConfig().then(d => {
|
||||
if (d.terminal) {
|
||||
setTerminalSettings({
|
||||
fontSize: d.terminal.font_size || 14,
|
||||
fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||
theme: d.terminal.theme || 'default',
|
||||
})
|
||||
}
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const initTerminal = useCallback((tabId, tab) => {
|
||||
@@ -145,7 +185,11 @@ export default function Shell({ api }) {
|
||||
const container = document.getElementById(`terminal-${tabId}`)
|
||||
if (!container) return
|
||||
|
||||
const { term, fitAddon } = createTerminal(container)
|
||||
const { term, fitAddon } = createTerminal(container, {
|
||||
fontSize: terminalSettings.fontSize,
|
||||
fontFamily: terminalSettings.fontFamily,
|
||||
theme: terminalSettings.theme,
|
||||
})
|
||||
|
||||
let initPayload
|
||||
if (tab.type === 'ssh') {
|
||||
@@ -307,21 +351,6 @@ export default function Shell({ api }) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAiSend = async () => {
|
||||
if (!aiInput.trim() || aiLoading) return
|
||||
const text = aiInput.trim()
|
||||
setAiMessages(prev => [...prev, { role: 'user', content: text }])
|
||||
setAiInput('')
|
||||
setAiLoading(true)
|
||||
try {
|
||||
const res = await api.runCommand(`echo "AI: ${text}"`, '')
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: res.output || t('shell.noResponse') }])
|
||||
} catch (err) {
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: `${t('shell.error')}: ${err.message}` }])
|
||||
}
|
||||
setAiLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shell-layout">
|
||||
<div className="shell-terminal-col">
|
||||
@@ -436,27 +465,6 @@ export default function Shell({ api }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="shell-ai-col">
|
||||
<div className="ai-panel-header">{t('shell.aiAssistant')}</div>
|
||||
<div className="ai-panel-messages" ref={aiMessagesRef}>
|
||||
{aiMessages.map((msg, i) => (
|
||||
<div key={i} className={`ai-message ${msg.role}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
))}
|
||||
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
||||
</div>
|
||||
<div className="ai-panel-input">
|
||||
<input
|
||||
value={aiInput}
|
||||
onChange={e => setAiInput(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleAiSend()}
|
||||
placeholder={t('shell.askAi')}
|
||||
/>
|
||||
<button className="sm" onClick={handleAiSend} disabled={!aiInput.trim()}>{t('shell.send')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSshModal && (
|
||||
<div className="shell-modal-overlay" onClick={() => setShowSshModal(false)}>
|
||||
<div className="shell-modal" onClick={e => e.stopPropagation()}>
|
||||
|
||||
@@ -178,9 +178,10 @@ export default function Studio({ api }) {
|
||||
|
||||
try {
|
||||
let accumulated = ''
|
||||
await api.sendChat(text, true).then(full => {
|
||||
accumulated = full
|
||||
}).catch(() => {})
|
||||
await api.sendChat(text, true, (partial) => {
|
||||
accumulated = partial
|
||||
setStreaming(partial)
|
||||
})
|
||||
|
||||
const finalContent = accumulated || t('studio.noResponse')
|
||||
setMessages(prev => [...prev, {
|
||||
|
||||
Reference in New Issue
Block a user