diff --git a/internal/api/handlers_info.go b/internal/api/handlers_info.go index 35aea93..4e3190c 100644 --- a/internal/api/handlers_info.go +++ b/internal/api/handlers_info.go @@ -477,25 +477,16 @@ func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) { } } case "zai": - if p.APIKey == "" { - q.Error = "no API key" - results = append(results, q) - continue - } - req, _ := http.NewRequest("GET", "https://api.z.ai/api/monitor/usage/quota/limit", nil) - req.Header.Set("Authorization", "Bearer "+p.APIKey) - req.Header.Set("Accept", "application/json") - resp, err := client.Do(req) - if err != nil { - q.Error = err.Error() + // Z.AI (GLM) est utilisé uniquement via Crush, pas de quota check externe + q.Healthy = true + q.Data = map[string]interface{}{"note": "crush-only"} + case "claude", "anthropic": + // Claude Code n'a pas d'API externe, vérifier l'installation + claudePath := "/usr/bin/claude" + if _, err := os.Stat(claudePath); err == nil { + q.Healthy = true } else { - body, _ := io.ReadAll(resp.Body) - resp.Body.Close() - var data map[string]interface{} - if json.Unmarshal(body, &data) == nil { - q.Data = data - q.Healthy = true - } + q.Error = "claude code not installed" } default: q.Error = "quota not supported" diff --git a/web/src/components/Config.jsx b/web/src/components/Config.jsx index a835888..4e99746 100644 --- a/web/src/components/Config.jsx +++ b/web/src/components/Config.jsx @@ -353,8 +353,7 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
{p.name} - {p.apiKey && {t('config.keyConfigured')}} - {!p.apiKey && {t('config.noKey')}} + {p.active && active} {isValidationTarget && validationStatus?.valid && {t('config.keyValid')}} {isValidationTarget && !validationStatus?.valid && {validationStatus?.error}}
diff --git a/web/src/components/Studio.jsx b/web/src/components/Studio.jsx index 63cff2a..eed1e38 100644 --- a/web/src/components/Studio.jsx +++ b/web/src/components/Studio.jsx @@ -285,6 +285,8 @@ export default function Studio({ api }) { const [streamToolCalls, setStreamToolCalls] = useState([]) const [loaded, setLoaded] = useState(false) const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 }) + const [contextCollapsed, setContextCollapsed] = useState(false) + const [messagesCollapsed, setMessagesCollapsed] = useState(false) const messagesEnd = useRef(null) const textareaRef = useRef(null) const abortRef = useRef(null) @@ -336,12 +338,18 @@ export default function Studio({ api }) { const handleSummarize = useCallback(async () => { setMessages(prev => [...prev, { id: Date.now().toString(), role: 'system', content: 'Résumé de la conversation en cours...', time: new Date().toISOString() }]) + setContextCollapsed('animating') try { const data = await api.summarizeChat() setTokenInfo(prev => ({ ...prev, used: data.tokens || 0 })) - setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: '✓ Conversation résumée automatiquement. Le contexte a été compressé.', time: new Date().toISOString() }]) + setTimeout(() => { + setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: '✓ Conversation résumée automatiquement. Le contexte a été compressé.', time: new Date().toISOString(), compressed: true }]) + setContextCollapsed(true) + setMessagesCollapsed(true) + }, 600) } catch (err) { setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: `Erreur de résumé: ${err.message}`, time: new Date().toISOString() }]) + setContextCollapsed(false) } }, [api]) @@ -396,7 +404,7 @@ export default function Studio({ api }) { if (text === '/model') { api.getProviders().then(data => { const active = data.providers?.find(p => p.active) - const modelMsg = active ? `Provider: ${active.name}\nModèle: ${active.model}` : 'Aucun provider actif configuré' + const modelMsg = active ? active.name : 'Aucun provider actif configuré' setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: modelMsg, time: new Date().toISOString() }]) }).catch(() => { setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'Erreur: impossible de récupérer les providers', time: new Date().toISOString() }]) @@ -525,6 +533,34 @@ export default function Studio({ api }) { } } + const handleToggleCollapsed = useCallback(() => { + setMessagesCollapsed(prev => !prev) + }, []) + + const renderMessages = () => { + if (messagesCollapsed && messages.length > 4) { + const visibleCount = 4 + const hiddenCount = messages.length - visibleCount + return ( + <> + {messages.slice(0, visibleCount).map(msg => ( + + ))} +
+ + + + {hiddenCount} messages antérieurs compressés + clic pour développer +
+ + ) + } + return messages.map(msg => ( + + )) + } + if (!loaded) { return (
@@ -540,27 +576,31 @@ export default function Studio({ api }) { return (
- {messages.map(msg => ( - - ))} + {renderMessages()} {(streaming || streamThinking || loading || streamToolCalls.length > 0) && ( )} -
+
-
-
+
+
= tokenInfo.summarizeAt ? 'warn' : ''}`} + className={`studio-token-fill ${tokenInfo.used >= tokenInfo.summarizeAt ? 'warn' : ''} ${contextCollapsed === true ? 'compressed' : ''} ${contextCollapsed === 'animating' ? 'animating' : ''}`} style={{ width: `${Math.min(100, (tokenInfo.used / tokenInfo.max) * 100)}%` }} />
- + {(tokenInfo.used / 1000).toFixed(1)}k / {(tokenInfo.max / 1000).toFixed(0)}k tokens - {tokenInfo.used >= tokenInfo.summarizeAt && ' · résumé automatique déclenché'} + {contextCollapsed === true && ' · compressé'} + {tokenInfo.used >= tokenInfo.summarizeAt && contextCollapsed !== true && ' · résumé auto.'} + {contextCollapsed === true && ( + + )}