feat(minecraft): add debug web dashboard
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { useLogger } from './utils/logger'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
export class DebugServer {
|
||||
private static instance: DebugServer
|
||||
private clients: Set<ServerResponse> = new Set()
|
||||
private server: http.Server | null = null
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): DebugServer {
|
||||
if (!DebugServer.instance) {
|
||||
DebugServer.instance = new DebugServer()
|
||||
}
|
||||
return DebugServer.instance
|
||||
}
|
||||
|
||||
public start(port = 3000): void {
|
||||
if (this.server)
|
||||
return
|
||||
|
||||
this.server = http.createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||
// Enable CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
||||
|
||||
if (req.url === '/') {
|
||||
// Serve dashboard
|
||||
const htmlPath = path.join(__dirname, 'web', 'dashboard.html')
|
||||
const html = fs.readFileSync(htmlPath, 'utf-8')
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(html)
|
||||
}
|
||||
else if (req.url === '/events') {
|
||||
// Serve SSE stream
|
||||
this.handleSSE(req, res)
|
||||
}
|
||||
else {
|
||||
res.writeHead(404)
|
||||
res.end('Not Found')
|
||||
}
|
||||
})
|
||||
|
||||
this.server.listen(port, () => {
|
||||
useLogger().log(`Debug server running at http://localhost:${port}`)
|
||||
})
|
||||
}
|
||||
|
||||
public broadcast(type: string, payload: any): void {
|
||||
const data = JSON.stringify(payload)
|
||||
const message = `event: ${type}\ndata: ${data}\n\n`
|
||||
|
||||
for (const client of this.clients) {
|
||||
client.write(message)
|
||||
}
|
||||
}
|
||||
|
||||
private handleSSE(req: IncomingMessage, res: ServerResponse): void {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
})
|
||||
|
||||
this.clients.add(res)
|
||||
|
||||
const keepAlive = setInterval(() => {
|
||||
res.write(': keep-alive\n\n')
|
||||
}, 15000)
|
||||
|
||||
req.on('close', () => {
|
||||
clearInterval(keepAlive)
|
||||
this.clients.delete(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type { MineflayerWithAgents } from './types'
|
||||
import { assistant } from 'neuri/openai'
|
||||
|
||||
import { config } from '../../composables/config'
|
||||
import { DebugServer } from '../../debug-server'
|
||||
|
||||
export async function handleLLMCompletion(context: NeuriContext, bot: MineflayerWithAgents, logger: Logger): Promise<string> {
|
||||
logger.log('rerouting...')
|
||||
@@ -24,6 +25,15 @@ export async function handleLLMCompletion(context: NeuriContext, bot: Mineflayer
|
||||
const content = await completion.firstContent()
|
||||
logger.withFields({ usage: completion.usage, content }).log('output')
|
||||
|
||||
// Broadcast LLM trace
|
||||
DebugServer.getInstance().broadcast('llm', {
|
||||
route: 'action',
|
||||
messages: context.messages,
|
||||
content,
|
||||
usage: completion.usage,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
bot.memory.chatHistory.push(assistant(content))
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { LLMConfig, LLMResponse } from './types'
|
||||
import { withRetry } from '@moeru/std'
|
||||
|
||||
import { config } from '../../composables/config'
|
||||
import { DebugServer } from '../../debug-server'
|
||||
import { useLogger } from '../../utils/logger'
|
||||
|
||||
export abstract class BaseLLMHandler {
|
||||
@@ -33,6 +34,15 @@ export abstract class BaseLLMHandler {
|
||||
const content = await completion.firstContent()
|
||||
this.logger.withFields({ usage: completion.usage, content }).log('Generated content')
|
||||
|
||||
// Broadcast LLM trace
|
||||
DebugServer.getInstance().broadcast('llm', {
|
||||
route,
|
||||
messages,
|
||||
content,
|
||||
usage: completion.usage,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
return {
|
||||
content,
|
||||
usage: completion.usage,
|
||||
|
||||
@@ -12,14 +12,20 @@ import { plugin as MineflayerTool } from 'mineflayer-tool'
|
||||
import { initBot } from './composables/bot'
|
||||
import { config, initEnv } from './composables/config'
|
||||
import { createNeuriAgent } from './composables/neuri'
|
||||
import { DebugServer } from './debug-server'
|
||||
import { LLMAgent } from './libs/llm-agent'
|
||||
import { wrapPlugin } from './libs/mineflayer'
|
||||
import { initLogger, useLogger } from './utils/logger'
|
||||
|
||||
// ...
|
||||
|
||||
async function main() {
|
||||
initLogger() // todo: save logs to file
|
||||
initEnv()
|
||||
|
||||
// Start debug server
|
||||
DebugServer.getInstance().start()
|
||||
|
||||
const { bot } = await initBot({
|
||||
botConfig: config.bot,
|
||||
plugins: [
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg'
|
||||
|
||||
import { DebugServer } from '../debug-server'
|
||||
|
||||
export type Logger = ReturnType<typeof useLogg>
|
||||
|
||||
export function initLogger() {
|
||||
setGlobalLogLevel(LogLevel.Debug)
|
||||
setGlobalFormat(Format.Pretty)
|
||||
@@ -21,5 +25,91 @@ export function useLogger() {
|
||||
const dirName = match?.[1] || 'unknown'
|
||||
const fileName = match?.[2] || 'unknown'
|
||||
|
||||
return useLogg(`${dirName}/${fileName}`).useGlobalConfig()
|
||||
const logger = useLogg(`${dirName}/${fileName}`).useGlobalConfig()
|
||||
|
||||
// Proxy logger to broadcast events
|
||||
// We need to preserve the original formatting/behavior while adding the side-effect
|
||||
return {
|
||||
log: (message: string, ...args: any[]) => {
|
||||
logger.log(message, ...args)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'INFO',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
timestamp: Date.now(),
|
||||
fields: (logger as any).fields, // Access fields if stored on instance, currently Logg API might differ so we simplify
|
||||
})
|
||||
},
|
||||
error: (message: string, ...args: any[]) => {
|
||||
logger.error(message, ...args)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'ERROR',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
},
|
||||
warn: (message: string, ...args: any[]) => {
|
||||
logger.warn(message, ...args)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'WARN',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
},
|
||||
withFields: (fields: Record<string, any>) => {
|
||||
const subLogger = logger.withFields(fields)
|
||||
// Return proxied sub-logger
|
||||
return {
|
||||
log: (message: string) => {
|
||||
subLogger.log(message)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'INFO',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
},
|
||||
error: (message: string) => {
|
||||
subLogger.error(message)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'ERROR',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
},
|
||||
warn: (message: string) => {
|
||||
subLogger.warn(message)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'WARN',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
},
|
||||
errorWithError: (message: string, error: unknown) => {
|
||||
subLogger.errorWithError(message, error)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'ERROR',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields: { ...fields, error },
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
},
|
||||
withFields: (newFields: Record<string, any>) => useLogger().withFields({ ...fields, ...newFields }), // Recursion hack for simplicity, ideally properly implement interface
|
||||
withError: (err: unknown) => useLogger().withFields({ ...fields, error: err }), // Recursion hack
|
||||
}
|
||||
},
|
||||
withError: (error: unknown) => {
|
||||
return useLogger().withFields({ error })
|
||||
},
|
||||
errorWithError: (message: string, error: unknown) => {
|
||||
logger.errorWithError(message, error)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'ERROR',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields: { error },
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
},
|
||||
} as unknown as ReturnType<typeof useLogg> // Force type for now as complete Logg interface is complex to mock fully perfectly without more boilerplate
|
||||
}
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Minecraft Service Debugger</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #1a1a1a;
|
||||
--text-color: #e0e0e0;
|
||||
--sidebar-width: 250px;
|
||||
--accent-color: #4a9eff;
|
||||
--border-color: #333;
|
||||
--log-bg: #111;
|
||||
--log-info: #4a9eff;
|
||||
--log-warn: #ffb74d;
|
||||
--log-error: #ef5350;
|
||||
--log-debug: #888;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #252525;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: #f44336;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: var(--sidebar-width);
|
||||
background-color: #252525;
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: #333;
|
||||
border-left: 3px solid var(--accent-color);
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-panel.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background-color: var(--log-bg);
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.level-info { color: var(--log-info); }
|
||||
.level-warn { color: var(--log-warn); }
|
||||
.level-error { color: var(--log-error); }
|
||||
.level-debug { color: var(--log-debug); }
|
||||
|
||||
.llm-card {
|
||||
background-color: #252525;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.llm-header {
|
||||
padding: 0.8rem;
|
||||
background-color: #333;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.llm-body {
|
||||
padding: 1rem;
|
||||
display: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.llm-body.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.json-block {
|
||||
background-color: #111;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8em;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #888;
|
||||
font-size: 0.85em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
background-color: #1e1e1e;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
border-left: 3px solid var(--accent-color);
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 0.8rem;
|
||||
border-radius: 4px;
|
||||
background-color: #1e1e1e;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.message-role {
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.7;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.role-system {
|
||||
border-left-color: #e91e63;
|
||||
background-color: #2a1a20;
|
||||
}
|
||||
|
||||
.role-user {
|
||||
border-left-color: #4caf50;
|
||||
background-color: #1a2a1a;
|
||||
}
|
||||
|
||||
.role-assistant {
|
||||
border-left-color: #2196f3;
|
||||
background-color: #1a202a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>Minecraft Service Debugger</div>
|
||||
<div>
|
||||
<span id="connection-status" class="status-dot"></span>
|
||||
<span id="status-text">Disconnected</span>
|
||||
<button onclick="reconnect()" style="margin-left: 1rem; padding: 0.25rem 0.75rem; cursor: pointer;">Reconnect</button>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<nav>
|
||||
<div class="nav-item active" onclick="switchTab('logs')">Logs</div>
|
||||
<div class="nav-item" onclick="switchTab('llm')">LLM Traces</div>
|
||||
</nav>
|
||||
|
||||
<div id="logs-panel" class="content-panel active">
|
||||
<div class="toolbar">
|
||||
<button onclick="clearLogs()">Clear</button>
|
||||
<label><input type="checkbox" id="auto-scroll" checked> Auto-scroll</label>
|
||||
</div>
|
||||
<div id="logs-list" class="log-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="llm-panel" class="content-panel">
|
||||
<div class="toolbar">
|
||||
<button onclick="clearLLM()">Clear</button>
|
||||
</div>
|
||||
<div id="llm-list" class="log-container"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let autoScroll = true;
|
||||
const statusDot = document.getElementById('connection-status');
|
||||
const statusText = document.getElementById('status-text');
|
||||
|
||||
document.getElementById('auto-scroll').addEventListener('change', (e) => {
|
||||
autoScroll = e.target.checked;
|
||||
});
|
||||
|
||||
function switchTab(tabId) {
|
||||
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
||||
document.querySelectorAll('.content-panel').forEach(el => el.classList.remove('active'));
|
||||
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabId + '-panel').classList.add('active');
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
document.getElementById('logs-list').innerHTML = '';
|
||||
}
|
||||
|
||||
function clearLLM() {
|
||||
document.getElementById('llm-list').innerHTML = '';
|
||||
}
|
||||
|
||||
function addLog(entry) {
|
||||
const container = document.getElementById('logs-list');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'log-entry level-' + (entry.level || 'info').toLowerCase();
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
const fieldsJson = entry.fields ? JSON.stringify(entry.fields) : '';
|
||||
|
||||
div.innerHTML = `<span class="log-time">[${time}]</span> <span class="log-level">[${entry.level}]</span> ${entry.message} ${fieldsJson ? `<br><span style="color:#666;font-size:0.9em">${fieldsJson}</span>` : ''}`;
|
||||
|
||||
container.appendChild(div);
|
||||
|
||||
if (autoScroll) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function addLLMTrace(trace) {
|
||||
if (!trace) {
|
||||
console.error('Received undefined trace');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('llm-list');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'llm-card';
|
||||
|
||||
const time = new Date(trace.timestamp || Date.now()).toLocaleTimeString();
|
||||
const usage = trace.usage || {};
|
||||
const tokens = usage.total_tokens || '?';
|
||||
|
||||
// Format messages
|
||||
let messagesHtml = '';
|
||||
if (trace.messages && Array.isArray(trace.messages)) {
|
||||
messagesHtml = trace.messages.map(msg => {
|
||||
const roleClass = 'role-' + (msg.role || 'unknown');
|
||||
const content = (msg.content || '').replace(/</g, '<').replace(/>/g, '>');
|
||||
return `
|
||||
<div class="message ${roleClass}">
|
||||
<div class="message-role">${msg.role || 'unknown'}</div>
|
||||
<div class="message-content">${content}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Format extra details (excluding messages/content/usage which are shown separately)
|
||||
const { messages, content, usage: _u, timestamp, ...other } = trace;
|
||||
const otherJson = Object.keys(other).length > 0 ? JSON.stringify(other, null, 2) : '';
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="llm-header" onclick="this.nextElementSibling.classList.toggle('open')">
|
||||
<div>
|
||||
<span class="badge">${time}</span>
|
||||
<span>LLM Completion (${trace.route || 'unknown'})</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge">${tokens} tokens</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="llm-body">
|
||||
<div class="section-title">Result</div>
|
||||
<pre class="content-block">${(trace.content || '').replace(/</g, '<').replace(/>/g, '>')}</pre>
|
||||
|
||||
<div class="section-title">Messages</div>
|
||||
<div class="messages-list">
|
||||
${messagesHtml || '<div style="color:#666">No messages</div>'}
|
||||
</div>
|
||||
|
||||
${otherJson ? `
|
||||
<div class="section-title">Metadata</div>
|
||||
<pre class="json-block">${otherJson}</pre>
|
||||
` : ''}
|
||||
|
||||
<div class="section-title">Usage</div>
|
||||
<pre class="json-block">${JSON.stringify(usage, null, 2)}</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertBefore(div, container.firstChild);
|
||||
}
|
||||
|
||||
let eventSource = null;
|
||||
|
||||
function reconnect() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
statusDot.classList.remove('connected');
|
||||
statusText.textContent = 'Reconnecting...';
|
||||
setTimeout(connectSSE, 100);
|
||||
}
|
||||
|
||||
function connectSSE() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
|
||||
eventSource = new EventSource('/events');
|
||||
|
||||
eventSource.onopen = () => {
|
||||
statusDot.classList.add('connected');
|
||||
statusText.textContent = 'Connected';
|
||||
addLog({ level: 'SYSTEM', message: 'Connected to debug server' });
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.error('SSE error:', err);
|
||||
statusDot.classList.remove('connected');
|
||||
statusText.textContent = 'Disconnected';
|
||||
};
|
||||
|
||||
eventSource.addEventListener('log', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
addLog(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse log', e);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener('llm', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
addLLMTrace(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse llm trace', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
connectSSE();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user