streamator-react 0.1.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/dist/index.cjs.js +148 -0
- package/dist/index.esm.js +143 -0
- package/dist/log-compat.css +4 -0
- package/dist/log-compat.js +3 -0
- package/dist/log.css +37 -0
- package/package.json +36 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
const fmt = t => `${t.toFixed(1)}s`;
|
|
7
|
+
function useLogStream(url, options = {}) {
|
|
8
|
+
const {
|
|
9
|
+
mode = "sse",
|
|
10
|
+
interval = 3000,
|
|
11
|
+
formatEvent
|
|
12
|
+
} = options;
|
|
13
|
+
const [logs, setLogs] = react.useState([]);
|
|
14
|
+
const [active, setActive] = react.useState(false);
|
|
15
|
+
const startRef = react.useRef(null);
|
|
16
|
+
const toEntry = (raw, elapsed) => ({
|
|
17
|
+
text: formatEvent ? formatEvent(raw) : raw.message ?? "",
|
|
18
|
+
level: raw.level ?? "info",
|
|
19
|
+
t: fmt(raw.t ?? elapsed),
|
|
20
|
+
_done: raw.event === "done"
|
|
21
|
+
});
|
|
22
|
+
react.useEffect(() => {
|
|
23
|
+
if (!url) return;
|
|
24
|
+
setLogs([]);
|
|
25
|
+
setActive(true);
|
|
26
|
+
startRef.current = Date.now();
|
|
27
|
+
if (mode === "sse") {
|
|
28
|
+
const es = new EventSource(url);
|
|
29
|
+
es.onmessage = e => {
|
|
30
|
+
const raw = JSON.parse(e.data);
|
|
31
|
+
if (raw.event === "done") {
|
|
32
|
+
setActive(false);
|
|
33
|
+
es.close();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const elapsed = (Date.now() - startRef.current) / 1000;
|
|
37
|
+
const entry = toEntry(raw, elapsed);
|
|
38
|
+
if (entry.text !== null) setLogs(l => [...l, entry]);
|
|
39
|
+
};
|
|
40
|
+
es.onerror = () => {
|
|
41
|
+
setActive(false);
|
|
42
|
+
es.close();
|
|
43
|
+
};
|
|
44
|
+
return () => {
|
|
45
|
+
es.close();
|
|
46
|
+
setActive(false);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (mode === "poll") {
|
|
50
|
+
let seen = 0;
|
|
51
|
+
const id = setInterval(async () => {
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(url);
|
|
54
|
+
const {
|
|
55
|
+
logs: all
|
|
56
|
+
} = await res.json();
|
|
57
|
+
const elapsed = (Date.now() - startRef.current) / 1000;
|
|
58
|
+
const newEntries = all.slice(seen).map(raw => toEntry(raw, elapsed));
|
|
59
|
+
seen = all.length;
|
|
60
|
+
const done = newEntries.some(e => e._done);
|
|
61
|
+
const visible = newEntries.filter(e => !e._done && e.text !== null);
|
|
62
|
+
if (visible.length) setLogs(l => [...l, ...visible]);
|
|
63
|
+
if (done) {
|
|
64
|
+
clearInterval(id);
|
|
65
|
+
setActive(false);
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
}, interval);
|
|
69
|
+
return () => {
|
|
70
|
+
clearInterval(id);
|
|
71
|
+
setActive(false);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}, [url, mode]);
|
|
75
|
+
return {
|
|
76
|
+
logs,
|
|
77
|
+
active
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function LogPanel({
|
|
82
|
+
logs,
|
|
83
|
+
active,
|
|
84
|
+
waitingText = "β³ Startingβ¦",
|
|
85
|
+
maxHeight,
|
|
86
|
+
className,
|
|
87
|
+
style,
|
|
88
|
+
renderEntry
|
|
89
|
+
}) {
|
|
90
|
+
const bottomRef = react.useRef(null);
|
|
91
|
+
react.useEffect(() => {
|
|
92
|
+
bottomRef.current?.scrollIntoView({
|
|
93
|
+
behavior: "smooth"
|
|
94
|
+
});
|
|
95
|
+
}, [logs]);
|
|
96
|
+
if (!active && logs.length === 0) return null;
|
|
97
|
+
const rootStyle = maxHeight ? {
|
|
98
|
+
...style,
|
|
99
|
+
"--streamator-max-height": maxHeight
|
|
100
|
+
} : style;
|
|
101
|
+
return /*#__PURE__*/jsxRuntime.jsxs("div", {
|
|
102
|
+
className: ["streamator-log", className].filter(Boolean).join(" "),
|
|
103
|
+
style: rootStyle,
|
|
104
|
+
children: [active && logs.length === 0 && /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
105
|
+
className: "streamator-log-waiting",
|
|
106
|
+
children: waitingText
|
|
107
|
+
}), logs.map((entry, i) => renderEntry ? renderEntry(entry, i) : /*#__PURE__*/jsxRuntime.jsxs("div", {
|
|
108
|
+
className: ["streamator-log-entry", entry.level !== "info" ? `streamator-log-entry--${entry.level}` : ""].filter(Boolean).join(" "),
|
|
109
|
+
children: [/*#__PURE__*/jsxRuntime.jsx("span", {
|
|
110
|
+
className: "streamator-log-time",
|
|
111
|
+
children: entry.t
|
|
112
|
+
}), /*#__PURE__*/jsxRuntime.jsx("span", {
|
|
113
|
+
children: entry.text
|
|
114
|
+
})]
|
|
115
|
+
}, i)), /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
116
|
+
ref: bottomRef
|
|
117
|
+
})]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const BASE_EVENT_LABELS = {
|
|
122
|
+
page_loaded: () => "π Page loaded",
|
|
123
|
+
batch_started: () => "π¦ Batch started",
|
|
124
|
+
loading_strategy: e => `βοΈ Loading strategy: ${e.strategy ?? ""}`,
|
|
125
|
+
llm_started: () => "π€ LLM started",
|
|
126
|
+
llm_done: () => "π€ LLM done",
|
|
127
|
+
cache_hit: () => "β‘ Cache hit",
|
|
128
|
+
search_started: () => "π Search started",
|
|
129
|
+
search_done: () => "π Search done",
|
|
130
|
+
browser_ready: () => "π Browser ready",
|
|
131
|
+
retry: e => `π Retry ${e.attempt ?? ""}`
|
|
132
|
+
};
|
|
133
|
+
function makeFormatEvent(overrides = {}) {
|
|
134
|
+
const labels = {
|
|
135
|
+
...BASE_EVENT_LABELS,
|
|
136
|
+
...overrides
|
|
137
|
+
};
|
|
138
|
+
return raw => {
|
|
139
|
+
const fn = labels[raw.event];
|
|
140
|
+
if (fn) return fn(raw);
|
|
141
|
+
return raw.message ?? null;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
exports.BASE_EVENT_LABELS = BASE_EVENT_LABELS;
|
|
146
|
+
exports.LogPanel = LogPanel;
|
|
147
|
+
exports.makeFormatEvent = makeFormatEvent;
|
|
148
|
+
exports.useLogStream = useLogStream;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
const fmt = t => `${t.toFixed(1)}s`;
|
|
5
|
+
function useLogStream(url, options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
mode = "sse",
|
|
8
|
+
interval = 3000,
|
|
9
|
+
formatEvent
|
|
10
|
+
} = options;
|
|
11
|
+
const [logs, setLogs] = useState([]);
|
|
12
|
+
const [active, setActive] = useState(false);
|
|
13
|
+
const startRef = useRef(null);
|
|
14
|
+
const toEntry = (raw, elapsed) => ({
|
|
15
|
+
text: formatEvent ? formatEvent(raw) : raw.message ?? "",
|
|
16
|
+
level: raw.level ?? "info",
|
|
17
|
+
t: fmt(raw.t ?? elapsed),
|
|
18
|
+
_done: raw.event === "done"
|
|
19
|
+
});
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!url) return;
|
|
22
|
+
setLogs([]);
|
|
23
|
+
setActive(true);
|
|
24
|
+
startRef.current = Date.now();
|
|
25
|
+
if (mode === "sse") {
|
|
26
|
+
const es = new EventSource(url);
|
|
27
|
+
es.onmessage = e => {
|
|
28
|
+
const raw = JSON.parse(e.data);
|
|
29
|
+
if (raw.event === "done") {
|
|
30
|
+
setActive(false);
|
|
31
|
+
es.close();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const elapsed = (Date.now() - startRef.current) / 1000;
|
|
35
|
+
const entry = toEntry(raw, elapsed);
|
|
36
|
+
if (entry.text !== null) setLogs(l => [...l, entry]);
|
|
37
|
+
};
|
|
38
|
+
es.onerror = () => {
|
|
39
|
+
setActive(false);
|
|
40
|
+
es.close();
|
|
41
|
+
};
|
|
42
|
+
return () => {
|
|
43
|
+
es.close();
|
|
44
|
+
setActive(false);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (mode === "poll") {
|
|
48
|
+
let seen = 0;
|
|
49
|
+
const id = setInterval(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(url);
|
|
52
|
+
const {
|
|
53
|
+
logs: all
|
|
54
|
+
} = await res.json();
|
|
55
|
+
const elapsed = (Date.now() - startRef.current) / 1000;
|
|
56
|
+
const newEntries = all.slice(seen).map(raw => toEntry(raw, elapsed));
|
|
57
|
+
seen = all.length;
|
|
58
|
+
const done = newEntries.some(e => e._done);
|
|
59
|
+
const visible = newEntries.filter(e => !e._done && e.text !== null);
|
|
60
|
+
if (visible.length) setLogs(l => [...l, ...visible]);
|
|
61
|
+
if (done) {
|
|
62
|
+
clearInterval(id);
|
|
63
|
+
setActive(false);
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
}, interval);
|
|
67
|
+
return () => {
|
|
68
|
+
clearInterval(id);
|
|
69
|
+
setActive(false);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}, [url, mode]);
|
|
73
|
+
return {
|
|
74
|
+
logs,
|
|
75
|
+
active
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function LogPanel({
|
|
80
|
+
logs,
|
|
81
|
+
active,
|
|
82
|
+
waitingText = "β³ Startingβ¦",
|
|
83
|
+
maxHeight,
|
|
84
|
+
className,
|
|
85
|
+
style,
|
|
86
|
+
renderEntry
|
|
87
|
+
}) {
|
|
88
|
+
const bottomRef = useRef(null);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
bottomRef.current?.scrollIntoView({
|
|
91
|
+
behavior: "smooth"
|
|
92
|
+
});
|
|
93
|
+
}, [logs]);
|
|
94
|
+
if (!active && logs.length === 0) return null;
|
|
95
|
+
const rootStyle = maxHeight ? {
|
|
96
|
+
...style,
|
|
97
|
+
"--streamator-max-height": maxHeight
|
|
98
|
+
} : style;
|
|
99
|
+
return /*#__PURE__*/jsxs("div", {
|
|
100
|
+
className: ["streamator-log", className].filter(Boolean).join(" "),
|
|
101
|
+
style: rootStyle,
|
|
102
|
+
children: [active && logs.length === 0 && /*#__PURE__*/jsx("div", {
|
|
103
|
+
className: "streamator-log-waiting",
|
|
104
|
+
children: waitingText
|
|
105
|
+
}), logs.map((entry, i) => renderEntry ? renderEntry(entry, i) : /*#__PURE__*/jsxs("div", {
|
|
106
|
+
className: ["streamator-log-entry", entry.level !== "info" ? `streamator-log-entry--${entry.level}` : ""].filter(Boolean).join(" "),
|
|
107
|
+
children: [/*#__PURE__*/jsx("span", {
|
|
108
|
+
className: "streamator-log-time",
|
|
109
|
+
children: entry.t
|
|
110
|
+
}), /*#__PURE__*/jsx("span", {
|
|
111
|
+
children: entry.text
|
|
112
|
+
})]
|
|
113
|
+
}, i)), /*#__PURE__*/jsx("div", {
|
|
114
|
+
ref: bottomRef
|
|
115
|
+
})]
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const BASE_EVENT_LABELS = {
|
|
120
|
+
page_loaded: () => "π Page loaded",
|
|
121
|
+
batch_started: () => "π¦ Batch started",
|
|
122
|
+
loading_strategy: e => `βοΈ Loading strategy: ${e.strategy ?? ""}`,
|
|
123
|
+
llm_started: () => "π€ LLM started",
|
|
124
|
+
llm_done: () => "π€ LLM done",
|
|
125
|
+
cache_hit: () => "β‘ Cache hit",
|
|
126
|
+
search_started: () => "π Search started",
|
|
127
|
+
search_done: () => "π Search done",
|
|
128
|
+
browser_ready: () => "π Browser ready",
|
|
129
|
+
retry: e => `π Retry ${e.attempt ?? ""}`
|
|
130
|
+
};
|
|
131
|
+
function makeFormatEvent(overrides = {}) {
|
|
132
|
+
const labels = {
|
|
133
|
+
...BASE_EVENT_LABELS,
|
|
134
|
+
...overrides
|
|
135
|
+
};
|
|
136
|
+
return raw => {
|
|
137
|
+
const fn = labels[raw.event];
|
|
138
|
+
if (fn) return fn(raw);
|
|
139
|
+
return raw.message ?? null;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { BASE_EVENT_LABELS, LogPanel, makeFormatEvent, useLogStream };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
.log { background: var(--streamator-bg, #f8f8f8); border: 1px solid var(--streamator-border, #e0e0e0); border-radius: var(--streamator-radius, 6px); color: var(--streamator-color, #444); font-size: var(--streamator-font-size, 0.8rem); max-height: var(--streamator-max-height, 160px); overflow-y: auto; padding: 8px 10px; font-family: monospace; }
|
|
2
|
+
.log-entry { display: flex; gap: 8px; line-height: 1.5; }
|
|
3
|
+
.log-time { color: var(--streamator-time-color, #aaa); font-variant-numeric: tabular-nums; flex-shrink: 0; }
|
|
4
|
+
.log-waiting { color: var(--streamator-time-color, #aaa); animation: streamator-pulse 1.2s ease-in-out infinite; }
|
package/dist/log.css
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.streamator-log {
|
|
2
|
+
background: var(--streamator-bg, #f8f8f8);
|
|
3
|
+
border: 1px solid var(--streamator-border, #e0e0e0);
|
|
4
|
+
border-radius: var(--streamator-radius, 6px);
|
|
5
|
+
color: var(--streamator-color, #444);
|
|
6
|
+
font-size: var(--streamator-font-size, 0.8rem);
|
|
7
|
+
max-height: var(--streamator-max-height, 160px);
|
|
8
|
+
overflow-y: auto;
|
|
9
|
+
padding: 8px 10px;
|
|
10
|
+
font-family: monospace;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.streamator-log-entry {
|
|
14
|
+
display: flex;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
line-height: 1.5;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.streamator-log-time {
|
|
20
|
+
color: var(--streamator-time-color, #aaa);
|
|
21
|
+
font-variant-numeric: tabular-nums;
|
|
22
|
+
flex-shrink: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.streamator-log-entry--success { color: var(--streamator-success-color, #2e7d32); }
|
|
26
|
+
.streamator-log-entry--warning { color: var(--streamator-warning-color, #f59e0b); }
|
|
27
|
+
.streamator-log-entry--error { color: var(--streamator-error-color, #c62828); }
|
|
28
|
+
|
|
29
|
+
.streamator-log-waiting {
|
|
30
|
+
color: var(--streamator-time-color, #aaa);
|
|
31
|
+
animation: streamator-pulse 1.2s ease-in-out infinite;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@keyframes streamator-pulse {
|
|
35
|
+
0%, 100% { opacity: 1; }
|
|
36
|
+
50% { opacity: 0.3; }
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "streamator-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Frontend primitives for displaying live log streams",
|
|
5
|
+
"author": "Arved KlΓΆhn <arved.kloehn@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": { "type": "git", "url": "https://github.com/Redundando/streamator" },
|
|
8
|
+
"keywords": ["streaming", "logging", "sse", "react", "log"],
|
|
9
|
+
"main": "dist/index.cjs.js",
|
|
10
|
+
"module": "dist/index.esm.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.esm.js",
|
|
14
|
+
"require": "./dist/index.cjs.js"
|
|
15
|
+
},
|
|
16
|
+
"./log.css": "./dist/log.css",
|
|
17
|
+
"./log-compat.css": "./dist/log-compat.css"
|
|
18
|
+
},
|
|
19
|
+
"files": ["dist"],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "rollup -c",
|
|
22
|
+
"watch": "rollup -c --watch"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": ">=17"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
30
|
+
"@rollup/plugin-babel": "^6.0.0",
|
|
31
|
+
"@babel/core": "^7.0.0",
|
|
32
|
+
"@babel/preset-react": "^7.0.0",
|
|
33
|
+
"rollup": "^4.0.0",
|
|
34
|
+
"rollup-plugin-postcss": "^4.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|