feat(studio): improve context compression UI and provider display
All checks were successful
Beta Release / beta (push) Successful in 45s
All checks were successful
Beta Release / beta (push) Successful in 45s
- Add visual indicator when messages are collapsed (folder icon)
- Add animation to token bar during compression (pulse effect)
- Token bar becomes more compact after compression with "· compressé" label
- Button "voir plus" to expand collapsed messages
- Add 24px spacing at end of feed to avoid last message clipping
- Simplify provider display: show name only, badge "active" instead of key status
- Dashboard: show provider name only without model suffix
- Studio /model: show just provider name, not model
- Z.AI (GLM): mark as crush-only, no external quota check
- Claude: check /usr/bin/claude installation instead of API
💘 Generated with Crush
This commit is contained in:
@@ -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 => (
|
||||
<FeedItem key={msg.id} msg={msg} />
|
||||
))}
|
||||
<div className="feed-collapsed-messages" onClick={handleToggleCollapsed}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
<span className="feed-collapsed-text">{hiddenCount} messages antérieurs compressés</span>
|
||||
<span className="feed-collapsed-count">clic pour développer</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return messages.map(msg => (
|
||||
<FeedItem key={msg.id} msg={msg} />
|
||||
))
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
return (
|
||||
<div className="studio-feed-layout">
|
||||
@@ -540,27 +576,31 @@ export default function Studio({ api }) {
|
||||
return (
|
||||
<div className="studio-feed-layout">
|
||||
<div className="studio-feed">
|
||||
{messages.map(msg => (
|
||||
<FeedItem key={msg.id} msg={msg} />
|
||||
))}
|
||||
{renderMessages()}
|
||||
{(streaming || streamThinking || loading || streamToolCalls.length > 0) && (
|
||||
<StreamingItem content={streaming} thinking={streamThinking} toolCalls={streamToolCalls} />
|
||||
)}
|
||||
<div ref={messagesEnd} />
|
||||
<div ref={messagesEnd} style={{ height: '24px' }} />
|
||||
</div>
|
||||
|
||||
<div className="studio-input-area">
|
||||
<div className="studio-token-bar">
|
||||
<div className="studio-token-track">
|
||||
<div className={`studio-token-bar ${contextCollapsed === true ? 'compressed' : ''}`}>
|
||||
<div className={`studio-token-track ${contextCollapsed === true ? 'compressed' : ''}`}>
|
||||
<div
|
||||
className={`studio-token-fill ${tokenInfo.used >= 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)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="studio-token-text">
|
||||
<span className={`studio-token-text ${contextCollapsed === true ? 'compressed' : ''}`}>
|
||||
{(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.'}
|
||||
</span>
|
||||
{contextCollapsed === true && (
|
||||
<button className="ghost sm" onClick={handleToggleCollapsed} style={{ marginLeft: '8px', fontSize: '10px' }}>
|
||||
voir plus
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="studio-input-row">
|
||||
<textarea
|
||||
|
||||
Reference in New Issue
Block a user