@adventurelabs/scout-core 1.4.17 → 1.4.19

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.
@@ -1,10 +1,19 @@
1
1
  import { IArtifact, IArtifactWithMediaUrl, ArtifactInsert, ArtifactUpdate } from "../types/db";
2
2
  import { IWebResponseCompatible } from "../types/requests";
3
3
  import { SupabaseClient } from "@supabase/supabase-js";
4
- export declare function server_get_artifacts_by_herd(herd_id: number, limit?: number, offset?: number, client?: SupabaseClient): Promise<IWebResponseCompatible<IArtifactWithMediaUrl[]>>;
4
+ export declare function server_get_artifact_by_id(id: number, client?: SupabaseClient): Promise<IWebResponseCompatible<IArtifactWithMediaUrl | null>>;
5
+ export declare function server_get_artifacts_by_herd(herd_id: number, limit?: number, offset?: number, options?: {
6
+ start_timestamp?: string;
7
+ end_timestamp?: string;
8
+ client?: SupabaseClient;
9
+ }): Promise<IWebResponseCompatible<IArtifactWithMediaUrl[]>>;
5
10
  export declare function server_get_artifacts_by_device_id(device_id: number, limit?: number, offset?: number, client?: SupabaseClient): Promise<IWebResponseCompatible<IArtifactWithMediaUrl[]>>;
6
11
  export declare function server_get_total_artifacts_by_herd(herd_id: number): Promise<IWebResponseCompatible<number>>;
