feat(shell): restore AI assistant panel
All checks were successful
Beta Release / beta (push) Successful in 38s

Re-add the AI assistant panel that was removed in previous refactoring.
The panel includes:
- Message history display
- Input field for AI queries
- Loading state indicator

Also restored the associated CSS styles and i18n translations.

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-22 18:51:33 +02:00
parent bcba5932d5
commit 4fd599adec
4 changed files with 107 additions and 3 deletions

View File

@@ -163,6 +163,17 @@ export default function Shell({ api }) {
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 || [])
@@ -361,6 +372,21 @@ 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">
@@ -475,6 +501,27 @@ 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()}>

View File

@@ -107,6 +107,9 @@ const en = {
systemTerminals: 'System terminals',
switchTerminal: 'Switch terminal',
localShell: 'Local Shell',
aiAssistant: 'AI Assistant',
aiWelcome: 'Hello! I can help you with terminal commands. Ask me anything!',
askAi: 'Ask AI assistant...',
},
config: {

View File

@@ -107,6 +107,9 @@ const fr = {
systemTerminals: 'Terminaux syst\u00e8me',
switchTerminal: 'Changer de terminal',
localShell: 'Shell local',
aiAssistant: 'Assistant IA',
aiWelcome: 'Bonjour ! Je peux vous aider avec les commandes du terminal. Demandez-moi n\'importe quoi !',
askAi: 'Interroger l\'assistant IA...',
},
config: {

View File

@@ -385,6 +385,15 @@ input::placeholder { color: var(--text-disabled); }
.connection-dot.on { background: var(--success); box-shadow: 0 0 6px var(--success); }
.connection-dot.off { background: var(--error); }
.shell-ai-col { width: 320px; border-left: 1px solid var(--border); background: var(--bg-surface); display: flex; flex-direction: column; flex-shrink: 0; }
.ai-panel-header { padding: 12px 16px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: 13px; color: var(--accent); }
.ai-panel-messages { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.ai-message { padding: 8px 12px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; word-break: break-word; }
.ai-message.ai { background: var(--bg-card); border-left: 3px solid var(--accent); }
.ai-message.user { background: var(--accent-bg); border-left: 3px solid var(--accent-muted); }
.ai-panel-input { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--border); }
.ai-panel-input input { flex: 1; font-size: 13px; padding: 6px 10px; }
.shell-modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
display: flex; align-items: center; justify-content: center; z-index: 1000;
@@ -570,18 +579,60 @@ input::placeholder { color: var(--text-disabled); }
.feed-loading { display: flex; align-items: center; justify-content: center; padding: 60px 0; }
.feed-item { display: flex; gap: 10px; padding: 8px 12px; border-radius: var(--radius); animation: fadeIn 0.15s ease-out; }
.feed-item:hover { background: var(--bg-card); }
.feed-item.user { background: var(--bg-card); border-left: 3px solid var(--accent-muted); }
.feed-item.assistant { }
.feed-item.user { background: var(--bg-card); border-left: 3px solid #FFD740; }
.feed-item.assistant { border-left: 3px solid transparent; }
.feed-item.assistant:hover { border-left-color: var(--accent-dark); }
.feed-item.system { align-items: center; gap: 8px; padding: 6px 12px; }
.feed-avatar { width: 24px; height: 24px; border-radius: 50%; background: var(--accent-bg); color: var(--accent); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 2px; }
.feed-avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 2px; font-size: 14px; }
.feed-avatar.user-rank { background: rgba(255, 215, 64, 0.15); }
.feed-avatar.ai-rank { background: var(--accent-bg); }
.feed-rank-icon { display: flex; align-items: center; justify-content: center; }
.feed-body { flex: 1; min-width: 0; }
.feed-header { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; }
.feed-rank-badge {
font-size: 9px; font-weight: 800; font-family: var(--font-mono);
padding: 1px 6px; border-radius: 3px; border: 1px solid;
letter-spacing: 0.5px; text-transform: uppercase;
background: rgba(255, 215, 64, 0.08);
}
.feed-role { font-size: 11px; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: 0.5px; }
.feed-time { font-size: 10px; color: var(--text-disabled); font-family: var(--font-mono); }
.feed-content { font-size: 14px; line-height: 1.7; color: var(--text-primary); word-break: break-word; }
.feed-system-badge { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-dim); flex-shrink: 0; }
.feed-system-text { font-size: 12px; color: var(--text-tertiary); font-style: italic; flex: 1; }
.feed-thinking-block {
background: var(--bg-surface); border: 1px solid var(--border); border-left: 2px solid var(--accent-dim);
border-radius: var(--radius); margin: 6px 0 8px; overflow: hidden;
transition: all 0.3s ease;
}
.feed-thinking-block.active {
border-left-color: var(--warning);
}
.feed-thinking-block.done {
border-left-color: var(--text-disabled);
opacity: 0.7;
}
.feed-thinking-block.done .feed-thinking-content {
max-height: 80px;
overflow-y: auto;
}
.feed-thinking-header {
display: flex; align-items: center; gap: 6px;
padding: 6px 10px; font-size: 10px; font-weight: 700;
color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.5px;
background: var(--bg-card); border-bottom: 1px solid var(--border);
}
.feed-thinking-header svg { color: var(--warning); }
.feed-thinking-dots { display: inline-flex; gap: 2px; margin-left: 4px; }
.feed-thinking-dots span { width: 4px; height: 4px; border-radius: 50%; background: var(--warning); animation: bounce 1.2s ease-in-out infinite; }
.feed-thinking-dots span:nth-child(2) { animation-delay: 0.15s; }
.feed-thinking-dots span:nth-child(3) { animation-delay: 0.3s; }
.feed-thinking-content {
padding: 8px 10px; font-size: 12px; color: var(--text-tertiary);
font-style: italic; line-height: 1.5; max-height: 120px; overflow-y: auto;
}
.studio-code-block {
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
overflow: hidden; margin: 8px 0;