The ThinkingBlock component existed but was dead code — the backend silently discarded all <think chunks. Now emits thinking SSE events so the UI can display AI reflections in real-time. \xe2\x98\x85 Generated with Crush Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
85 lines
3.7 KiB
JavaScript
85 lines
3.7 KiB
JavaScript
const API_BASE = '/api'
|
|
|
|
async function request(path, options = {}) {
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
...options,
|
|
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
|
})
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: res.statusText }))
|
|
throw new Error(err.error || res.statusText)
|
|
}
|
|
return res.json()
|
|
}
|
|
|
|
const api = {
|
|
getInfo: () => request('/info'),
|
|
getSystem: () => request('/system'),
|
|
getTools: () => request('/tools'),
|
|
getConfig: () => request('/config'),
|
|
getProviders: () => request('/providers'),
|
|
getSkills: () => request('/skills'),
|
|
getLSP: () => request('/lsp'),
|
|
getMCP: () => request('/mcp'),
|
|
getUpdates: () => request('/updates'),
|
|
runScan: () => request('/scan', { method: 'POST' }),
|
|
installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }),
|
|
configureMCP: () => request('/mcp/configure', { method: 'POST' }),
|
|
savePreferences: (prefs) => request('/preferences', { method: 'PUT', body: JSON.stringify(prefs) }),
|
|
saveProfile: (profile) => request('/config/profile', { method: 'PUT', body: JSON.stringify(profile) }),
|
|
saveProvider: (provider) => request('/config/provider', { method: 'PUT', body: JSON.stringify(provider) }),
|
|
validateProvider: (provider) => request('/providers/validate', { method: 'POST', body: JSON.stringify(provider) }),
|
|
runUpdate: (tool) => request('/update/run', { method: 'POST', body: JSON.stringify({ tool: tool || '' }) }),
|
|
runCommand: (command, cwd) => request('/terminal', { method: 'POST', body: JSON.stringify({ command, cwd }) }),
|
|
getTerminalSessions: () => request('/terminal/sessions'),
|
|
addSSHConnection: (conn) => request('/terminal/sessions', { method: 'POST', body: JSON.stringify(conn) }),
|
|
deleteSSHConnection: (name) => request(`/terminal/sessions/${encodeURIComponent(name)}`, { method: 'DELETE' }),
|
|
getTerminalThemes: () => request('/terminal/themes'),
|
|
saveTerminalSettings: (settings) => request('/terminal/settings', { method: 'PUT', body: JSON.stringify(settings) }),
|
|
getChatHistory: () => request('/chat/history'),
|
|
clearChat: () => request('/chat/clear', { method: 'POST' }),
|
|
sendChat: (message, stream = true, onChunk) => {
|
|
if (!stream) {
|
|
return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false }) })
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
fetch(`${API_BASE}/chat`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ message, stream: true }),
|
|
}).then(async (res) => {
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: res.statusText }))
|
|
reject(new Error(err.error || res.statusText))
|
|
return
|
|
}
|
|
const reader = res.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let full = ''
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
const text = decoder.decode(value, { stream: true })
|
|
for (const line of text.split('\n')) {
|
|
if (!line.startsWith('data: ')) continue
|
|
try {
|
|
const data = JSON.parse(line.slice(6))
|
|
if (data.error) { reject(new Error(data.error)); return }
|
|
if (data.done) { resolve(full); return }
|
|
if (data.content) {
|
|
full += data.content
|
|
if (onChunk) onChunk(full, data)
|
|
} else if (data.thinking !== undefined || data.thinking_end) {
|
|
if (onChunk) onChunk(full, data)
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
resolve(full)
|
|
}).catch(reject)
|
|
})
|
|
},
|
|
}
|
|
|
|
export default api
|