Compare commits
1 Commits
v0.3.5
...
v0.3.5-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00118f0803 |
@@ -1,13 +1,11 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { User, Brain, RefreshCw, Globe, Wrench, Monitor } from 'lucide-react'
|
import { User, Brain, RefreshCw, Wrench, Monitor, AlertTriangle, X } from 'lucide-react'
|
||||||
import { useI18n, LANGUAGES } from '../i18n'
|
import { useI18n } from '../i18n'
|
||||||
import { getLayoutList } from '../i18n/keyboards'
|
|
||||||
|
|
||||||
const PANELS = [
|
const PANELS = [
|
||||||
{ id: 'profile', icon: User },
|
{ id: 'profile', icon: User },
|
||||||
{ id: 'providers', icon: Brain },
|
{ id: 'providers', icon: Brain },
|
||||||
{ id: 'updates', icon: RefreshCw },
|
{ id: 'updates', icon: RefreshCw },
|
||||||
{ id: 'locale', icon: Globe },
|
|
||||||
{ id: 'skills', icon: Wrench },
|
{ id: 'skills', icon: Wrench },
|
||||||
{ id: 'system', icon: Monitor },
|
{ id: 'system', icon: Monitor },
|
||||||
]
|
]
|
||||||
@@ -29,8 +27,6 @@ export default function Config({ api }) {
|
|||||||
const [toast, setToast] = useState(null)
|
const [toast, setToast] = useState(null)
|
||||||
|
|
||||||
|
|
||||||
const layouts = getLayoutList()
|
|
||||||
|
|
||||||
const loadData = useCallback(() => {
|
const loadData = useCallback(() => {
|
||||||
api.getConfig().then(d => {
|
api.getConfig().then(d => {
|
||||||
setConfig(d)
|
setConfig(d)
|
||||||
@@ -168,13 +164,6 @@ export default function Config({ api }) {
|
|||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activePanel === 'locale' && (
|
|
||||||
<PanelLocale
|
|
||||||
language={language} keyboard={keyboard} layouts={layouts}
|
|
||||||
api={api}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activePanel === 'skills' && (
|
{activePanel === 'skills' && (
|
||||||
<PanelSkills skillList={skillList} t={t} />
|
<PanelSkills skillList={skillList} t={t} />
|
||||||
)}
|
)}
|
||||||
@@ -320,21 +309,36 @@ function getFieldLabel(key, t) {
|
|||||||
|
|
||||||
function PanelProviders({ providers, editProvider, providerForm, setProviderForm, setEditProvider, openProviderEdit, handleSaveProvider, api, loadData, t }) {
|
function PanelProviders({ providers, editProvider, providerForm, setProviderForm, setEditProvider, openProviderEdit, handleSaveProvider, api, loadData, t }) {
|
||||||
const [validating, setValidating] = useState(null)
|
const [validating, setValidating] = useState(null)
|
||||||
const [validationStatus, setValidationStatus] = useState(null)
|
const [keyStatus, setKeyStatus] = useState({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
providers.forEach(p => {
|
||||||
|
if (p.apiKey && !keyStatus[p.name]) {
|
||||||
|
validateKey(p)
|
||||||
|
} else if (!p.apiKey) {
|
||||||
|
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: false, checked: true, error: 'Aucune clé' } }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [providers])
|
||||||
|
|
||||||
|
const validateKey = async (p) => {
|
||||||
|
setValidating(p.name)
|
||||||
|
try {
|
||||||
|
await api.validateProvider({ name: p.name, api_key: p.apiKey, model: p.model, base_url: p.baseURL || '' })
|
||||||
|
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: true, checked: true } }))
|
||||||
|
} catch (err) {
|
||||||
|
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: false, checked: true, error: err.message || 'Clé invalide' } }))
|
||||||
|
}
|
||||||
|
setValidating(null)
|
||||||
|
}
|
||||||
|
|
||||||
const handleValidate = async (name, apiKey, model, baseUrl) => {
|
const handleValidate = async (name, apiKey, model, baseUrl) => {
|
||||||
setValidating(name)
|
setValidating(name)
|
||||||
setValidationStatus(null)
|
|
||||||
try {
|
try {
|
||||||
await api.validateProvider({ name, api_key: apiKey, model, base_url: baseUrl })
|
await api.validateProvider({ name, api_key: apiKey, model, base_url: baseUrl })
|
||||||
setValidationStatus({ provider: name, valid: true })
|
setKeyStatus(prev => ({ ...prev, [name]: { valid: true, checked: true } }))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err.message || ''
|
setKeyStatus(prev => ({ ...prev, [name]: { valid: false, checked: true, error: err.message || 'Clé invalide' } }))
|
||||||
if (msg.includes('invalid_api_key')) {
|
|
||||||
setValidationStatus({ provider: name, valid: false, error: t('config.keyInvalid') })
|
|
||||||
} else {
|
|
||||||
setValidationStatus({ provider: name, valid: false, error: `${t('config.connectionFailed')}: ${msg}` })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setValidating(null)
|
setValidating(null)
|
||||||
}
|
}
|
||||||
@@ -345,8 +349,8 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
|||||||
<div className="config-providers-list">
|
<div className="config-providers-list">
|
||||||
{displayed.map((p, i) => {
|
{displayed.map((p, i) => {
|
||||||
const isEditing = editProvider === p.name
|
const isEditing = editProvider === p.name
|
||||||
const isValidationTarget = validationStatus?.provider === p.name
|
|
||||||
const currentModel = providerForm[p.name]?.model || p.model
|
const currentModel = providerForm[p.name]?.model || p.model
|
||||||
|
const status = keyStatus[p.name]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={i} className="config-card provider-card-v2">
|
<div key={i} className="config-card provider-card-v2">
|
||||||
@@ -354,8 +358,8 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
|||||||
<div className="provider-card-identity">
|
<div className="provider-card-identity">
|
||||||
<span className="provider-card-name">{p.name.toUpperCase()}</span>
|
<span className="provider-card-name">{p.name.toUpperCase()}</span>
|
||||||
{p.active && <span className="badge ok" style={{ marginLeft: 6 }}>active</span>}
|
{p.active && <span className="badge ok" style={{ marginLeft: 6 }}>active</span>}
|
||||||
{isValidationTarget && validationStatus?.valid && <span className="badge ok">{t('config.keyValid')}</span>}
|
{status?.checked && status?.valid && <span className="badge ok">✓ {t('config.keyValid')}</span>}
|
||||||
{isValidationTarget && !validationStatus?.valid && <span className="badge error">{validationStatus?.error}</span>}
|
{status?.checked && !status?.valid && <span className="badge error">✗ {status.error || t('config.keyInvalid')}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -402,7 +406,14 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PanelUpdates({ updates, checking, updating, needsUpdateCount, installedCount, missingCount, handleCheckUpdates, handleUpdateTool, handleUpdateAll, t }) {
|
function PanelUpdates({ updates, tools, checking, updating, needsUpdateCount, installedCount, missingCount, handleCheckUpdates, handleUpdateTool, handleUpdateAll, t }) {
|
||||||
|
const handleInstallTool = (tool) => {
|
||||||
|
window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
|
||||||
|
window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Installe l'outil ${tool} sur mon système. Vérifie d'abord s'il est déjà installé, puis installe-le si nécessaire avec les commandes appropriées.` } }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingTools = tools.filter(t => !t.installed)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="config-card">
|
<div className="config-card">
|
||||||
@@ -425,6 +436,30 @@ function PanelUpdates({ updates, checking, updating, needsUpdateCount, installed
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{missingTools.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="section-title" style={{ marginTop: 12, marginBottom: 4 }}>{t('config.missing') || 'Modules manquants'}</div>
|
||||||
|
<div className="config-update-list">
|
||||||
|
{missingTools.map((tool, i) => (
|
||||||
|
<div key={`miss-${i}`} className="config-update-row">
|
||||||
|
<div className="config-update-info">
|
||||||
|
<span className="config-update-name">{tool.name}</span>
|
||||||
|
<span className="config-update-versions">
|
||||||
|
<span style={{ color: 'var(--danger)' }}>{t('config.notInstalled') || 'Non installé'}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="sm primary"
|
||||||
|
onClick={() => handleInstallTool(tool.name)}
|
||||||
|
>
|
||||||
|
{t('config.install') || 'Installer'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{updates.length === 0 ? (
|
{updates.length === 0 ? (
|
||||||
<div className="config-card">
|
<div className="config-card">
|
||||||
<div className="empty-state">{t('config.noUpdates')}</div>
|
<div className="empty-state">{t('config.noUpdates')}</div>
|
||||||
@@ -460,98 +495,7 @@ function PanelUpdates({ updates, checking, updating, needsUpdateCount, installed
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PanelLocale({ language, keyboard, layouts, api, t }) {
|
|
||||||
const { setLanguage, setKeyboard } = useI18n()
|
|
||||||
const [editLocale, setEditLocale] = useState(false)
|
|
||||||
const [draftLang, setDraftLang] = useState(language)
|
|
||||||
const [draftKbd, setDraftKbd] = useState(keyboard)
|
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [toast, setToast] = useState(null)
|
|
||||||
|
|
||||||
const showToast = (msg) => {
|
|
||||||
setToast(msg)
|
|
||||||
setTimeout(() => setToast(null), 2500)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
setSaving(true)
|
|
||||||
try {
|
|
||||||
await api.savePreferences({ language: draftLang, keyboard_layout: draftKbd })
|
|
||||||
setLanguage(draftLang)
|
|
||||||
setKeyboard(draftKbd)
|
|
||||||
setEditLocale(false)
|
|
||||||
showToast(t('config.saved'))
|
|
||||||
} catch (err) {
|
|
||||||
showToast(`${t('config.error')}: ${err.message}`)
|
|
||||||
}
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentLang = LANGUAGES.find(l => l.id === language)
|
|
||||||
const currentKbd = layouts.find(l => l.id === keyboard)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="config-profile-center">
|
|
||||||
{toast && <div className="config-toast">{toast}</div>}
|
|
||||||
<div className="config-card">
|
|
||||||
<div className="config-card-row">
|
|
||||||
<span className="config-card-label">{t('config.language')}</span>
|
|
||||||
<span className="config-card-value">{currentLang?.name || language}</span>
|
|
||||||
</div>
|
|
||||||
<div className="config-card-row">
|
|
||||||
<span className="config-card-label">{t('config.keyboardLayout')}</span>
|
|
||||||
<span className="config-card-value">{currentKbd?.name || keyboard}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{editLocale && (
|
|
||||||
<div className="config-card">
|
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.language')}</span>
|
|
||||||
<div className="chip-row">
|
|
||||||
{LANGUAGES.map(lang => (
|
|
||||||
<div
|
|
||||||
key={lang.id}
|
|
||||||
className={`chip ${draftLang === lang.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setDraftLang(lang.id)}
|
|
||||||
>
|
|
||||||
{lang.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.keyboardLayout')}</span>
|
|
||||||
<div className="chip-row">
|
|
||||||
{layouts.map(l => (
|
|
||||||
<div
|
|
||||||
key={l.id}
|
|
||||||
className={`chip ${draftKbd === l.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setDraftKbd(l.id)}
|
|
||||||
>
|
|
||||||
{l.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="config-card">
|
|
||||||
<div className="config-card-actions" style={{ justifyContent: 'center' }}>
|
|
||||||
{editLocale ? (
|
|
||||||
<>
|
|
||||||
<button className="primary sm" onClick={handleSave} disabled={saving}>
|
|
||||||
{saving ? t('config.saving') : t('config.save')}
|
|
||||||
</button>
|
|
||||||
<button className="ghost sm" onClick={() => setEditLocale(false)}>{t('config.cancel')}</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<button className="primary sm" onClick={() => { setDraftLang(language); setDraftKbd(keyboard); setEditLocale(true) }}>{t('config.editProfile')}</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PanelSkills({ skillList, t }) {
|
function PanelSkills({ skillList, t }) {
|
||||||
const [selected, setSelected] = useState(null)
|
const [selected, setSelected] = useState(null)
|
||||||
@@ -634,7 +578,7 @@ function PanelSkills({ skillList, t }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PanelSystem({ api, t }) {
|
function PanelSystem({ api, t }) {
|
||||||
const [resetConfirm, setResetConfirm] = useState(false)
|
const [showResetModal, setShowResetModal] = useState(false)
|
||||||
const [toast, setToast] = useState(null)
|
const [toast, setToast] = useState(null)
|
||||||
|
|
||||||
const showToast = (msg) => {
|
const showToast = (msg) => {
|
||||||
@@ -645,7 +589,7 @@ function PanelSystem({ api, t }) {
|
|||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
try {
|
try {
|
||||||
await api.resetConfig()
|
await api.resetConfig()
|
||||||
setResetConfirm(false)
|
setShowResetModal(false)
|
||||||
showToast(t('config.resetDone'))
|
showToast(t('config.resetDone'))
|
||||||
setTimeout(() => window.location.reload(), 1500)
|
setTimeout(() => window.location.reload(), 1500)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -653,49 +597,66 @@ function PanelSystem({ api, t }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleApplyStarship = async () => {
|
const handleApplyStarship = () => {
|
||||||
try {
|
window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
|
||||||
await api.applyStarshipTheme('charm')
|
window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Vérifie si starship est installé sur le système. S'il ne l'est pas, installe-le (avec curl ou le gestionnaire de paquets). Ensuite, applique la configuration du thème "charm" pour starship. Assure-toi que starship est bien initialisé dans le shell de l'utilisateur.` } }))
|
||||||
showToast(t('config.starshipApplied'))
|
|
||||||
} catch (err) {
|
|
||||||
showToast(`${t('config.error')}: ${err.message}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{toast && <div className="config-toast">{toast}</div>}
|
{toast && <div className="config-toast">{toast}</div>}
|
||||||
|
|
||||||
|
<div className="section-title" style={{ marginBottom: 8 }}>Configuration Système</div>
|
||||||
<div className="config-card">
|
<div className="config-card">
|
||||||
<div className="config-card-row" style={{ marginBottom: 16 }}>
|
<div className="config-card-row" style={{ marginBottom: 16 }}>
|
||||||
<span className="config-card-label" style={{ fontWeight: 600 }}>{t('config.applyStarship')}</span>
|
<span className="config-card-label" style={{ fontWeight: 600 }}>{t('config.applyStarship')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 12 }}>
|
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 12 }}>
|
||||||
{t('config.starshipApplied')}
|
Vérifie l'installation de starship et configure le thème charm via l'IA.
|
||||||
</div>
|
</div>
|
||||||
<button className="sm primary" onClick={handleApplyStarship}>
|
<button className="sm primary" onClick={handleApplyStarship}>
|
||||||
{t('config.applyStarship')}
|
{t('config.applyStarship')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="config-card" style={{ marginTop: 12 }}>
|
|
||||||
|
<div className="section-title" style={{ marginTop: 20, marginBottom: 8, color: 'var(--danger)' }}>
|
||||||
|
<AlertTriangle size={14} style={{ verticalAlign: 'middle', marginRight: 6 }} />
|
||||||
|
Zone Rouge
|
||||||
|
</div>
|
||||||
|
<div className="config-card" style={{ borderColor: 'var(--danger)', borderWidth: 1, borderStyle: 'solid' }}>
|
||||||
<div className="config-card-row" style={{ marginBottom: 16 }}>
|
<div className="config-card-row" style={{ marginBottom: 16 }}>
|
||||||
<span className="config-card-label" style={{ fontWeight: 600 }}>{t('config.resetConfig')}</span>
|
<span className="config-card-label" style={{ fontWeight: 600, color: 'var(--danger)' }}>{t('config.resetConfig')}</span>
|
||||||
</div>
|
</div>
|
||||||
{resetConfirm ? (
|
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 12 }}>
|
||||||
<div>
|
Cette action supprimera toute votre configuration et relancera l'application.
|
||||||
<div style={{ fontSize: 12, color: 'var(--warning)', marginBottom: 12 }}>
|
</div>
|
||||||
{t('config.resetConfirm')}
|
<button className="sm ghost danger" onClick={() => setShowResetModal(true)}>
|
||||||
|
{t('config.resetConfig')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showResetModal && (
|
||||||
|
<div className="shell-modal-overlay" onClick={() => setShowResetModal(false)}>
|
||||||
|
<div className="shell-modal" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="shell-modal-header" style={{ color: 'var(--danger)' }}>
|
||||||
|
<AlertTriangle size={16} style={{ verticalAlign: 'middle', marginRight: 8 }} />
|
||||||
|
{t('config.resetConfig')}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div className="shell-modal-body">
|
||||||
<button className="sm" onClick={() => setResetConfirm(false)}>{t('config.cancel')}</button>
|
<p style={{ color: 'var(--warning)', fontSize: 13, marginBottom: 12 }}>
|
||||||
<button className="sm danger" onClick={handleReset}>{t('config.resetConfig')}</button>
|
{t('config.resetConfirm')}
|
||||||
|
</p>
|
||||||
|
<p style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>
|
||||||
|
Cette action est irréversible. Toute votre configuration (profil, clés API, préférences) sera supprimée.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="shell-modal-footer">
|
||||||
|
<button className="ghost" onClick={() => setShowResetModal(false)}>{t('config.cancel')}</button>
|
||||||
|
<button className="danger" onClick={handleReset}>{t('config.resetConfig')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<button className="sm ghost danger" onClick={() => setResetConfirm(true)}>
|
)}
|
||||||
{t('config.resetConfig')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||||
import { Terminal as XTerm } from '@xterm/xterm'
|
import { Terminal as XTerm } from '@xterm/xterm'
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||||
import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot } from 'lucide-react'
|
import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye } from 'lucide-react'
|
||||||
import '@xterm/xterm/css/xterm.css'
|
import '@xterm/xterm/css/xterm.css'
|
||||||
import { useI18n } from '../i18n'
|
import { useI18n } from '../i18n'
|
||||||
|
|
||||||
const AI_TAB_ID = 0
|
|
||||||
const MAX_TABS = 7
|
const MAX_TABS = 7
|
||||||
const SHELL_MAX_TOKENS = 100000
|
const SHELL_MAX_TOKENS = 100000
|
||||||
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
|
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
|
||||||
|
const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers'
|
||||||
|
|
||||||
function renderContent(text) {
|
function renderContent(text) {
|
||||||
const parts = []
|
const parts = []
|
||||||
@@ -132,7 +132,7 @@ function createTerminal(container, settings = {}) {
|
|||||||
const theme = getTheme(settings.theme || 'default')
|
const theme = getTheme(settings.theme || 'default')
|
||||||
const term = new XTerm({
|
const term = new XTerm({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontSize: settings.fontSize || 14,
|
fontSize: settings.fontSize || 12,
|
||||||
fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||||
theme,
|
theme,
|
||||||
allowTransparency: false,
|
allowTransparency: false,
|
||||||
@@ -201,7 +201,7 @@ export default function Shell({ api }) {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const tabsRef = useRef({})
|
const tabsRef = useRef({})
|
||||||
const nextIdRef = useRef(1)
|
const nextIdRef = useRef(1)
|
||||||
const settingsRef = useRef({ fontSize: 14, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
|
const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
|
||||||
|
|
||||||
const savedTabs = (() => {
|
const savedTabs = (() => {
|
||||||
try {
|
try {
|
||||||
@@ -217,15 +217,13 @@ export default function Shell({ api }) {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
const [tabs, setTabs] = useState(savedTabs || [
|
const [tabs, setTabs] = useState(savedTabs || [
|
||||||
{ id: AI_TAB_ID, name: 'AI Terminal', type: 'ai', shell: '', connected: false, ai: true },
|
|
||||||
{ id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
|
{ id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
|
||||||
])
|
])
|
||||||
const [activeTab, setActiveTab] = useState(() => {
|
const [activeTab, setActiveTab] = useState(() => {
|
||||||
if (savedTabs) {
|
if (savedTabs) {
|
||||||
const aiTab = savedTabs.find(t => t.ai)
|
return savedTabs[0]?.id || 1
|
||||||
return aiTab ? aiTab.id : savedTabs[0].id
|
|
||||||
}
|
}
|
||||||
return AI_TAB_ID
|
return 1
|
||||||
})
|
})
|
||||||
const [sshConnections, setSshConnections] = useState([])
|
const [sshConnections, setSshConnections] = useState([])
|
||||||
const [systemTerminals, setSystemTerminals] = useState([])
|
const [systemTerminals, setSystemTerminals] = useState([])
|
||||||
@@ -234,7 +232,7 @@ export default function Shell({ api }) {
|
|||||||
const [editingTab, setEditingTab] = useState(null)
|
const [editingTab, setEditingTab] = useState(null)
|
||||||
const [editName, setEditName] = useState('')
|
const [editName, setEditName] = useState('')
|
||||||
const [terminalSettings, setTerminalSettings] = useState({
|
const [terminalSettings, setTerminalSettings] = useState({
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
})
|
})
|
||||||
@@ -253,7 +251,6 @@ export default function Shell({ api }) {
|
|||||||
const [analyzing, setAnalyzing] = useState(false)
|
const [analyzing, setAnalyzing] = useState(false)
|
||||||
const [showAnalysis, setShowAnalysis] = useState(false)
|
const [showAnalysis, setShowAnalysis] = useState(false)
|
||||||
const [analysisContent, setAnalysisContent] = useState('')
|
const [analysisContent, setAnalysisContent] = useState('')
|
||||||
const [renderTick, setRenderTick] = useState(0)
|
|
||||||
const aiMessagesRef = useRef(null)
|
const aiMessagesRef = useRef(null)
|
||||||
const aiLoadedRef = useRef(false)
|
const aiLoadedRef = useRef(false)
|
||||||
|
|
||||||
@@ -261,12 +258,6 @@ export default function Shell({ api }) {
|
|||||||
aiMessagesRef.current?.scrollTo(0, aiMessagesRef.current.scrollHeight)
|
aiMessagesRef.current?.scrollTo(0, aiMessagesRef.current.scrollHeight)
|
||||||
}, [aiMessages])
|
}, [aiMessages])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const ms = aiLoading ? 1000 : 5000
|
|
||||||
const iv = setInterval(() => setRenderTick(t => t + 1), ms)
|
|
||||||
return () => clearInterval(iv)
|
|
||||||
}, [aiLoading])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getShellAnalysis?.().then(d => {
|
api.getShellAnalysis?.().then(d => {
|
||||||
if (d?.analysis) setAnalysisContent(d.analysis)
|
if (d?.analysis) setAnalysisContent(d.analysis)
|
||||||
@@ -305,7 +296,7 @@ export default function Shell({ api }) {
|
|||||||
api.getConfig().then(d => {
|
api.getConfig().then(d => {
|
||||||
if (d.terminal) {
|
if (d.terminal) {
|
||||||
setTerminalSettings({
|
setTerminalSettings({
|
||||||
fontSize: d.terminal.font_size || 14,
|
fontSize: d.terminal.font_size || 12,
|
||||||
fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
|
||||||
theme: d.terminal.theme || 'default',
|
theme: d.terminal.theme || 'default',
|
||||||
})
|
})
|
||||||
@@ -346,11 +337,58 @@ export default function Shell({ api }) {
|
|||||||
|
|
||||||
const ws = connectWebSocket(term, fitAddon, initPayload)
|
const ws = connectWebSocket(term, fitAddon, initPayload)
|
||||||
|
|
||||||
|
// Restore saved terminal buffer
|
||||||
|
try {
|
||||||
|
const savedBuffers = JSON.parse(localStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
|
||||||
|
if (savedBuffers[tabId]) {
|
||||||
|
term.write(savedBuffers[tabId])
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const saveBuffer = () => {
|
||||||
|
try {
|
||||||
|
const buf = term.buffer.active
|
||||||
|
const lines = []
|
||||||
|
for (let i = 0; i < buf.length; i++) {
|
||||||
|
const line = buf.getLine(i)
|
||||||
|
if (line) lines.push(line.translateToString(true))
|
||||||
|
}
|
||||||
|
const savedBuffers = JSON.parse(localStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
|
||||||
|
savedBuffers[tabId] = lines.join('\n')
|
||||||
|
localStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufferSaveInterval = setInterval(saveBuffer, 5000)
|
||||||
|
|
||||||
|
// Detect clear command to wipe saved buffer
|
||||||
|
let inputBuffer = ''
|
||||||
|
term.onData((data) => {
|
||||||
|
if (data === '\r') {
|
||||||
|
const cmd = inputBuffer.replace(/[\x1b\x00-\x1f]/g, '').trim().toLowerCase()
|
||||||
|
if (cmd === 'clear') {
|
||||||
|
try {
|
||||||
|
const savedBuffers = JSON.parse(localStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
|
||||||
|
delete savedBuffers[tabId]
|
||||||
|
localStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
inputBuffer = ''
|
||||||
|
} else if (data === '\x7f' || data === '\b') {
|
||||||
|
inputBuffer = inputBuffer.slice(0, -1)
|
||||||
|
} else if (data === '\x03') {
|
||||||
|
inputBuffer = ''
|
||||||
|
} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
||||||
|
inputBuffer += data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: true } : t))
|
setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: true } : t))
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
|
saveBuffer()
|
||||||
setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: false } : t))
|
setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected: false } : t))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +407,7 @@ export default function Shell({ api }) {
|
|||||||
resizeObserver.observe(container)
|
resizeObserver.observe(container)
|
||||||
window.addEventListener('resize', onResize)
|
window.addEventListener('resize', onResize)
|
||||||
|
|
||||||
tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize }
|
tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -444,7 +482,7 @@ export default function Shell({ api }) {
|
|||||||
if (tabs.length >= MAX_TABS) return
|
if (tabs.length >= MAX_TABS) return
|
||||||
const id = nextIdRef.current++
|
const id = nextIdRef.current++
|
||||||
const newTab = { id, name: name || `${t('shell.localShell')} ${tabs.length}`, type: 'local', shell: shell || '', connected: false }
|
const newTab = { id, name: name || `${t('shell.localShell')} ${tabs.length}`, type: 'local', shell: shell || '', connected: false }
|
||||||
setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, ai: t.ai || false, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
|
setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
|
||||||
setActiveTab(id)
|
setActiveTab(id)
|
||||||
setShowMenu(false)
|
setShowMenu(false)
|
||||||
}
|
}
|
||||||
@@ -462,7 +500,7 @@ export default function Shell({ api }) {
|
|||||||
key_path: conn.key_path || '',
|
key_path: conn.key_path || '',
|
||||||
connected: false,
|
connected: false,
|
||||||
}
|
}
|
||||||
setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, ai: t.ai || false, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
|
setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
|
||||||
setActiveTab(id)
|
setActiveTab(id)
|
||||||
setShowMenu(false)
|
setShowMenu(false)
|
||||||
}
|
}
|
||||||
@@ -470,10 +508,12 @@ export default function Shell({ api }) {
|
|||||||
const closeTab = (tabId, e) => {
|
const closeTab = (tabId, e) => {
|
||||||
if (e) e.stopPropagation()
|
if (e) e.stopPropagation()
|
||||||
const tab = tabs.find(t => t.id === tabId)
|
const tab = tabs.find(t => t.id === tabId)
|
||||||
if (!tab || tab.ai || tabs.length <= 1) return
|
if (!tab || tabs.length <= 1) return
|
||||||
|
|
||||||
if (tabsRef.current[tabId]) {
|
if (tabsRef.current[tabId]) {
|
||||||
const { ws, resizeObserver, onResize, term } = tabsRef.current[tabId]
|
const { ws, resizeObserver, onResize, term, bufferSaveInterval, saveBuffer } = tabsRef.current[tabId]
|
||||||
|
if (saveBuffer) saveBuffer()
|
||||||
|
if (bufferSaveInterval) clearInterval(bufferSaveInterval)
|
||||||
window.removeEventListener('resize', onResize)
|
window.removeEventListener('resize', onResize)
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect()
|
||||||
ws.close()
|
ws.close()
|
||||||
@@ -527,19 +567,16 @@ export default function Shell({ api }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendToTerminal = useCallback((code) => {
|
const sendToTerminal = useCallback((code) => {
|
||||||
const aiEntry = tabsRef.current[AI_TAB_ID]
|
const entry = tabsRef.current[activeTab]
|
||||||
if (aiEntry?.ws && aiEntry.ws.readyState === WebSocket.OPEN) {
|
if (entry?.ws && entry.ws.readyState === WebSocket.OPEN) {
|
||||||
aiEntry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
|
entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
|
||||||
}
|
}
|
||||||
}, [])
|
}, [activeTab])
|
||||||
|
|
||||||
const focusAiTerminal = useCallback(() => {
|
const focusAiTerminal = useCallback(() => {
|
||||||
setActiveTab(AI_TAB_ID)
|
const entry = tabsRef.current[activeTab]
|
||||||
setTimeout(() => {
|
if (entry) entry.term.focus()
|
||||||
const entry = tabsRef.current[AI_TAB_ID]
|
}, [activeTab])
|
||||||
if (entry) entry.term.focus()
|
|
||||||
}, 150)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleAiSend = async () => {
|
const handleAiSend = async () => {
|
||||||
if (!aiInput.trim() || aiLoading || aiAtLimit) return
|
if (!aiInput.trim() || aiLoading || aiAtLimit) return
|
||||||
@@ -596,21 +633,7 @@ export default function Shell({ api }) {
|
|||||||
setAiLoading(false)
|
setAiLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const handleAiSendDirect = useCallback(async (text) => {
|
||||||
const handler = (e) => {
|
|
||||||
const msg = e.detail?.message
|
|
||||||
if (!msg) return
|
|
||||||
setAiInput(msg)
|
|
||||||
setActiveTab(AI_TAB_ID)
|
|
||||||
setTimeout(() => {
|
|
||||||
handleAiSendDirect(msg)
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
window.addEventListener('ask-ai-terminal', handler)
|
|
||||||
return () => window.removeEventListener('ask-ai-terminal', handler)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleAiSendDirect = async (text) => {
|
|
||||||
if (!text || aiLoading || aiAtLimit) return
|
if (!text || aiLoading || aiAtLimit) return
|
||||||
setAiInput('')
|
setAiInput('')
|
||||||
|
|
||||||
@@ -652,7 +675,20 @@ export default function Shell({ api }) {
|
|||||||
setAiMessages(prev => [...prev.filter(m => !m._streaming), { role: 'assistant', content: `Erreur: ${err.message}` }])
|
setAiMessages(prev => [...prev.filter(m => !m._streaming), { role: 'assistant', content: `Erreur: ${err.message}` }])
|
||||||
}
|
}
|
||||||
setAiLoading(false)
|
setAiLoading(false)
|
||||||
}
|
}, [api, t, aiLoading, aiAtLimit])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e) => {
|
||||||
|
const msg = e.detail?.message
|
||||||
|
if (!msg) return
|
||||||
|
setAiInput(msg)
|
||||||
|
setTimeout(() => {
|
||||||
|
handleAiSendDirect(msg)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
window.addEventListener('ask-ai-terminal', handler)
|
||||||
|
return () => window.removeEventListener('ask-ai-terminal', handler)
|
||||||
|
}, [handleAiSendDirect])
|
||||||
|
|
||||||
const handleAnalyze = async () => {
|
const handleAnalyze = async () => {
|
||||||
setAnalyzing(true)
|
setAnalyzing(true)
|
||||||
@@ -681,14 +717,13 @@ export default function Shell({ api }) {
|
|||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`shell-tab ${activeTab === tab.id ? 'active' : ''} ${tab.ai ? 'ai-tab' : ''}`}
|
className={`shell-tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
onDoubleClick={(e) => !tab.ai && startRename(tab.id, e)}
|
onDoubleClick={(e) => startRename(tab.id, e)}
|
||||||
>
|
>
|
||||||
<span className={`connection-dot ${tab.connected ? 'on' : 'off'}`} />
|
<span className={`connection-dot ${tab.connected ? 'on' : 'off'}`} />
|
||||||
{tab.ai && <Bot size={12} />}
|
{tab.type === 'ssh' && <Globe size={12} />}
|
||||||
{!tab.ai && tab.type === 'ssh' && <Globe size={12} />}
|
{tab.type === 'local' && <Monitor size={12} />}
|
||||||
{!tab.ai && tab.type === 'local' && <Monitor size={12} />}
|
|
||||||
{editingTab === tab.id ? (
|
{editingTab === tab.id ? (
|
||||||
<input
|
<input
|
||||||
className="shell-tab-rename"
|
className="shell-tab-rename"
|
||||||
@@ -703,7 +738,7 @@ export default function Shell({ api }) {
|
|||||||
<span className="shell-tab-name">{tab.name}</span>
|
<span className="shell-tab-name">{tab.name}</span>
|
||||||
)}
|
)}
|
||||||
<span className="shell-tab-index">{i + 1}</span>
|
<span className="shell-tab-index">{i + 1}</span>
|
||||||
{!tab.ai && tabs.length > 1 && (
|
{tabs.length > 1 && (
|
||||||
<button
|
<button
|
||||||
className="shell-tab-close"
|
className="shell-tab-close"
|
||||||
onClick={(e) => closeTab(tab.id, e)}
|
onClick={(e) => closeTab(tab.id, e)}
|
||||||
@@ -823,7 +858,7 @@ export default function Shell({ api }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="ai-panel-messages" ref={aiMessagesRef}>
|
<div className="ai-panel-messages" ref={aiMessagesRef}>
|
||||||
{aiMessages.map((msg, i) => (
|
{aiMessages.map((msg, i) => (
|
||||||
<ShellAIMessage key={`${i}-${renderTick}`} msg={msg} sendToTerminal={sendToTerminal} renderTick={renderTick} />
|
<ShellAIMessage key={i} msg={msg} sendToTerminal={sendToTerminal} />
|
||||||
))}
|
))}
|
||||||
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
||||||
</div>
|
</div>
|
||||||
@@ -915,7 +950,7 @@ export default function Shell({ api }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShellAIMessage({ msg, sendToTerminal, renderTick }) {
|
function ShellAIMessage({ msg, sendToTerminal }) {
|
||||||
const role = msg.role === 'user' ? 'user' : msg.role === 'system' ? 'system' : 'assistant'
|
const role = msg.role === 'user' ? 'user' : msg.role === 'system' ? 'system' : 'assistant'
|
||||||
const content = msg.content || ''
|
const content = msg.content || ''
|
||||||
|
|
||||||
@@ -934,7 +969,7 @@ function ShellAIMessage({ msg, sendToTerminal, renderTick }) {
|
|||||||
{parts.map((part, i) => {
|
{parts.map((part, i) => {
|
||||||
if (part.type === 'code') {
|
if (part.type === 'code') {
|
||||||
return (
|
return (
|
||||||
<div key={`${i}-${renderTick}`} className="shell-code-block">
|
<div key={i} className="shell-code-block">
|
||||||
{part.lang && <div className="shell-code-lang">{part.lang}</div>}
|
{part.lang && <div className="shell-code-lang">{part.lang}</div>}
|
||||||
<pre><code>{part.content}</code></pre>
|
<pre><code>{part.content}</code></pre>
|
||||||
<div className="shell-code-actions">
|
<div className="shell-code-actions">
|
||||||
@@ -948,7 +983,7 @@ function ShellAIMessage({ msg, sendToTerminal, renderTick }) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <span key={`${i}-${renderTick}`} dangerouslySetInnerHTML={{ __html: formatText(part.content) }} />
|
return <span key={i} dangerouslySetInnerHTML={{ __html: formatText(part.content) }} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -309,7 +309,6 @@ export default function Studio({ api }) {
|
|||||||
const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 })
|
const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 })
|
||||||
const [contextCollapsed, setContextCollapsed] = useState(false)
|
const [contextCollapsed, setContextCollapsed] = useState(false)
|
||||||
const [messagesCollapsed, setMessagesCollapsed] = useState(false)
|
const [messagesCollapsed, setMessagesCollapsed] = useState(false)
|
||||||
const [renderTick, setRenderTick] = useState(0)
|
|
||||||
const messagesEnd = useRef(null)
|
const messagesEnd = useRef(null)
|
||||||
const feedRef = useRef(null)
|
const feedRef = useRef(null)
|
||||||
const textareaRef = useRef(null)
|
const textareaRef = useRef(null)
|
||||||
@@ -342,12 +341,6 @@ export default function Studio({ api }) {
|
|||||||
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' })
|
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
}, [messages, streaming, streamThinking, streamToolCalls])
|
}, [messages, streaming, streamThinking, streamToolCalls])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const ms = loading ? 1000 : 5000
|
|
||||||
const iv = setInterval(() => setRenderTick(t => t + 1), ms)
|
|
||||||
return () => clearInterval(iv)
|
|
||||||
}, [loading])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onTab = (e) => {
|
const onTab = (e) => {
|
||||||
if (e.key !== 'Tab') return
|
if (e.key !== 'Tab') return
|
||||||
@@ -648,7 +641,7 @@ export default function Studio({ api }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{messages.slice(0, visibleCount).map(msg => (
|
{messages.slice(0, visibleCount).map(msg => (
|
||||||
<FeedItem key={`${msg.id}-${renderTick}`} msg={msg} />
|
<FeedItem key={msg.id} msg={msg} />
|
||||||
))}
|
))}
|
||||||
<div className="feed-collapsed-messages" onClick={handleToggleCollapsed}>
|
<div className="feed-collapsed-messages" onClick={handleToggleCollapsed}>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
@@ -661,7 +654,7 @@ export default function Studio({ api }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return messages.map(msg => (
|
return messages.map(msg => (
|
||||||
<FeedItem key={`${msg.id}-${renderTick}`} msg={msg} />
|
<FeedItem key={msg.id} msg={msg} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user