@databiosphere/findable-ui 49.1.0 → 49.2.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.
Files changed (49) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/lib/views/ExploreView/entityList/filters/components/ToggleButtonGroup/toggleButtonGroup.d.ts +2 -3
  4. package/lib/views/ExploreView/entityList/filters/components/ToggleButtonGroup/toggleButtonGroup.js +3 -4
  5. package/lib/views/ResearchView/assistant/assistant.js +3 -5
  6. package/lib/views/ResearchView/assistant/components/Form/form.d.ts +1 -2
  7. package/lib/views/ResearchView/assistant/components/Form/form.js +5 -18
  8. package/lib/views/ResearchView/assistant/components/Form/types.d.ts +1 -2
  9. package/lib/views/ResearchView/assistant/components/Form/utils.d.ts +1 -1
  10. package/lib/views/ResearchView/assistant/components/Messages/hooks/UseScroll/hook.d.ts +2 -1
  11. package/lib/views/ResearchView/assistant/components/Messages/hooks/UseScroll/hook.js +5 -2
  12. package/lib/views/ResearchView/assistant/components/ToggleButtonGroup/toggleButtonGroup.d.ts +2 -3
  13. package/lib/views/ResearchView/assistant/components/ToggleButtonGroup/toggleButtonGroup.js +3 -4
  14. package/lib/views/ResearchView/assistant/stories/assistant.stories.js +1 -1
  15. package/lib/views/ResearchView/state/provider.d.ts +4 -1
  16. package/lib/views/ResearchView/state/provider.js +5 -2
  17. package/lib/views/ResearchView/state/query/context.d.ts +5 -0
  18. package/lib/views/ResearchView/state/query/context.js +7 -0
  19. package/lib/views/ResearchView/state/query/hooks/UseQuery/hook.d.ts +6 -0
  20. package/lib/views/ResearchView/state/query/hooks/UseQuery/hook.js +9 -0
  21. package/lib/views/ResearchView/state/query/hooks/UseSubmit/hook.d.ts +7 -0
  22. package/lib/views/ResearchView/state/query/hooks/UseSubmit/hook.js +46 -0
  23. package/lib/views/ResearchView/state/query/provider.d.ts +13 -0
  24. package/lib/views/ResearchView/state/query/provider.js +15 -0
  25. package/lib/views/ResearchView/{query → state/query}/types.d.ts +5 -12
  26. package/package.json +1 -1
  27. package/src/views/ExploreView/entityList/filters/components/ToggleButtonGroup/toggleButtonGroup.tsx +8 -4
  28. package/src/views/ResearchView/assistant/assistant.tsx +3 -5
  29. package/src/views/ResearchView/assistant/components/Form/form.tsx +4 -19
  30. package/src/views/ResearchView/assistant/components/Form/types.ts +0 -2
  31. package/src/views/ResearchView/assistant/components/Form/utils.ts +1 -1
  32. package/src/views/ResearchView/assistant/components/Messages/hooks/UseScroll/hook.ts +5 -2
  33. package/src/views/ResearchView/assistant/components/ToggleButtonGroup/toggleButtonGroup.tsx +8 -4
  34. package/src/views/ResearchView/assistant/stories/assistant.stories.tsx +1 -1
  35. package/src/views/ResearchView/state/provider.tsx +8 -1
  36. package/src/views/ResearchView/state/query/context.ts +9 -0
  37. package/src/views/ResearchView/state/query/hooks/UseQuery/hook.ts +11 -0
  38. package/src/views/ResearchView/state/query/hooks/UseSubmit/hook.ts +66 -0
  39. package/src/views/ResearchView/state/query/provider.tsx +27 -0
  40. package/src/views/ResearchView/{query → state/query}/types.ts +9 -15
  41. package/tests/research.queryProvider.test.ts +321 -0
  42. package/lib/views/ResearchView/adapter/useAdapter.d.ts +0 -6
  43. package/lib/views/ResearchView/adapter/useAdapter.js +0 -15
  44. package/lib/views/ResearchView/query/useQuery.d.ts +0 -7
  45. package/lib/views/ResearchView/query/useQuery.js +0 -44
  46. package/src/views/ResearchView/adapter/useAdapter.ts +0 -19
  47. package/src/views/ResearchView/query/useQuery.ts +0 -60
  48. package/tests/research.useQuery.test.ts +0 -165
  49. /package/lib/views/ResearchView/{query → state/query}/types.js +0 -0
