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.
@@ -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; }
@@ -0,0 +1,3 @@
1
+ var undefined$1 = undefined;
2
+
3
+ export { undefined$1 as default };
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
+ }