7
- export declare function server_get_artifacts_by_device_ids_batch(device_ids: number[], limit_per_device?: number, client?: SupabaseClient): Promise<{
12
+ export declare function server_get_artifacts_by_device_ids_batch(device_ids: number[], limit_per_device?: number, options?: {
13
+ start_timestamp?: string;
14
+ end_timestamp?: string;
15
+ client?: SupabaseClient;
16
+ }): Promise<{
8
17
  [device_id: number]: IArtifactWithMediaUrl[];
9
18
  }>;
10
19
  export declare function server_insert_artifact(artifact: ArtifactInsert | ArtifactInsert[], client?: SupabaseClient): Promise<IWebResponseCompatible<IArtifact[]>>;
@@ -1,13 +1,38 @@
1
1
  "use server";
2
2
  import { newServerClient } from "../supabase/server";
3
3
  import { IWebResponse } from "../types/requests";
4
- import { generateSignedUrlsBatch } from "./storage";
5
- export async function server_get_artifacts_by_herd(herd_id, limit = 50, offset = 0, client) {
4
+ import { generateSignedUrlsBatch, generateSignedUrl } from "./storage";
5
+ export async function server_get_artifact_by_id(id, client) {
6
6
  const supabase = client || (await newServerClient());
7
+ const { data, error } = await supabase
8
+ .from("artifacts")
9
+ .select("*")
10
+ .eq("id", id)
11
+ .single();
12
+ if (error) {
13
+ return IWebResponse.error(error.message).to_compatible();
14
+ }
15
+ if (!data) {
16
+ return IWebResponse.success(null).to_compatible();
17
+ }
18
+ const artifact = data;
19
+ let media_url = null;
20
+ if (artifact.file_path) {
21
+ media_url = await generateSignedUrl(artifact.file_path, undefined, client);
22
+ }
23
+ return IWebResponse.success({
24
+ ...artifact,
25
+ media_url,
26
+ }).to_compatible();
27
+ }
28
+ export async function server_get_artifacts_by_herd(herd_id, limit = 50, offset = 0, options) {
29
+ const supabase = options?.client || (await newServerClient());
7
30
  const { data, error } = await supabase.rpc("get_artifacts_for_herd", {
8
31
  herd_id_caller: herd_id,
9
32
  limit_caller: limit,
10
33
  offset_caller: offset,
34
+ start_timestamp: options?.start_timestamp,
35
+ end_timestamp: options?.end_timestamp,
11
36
  });
12
37
  if (error) {
13
38
  return IWebResponse.error(error.message).to_compatible();
@@ -22,7 +47,7 @@ export async function server_get_artifacts_by_herd(herd_id, limit = 50, offset =
22
47
  if (uniqueFilePaths.length === 0) {
23
48
  return IWebResponse.success(data).to_compatible();
24
49
  }
25
- const signedUrls = await generateSignedUrlsBatch(uniqueFilePaths, undefined, client);
50
+ const signedUrls = await generateSignedUrlsBatch(uniqueFilePaths, undefined, options?.client);
26
51
  const urlMap = new Map();
27
52
  uniqueFilePaths.forEach((path, index) => {
28
53
  urlMap.set(path, signedUrls[index]);
@@ -78,14 +103,16 @@ export async function server_get_total_artifacts_by_herd(herd_id) {
78
103
  }
79
104
  return IWebResponse.success(data || 0).to_compatible();
80
105
  }
81
- export async function server_get_artifacts_by_device_ids_batch(device_ids, limit_per_device = 10, client) {
82
- const supabase = client || (await newServerClient());
106
+ export async function server_get_artifacts_by_device_ids_batch(device_ids, limit_per_device = 10, options) {
107
+ const supabase = options?.client || (await newServerClient());
83
108
  if (device_ids.length === 0) {
84
109
  return {};
85
110
  }
86
111
  const { data, error } = await supabase.rpc("get_artifacts_for_devices_batch", {
87
112
  device_ids: device_ids,
88
113
  limit_per_device: limit_per_device,
114
+ start_timestamp: options?.start_timestamp,
115
+ end_timestamp: options?.end_timestamp,
89
116
  });
90
117
  if (error || !data) {
91
118
  console.warn("Error fetching artifacts batch:", error?.message);
@@ -97,7 +124,7 @@ export async function server_get_artifacts_by_device_ids_batch(device_ids, limit
97
124
  .filter((path) => !!path)));
98
125
  let artifactsWithUrls = data;
99
126
  if (uniqueFilePaths.length > 0) {
100
- const signedUrls = await generateSignedUrlsBatch(uniqueFilePaths, undefined, client);
127
+ const signedUrls = await generateSignedUrlsBatch(uniqueFilePaths, undefined, options?.client);
101
128
  const urlMap = new Map();
102
129
  uniqueFilePaths.forEach((path, index) => {
103
130
  urlMap.set(path, signedUrls[index]);
@@ -1,5 +1,5 @@
1
1
  import { BoundingBox, EnumSourceBoundingBox } from "../types/bounding_boxes";
2
- import { Tag } from "../types/db";
3
- export declare function convertManualBoundingBoxToTag(boundingBox: BoundingBox, event_id: number): Tag;
2
+ import { Tag, TagInsert } from "../types/db";
3
+ export declare function convertManualBoundingBoxToTag(boundingBox: BoundingBox, event_id: number): TagInsert;
4
4
  export declare function convertTagToBoundingBox(tag: Tag, source: EnumSourceBoundingBox): BoundingBox;
5
5
  export declare function formatBoundingBoxLabel(label: string): string;
@@ -1,12 +1,11 @@
1
1
  import { SelectionStatus, } from "../types/bounding_boxes";
2
2
  export function convertManualBoundingBoxToTag(boundingBox, event_id) {
3
3
  const newClassName = boundingBox.label;
4
- // try to convert if nan, make it 0
5
4
  let newId = Number(boundingBox.id);
6
5
  if (isNaN(newId)) {
7
6
  newId = 0;
8
7
  }
9
- const newTag = {
8
+ return {
10
9
  id: newId,
11
10
  x: boundingBox.xCenterPercentage,
12
11
  y: boundingBox.yCenterPercentage,
@@ -15,11 +14,10 @@ export function convertManualBoundingBoxToTag(boundingBox, event_id) {
15
14
  inserted_at: new Date().toISOString(),
16
15
  conf: 1,
17
16
  observation_type: "manual",
18
- class_name: newClassName,
17
+ class: newClassName,
19
18
  event_id: event_id,
20
19
  location: null,
21
20
  };
22
- return newTag;
23
21
  }
24
22
  export function convertTagToBoundingBox(tag, source) {
25
23
  const newBoundingBox = {
@@ -27,7 +25,7 @@ export function convertTagToBoundingBox(tag, source) {
27
25
  yCenterPercentage: tag.y,
28
26
  widthPercentage: tag.width,
29
27
  heightPercentage: tag.height,
30
- label: tag.class_name,
28
+ label: tag.class,
31
29
  id: tag.id ? tag.id.toString() : "0",
32
30
  left: 0,
33
31
  top: 0,
@@ -1,7 +1,8 @@
1
1
  import { IWebResponseCompatible } from "../types/requests";
2
2
  import { EnumSessionsVisibility } from "../types/events";
3
- import { IEvent, EventInsert, EventUpdate } from "../types/db";
3
+ import { IEvent, IEventWithMediaUrl, EventInsert, EventUpdate } from "../types/db";
4
4
  import { SupabaseClient } from "@supabase/supabase-js";
5
+ export declare function server_get_event_by_id(id: number, client?: SupabaseClient): Promise<IWebResponseCompatible<IEventWithMediaUrl | null>>;
5
6
  export declare function server_get_total_events_by_herd(herd_id: number, sessions_visibility: EnumSessionsVisibility): Promise<IWebResponseCompatible<number>>;
6
7
  export declare function server_insert_event(events: EventInsert | EventInsert[], client?: SupabaseClient): Promise<IWebResponseCompatible<IEvent[]>>;
7
8
  export declare function server_update_event(events: (EventUpdate & {
@@ -2,6 +2,30 @@
2
2
  import { newServerClient } from "../supabase/server";
3
3
  import { EnumWebResponse, IWebResponse, } from "../types/requests";
4
4
  import { EnumSessionsVisibility } from "../types/events";
5
+ import { generateSignedUrl } from "./storage";
6
+ export async function server_get_event_by_id(id, client) {
7
+ const supabase = client || (await newServerClient());
8
+ const { data, error } = await supabase
9
+ .from("events")
10
+ .select("*")
11
+ .eq("id", id)
12
+ .single();
13
+ if (error) {
14
+ return IWebResponse.error(error.message).to_compatible();
15
+ }
16
+ if (!data) {
17
+ return IWebResponse.success(null).to_compatible();
18
+ }
19
+ const event = data;
20
+ let media_url = null;
21
+ if (event.file_path) {
22
+ media_url = await generateSignedUrl(event.file_path, undefined, client);
23
+ }
24
+ return IWebResponse.success({
25
+ ...event,
26
+ media_url,
27
+ }).to_compatible();
28
+ }
5
29
  // function to get total number of events for a herd
6
30
  export async function server_get_total_events_by_herd(herd_id, sessions_visibility) {
7
31
  const supabase = await newServerClient();
@@ -125,7 +125,7 @@ export async function server_update_tags(tags) {
125
125
  y: updateData.y,
126
126
  width: updateData.width,
127
127
  height: updateData.height,
128
- class_name: updateData.class_name,
128
+ class: updateData.class,
129
129
  conf: updateData.conf,
130
130
  observation_type: updateData.observation_type,
131
131
  })
@@ -402,21 +402,40 @@ export async function get_event_and_tags_by_event_id(event_id) {
402
402
  timestamp_observation: data[0].timestamp_observation,
403
403
  is_public: data[0].is_public,
404
404
  herd_id: deviceData.herd_id,
405
- tags: (data[0].tags || []).map((tag) => ({
406
- id: tag.id,
407
- inserted_at: tag.inserted_at,
408
- x: tag.x,
409
- y: tag.y,
410
- width: tag.width,
411
- conf: tag.conf,
412
- observation_type: tag.observation_type,
413
- event_id: tag.event_id,
414
- class_name: tag.class_name,
415
- height: tag.height,
416
- location: tag.location,
417
- latitude: tag.location ? extractLatitude(tag.location) : null,
418
- longitude: tag.location ? extractLongitude(tag.location) : null,
419
- })),
405
+ tags: (data[0].tags || []).map((tag) => {
406
+ const loc = tag.location;
407
+ const lat = loc ? extractLatitude(loc) : null;
408
+ const lon = loc ? extractLongitude(loc) : null;
409
+ return {
410
+ id: tag.id,
411
+ inserted_at: tag.inserted_at,
412
+ artifact_id: tag.artifact_id,
413
+ event_id: tag.event_id,
414
+ detector: tag.detector,
415
+ width: tag.width,
416
+ height: tag.height,
417
+ conf: tag.conf,
418
+ x: tag.x,
419
+ y: tag.y,
420
+ class: tag.class,
421
+ timestamp_observation: tag.timestamp_observation,
422
+ frame_index: tag.frame_index ?? null,
423
+ origin_location: tag.origin_location ?? null,
424
+ origin_pitch: tag.origin_pitch,
425
+ origin_heading: tag.origin_heading,
426
+ origin_roll: tag.origin_roll,
427
+ sensor_pitch: tag.sensor_pitch,
428
+ sensor_yaw: tag.sensor_yaw,
429
+ sensor_roll: tag.sensor_roll,
430
+ origin_height: tag.origin_height,
431
+ subject_location: tag.subject_location ?? null,
432
+ subject_height: tag.subject_height,
433
+ origin_latitude: lat,
434
+ origin_longitude: lon,
435
+ subject_latitude: null,
436
+ subject_longitude: null,
437
+ };
438
+ }),
420
439
  earthranger_url: data[0].earthranger_url,
421
440
  file_path: data[0].file_path,
422
441
  };
@@ -1,5 +1,5 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
- import { IArtifactWithMediaUrl, ISessionWithCoordinates, IEventAndTagsPrettyLocation } from "../types/db";
2
+ import { IArtifactWithMediaUrl, ISessionWithCoordinates, IEventAndTagsPrettyLocation, IFeedItem } from "../types/db";
3
3
  interface UseInfiniteScrollOptions {
4
4
  limit?: number;
5
5
  enabled?: boolean;
@@ -20,5 +20,7 @@ export declare const useInfiniteEventsByHerd: (herdId: number, options: UseInfin
20
20
  export declare const useInfiniteEventsByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IEventAndTagsPrettyLocation>;
21
21
  export declare const useInfiniteArtifactsByHerd: (herdId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IArtifactWithMediaUrl>;
22
22
  export declare const useInfiniteArtifactsByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IArtifactWithMediaUrl>;
23
+ export declare const useInfiniteFeedByHerd: (herdId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IFeedItem>;
24
+ export declare const useInfiniteFeedByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IFeedItem>;
23
25
  export declare const useIntersectionObserver: (callback: () => void, options?: IntersectionObserverInit) => import("react").Dispatch<import("react").SetStateAction<Element | null>>;
24
26
  export {};
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useMemo, useEffect, useRef } from "react";
2
- import { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, } from "../store/api";
2
+ import { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, useGetFeedInfiniteByHerdQuery, useGetFeedInfiniteByDeviceQuery, } from "../store/api";
3
3
  // =====================================================
4
4
  // SESSIONS INFINITE SCROLL HOOKS
5
5
  // =====================================================
@@ -407,6 +407,127 @@ export const useInfiniteArtifactsByDevice = (deviceId, options) => {
407
407
  error: currentQuery.error,
408
408
  };
409
409
  };
410
+ const feedCursorEq = (a, b) => {
411
+ if (a === null && b === null)
412
+ return true;
413
+ if (!a || !b)
414
+ return false;
415
+ return a.timestamp === b.timestamp && a.id === b.id && a.feed_type === b.feed_type;
416
+ };
417
+ export const useInfiniteFeedByHerd = (herdId, options) => {
418
+ const [pages, setPages] = useState([]);
419
+ const [currentCursor, setCurrentCursor] = useState(null);
420
+ const prevHerdIdRef = useRef();
421
+ const currentQuery = useGetFeedInfiniteByHerdQuery({
422
+ herdId,
423
+ limit: options.limit || 20,
424
+ cursor: currentCursor,
425
+ supabase: options.supabase,
426
+ }, { skip: !options.enabled || !herdId });
427
+ useEffect(() => {
428
+ if (prevHerdIdRef.current !== undefined &&
429
+ prevHerdIdRef.current !== herdId &&
430
+ options.enabled &&
431
+ herdId) {
432
+ setPages([]);
433
+ setCurrentCursor(null);
434
+ }
435
+ prevHerdIdRef.current = herdId;
436
+ }, [herdId, options.enabled]);
437
+ useEffect(() => {
438
+ if (currentQuery.data && !currentQuery.isLoading) {
439
+ setPages((prev) => {
440
+ const existingPage = prev.find((p) => feedCursorEq(p.cursor, currentCursor));
441
+ if (!existingPage) {
442
+ return [
443
+ ...prev,
444
+ { cursor: currentCursor, data: currentQuery.data.items },
445
+ ];
446
+ }
447
+ return prev;
448
+ });
449
+ }
450
+ }, [currentQuery.data, currentQuery.isLoading, currentCursor]);
451
+ const loadMore = useCallback(() => {
452
+ if (currentQuery.data?.hasMore &&
453
+ currentQuery.data.nextCursor &&
454
+ !currentQuery.isLoading) {
455
+ setCurrentCursor(currentQuery.data.nextCursor);
456
+ }
457
+ }, [currentQuery.data, currentQuery.isLoading]);
458
+ const refetch = useCallback(() => {
459
+ setPages([]);
460
+ setCurrentCursor(null);
461
+ currentQuery.refetch();
462
+ }, [currentQuery]);
463
+ const allItems = useMemo(() => pages.flatMap((p) => p.data), [pages]);
464
+ return {
465
+ items: allItems,
466
+ isLoading: currentQuery.isLoading && pages.length === 0,
467
+ isLoadingMore: currentQuery.isLoading && pages.length > 0,
468
+ hasMore: currentQuery.data?.hasMore ?? false,
469
+ loadMore,
470
+ refetch,
471
+ error: currentQuery.error,
472
+ };
473
+ };
474
+ export const useInfiniteFeedByDevice = (deviceId, options) => {
475
+ const [pages, setPages] = useState([]);
476
+ const [currentCursor, setCurrentCursor] = useState(null);
477
+ const prevDeviceIdRef = useRef();
478
+ const currentQuery = useGetFeedInfiniteByDeviceQuery({
479
+ deviceId,
480
+ limit: options.limit || 20,
481
+ cursor: currentCursor,
482
+ supabase: options.supabase,
483
+ }, { skip: !options.enabled || !deviceId });
484
+ useEffect(() => {
485
+ if (prevDeviceIdRef.current !== undefined &&
486
+ prevDeviceIdRef.current !== deviceId &&
487
+ options.enabled &&
488
+ deviceId) {
489
+ setPages([]);
490
+ setCurrentCursor(null);
491
+ }
492
+ prevDeviceIdRef.current = deviceId;
493
+ }, [deviceId, options.enabled]);
494
+ useEffect(() => {
495
+ if (currentQuery.data && !currentQuery.isLoading) {
496
+ setPages((prev) => {
497
+ const existingPage = prev.find((p) => feedCursorEq(p.cursor, currentCursor));
498
+ if (!existingPage) {
499
+ return [
500
+ ...prev,
501
+ { cursor: currentCursor, data: currentQuery.data.items },
502
+ ];
503
+ }
504
+ return prev;
505
+ });
506
+ }
507
+ }, [currentQuery.data, currentQuery.isLoading, currentCursor]);
508
+ const loadMore = useCallback(() => {
509
+ if (currentQuery.data?.hasMore &&
510
+ currentQuery.data.nextCursor &&
511
+ !currentQuery.isLoading) {
512
+ setCurrentCursor(currentQuery.data.nextCursor);
513
+ }
514
+ }, [currentQuery.data, currentQuery.isLoading]);
515
+ const refetch = useCallback(() => {
516
+ setPages([]);
517
+ setCurrentCursor(null);
518
+ currentQuery.refetch();
519
+ }, [currentQuery]);
520
+ const allItems = useMemo(() => pages.flatMap((p) => p.data), [pages]);
521
+ return {
522
+ items: allItems,
523
+ isLoading: currentQuery.isLoading && pages.length === 0,
524
+ isLoadingMore: currentQuery.isLoading && pages.length > 0,
525
+ hasMore: currentQuery.data?.hasMore ?? false,
526
+ loadMore,
527
+ refetch,
528
+ error: currentQuery.error,
529
+ };
530
+ };
410
531
  // =====================================================
411
532
  // INTERSECTION OBSERVER HOOK FOR AUTO-LOADING
412
533
  // =====================================================