@screenpipe-ui/core 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,96 @@
1
+ import { ScreenpipeQueryParams, ScreenpipeResponse, ContentItem, PaginationInfo } from '@screenpipe/js';
2
+ export { AudioContent, ContentItem, ContentType, OCRContent, PaginationInfo, ScreenpipeQueryParams, ScreenpipeResponse, Speaker, UiContent } from '@screenpipe/js';
3
+ import * as zustand_vanilla from 'zustand/vanilla';
4
+
5
+ interface HealthCheckResponse {
6
+ status: string;
7
+ status_code: number;
8
+ last_frame_timestamp?: string;
9
+ last_audio_timestamp?: string;
10
+ frame_status: string;
11
+ audio_status: string;
12
+ message: string;
13
+ }
14
+ interface ScreenpipeClientConfig {
15
+ baseUrl?: string;
16
+ }
17
+ declare class ScreenpipeUIClient {
18
+ private baseUrl;
19
+ constructor(config?: ScreenpipeClientConfig);
20
+ private get;
21
+ search(params?: ScreenpipeQueryParams): Promise<ScreenpipeResponse>;
22
+ health(): Promise<HealthCheckResponse>;
23
+ getFrameUrl(frameId: number): string;
24
+ }
25
+ declare function createClient(config?: ScreenpipeClientConfig): ScreenpipeUIClient;
26
+ declare function getContentText(item: ContentItem): string;
27
+ declare function getContentTimestamp(item: ContentItem): string;
28
+ declare function getContentAppName(item: ContentItem): string;
29
+
30
+ interface TimeRange {
31
+ start: string;
32
+ end: string;
33
+ }
34
+
35
+ interface SearchState {
36
+ query: string;
37
+ contentType: ScreenpipeQueryParams["contentType"];
38
+ results: ContentItem[];
39
+ pagination: PaginationInfo;
40
+ loading: boolean;
41
+ error: string | null;
42
+ setQuery: (q: string) => void;
43
+ setContentType: (t: ScreenpipeQueryParams["contentType"]) => void;
44
+ executeSearch: (client: ScreenpipeUIClient) => Promise<void>;
45
+ nextPage: (client: ScreenpipeUIClient) => Promise<void>;
46
+ prevPage: (client: ScreenpipeUIClient) => Promise<void>;
47
+ reset: () => void;
48
+ }
49
+ declare function createSearchStore(): zustand_vanilla.StoreApi<SearchState>;
50
+
51
+ interface TimelineState {
52
+ items: ContentItem[];
53
+ startTime: string;
54
+ endTime: string;
55
+ appFilter: string | undefined;
56
+ loading: boolean;
57
+ error: string | null;
58
+ setTimeRange: (start: string, end: string) => void;
59
+ setAppFilter: (app: string | undefined) => void;
60
+ loadTimeline: (client: ScreenpipeUIClient) => Promise<void>;
61
+ reset: () => void;
62
+ }
63
+ declare function createTimelineStore(): zustand_vanilla.StoreApi<TimelineState>;
64
+
65
+ interface HealthState {
66
+ health: HealthCheckResponse | null;
67
+ loading: boolean;
68
+ error: string | null;
69
+ checkHealth: (client: ScreenpipeUIClient) => Promise<void>;
70
+ }
71
+ declare function createHealthStore(): zustand_vanilla.StoreApi<HealthState>;
72
+
73
+ declare function timeAgo(timestamp: string): string;
74
+ declare function formatDuration(ms: number): string;
75
+ declare function formatTime(timestamp: string): string;
76
+ declare function formatDate(timestamp: string): string;
77
+ declare function formatDateTime(timestamp: string): string;
78
+ declare function todayRange(): {
79
+ start: string;
80
+ end: string;
81
+ };
82
+
83
+ declare function truncate(text: string, maxLen?: number): string;
84
+ declare function contentTypeLabel(item: ContentItem): string;
85
+ declare function highlightMatch(text: string, query: string): string;
86
+ declare function stripAnsi(text: string): string;
87
+ declare function contentPreview(item: ContentItem, maxLen?: number): string;
88
+
89
+ interface Column {
90
+ header: string;
91
+ width: number;
92
+ align?: "left" | "right";
93
+ }
94
+ declare function formatTable(columns: Column[], rows: string[][]): string;
95
+
96
+ export { type Column, type HealthCheckResponse, type HealthState, type ScreenpipeClientConfig, ScreenpipeUIClient, type SearchState, type TimeRange, type TimelineState, contentPreview, contentTypeLabel, createClient, createHealthStore, createSearchStore, createTimelineStore, formatDate, formatDateTime, formatDuration, formatTable, formatTime, getContentAppName, getContentText, getContentTimestamp, highlightMatch, stripAnsi, timeAgo, todayRange, truncate };
package/dist/index.js ADDED
@@ -0,0 +1,350 @@
1
+ // src/client.ts
2
+ var DEFAULT_BASE_URL = "http://localhost:3030";
3
+ var ScreenpipeUIClient = class {
4
+ baseUrl;
5
+ constructor(config) {
6
+ this.baseUrl = (config?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
7
+ }
8
+ async get(path, params) {
9
+ const qs = new URLSearchParams();
10
+ if (params) {
11
+ for (const [key, value] of Object.entries(params)) {
12
+ if (value === void 0 || value === null || value === "") continue;
13
+ const snakeKey = key.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
14
+ if (Array.isArray(value)) {
15
+ if (value.length > 0) qs.append(snakeKey, value.join(","));
16
+ } else {
17
+ qs.append(snakeKey, String(value));
18
+ }
19
+ }
20
+ }
21
+ const url = `${this.baseUrl}${path}${qs.toString() ? `?${qs}` : ""}`;
22
+ const res = await fetch(url);
23
+ if (!res.ok) {
24
+ const body = await res.text().catch(() => "");
25
+ throw new Error(`GET ${path} failed (${res.status}): ${body}`);
26
+ }
27
+ return res.json();
28
+ }
29
+ async search(params = {}) {
30
+ return this.get("/search", params);
31
+ }
32
+ async health() {
33
+ return this.get("/health");
34
+ }
35
+ getFrameUrl(frameId) {
36
+ return `${this.baseUrl}/frames/${frameId}`;
37
+ }
38
+ };
39
+ function createClient(config) {
40
+ return new ScreenpipeUIClient(config);
41
+ }
42
+ function getContentText(item) {
43
+ switch (item.type) {
44
+ case "OCR":
45
+ return item.content.text;
46
+ case "Audio":
47
+ return item.content.transcription;
48
+ case "UI":
49
+ return item.content.text;
50
+ }
51
+ }
52
+ function getContentTimestamp(item) {
53
+ return item.content.timestamp;
54
+ }
55
+ function getContentAppName(item) {
56
+ switch (item.type) {
57
+ case "OCR":
58
+ return item.content.appName;
59
+ case "Audio":
60
+ return item.content.deviceName;
61
+ case "UI":
62
+ return item.content.appName;
63
+ }
64
+ }
65
+
66
+ // src/stores/search-store.ts
67
+ import { createStore } from "zustand/vanilla";
68
+ var DEFAULT_LIMIT = 20;
69
+ var initialState = {
70
+ query: "",
71
+ contentType: "all",
72
+ results: [],
73
+ pagination: { limit: DEFAULT_LIMIT, offset: 0, total: 0 },
74
+ loading: false,
75
+ error: null
76
+ };
77
+ function createSearchStore() {
78
+ return createStore((set, get) => ({
79
+ ...initialState,
80
+ setQuery: (q) => set({ query: q }),
81
+ setContentType: (t) => set({ contentType: t }),
82
+ executeSearch: async (client) => {
83
+ const { query, contentType } = get();
84
+ set({ loading: true, error: null });
85
+ try {
86
+ const res = await client.search({
87
+ q: query || void 0,
88
+ contentType,
89
+ limit: DEFAULT_LIMIT,
90
+ offset: 0
91
+ });
92
+ set({
93
+ results: res.data,
94
+ pagination: res.pagination,
95
+ loading: false
96
+ });
97
+ } catch (e) {
98
+ set({
99
+ error: e instanceof Error ? e.message : String(e),
100
+ loading: false
101
+ });
102
+ }
103
+ },
104
+ nextPage: async (client) => {
105
+ const { query, contentType, pagination } = get();
106
+ const newOffset = pagination.offset + pagination.limit;
107
+ if (newOffset >= pagination.total) return;
108
+ set({ loading: true, error: null });
109
+ try {
110
+ const res = await client.search({
111
+ q: query || void 0,
112
+ contentType,
113
+ limit: pagination.limit,
114
+ offset: newOffset
115
+ });
116
+ set({
117
+ results: res.data,
118
+ pagination: res.pagination,
119
+ loading: false
120
+ });
121
+ } catch (e) {
122
+ set({
123
+ error: e instanceof Error ? e.message : String(e),
124
+ loading: false
125
+ });
126
+ }
127
+ },
128
+ prevPage: async (client) => {
129
+ const { query, contentType, pagination } = get();
130
+ const newOffset = Math.max(0, pagination.offset - pagination.limit);
131
+ if (newOffset === pagination.offset) return;
132
+ set({ loading: true, error: null });
133
+ try {
134
+ const res = await client.search({
135
+ q: query || void 0,
136
+ contentType,
137
+ limit: pagination.limit,
138
+ offset: newOffset
139
+ });
140
+ set({
141
+ results: res.data,
142
+ pagination: res.pagination,
143
+ loading: false
144
+ });
145
+ } catch (e) {
146
+ set({
147
+ error: e instanceof Error ? e.message : String(e),
148
+ loading: false
149
+ });
150
+ }
151
+ },
152
+ reset: () => set(initialState)
153
+ }));
154
+ }
155
+
156
+ // src/stores/timeline-store.ts
157
+ import { createStore as createStore2 } from "zustand/vanilla";
158
+
159
+ // src/formatters/time.ts
160
+ var MINUTE = 60 * 1e3;
161
+ var HOUR = 60 * MINUTE;
162
+ var DAY = 24 * HOUR;
163
+ function timeAgo(timestamp) {
164
+ const diff = Date.now() - new Date(timestamp).getTime();
165
+ if (diff < MINUTE) return "just now";
166
+ if (diff < HOUR) {
167
+ const mins = Math.floor(diff / MINUTE);
168
+ return `${mins}m ago`;
169
+ }
170
+ if (diff < DAY) {
171
+ const hours = Math.floor(diff / HOUR);
172
+ return `${hours}h ago`;
173
+ }
174
+ const days = Math.floor(diff / DAY);
175
+ return `${days}d ago`;
176
+ }
177
+ function formatDuration(ms) {
178
+ if (ms < MINUTE) return `${Math.round(ms / 1e3)}s`;
179
+ if (ms < HOUR) return `${Math.floor(ms / MINUTE)}m`;
180
+ const h = Math.floor(ms / HOUR);
181
+ const m = Math.floor(ms % HOUR / MINUTE);
182
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
183
+ }
184
+ function formatTime(timestamp) {
185
+ return new Date(timestamp).toLocaleTimeString(void 0, {
186
+ hour: "2-digit",
187
+ minute: "2-digit"
188
+ });
189
+ }
190
+ function formatDate(timestamp) {
191
+ return new Date(timestamp).toLocaleDateString(void 0, {
192
+ month: "short",
193
+ day: "numeric",
194
+ year: "numeric"
195
+ });
196
+ }
197
+ function formatDateTime(timestamp) {
198
+ return `${formatDate(timestamp)} ${formatTime(timestamp)}`;
199
+ }
200
+ function todayRange() {
201
+ const now = /* @__PURE__ */ new Date();
202
+ const start = new Date(now);
203
+ start.setHours(0, 0, 0, 0);
204
+ return {
205
+ start: start.toISOString(),
206
+ end: now.toISOString()
207
+ };
208
+ }
209
+
210
+ // src/stores/timeline-store.ts
211
+ function createTimelineStore() {
212
+ const { start, end } = todayRange();
213
+ return createStore2((set, get) => ({
214
+ items: [],
215
+ startTime: start,
216
+ endTime: end,
217
+ appFilter: void 0,
218
+ loading: false,
219
+ error: null,
220
+ setTimeRange: (start2, end2) => set({ startTime: start2, endTime: end2 }),
221
+ setAppFilter: (app) => set({ appFilter: app }),
222
+ loadTimeline: async (client) => {
223
+ const { startTime, endTime, appFilter } = get();
224
+ set({ loading: true, error: null });
225
+ try {
226
+ const res = await client.search({
227
+ startTime,
228
+ endTime,
229
+ appName: appFilter,
230
+ limit: 100,
231
+ contentType: "all"
232
+ });
233
+ set({ items: res.data, loading: false });
234
+ } catch (e) {
235
+ set({
236
+ error: e instanceof Error ? e.message : String(e),
237
+ loading: false
238
+ });
239
+ }
240
+ },
241
+ reset: () => {
242
+ const { start: start2, end: end2 } = todayRange();
243
+ set({
244
+ items: [],
245
+ startTime: start2,
246
+ endTime: end2,
247
+ appFilter: void 0,
248
+ loading: false,
249
+ error: null
250
+ });
251
+ }
252
+ }));
253
+ }
254
+
255
+ // src/stores/health-store.ts
256
+ import { createStore as createStore3 } from "zustand/vanilla";
257
+ function createHealthStore() {
258
+ return createStore3((set) => ({
259
+ health: null,
260
+ loading: false,
261
+ error: null,
262
+ checkHealth: async (client) => {
263
+ set({ loading: true, error: null });
264
+ try {
265
+ const health = await client.health();
266
+ set({ health, loading: false });
267
+ } catch (e) {
268
+ set({
269
+ error: e instanceof Error ? e.message : String(e),
270
+ loading: false
271
+ });
272
+ }
273
+ }
274
+ }));
275
+ }
276
+
277
+ // src/formatters/content.ts
278
+ function truncate(text, maxLen = 80) {
279
+ if (text.length <= maxLen) return text;
280
+ return text.slice(0, maxLen - 1) + "\u2026";
281
+ }
282
+ function contentTypeLabel(item) {
283
+ switch (item.type) {
284
+ case "OCR":
285
+ return "screen";
286
+ case "Audio":
287
+ return "audio";
288
+ case "UI":
289
+ return "ui";
290
+ }
291
+ }
292
+ function highlightMatch(text, query) {
293
+ if (!query) return text;
294
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
295
+ const re = new RegExp(`(${escaped})`, "gi");
296
+ return text.replace(re, "\x1B[1;33m$1\x1B[0m");
297
+ }
298
+ function stripAnsi(text) {
299
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
300
+ }
301
+ function contentPreview(item, maxLen = 80) {
302
+ switch (item.type) {
303
+ case "OCR":
304
+ return truncate(item.content.text.replace(/\n/g, " "), maxLen);
305
+ case "Audio":
306
+ return truncate(item.content.transcription.replace(/\n/g, " "), maxLen);
307
+ case "UI":
308
+ return truncate(item.content.text.replace(/\n/g, " "), maxLen);
309
+ }
310
+ }
311
+
312
+ // src/formatters/table.ts
313
+ function formatTable(columns, rows) {
314
+ const divider = columns.map((c) => "\u2500".repeat(c.width)).join("\u2500\u252C\u2500");
315
+ const headerRow = columns.map((c) => pad(c.header, c.width, c.align)).join(" \u2502 ");
316
+ const dataRows = rows.map(
317
+ (row) => columns.map((c, i) => pad(row[i] ?? "", c.width, c.align)).join(" \u2502 ")
318
+ );
319
+ return [headerRow, divider, ...dataRows].join("\n");
320
+ }
321
+ function pad(text, width, align = "left") {
322
+ const visible = text.replace(/\x1b\[[0-9;]*m/g, "");
323
+ const padding = Math.max(0, width - visible.length);
324
+ if (align === "right") {
325
+ return " ".repeat(padding) + text;
326
+ }
327
+ return text + " ".repeat(padding);
328
+ }
329
+ export {
330
+ ScreenpipeUIClient,
331
+ contentPreview,
332
+ contentTypeLabel,
333
+ createClient,
334
+ createHealthStore,
335
+ createSearchStore,
336
+ createTimelineStore,
337
+ formatDate,
338
+ formatDateTime,
339
+ formatDuration,
340
+ formatTable,
341
+ formatTime,
342
+ getContentAppName,
343
+ getContentText,
344
+ getContentTimestamp,
345
+ highlightMatch,
346
+ stripAnsi,
347
+ timeAgo,
348
+ todayRange,
349
+ truncate
350
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@screenpipe-ui/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": ["dist"],
14
+ "publishConfig": { "access": "public" },
15
+ "scripts": {
16
+ "test": "bun test",
17
+ "build": "tsup src/index.ts --format esm --dts --clean"
18
+ },
19
+ "dependencies": {
20
+ "@screenpipe/js": "^1.0.21",
21
+ "zustand": "^5.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/bun": "^1.1.14",
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.3.3"
27
+ }
28
+ }