@@ -2,26 +2,33 @@ import { JSX, ReactNode } from "react";
2
2
  import { ChatContext } from "./context";
3
3
  import { useChatReducer } from "./hooks/UseChatReducer/hook";
4
4
  import { InitialArgs } from "./initializer/types";
5
+ import { QueryProvider } from "./query/provider";
5
6
 
6
7
  /**
7
8
  * Provider for Chat state.
8
9
  * Manages chat state such as query and response data.
10
+ * Nests QueryProvider to own the fetch lifecycle for query submission.
9
11
  *
10
12
  * @param props - Props.
11
13
  * @param props.children - Children.
12
14
  * @param props.initialArgs - Initial arguments.
15
+ * @param props.url - URL for the query endpoint.
13
16
  *
14
17
  * @returns A context provider wrapping the given children.
15
18
  */
16
19
  export function ChatProvider({
17
20
  children,
18
21
  initialArgs,
22
+ url,
19
23
  }: {
20
24
  children: ReactNode;
21
25
  initialArgs?: InitialArgs;
26
+ url: string;
22
27
  }): JSX.Element {
23
28
  const reducer = useChatReducer(initialArgs);
24
29
  return (
25
- <ChatContext.Provider value={reducer}>{children}</ChatContext.Provider>
30
+ <ChatContext.Provider value={reducer}>
31
+ <QueryProvider url={url}>{children}</QueryProvider>
32
+ </ChatContext.Provider>
26
33
  );
27
34
  }
