feat(chat): add auto-summarization with token tracking UI
All checks were successful
Beta Release / beta (push) Successful in 47s
All checks were successful
Beta Release / beta (push) Successful in 47s
Add /summarize command, token usage bar, and summary endpoint. Add JSON tags to config/platform/scanner structs for API serialization. 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
@@ -56,6 +56,7 @@ const api = {
|
||||
saveTerminalSettings: (settings) => request('/terminal/settings', { method: 'PUT', body: JSON.stringify(settings) }),
|
||||
getChatHistory: () => request('/chat/history'),
|
||||
clearChat: () => request('/chat/clear', { method: 'POST' }),
|
||||
summarizeChat: () => request('/chat/summarize', { method: 'POST' }),
|
||||
sendChat: (message, stream = true, onChunk, signal) => {
|
||||
if (!stream) {
|
||||
return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false }) })
|
||||
|
||||
@@ -284,6 +284,7 @@ export default function Studio({ api }) {
|
||||
const [streamThinking, setStreamThinking] = useState('')
|
||||
const [streamToolCalls, setStreamToolCalls] = useState([])
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 })
|
||||
const messagesEnd = useRef(null)
|
||||
const textareaRef = useRef(null)
|
||||
const abortRef = useRef(null)
|
||||
@@ -297,6 +298,11 @@ export default function Studio({ api }) {
|
||||
{ id: 'welcome', role: 'system', content: t('studio.welcomeNew'), time: new Date().toISOString() },
|
||||
])
|
||||
}
|
||||
setTokenInfo({
|
||||
used: data.tokens || 0,
|
||||
max: data.max_tokens || 100000,
|
||||
summarizeAt: data.summarize_at || 80000,
|
||||
})
|
||||
setLoaded(true)
|
||||
}).catch(() => {
|
||||
setMessages([
|
||||
@@ -317,6 +323,28 @@ export default function Studio({ api }) {
|
||||
}
|
||||
}, [input])
|
||||
|
||||
const refreshTokens = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.getChatHistory()
|
||||
setTokenInfo({
|
||||
used: data.tokens || 0,
|
||||
max: data.max_tokens || 100000,
|
||||
summarizeAt: data.summarize_at || 80000,
|
||||
})
|
||||
} catch {}
|
||||
}, [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() }])
|
||||
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() }])
|
||||
} catch (err) {
|
||||
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: `Erreur de résumé: ${err.message}`, time: new Date().toISOString() }])
|
||||
}
|
||||
}, [api])
|
||||
|
||||
const handleClear = useCallback(async () => {
|
||||
try {
|
||||
await api.clearChat()
|
||||
@@ -341,6 +369,7 @@ export default function Studio({ api }) {
|
||||
'## Commandes Studio',
|
||||
'',
|
||||
'- `/clear` - Effacer la conversation',
|
||||
'- `/summarize` - Résumer la conversation précédente',
|
||||
'- `/help` - Afficher cette aide',
|
||||
'- `/plan <objectif>` - Demander un plan structuré',
|
||||
'- `/export` - Exporter la conversation en Markdown',
|
||||
@@ -359,6 +388,11 @@ export default function Studio({ api }) {
|
||||
return
|
||||
}
|
||||
|
||||
if (text === '/summarize') {
|
||||
handleSummarize()
|
||||
return
|
||||
}
|
||||
|
||||
if (text === '/model') {
|
||||
api.getProviders().then(data => {
|
||||
const active = data.providers?.find(p => p.active)
|
||||
@@ -474,8 +508,9 @@ export default function Studio({ api }) {
|
||||
setStreamThinking('')
|
||||
setStreamToolCalls([])
|
||||
abortRef.current = null
|
||||
refreshTokens()
|
||||
}
|
||||
}, [input, loading, api, t, handleClear, streaming])
|
||||
}, [input, loading, api, t, handleClear, streaming, refreshTokens, handleSummarize])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
if (abortRef.current) {
|
||||
@@ -515,6 +550,18 @@ export default function Studio({ api }) {
|
||||
</div>
|
||||
|
||||
<div className="studio-input-area">
|
||||
<div className="studio-token-bar">
|
||||
<div className="studio-token-track">
|
||||
<div
|
||||
className={`studio-token-fill ${tokenInfo.used >= tokenInfo.summarizeAt ? 'warn' : ''}`}
|
||||
style={{ width: `${Math.min(100, (tokenInfo.used / tokenInfo.max) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="studio-token-text">
|
||||
{(tokenInfo.used / 1000).toFixed(1)}k / {(tokenInfo.max / 1000).toFixed(0)}k tokens
|
||||
{tokenInfo.used >= tokenInfo.summarizeAt && ' · résumé automatique déclenché'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="studio-input-row">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
@@ -543,7 +590,7 @@ export default function Studio({ api }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="studio-input-hint">
|
||||
{t('studio.inputHint')} · /clear /help /plan /export /model
|
||||
{t('studio.inputHint')} · /clear /summarize /help /plan /export /model
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -796,6 +796,11 @@ input::placeholder { color: var(--text-disabled); }
|
||||
.studio-thinking span:nth-child(3) { animation-delay: 0.3s; }
|
||||
@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; } 40% { transform: translateY(-6px); opacity: 1; } }
|
||||
.studio-input-area { padding: 12px 20px 8px; border-top: 1px solid var(--border); background: var(--bg-surface); }
|
||||
.studio-token-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.studio-token-track { flex: 1; height: 3px; background: var(--bg-input); border-radius: 2px; overflow: hidden; }
|
||||
.studio-token-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.4s, background 0.3s; }
|
||||
.studio-token-fill.warn { background: var(--warning); }
|
||||
.studio-token-text { font-size: 10px; font-family: var(--font-mono); color: var(--text-tertiary); white-space: nowrap; }
|
||||
.studio-input-row { display: flex; gap: 8px; align-items: flex-end; }
|
||||
.studio-input-row textarea {
|
||||
flex: 1; resize: none; min-height: 42px; max-height: 200px; padding: 10px 14px;
|
||||
|
||||
Reference in New Issue
Block a user