From 2004c15dd7ec04d202526874a84c4a5f5f92de9c Mon Sep 17 00:00:00 2001 From: Augustin Date: Wed, 22 Apr 2026 18:51:33 +0200 Subject: [PATCH] feat(shell): restore AI assistant panel 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 --- web/src/components/Shell.jsx | 47 +++++++++++++++++++++++++++++ web/src/i18n/en.js | 3 ++ web/src/i18n/fr.js | 3 ++ web/src/styles/global.css | 57 ++++++++++++++++++++++++++++++++++-- 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx index 87b9868..964dcab 100644 --- a/web/src/components/Shell.jsx +++ b/web/src/components/Shell.jsx @@ -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 (
@@ -475,6 +501,27 @@ export default function Shell({ api }) {
+
+
{t('shell.aiAssistant')}
+
+ {aiMessages.map((msg, i) => ( +
+ {msg.content} +
+ ))} + {aiLoading &&
} +
+
+ setAiInput(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleAiSend()} + placeholder={t('shell.askAi')} + /> + +
+
+ {showSshModal && (
setShowSshModal(false)}>
e.stopPropagation()}> diff --git a/web/src/i18n/en.js b/web/src/i18n/en.js index 4d4d563..40a3ecd 100644 --- a/web/src/i18n/en.js +++ b/web/src/i18n/en.js @@ -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: { diff --git a/web/src/i18n/fr.js b/web/src/i18n/fr.js index 1465c65..cd32c92 100644 --- a/web/src/i18n/fr.js +++ b/web/src/i18n/fr.js @@ -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: { diff --git a/web/src/styles/global.css b/web/src/styles/global.css index ea7fd40..edc234b 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -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;