@@ -0,0 +1,9 @@
1
+ import { createContext } from "react";
2
+ import { QueryContextValue } from "./types";
3
+
4
+ /**
5
+ * Context for the query submission provider.
6
+ */
7
+ export const QueryContext = createContext<QueryContextValue>({
8
+ onSubmit: () => Promise.resolve(),
9
+ });
@@ -0,0 +1,11 @@
1
+ import { useContext } from "react";
2
+ import { QueryContext } from "../../context";
3
+ import { QueryContextValue } from "../../types";
4
+
5
+ /**
6
+ * Hook to access query submission from the QueryProvider.
7
+ * @returns Query context value with onSubmit.
8
+ */
9
+ export const useQuery = (): QueryContextValue => {
10
+ return useContext(QueryContext);
11
+ };
@@ -0,0 +1,66 @@
1
+ import { FormEvent, useCallback, useRef } from "react";
2
+ import { fetchResponse } from "../../../../query/fetch";
3
+ import { useChatDispatch } from "../../../hooks/UseChatDispatch/hook";
4
+ import { MessageResponse } from "../../../types";
5
+ import {
6
+ OnSubmitOptions,
7
+ OnSubmitPayload,
8
+ QueryContextValue,
9
+ } from "../../types";
10
+
11
+ /**
12
+ * Hook that manages query submission and abort lifecycle.
13
+ * @param url - The API URL to send queries to.
14
+ * @returns Object containing the onSubmit handler.
15
+ */
16
+ export const useSubmit = (url: string): Pick<QueryContextValue, "onSubmit"> => {
17
+ const abortRef = useRef<AbortController>(null);
18
+ const dispatch = useChatDispatch();
19
+
20
+ const onSubmit = useCallback(
21
+ async (
22
+ e: FormEvent<HTMLFormElement>,
23
+ payload: OnSubmitPayload,
24
+ options: OnSubmitOptions,
25
+ ): Promise<void> => {
26
+ e.preventDefault();
27
+
28
+ if (options.status.loading) return;
29
+
30
+ const { query } = payload;
31
+ if (!query) return;
32
+
33
+ const form = e.currentTarget;
34
+
35
+ // Dispatch query and loading state.
36
+ dispatch.onSetQuery(query);
37
+ dispatch.onSetStatus(true);
38
+ form.reset();
39
+ options.onMutate?.(form, query);
40
+
41
+ // Abort any in-flight request.
42
+ abortRef.current?.abort();
43
+ const controller = new AbortController();
44
+ abortRef.current = controller;
45
+
46
+ await fetchResponse(url, query, {
47
+ controller,
48
+ onError: (error) => {
49
+ dispatch.onSetError(error.message);
50
+ options.onError?.(error);
51
+ },
52
+ onSettled: () => {
53
+ dispatch.onSetStatus(false);
54
+ options.onSettled?.(form);
55
+ },
56
+ onSuccess: (data) => {
57
+ dispatch.onSetMessage(data as MessageResponse);
58
+ options.onSuccess?.(data);
59
+ },
60
+ });
61
+ },
62
+ [dispatch, url],
63
+ );
64
+
65
+ return { onSubmit };
66
+ };
@@ -0,0 +1,27 @@
1
+ import { JSX, ReactNode } from "react";
2
+ import { QueryContext } from "./context";
3
+ import { useSubmit } from "./hooks/UseSubmit/hook";
4
+
5
+ /**
6
+ * Provider that owns the fetch lifecycle for query submission.
7
+ * Persists across page navigation so in-flight requests are not aborted.
8
+ * @param props - Props.
9
+ * @param props.children - Children.
10
+ * @param props.url - URL for the query endpoint.
11
+ * @returns A context provider wrapping the given children.
12
+ */
13
+ export function QueryProvider({
14
+ children,
15
+ url,
16
+ }: {
17
+ children: ReactNode;
18
+ url: string;
19
+ }): JSX.Element {
20
+ const { onSubmit } = useSubmit(url);
21
+
22
+ return (
23
+ <QueryContext.Provider value={{ onSubmit }}>
24
+ {children}
25
+ </QueryContext.Provider>
26
+ );
27
+ }
@@ -1,15 +1,5 @@
1
1
  import { FormEvent } from "react";
2
-
3
- /**
4
- * Actions returned by the useQuery hook.
5
- */
6
- export interface Actions {
7
- onSubmit: (
8
- e: FormEvent<HTMLFormElement>,
9
- payload: OnSubmitPayload,
10
- options?: OnSubmitOptions,
11
- ) => Promise<void>;
12
- }
2
+ import { Status } from "../types";
13
3
 
14
4
  /**
15
5
  * Options for the onSubmit action.
@@ -19,7 +9,7 @@ export interface OnSubmitOptions {
19
9
  onMutate?: (form: HTMLFormElement, query: string) => void;
20
10
  onSettled?: (form: HTMLFormElement) => void;
21
11
  onSuccess?: (data: unknown) => void;
22
- status?: { loading: boolean };
12
+ status: Status;
23
13
  }
24
14
 
25
15
  /**
@@ -30,8 +20,12 @@ export interface OnSubmitPayload {
30
20
  }
31
21
 
32
22
  /**
33
- * Return type for the useQuery hook.
23
+ * Context value for the QueryProvider.
34
24
  */
