@cephalization/math 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +3 -1
- package/src/ui/app.tsx +329 -0
- package/src/ui/index.html +30 -0
package/README.md
CHANGED
|
@@ -33,11 +33,11 @@ Why Bun?
|
|
|
33
33
|
### From npm (recommended)
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
#
|
|
37
|
-
bunx @cephalization/math <command>
|
|
38
|
-
|
|
39
|
-
# Global install
|
|
36
|
+
# Global install (recommended)
|
|
40
37
|
bun install -g @cephalization/math
|
|
38
|
+
|
|
39
|
+
# One-off usage
|
|
40
|
+
bunx @cephalization/math <command>
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### From source (for development)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cephalization/math",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"author": "Tony Powell <powell.anthonyd@proton.me>",
|
|
5
5
|
"access": "public",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"index.ts",
|
|
21
21
|
"src/**/*.ts",
|
|
22
|
+
"src/**/*.tsx",
|
|
23
|
+
"src/**/*.html",
|
|
22
24
|
"README.md"
|
|
23
25
|
],
|
|
24
26
|
"scripts": {
|
package/src/ui/app.tsx
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React app for the Math Agent UI.
|
|
3
|
+
* Connects to WebSocket server and displays loop logs and agent output.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useState, useRef, useCallback } from "react";
|
|
7
|
+
import { createRoot } from "react-dom/client";
|
|
8
|
+
import type { BufferLogEntry, BufferAgentOutput } from "./buffer";
|
|
9
|
+
import type { WebSocketMessage } from "./server";
|
|
10
|
+
import type { LogCategory } from "../agent";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Connection state for the WebSocket.
|
|
14
|
+
*/
|
|
15
|
+
type ConnectionState = "connecting" | "connected" | "disconnected";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reconnection interval in milliseconds.
|
|
19
|
+
*/
|
|
20
|
+
const RECONNECT_INTERVAL = 3000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Color mapping for log categories matching terminal colors.
|
|
24
|
+
*/
|
|
25
|
+
const categoryColors: Record<LogCategory, string> = {
|
|
26
|
+
info: "#60a5fa", // blue
|
|
27
|
+
success: "#4ade80", // green
|
|
28
|
+
warning: "#facc15", // yellow
|
|
29
|
+
error: "#f87171", // red
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Main App component that displays loop status and agent output.
|
|
34
|
+
*/
|
|
35
|
+
function App() {
|
|
36
|
+
const [logs, setLogs] = useState<BufferLogEntry[]>([]);
|
|
37
|
+
const [output, setOutput] = useState<BufferAgentOutput[]>([]);
|
|
38
|
+
const [connectionState, setConnectionState] = useState<ConnectionState>("connecting");
|
|
39
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
40
|
+
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
|
+
const logContainerRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const outputContainerRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
|
|
44
|
+
// Auto-scroll to bottom when logs change
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (logContainerRef.current) {
|
|
47
|
+
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
|
48
|
+
}
|
|
49
|
+
}, [logs]);
|
|
50
|
+
|
|
51
|
+
// Auto-scroll to bottom when output changes
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (outputContainerRef.current) {
|
|
54
|
+
outputContainerRef.current.scrollTop = outputContainerRef.current.scrollHeight;
|
|
55
|
+
}
|
|
56
|
+
}, [output]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a WebSocket connection and set up event handlers.
|
|
60
|
+
*/
|
|
61
|
+
const connect = useCallback(() => {
|
|
62
|
+
// Clear any existing reconnect timer
|
|
63
|
+
if (reconnectTimerRef.current) {
|
|
64
|
+
clearTimeout(reconnectTimerRef.current);
|
|
65
|
+
reconnectTimerRef.current = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Close existing connection if any
|
|
69
|
+
if (wsRef.current) {
|
|
70
|
+
wsRef.current.close();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setConnectionState("connecting");
|
|
74
|
+
|
|
75
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
76
|
+
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
|
77
|
+
const ws = new WebSocket(wsUrl);
|
|
78
|
+
wsRef.current = ws;
|
|
79
|
+
|
|
80
|
+
ws.onopen = () => {
|
|
81
|
+
setConnectionState("connected");
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
ws.onclose = () => {
|
|
85
|
+
setConnectionState("disconnected");
|
|
86
|
+
// Schedule reconnection attempt
|
|
87
|
+
reconnectTimerRef.current = setTimeout(() => {
|
|
88
|
+
connect();
|
|
89
|
+
}, RECONNECT_INTERVAL);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
ws.onerror = () => {
|
|
93
|
+
// onclose will be called after onerror, so we don't need to handle reconnection here
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
ws.onmessage = (event) => {
|
|
97
|
+
const message: WebSocketMessage = JSON.parse(event.data);
|
|
98
|
+
|
|
99
|
+
switch (message.type) {
|
|
100
|
+
case "connected":
|
|
101
|
+
// Connection confirmed
|
|
102
|
+
break;
|
|
103
|
+
case "history":
|
|
104
|
+
// Full history received - replace existing state
|
|
105
|
+
setLogs(message.logs);
|
|
106
|
+
setOutput(message.output);
|
|
107
|
+
break;
|
|
108
|
+
case "log":
|
|
109
|
+
// New log entry
|
|
110
|
+
setLogs((prev) => [...prev, message.entry]);
|
|
111
|
+
break;
|
|
112
|
+
case "output":
|
|
113
|
+
// New agent output
|
|
114
|
+
setOutput((prev) => [...prev, message.entry]);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
connect();
|
|
122
|
+
|
|
123
|
+
return () => {
|
|
124
|
+
// Clean up on unmount
|
|
125
|
+
if (reconnectTimerRef.current) {
|
|
126
|
+
clearTimeout(reconnectTimerRef.current);
|
|
127
|
+
}
|
|
128
|
+
if (wsRef.current) {
|
|
129
|
+
wsRef.current.close();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}, [connect]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the color for a log category.
|
|
136
|
+
*/
|
|
137
|
+
const getCategoryColor = (category: LogCategory): string => {
|
|
138
|
+
return categoryColors[category] || "#e0e0e0";
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the status display properties based on connection state.
|
|
143
|
+
*/
|
|
144
|
+
const getStatusDisplay = () => {
|
|
145
|
+
switch (connectionState) {
|
|
146
|
+
case "connecting":
|
|
147
|
+
return { color: "#facc15", text: "Connecting..." }; // yellow
|
|
148
|
+
case "connected":
|
|
149
|
+
return { color: "#4ade80", text: "Connected" }; // green
|
|
150
|
+
case "disconnected":
|
|
151
|
+
return { color: "#f87171", text: "Disconnected" }; // red
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const statusDisplay = getStatusDisplay();
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div style={styles.container}>
|
|
159
|
+
{/* Disconnected banner */}
|
|
160
|
+
{connectionState === "disconnected" && (
|
|
161
|
+
<div style={styles.disconnectedBanner}>
|
|
162
|
+
Connection lost. Reconnecting...
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<header style={styles.header}>
|
|
167
|
+
<h1 style={styles.title}>Math Agent UI</h1>
|
|
168
|
+
<div style={styles.statusContainer}>
|
|
169
|
+
<span
|
|
170
|
+
style={{
|
|
171
|
+
...styles.statusDot,
|
|
172
|
+
backgroundColor: statusDisplay.color,
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
<span style={{ ...styles.statusText, color: statusDisplay.color }}>
|
|
176
|
+
{statusDisplay.text}
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
</header>
|
|
180
|
+
|
|
181
|
+
<div style={styles.content}>
|
|
182
|
+
<section style={styles.section}>
|
|
183
|
+
<h2 style={styles.sectionTitle}>Loop Status</h2>
|
|
184
|
+
<div ref={logContainerRef} style={styles.logContainer}>
|
|
185
|
+
{logs.length === 0 ? (
|
|
186
|
+
<p style={styles.empty}>No logs yet</p>
|
|
187
|
+
) : (
|
|
188
|
+
logs.map((log, index) => (
|
|
189
|
+
<div key={index} style={styles.logEntry}>
|
|
190
|
+
<span style={{ ...styles.timestamp, color: getCategoryColor(log.category) }}>
|
|
191
|
+
{new Date(log.timestamp).toLocaleTimeString()}
|
|
192
|
+
</span>
|
|
193
|
+
<span style={{ ...styles.category, color: getCategoryColor(log.category) }}>
|
|
194
|
+
[{log.category}]
|
|
195
|
+
</span>
|
|
196
|
+
<span style={styles.message}>{log.message}</span>
|
|
197
|
+
</div>
|
|
198
|
+
))
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
|
|
203
|
+
<section style={styles.section}>
|
|
204
|
+
<h2 style={styles.sectionTitle}>Agent Output</h2>
|
|
205
|
+
<div ref={outputContainerRef} style={styles.outputContainer}>
|
|
206
|
+
{output.length === 0 ? (
|
|
207
|
+
<p style={styles.empty}>No output yet</p>
|
|
208
|
+
) : (
|
|
209
|
+
output.map((out, index) => (
|
|
210
|
+
<pre key={index} style={styles.outputEntry}>
|
|
211
|
+
{out.text}
|
|
212
|
+
</pre>
|
|
213
|
+
))
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</section>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Inline styles for the app components.
|
|
224
|
+
*/
|
|
225
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
226
|
+
container: {
|
|
227
|
+
height: "100%",
|
|
228
|
+
display: "flex",
|
|
229
|
+
flexDirection: "column",
|
|
230
|
+
padding: "16px",
|
|
231
|
+
},
|
|
232
|
+
header: {
|
|
233
|
+
display: "flex",
|
|
234
|
+
justifyContent: "space-between",
|
|
235
|
+
alignItems: "center",
|
|
236
|
+
marginBottom: "16px",
|
|
237
|
+
paddingBottom: "16px",
|
|
238
|
+
borderBottom: "1px solid #333",
|
|
239
|
+
},
|
|
240
|
+
title: {
|
|
241
|
+
fontSize: "24px",
|
|
242
|
+
fontWeight: "bold",
|
|
243
|
+
margin: 0,
|
|
244
|
+
},
|
|
245
|
+
statusContainer: {
|
|
246
|
+
display: "flex",
|
|
247
|
+
alignItems: "center",
|
|
248
|
+
gap: "8px",
|
|
249
|
+
},
|
|
250
|
+
statusDot: {
|
|
251
|
+
width: "10px",
|
|
252
|
+
height: "10px",
|
|
253
|
+
borderRadius: "50%",
|
|
254
|
+
},
|
|
255
|
+
statusText: {
|
|
256
|
+
fontSize: "14px",
|
|
257
|
+
},
|
|
258
|
+
disconnectedBanner: {
|
|
259
|
+
backgroundColor: "#7f1d1d",
|
|
260
|
+
color: "#fecaca",
|
|
261
|
+
padding: "12px 16px",
|
|
262
|
+
marginBottom: "16px",
|
|
263
|
+
borderRadius: "8px",
|
|
264
|
+
textAlign: "center",
|
|
265
|
+
fontWeight: "500",
|
|
266
|
+
},
|
|
267
|
+
content: {
|
|
268
|
+
display: "flex",
|
|
269
|
+
flex: 1,
|
|
270
|
+
gap: "16px",
|
|
271
|
+
overflow: "hidden",
|
|
272
|
+
},
|
|
273
|
+
section: {
|
|
274
|
+
flex: 1,
|
|
275
|
+
display: "flex",
|
|
276
|
+
flexDirection: "column",
|
|
277
|
+
backgroundColor: "#252525",
|
|
278
|
+
borderRadius: "8px",
|
|
279
|
+
padding: "16px",
|
|
280
|
+
overflow: "hidden",
|
|
281
|
+
},
|
|
282
|
+
sectionTitle: {
|
|
283
|
+
fontSize: "18px",
|
|
284
|
+
fontWeight: "600",
|
|
285
|
+
marginBottom: "12px",
|
|
286
|
+
color: "#a0a0a0",
|
|
287
|
+
},
|
|
288
|
+
logContainer: {
|
|
289
|
+
flex: 1,
|
|
290
|
+
overflow: "auto",
|
|
291
|
+
fontFamily: "monospace",
|
|
292
|
+
fontSize: "13px",
|
|
293
|
+
},
|
|
294
|
+
outputContainer: {
|
|
295
|
+
flex: 1,
|
|
296
|
+
overflow: "auto",
|
|
297
|
+
fontFamily: "monospace",
|
|
298
|
+
fontSize: "13px",
|
|
299
|
+
},
|
|
300
|
+
logEntry: {
|
|
301
|
+
marginBottom: "4px",
|
|
302
|
+
lineHeight: "1.4",
|
|
303
|
+
},
|
|
304
|
+
timestamp: {
|
|
305
|
+
marginRight: "8px",
|
|
306
|
+
},
|
|
307
|
+
category: {
|
|
308
|
+
marginRight: "8px",
|
|
309
|
+
fontWeight: "bold",
|
|
310
|
+
},
|
|
311
|
+
message: {
|
|
312
|
+
color: "#e0e0e0",
|
|
313
|
+
},
|
|
314
|
+
outputEntry: {
|
|
315
|
+
margin: 0,
|
|
316
|
+
padding: "4px 0",
|
|
317
|
+
whiteSpace: "pre-wrap",
|
|
318
|
+
wordBreak: "break-word",
|
|
319
|
+
color: "#e0e0e0",
|
|
320
|
+
},
|
|
321
|
+
empty: {
|
|
322
|
+
color: "#666",
|
|
323
|
+
fontStyle: "italic",
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Mount the React app
|
|
328
|
+
const root = createRoot(document.getElementById("root")!);
|
|
329
|
+
root.render(<App />);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Math Agent UI</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
html, body, #root {
|
|
15
|
+
height: 100%;
|
|
16
|
+
width: 100%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
background-color: #1a1a1a;
|
|
21
|
+
color: #e0e0e0;
|
|
22
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<div id="root"></div>
|
|
28
|
+
<script type="module" src="./app.tsx"></script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|