35
- export interface UseQuery {
36
- actions: Actions;
25
+ export interface QueryContextValue {
26
+ onSubmit: (
27
+ e: FormEvent<HTMLFormElement>,
28
+ payload: OnSubmitPayload,
29
+ options: OnSubmitOptions,
30
+ ) => Promise<void>;
37
31
  }
@@ -0,0 +1,321 @@
1
+ import { jest } from "@jest/globals";
2
+ import { act, renderHook } from "@testing-library/react";
3
+ import { FormEvent, ReactNode } from "react";
4
+ import React from "react";
5
+
6
+ /**
7
+ * Fetch callbacks passed to fetchResponse.
8
+ */
9
+ interface FetchCallbacks {
10
+ controller: AbortController;
11
+ onError: (error: Error) => void;
12
+ onSettled: () => void;
13
+ onSuccess: (data: unknown) => void;
14
+ }
15
+
16
+ // Mock fetchResponse
17
+ const mockFetchResponse = jest.fn();
18
+
19
+ jest.unstable_mockModule("../src/views/ResearchView/query/fetch", () => ({
20
+ fetchResponse: mockFetchResponse,
21
+ }));
22
+
23
+ const { useQuery } =
24
+ await import("../src/views/ResearchView/state/query/hooks/UseQuery/hook");
25
+ const { ChatProvider } =
26
+ await import("../src/views/ResearchView/state/provider");
27
+
28
+ /**
29
+ * Creates a mock form event for testing.
30
+ * @returns Mock FormEvent.
31
+ */
32
+ function createMockFormEvent(): FormEvent<HTMLFormElement> {
33
+ const mockForm = document.createElement("form");
34
+
35
+ // Mock reset
36
+ mockForm.reset = jest.fn();
37
+
38
+ return {
39
+ currentTarget: mockForm,
40
+ preventDefault: jest.fn(),
41
+ } as unknown as FormEvent<HTMLFormElement>;
42
+ }
43
+
44
+ /**
45
+ * Creates a wrapper component that provides ChatProvider with a URL.
46
+ * @param url - The query endpoint URL.
47
+ * @returns A wrapper component for renderHook.
48
+ */
49
+ function createWrapper(
50
+ url = "https://api.example.com",
51
+ ): ({ children }: { children: ReactNode }) => ReactNode {
52
+ return function Wrapper({ children }: { children: ReactNode }): ReactNode {
53
+ return React.createElement(ChatProvider, { url }, children);
54
+ };
55
+ }
56
+
57
+ describe("QueryProvider", () => {
58
+ beforeEach(() => {
59
+ mockFetchResponse.mockReset();
60
+ mockFetchResponse.mockImplementation(
61
+ async (_url: unknown, _query: unknown, callbacks: unknown) => {
62
+ (callbacks as FetchCallbacks).onSettled();
63
+ },
64
+ );
65
+ });
66
+
67
+ describe("initial state", () => {
68
+ it("should return onSubmit function", () => {
69
+ const { result } = renderHook(() => useQuery(), {
70
+ wrapper: createWrapper(),
71
+ });
72
+
73
+ expect(typeof result.current.onSubmit).toBe("function");
74
+ });
75
+ });
76
+
77
+ describe("submit guards", () => {
78
+ it("should not submit if status is loading", async () => {
79
+ const { result } = renderHook(() => useQuery(), {
80
+ wrapper: createWrapper(),
81
+ });
82
+ const event = createMockFormEvent();
83
+
84
+ await act(async () => {
85
+ await result.current.onSubmit(
86
+ event,
87
+ { query: "valid query" },
88
+ { status: { loading: true } },
89
+ );
90
+ });
91
+
92
+ expect(mockFetchResponse).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it("should not submit if query is empty", async () => {
96
+ const { result } = renderHook(() => useQuery(), {
97
+ wrapper: createWrapper(),
98
+ });
99
+ const event = createMockFormEvent();
100
+
101
+ await act(async () => {
102
+ await result.current.onSubmit(
103
+ event,
104
+ { query: "" },
105
+ { status: { loading: false } },
106
+ );
107
+ });
108
+
109
+ expect(mockFetchResponse).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it("should submit if query is provided", async () => {
113
+ const { result } = renderHook(() => useQuery(), {
114
+ wrapper: createWrapper(),
115
+ });
116
+ const event = createMockFormEvent();
117
+
118
+ await act(async () => {
119
+ await result.current.onSubmit(
120
+ event,
121
+ { query: "valid query" },
122
+ { status: { loading: false } },
123
+ );
124
+ });
125
+
126
+ expect(mockFetchResponse).toHaveBeenCalled();
127
+ });
128
+ });
129
+
130
+ describe("submit behavior", () => {
131
+ it("should call preventDefault on form event", async () => {
132
+ const { result } = renderHook(() => useQuery(), {
133
+ wrapper: createWrapper(),
134
+ });
135
+ const event = createMockFormEvent();
136
+
137
+ await act(async () => {
138
+ await result.current.onSubmit(
139
+ event,
140
+ { query: "diabetes studies" },
141
+ { status: { loading: false } },
142
+ );
143
+ });
144
+
145
+ expect(event.preventDefault).toHaveBeenCalled();
146
+ });
147
+
148
+ it("should call fetchResponse with correct arguments", async () => {
149
+ const { result } = renderHook(() => useQuery(), {
150
+ wrapper: createWrapper(),
151
+ });
152
+ const event = createMockFormEvent();
153
+
154
+ await act(async () => {
155
+ await result.current.onSubmit(
156
+ event,
157
+ { query: "diabetes studies" },
158
+ { status: { loading: false } },
159
+ );
160
+ });
161
+
162
+ expect(mockFetchResponse).toHaveBeenCalledWith(
163
+ "https://api.example.com",
164
+ "diabetes studies",
165
+ expect.objectContaining({
166
+ controller: expect.any(AbortController),
167
+ onError: expect.any(Function),
168
+ onSettled: expect.any(Function),
169
+ onSuccess: expect.any(Function),
170
+ }),
171
+ );
172
+ });
173
+
174
+ it("should pass url to fetchResponse", async () => {
175
+ const testUrl = "https://custom-api.example.com/search";
176
+ const { result } = renderHook(() => useQuery(), {
177
+ wrapper: createWrapper(testUrl),
178
+ });
179
+ const event = createMockFormEvent();
180
+
181
+ await act(async () => {
182
+ await result.current.onSubmit(
183
+ event,
184
+ { query: "cancer studies" },
185
+ { status: { loading: false } },
186
+ );
187
+ });
188
+
189
+ expect(mockFetchResponse).toHaveBeenCalledWith(
190
+ testUrl,
191
+ expect.any(String),
192
+ expect.any(Object),
193
+ );
194
+ });
195
+ });
196
+
197
+ describe("option callbacks", () => {
198
+ it("should call onMutate after dispatching query", async () => {
199
+ const onMutate = jest.fn();
200
+ const { result } = renderHook(() => useQuery(), {
201
+ wrapper: createWrapper(),
202
+ });
203
+ const event = createMockFormEvent();
204
+
205
+ await act(async () => {
206
+ await result.current.onSubmit(
207
+ event,
208
+ { query: "test query" },
209
+ { onMutate, status: { loading: false } },
210
+ );
211
+ });
212
+
213
+ expect(onMutate).toHaveBeenCalledWith(event.currentTarget, "test query");
214
+ });
215
+
216
+ it("should call onSettled after fetch completes", async () => {
217
+ const onSettled = jest.fn();
218
+ const { result } = renderHook(() => useQuery(), {
219
+ wrapper: createWrapper(),
220
+ });
221
+ const event = createMockFormEvent();
222
+
223
+ await act(async () => {
224
+ await result.current.onSubmit(
225
+ event,
226
+ { query: "test query" },
227
+ { onSettled, status: { loading: false } },
228
+ );
229
+ });
230
+
231
+ expect(onSettled).toHaveBeenCalledWith(event.currentTarget);
232
+ });
233
+
234
+ it("should call onSuccess after successful fetch", async () => {
235
+ const mockData = { message: "success" };
236
+ mockFetchResponse.mockImplementation(
237
+ async (_url: unknown, _query: unknown, callbacks: unknown) => {
238
+ (callbacks as FetchCallbacks).onSuccess(mockData);
239
+ (callbacks as FetchCallbacks).onSettled();
240
+ },
241
+ );
242
+
243
+ const onSuccess = jest.fn();
244
+ const { result } = renderHook(() => useQuery(), {
245
+ wrapper: createWrapper(),
246
+ });
247
+ const event = createMockFormEvent();
248
+
249
+ await act(async () => {
250
+ await result.current.onSubmit(
251
+ event,
252
+ { query: "test query" },
253
+ { onSuccess, status: { loading: false } },
254
+ );
255
+ });
256
+
257
+ expect(onSuccess).toHaveBeenCalledWith(mockData);
258
+ });
259
+
260
+ it("should call onError after failed fetch", async () => {
261
+ const mockError = new Error("Network error");
262
+ mockFetchResponse.mockImplementation(
263
+ async (_url: unknown, _query: unknown, callbacks: unknown) => {
264
+ (callbacks as FetchCallbacks).onError(mockError);
265
+ (callbacks as FetchCallbacks).onSettled();
266
+ },
267
+ );
268
+
269
+ const onError = jest.fn();
270
+ const { result } = renderHook(() => useQuery(), {
271
+ wrapper: createWrapper(),
272
+ });
273
+ const event = createMockFormEvent();
274
+
275
+ await act(async () => {
276
+ await result.current.onSubmit(
277
+ event,
278
+ { query: "test query" },
279
+ { onError, status: { loading: false } },
280
+ );
281
+ });
282
+
283
+ expect(onError).toHaveBeenCalledWith(mockError);
284
+ });
285
+ });
286
+
287
+ describe("abort handling", () => {
288
+ it("should create new AbortController for each submit", async () => {
289
+ const controllers: AbortController[] = [];
290
+ mockFetchResponse.mockImplementation(
291
+ async (_url: unknown, _query: unknown, callbacks: unknown) => {
292
+ controllers.push((callbacks as FetchCallbacks).controller);
293
+ (callbacks as FetchCallbacks).onSettled();
294
+ },
295
+ );
296
+
297
+ const { result } = renderHook(() => useQuery(), {
298
+ wrapper: createWrapper(),
299
+ });
300
+
301
+ await act(async () => {
302
+ await result.current.onSubmit(
303
+ createMockFormEvent(),
304
+ { query: "query 1" },
305
+ { status: { loading: false } },
306
+ );
307
+ });
308
+
309
+ await act(async () => {
310
+ await result.current.onSubmit(
311
+ createMockFormEvent(),
312
+ { query: "query 2" },
313
+ { status: { loading: false } },
314
+ );
315
+ });
316
+
317
+ expect(controllers).toHaveLength(2);
318
+ expect(controllers[0]).not.toBe(controllers[1]);
319
+ });
320
+ });
321
+ });
@@ -1,6 +0,0 @@
1
- import { UseQuery } from "../query/types";
2
- /**
3
- * Adapter hook that wires AI query to app config.
4
- * @returns AI query interface with actions.
5
- */
6
- export declare function useAdapter(): UseQuery;
@@ -1,15 +0,0 @@
1
- import { useConfig } from "../../../hooks/useConfig";
2
- import { useQuery } from "../query/useQuery";
3
- /**
4
- * Adapter hook that wires AI query to app config.
5
- * @returns AI query interface with actions.
6
- */
7
- export function useAdapter() {
8
- const { config } = useConfig();
9
- const { ai } = config;
10
- const { url } = ai || {};
11
- if (!url) {
12
- throw new Error("Chat URL is not configured");
13
- }
14
- return useQuery(url);
15
- }
@@ -1,7 +0,0 @@
1
- import { UseQuery } from "./types";
2
- /**
3
- * Custom hook for managing the actions of a chat query form.
4
- * @param url - The URL to send the query to.
5
- * @returns An object containing the actions of the query.
6
- */
7
- export declare const useQuery: (url: string) => UseQuery;
@@ -1,44 +0,0 @@
1
- import { useCallback, useEffect, useRef } from "react";
2
- import { fetchResponse } from "./fetch";
3
- /**
4
- * Custom hook for managing the actions of a chat query form.
5
- * @param url - The URL to send the query to.
6
- * @returns An object containing the actions of the query.
7
- */
8
- export const useQuery = (url) => {
9
- const abortRef = useRef(null);
10
- const onSubmit = useCallback(async (e, payload, options) => {
11
- e.preventDefault();
12
- if (options?.status?.loading)
13
- return;
14
- const form = e.currentTarget;
15
- const { query } = payload;
16
- if (!query)
17
- return;
18
- // Call onMutate callback
19
- options?.onMutate?.(form, query);
20
- // Abort any in-flight request
21
- abortRef.current?.abort();
22
- const controller = new AbortController();
23
- abortRef.current = controller;
24
- await fetchResponse(url, query, {
25
- controller,
26
- onError: (error) => {
27
- options?.onError?.(error);
28
- },
29
- onSettled: () => {
30
- options?.onSettled?.(form);
31
- },
32
- onSuccess: (data) => {
33
- options?.onSuccess?.(data);
34
- },
35
- });
36
- }, [url]);
37
- // Abort any in-flight request on unmount.
38
- useEffect(() => {
39
- return () => {
40
- abortRef.current?.abort();
41
- };
42
- }, []);
43
- return { actions: { onSubmit } };
44
- };
@@ -1,19 +0,0 @@
1
- import { useConfig } from "../../../hooks/useConfig";
2
- import { UseQuery } from "../query/types";
3
- import { useQuery } from "../query/useQuery";
4
-
5
- /**
6
- * Adapter hook that wires AI query to app config.
7
- * @returns AI query interface with actions.
8
- */
9
- export function useAdapter(): UseQuery {
10
- const { config } = useConfig();
11
- const { ai } = config;
12
- const { url } = ai || {};
13
-
14
- if (!url) {
15
- throw new Error("Chat URL is not configured");
16
- }
17
-
18
- return useQuery(url);
19
- }
@@ -1,60 +0,0 @@
1
- import { FormEvent, useCallback, useEffect, useRef } from "react";
2
- import { OnSubmitOptions, OnSubmitPayload, UseQuery } from "./types";
3
- import { fetchResponse } from "./fetch";
4
-
5
- /**
6
- * Custom hook for managing the actions of a chat query form.
7
- * @param url - The URL to send the query to.
8
- * @returns An object containing the actions of the query.
9
- */
10
- export const useQuery = (url: string): UseQuery => {
11
- const abortRef = useRef<AbortController>(null);
12
-
13
- const onSubmit = useCallback(
14
- async (
15
- e: FormEvent<HTMLFormElement>,
16
- payload: OnSubmitPayload,
17
- options?: OnSubmitOptions,
18
- ) => {
19
- e.preventDefault();
20
-
21
- if (options?.status?.loading) return;
22
-
23
- const form = e.currentTarget;
24
- const { query } = payload;
25
-
26
- if (!query) return;
27
-
28
- // Call onMutate callback
29
- options?.onMutate?.(form, query);
30
-
31
- // Abort any in-flight request
32
- abortRef.current?.abort();
33
- const controller = new AbortController();
34
- abortRef.current = controller;
35
-
36
- await fetchResponse(url, query, {
37
- controller,
38
- onError: (error) => {
39
- options?.onError?.(error);
40
- },
41
- onSettled: () => {
42
- options?.onSettled?.(form);
43
- },
44
- onSuccess: (data) => {
45
- options?.onSuccess?.(data);
46
- },
47
- });
48
- },
49
- [url],
50
- );
51
-
52
- // Abort any in-flight request on unmount.
53
- useEffect(() => {
54
- return (): void => {
55
- abortRef.current?.abort();
56
- };
57
- }, []);
58
-
59
- return { actions: { onSubmit } };
60
- };