@weirdfingers/boards 0.1.4
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.d.mts +508 -0
- package/dist/index.d.ts +508 -0
- package/dist/index.js +1150 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1096 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
- package/src/auth/context.tsx +104 -0
- package/src/auth/hooks/useAuth.ts +6 -0
- package/src/auth/providers/__tests__/none.test.ts +187 -0
- package/src/auth/providers/base.ts +62 -0
- package/src/auth/providers/none.ts +157 -0
- package/src/auth/types.ts +67 -0
- package/src/config/ApiConfigContext.tsx +47 -0
- package/src/graphql/client.ts +130 -0
- package/src/graphql/operations.ts +293 -0
- package/src/hooks/useBoard.ts +323 -0
- package/src/hooks/useBoards.ts +138 -0
- package/src/hooks/useGeneration.ts +429 -0
- package/src/hooks/useGenerators.ts +44 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +25 -0
- package/src/providers/BoardsProvider.tsx +68 -0
- package/src/test-setup.ts +29 -0
- package/tsconfig.json +37 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for managing AI generations with real-time progress via SSE.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useState, useEffect, useRef } from "react";
|
|
6
|
+
import { fetchEventSource } from "@microsoft/fetch-event-source";
|
|
7
|
+
import { useMutation } from "urql";
|
|
8
|
+
import {
|
|
9
|
+
CREATE_GENERATION,
|
|
10
|
+
CANCEL_GENERATION,
|
|
11
|
+
RETRY_GENERATION,
|
|
12
|
+
CreateGenerationInput,
|
|
13
|
+
ArtifactType,
|
|
14
|
+
} from "../graphql/operations";
|
|
15
|
+
import { useAuth } from "../auth/context";
|
|
16
|
+
import { useApiConfig } from "../config/ApiConfigContext";
|
|
17
|
+
|
|
18
|
+
export interface GenerationRequest {
|
|
19
|
+
model: string;
|
|
20
|
+
artifactType: ArtifactType; // Allow string for flexibility with new types
|
|
21
|
+
inputs: GenerationInputs;
|
|
22
|
+
boardId: string;
|
|
23
|
+
options?: GenerationOptions;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GenerationInputs {
|
|
27
|
+
prompt: string;
|
|
28
|
+
negativePrompt?: string;
|
|
29
|
+
image?: string | File;
|
|
30
|
+
mask?: string | File;
|
|
31
|
+
loras?: LoRAInput[];
|
|
32
|
+
seed?: number;
|
|
33
|
+
steps?: number;
|
|
34
|
+
guidance?: number;
|
|
35
|
+
aspectRatio?: string;
|
|
36
|
+
style?: string;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GenerationOptions {
|
|
41
|
+
priority?: "low" | "normal" | "high";
|
|
42
|
+
timeout?: number;
|
|
43
|
+
webhookUrl?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LoRAInput {
|
|
48
|
+
id: string;
|
|
49
|
+
weight: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface GenerationProgress {
|
|
53
|
+
jobId: string;
|
|
54
|
+
status: "queued" | "processing" | "completed" | "failed" | "cancelled";
|
|
55
|
+
progress: number; // 0-100
|
|
56
|
+
phase: string;
|
|
57
|
+
message?: string | null;
|
|
58
|
+
estimatedTimeRemaining?: number;
|
|
59
|
+
currentStep?: string;
|
|
60
|
+
logs?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GenerationResult {
|
|
64
|
+
id: string;
|
|
65
|
+
jobId: string;
|
|
66
|
+
boardId: string;
|
|
67
|
+
request: GenerationRequest;
|
|
68
|
+
artifacts: Artifact[];
|
|
69
|
+
credits: {
|
|
70
|
+
cost: number;
|
|
71
|
+
balanceBefore: number;
|
|
72
|
+
balance: number;
|
|
73
|
+
};
|
|
74
|
+
performance: {
|
|
75
|
+
queueTime: number;
|
|
76
|
+
processingTime: number;
|
|
77
|
+
totalTime: number;
|
|
78
|
+
};
|
|
79
|
+
createdAt: Date;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Artifact {
|
|
83
|
+
id: string;
|
|
84
|
+
type: string;
|
|
85
|
+
url: string;
|
|
86
|
+
thumbnailUrl?: string;
|
|
87
|
+
metadata: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface GenerationHook {
|
|
91
|
+
// Current generation state
|
|
92
|
+
progress: GenerationProgress | null;
|
|
93
|
+
result: GenerationResult | null;
|
|
94
|
+
error: Error | null;
|
|
95
|
+
isGenerating: boolean;
|
|
96
|
+
|
|
97
|
+
// Operations
|
|
98
|
+
submit: (request: GenerationRequest) => Promise<string>;
|
|
99
|
+
cancel: (jobId: string) => Promise<void>;
|
|
100
|
+
retry: (jobId: string) => Promise<void>;
|
|
101
|
+
|
|
102
|
+
// History
|
|
103
|
+
history: GenerationResult[];
|
|
104
|
+
clearHistory: () => void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function useGeneration(): GenerationHook {
|
|
108
|
+
const [progress, setProgress] = useState<GenerationProgress | null>(null);
|
|
109
|
+
const [result, setResult] = useState<GenerationResult | null>(null);
|
|
110
|
+
const [error, setError] = useState<Error | null>(null);
|
|
111
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
112
|
+
const [history, setHistory] = useState<GenerationResult[]>([]);
|
|
113
|
+
|
|
114
|
+
// Get API configuration and auth
|
|
115
|
+
const { apiUrl } = useApiConfig();
|
|
116
|
+
const auth = useAuth();
|
|
117
|
+
|
|
118
|
+
// Keep track of active SSE connections (using AbortControllers)
|
|
119
|
+
const abortControllers = useRef<Map<string, AbortController>>(new Map());
|
|
120
|
+
|
|
121
|
+
// Mutations
|
|
122
|
+
const [, createGenerationMutation] = useMutation(CREATE_GENERATION);
|
|
123
|
+
const [, cancelGenerationMutation] = useMutation(CANCEL_GENERATION);
|
|
124
|
+
const [, retryGenerationMutation] = useMutation(RETRY_GENERATION);
|
|
125
|
+
|
|
126
|
+
// Clean up SSE connections on unmount
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
return () => {
|
|
129
|
+
abortControllers.current.forEach((controller) => {
|
|
130
|
+
controller.abort();
|
|
131
|
+
});
|
|
132
|
+
abortControllers.current.clear();
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
const connectToSSE = useCallback(
|
|
137
|
+
async (jobId: string) => {
|
|
138
|
+
// Close existing connection if any
|
|
139
|
+
const existingController = abortControllers.current.get(jobId);
|
|
140
|
+
if (existingController) {
|
|
141
|
+
existingController.abort();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Create new abort controller
|
|
145
|
+
const abortController = new AbortController();
|
|
146
|
+
abortControllers.current.set(jobId, abortController);
|
|
147
|
+
|
|
148
|
+
// Get auth token
|
|
149
|
+
const token = await auth.getToken();
|
|
150
|
+
|
|
151
|
+
// Build headers
|
|
152
|
+
const headers: Record<string, string> = {
|
|
153
|
+
Accept: "text/event-stream",
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (token) {
|
|
157
|
+
headers.Authorization = `Bearer ${token}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Connect to SSE endpoint directly
|
|
161
|
+
const sseUrl = `${apiUrl}/api/sse/generations/${jobId}/progress`;
|
|
162
|
+
console.log("SSE: Connecting to", sseUrl, "with headers:", headers);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await fetchEventSource(sseUrl, {
|
|
166
|
+
headers,
|
|
167
|
+
signal: abortController.signal,
|
|
168
|
+
|
|
169
|
+
async onopen(response) {
|
|
170
|
+
console.log(
|
|
171
|
+
"SSE: Connection opened",
|
|
172
|
+
response.status,
|
|
173
|
+
response.statusText
|
|
174
|
+
);
|
|
175
|
+
if (response.ok) {
|
|
176
|
+
console.log("SSE: Connection successful");
|
|
177
|
+
} else {
|
|
178
|
+
console.error("SSE: Connection failed", response.status);
|
|
179
|
+
throw new Error(`SSE connection failed: ${response.statusText}`);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
onmessage(event) {
|
|
184
|
+
console.log("SSE: Raw event received:", event);
|
|
185
|
+
|
|
186
|
+
// Skip empty messages (like keep-alive comments)
|
|
187
|
+
if (!event.data || event.data.trim() === "") {
|
|
188
|
+
console.log("SSE: Skipping empty message");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const progressData: GenerationProgress = JSON.parse(event.data);
|
|
194
|
+
console.log("SSE: progress data received:", progressData);
|
|
195
|
+
setProgress(progressData);
|
|
196
|
+
|
|
197
|
+
// If generation is complete, handle the result
|
|
198
|
+
if (
|
|
199
|
+
progressData.status === "completed" ||
|
|
200
|
+
progressData.status === "failed" ||
|
|
201
|
+
progressData.status === "cancelled"
|
|
202
|
+
) {
|
|
203
|
+
setIsGenerating(false);
|
|
204
|
+
|
|
205
|
+
if (progressData.status === "completed") {
|
|
206
|
+
// TODO: Fetch the complete result from GraphQL
|
|
207
|
+
// For now, create a mock result
|
|
208
|
+
const mockResult: GenerationResult = {
|
|
209
|
+
id: progressData.jobId,
|
|
210
|
+
jobId: progressData.jobId,
|
|
211
|
+
boardId: "", // Would be filled from the original request
|
|
212
|
+
request: {} as GenerationRequest,
|
|
213
|
+
artifacts: [],
|
|
214
|
+
credits: { cost: 0, balanceBefore: 0, balance: 0 },
|
|
215
|
+
performance: {
|
|
216
|
+
queueTime: 0,
|
|
217
|
+
processingTime: 0,
|
|
218
|
+
totalTime: 0,
|
|
219
|
+
},
|
|
220
|
+
createdAt: new Date(),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
setResult(mockResult);
|
|
224
|
+
setHistory((prev) => [...prev, mockResult]);
|
|
225
|
+
} else if (progressData.status === "failed") {
|
|
226
|
+
setError(new Error("Generation failed"));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Close connection
|
|
230
|
+
abortController.abort();
|
|
231
|
+
abortControllers.current.delete(jobId);
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error("Failed to parse SSE message:", err);
|
|
235
|
+
setError(new Error("Failed to parse progress update"));
|
|
236
|
+
setIsGenerating(false);
|
|
237
|
+
abortController.abort();
|
|
238
|
+
abortControllers.current.delete(jobId);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
onerror(err) {
|
|
243
|
+
console.error("SSE connection error:", err);
|
|
244
|
+
console.error("SSE error details:", {
|
|
245
|
+
message: err instanceof Error ? err.message : String(err),
|
|
246
|
+
jobId,
|
|
247
|
+
url: sseUrl,
|
|
248
|
+
});
|
|
249
|
+
setError(new Error("Lost connection to generation progress"));
|
|
250
|
+
setIsGenerating(false);
|
|
251
|
+
abortController.abort();
|
|
252
|
+
abortControllers.current.delete(jobId);
|
|
253
|
+
// Re-throw to stop retry
|
|
254
|
+
throw err;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
openWhenHidden: true, // Keep connection open when tab is hidden
|
|
258
|
+
});
|
|
259
|
+
} catch (err) {
|
|
260
|
+
// Connection was aborted or failed
|
|
261
|
+
if (abortController.signal.aborted) {
|
|
262
|
+
console.log("SSE connection aborted for job:", jobId);
|
|
263
|
+
} else {
|
|
264
|
+
console.error("SSE connection failed:", err);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
[apiUrl, auth]
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const submit = useCallback(
|
|
272
|
+
async (request: GenerationRequest): Promise<string> => {
|
|
273
|
+
setError(null);
|
|
274
|
+
setProgress(null);
|
|
275
|
+
setResult(null);
|
|
276
|
+
setIsGenerating(true);
|
|
277
|
+
|
|
278
|
+
// Convert the request to the GraphQL input format
|
|
279
|
+
const input: CreateGenerationInput = {
|
|
280
|
+
boardId: request.boardId,
|
|
281
|
+
generatorName: request.model,
|
|
282
|
+
artifactType: request.artifactType,
|
|
283
|
+
inputParams: {
|
|
284
|
+
...request.inputs,
|
|
285
|
+
...request.options,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Retry logic for generation submission
|
|
290
|
+
let lastError: Error | null = null;
|
|
291
|
+
const maxRetries = 2; // Fewer retries for generation as it's expensive
|
|
292
|
+
|
|
293
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
294
|
+
try {
|
|
295
|
+
// Submit generation via GraphQL
|
|
296
|
+
const result = await createGenerationMutation({ input });
|
|
297
|
+
|
|
298
|
+
if (result.error) {
|
|
299
|
+
throw new Error(result.error.message);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!result.data?.createGeneration) {
|
|
303
|
+
throw new Error("Failed to create generation");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const jobId = result.data.createGeneration.id;
|
|
307
|
+
|
|
308
|
+
// Connect to SSE for progress updates
|
|
309
|
+
connectToSSE(jobId);
|
|
310
|
+
|
|
311
|
+
// Re-enable the submit button now that submission is complete
|
|
312
|
+
// The SSE connection will continue tracking progress in the background
|
|
313
|
+
setIsGenerating(false);
|
|
314
|
+
|
|
315
|
+
return jobId;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
lastError =
|
|
318
|
+
err instanceof Error
|
|
319
|
+
? err
|
|
320
|
+
: new Error("Failed to submit generation");
|
|
321
|
+
|
|
322
|
+
// Don't retry on certain types of errors
|
|
323
|
+
if (
|
|
324
|
+
lastError.message.includes("insufficient credits") ||
|
|
325
|
+
lastError.message.includes("validation") ||
|
|
326
|
+
lastError.message.includes("unauthorized") ||
|
|
327
|
+
lastError.message.includes("forbidden")
|
|
328
|
+
) {
|
|
329
|
+
setError(lastError);
|
|
330
|
+
setIsGenerating(false);
|
|
331
|
+
throw lastError;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// If this was the last attempt, throw the error
|
|
335
|
+
if (attempt === maxRetries) {
|
|
336
|
+
setError(lastError);
|
|
337
|
+
setIsGenerating(false);
|
|
338
|
+
throw lastError;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Wait before retrying (shorter delay for generations)
|
|
342
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const finalError =
|
|
347
|
+
lastError || new Error("Failed to submit generation after retries");
|
|
348
|
+
setError(finalError);
|
|
349
|
+
setIsGenerating(false);
|
|
350
|
+
throw finalError;
|
|
351
|
+
},
|
|
352
|
+
[createGenerationMutation, connectToSSE]
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const cancel = useCallback(
|
|
356
|
+
async (jobId: string): Promise<void> => {
|
|
357
|
+
try {
|
|
358
|
+
// Cancel via GraphQL
|
|
359
|
+
const result = await cancelGenerationMutation({ id: jobId });
|
|
360
|
+
|
|
361
|
+
if (result.error) {
|
|
362
|
+
throw new Error(result.error.message);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Close SSE connection
|
|
366
|
+
const controller = abortControllers.current.get(jobId);
|
|
367
|
+
if (controller) {
|
|
368
|
+
controller.abort();
|
|
369
|
+
abortControllers.current.delete(jobId);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
setIsGenerating(false);
|
|
373
|
+
setProgress((prev) => (prev ? { ...prev, status: "cancelled" } : null));
|
|
374
|
+
} catch (err) {
|
|
375
|
+
setError(
|
|
376
|
+
err instanceof Error ? err : new Error("Failed to cancel generation")
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
[cancelGenerationMutation]
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const retry = useCallback(
|
|
384
|
+
async (jobId: string): Promise<void> => {
|
|
385
|
+
try {
|
|
386
|
+
setError(null);
|
|
387
|
+
setIsGenerating(true);
|
|
388
|
+
|
|
389
|
+
// Retry via GraphQL
|
|
390
|
+
const result = await retryGenerationMutation({ id: jobId });
|
|
391
|
+
|
|
392
|
+
if (result.error) {
|
|
393
|
+
throw new Error(result.error.message);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!result.data?.retryGeneration) {
|
|
397
|
+
throw new Error("Failed to retry generation");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const newJobId = result.data.retryGeneration.id;
|
|
401
|
+
|
|
402
|
+
// Connect to SSE for the retried job
|
|
403
|
+
connectToSSE(newJobId);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
setError(
|
|
406
|
+
err instanceof Error ? err : new Error("Failed to retry generation")
|
|
407
|
+
);
|
|
408
|
+
setIsGenerating(false);
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
[retryGenerationMutation, connectToSSE]
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const clearHistory = useCallback(() => {
|
|
415
|
+
setHistory([]);
|
|
416
|
+
}, []);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
progress,
|
|
420
|
+
result,
|
|
421
|
+
error,
|
|
422
|
+
isGenerating,
|
|
423
|
+
submit,
|
|
424
|
+
cancel,
|
|
425
|
+
retry,
|
|
426
|
+
history,
|
|
427
|
+
clearHistory,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for fetching available generators.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { useQuery } from "urql";
|
|
7
|
+
import { ArtifactType, GET_GENERATORS } from "../graphql/operations";
|
|
8
|
+
|
|
9
|
+
interface Generator {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
artifactType: ArtifactType;
|
|
13
|
+
inputSchema: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UseGeneratorsOptions {
|
|
17
|
+
artifactType?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface GeneratorsHook {
|
|
21
|
+
generators: Generator[];
|
|
22
|
+
loading: boolean;
|
|
23
|
+
error: Error | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useGenerators(
|
|
27
|
+
options: UseGeneratorsOptions = {}
|
|
28
|
+
): GeneratorsHook {
|
|
29
|
+
const { artifactType } = options;
|
|
30
|
+
|
|
31
|
+
// Query for generators
|
|
32
|
+
const [{ data, fetching, error }] = useQuery({
|
|
33
|
+
query: GET_GENERATORS,
|
|
34
|
+
variables: artifactType ? { artifactType } : {},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const generators = useMemo(() => data?.generators || [], [data?.generators]);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
generators,
|
|
41
|
+
loading: fetching,
|
|
42
|
+
error: error ? new Error(error.message) : null,
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const VERSION = "0.1.0";
|
|
2
|
+
|
|
3
|
+
// Core auth exports
|
|
4
|
+
export * from "./auth/types";
|
|
5
|
+
export * from "./auth/hooks/useAuth";
|
|
6
|
+
export { AuthProvider } from "./auth/context";
|
|
7
|
+
export { BaseAuthProvider } from "./auth/providers/base";
|
|
8
|
+
export { NoAuthProvider } from "./auth/providers/none"; // Only no-auth included for dev
|
|
9
|
+
|
|
10
|
+
// API configuration
|
|
11
|
+
export { useApiConfig } from "./config/ApiConfigContext";
|
|
12
|
+
export type { ApiConfig } from "./config/ApiConfigContext";
|
|
13
|
+
|
|
14
|
+
// GraphQL exports
|
|
15
|
+
export { createGraphQLClient } from "./graphql/client";
|
|
16
|
+
export * from "./graphql/operations";
|
|
17
|
+
|
|
18
|
+
// Core hooks
|
|
19
|
+
export { useBoards } from "./hooks/useBoards";
|
|
20
|
+
export { useBoard } from "./hooks/useBoard";
|
|
21
|
+
export { useGeneration } from "./hooks/useGeneration";
|
|
22
|
+
export { useGenerators } from "./hooks/useGenerators";
|
|
23
|
+
|
|
24
|
+
// Provider components
|
|
25
|
+
export { BoardsProvider } from "./providers/BoardsProvider";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main provider component that sets up GraphQL client and auth context.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ReactNode } from "react";
|
|
6
|
+
import { Provider as UrqlProvider } from "urql";
|
|
7
|
+
import { createGraphQLClient } from "../graphql/client";
|
|
8
|
+
import { AuthProvider } from "../auth/context";
|
|
9
|
+
import { BaseAuthProvider } from "../auth/providers/base";
|
|
10
|
+
import { ApiConfigProvider, ApiConfig } from "../config/ApiConfigContext";
|
|
11
|
+
|
|
12
|
+
interface BoardsProviderProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Base URL for the backend API (e.g., "http://localhost:8088")
|
|
16
|
+
* Used for REST endpoints like SSE streams
|
|
17
|
+
*/
|
|
18
|
+
apiUrl: string;
|
|
19
|
+
/**
|
|
20
|
+
* GraphQL endpoint URL (e.g., "http://localhost:8088/graphql")
|
|
21
|
+
* If not provided, defaults to `${apiUrl}/graphql`
|
|
22
|
+
*/
|
|
23
|
+
graphqlUrl?: string;
|
|
24
|
+
/**
|
|
25
|
+
* WebSocket URL for GraphQL subscriptions
|
|
26
|
+
*/
|
|
27
|
+
subscriptionUrl?: string;
|
|
28
|
+
authProvider: BaseAuthProvider;
|
|
29
|
+
tenantId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function BoardsProvider({
|
|
33
|
+
children,
|
|
34
|
+
apiUrl,
|
|
35
|
+
graphqlUrl,
|
|
36
|
+
subscriptionUrl,
|
|
37
|
+
authProvider,
|
|
38
|
+
tenantId,
|
|
39
|
+
}: BoardsProviderProps) {
|
|
40
|
+
// Default graphqlUrl if not provided
|
|
41
|
+
const resolvedGraphqlUrl = graphqlUrl || `${apiUrl}/graphql`;
|
|
42
|
+
|
|
43
|
+
// Create API config for hooks
|
|
44
|
+
const apiConfig: ApiConfig = {
|
|
45
|
+
apiUrl,
|
|
46
|
+
graphqlUrl: resolvedGraphqlUrl,
|
|
47
|
+
subscriptionUrl,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Create the GraphQL client with auth integration
|
|
51
|
+
const client = createGraphQLClient({
|
|
52
|
+
url: resolvedGraphqlUrl,
|
|
53
|
+
subscriptionUrl,
|
|
54
|
+
auth: {
|
|
55
|
+
getToken: () =>
|
|
56
|
+
authProvider.getAuthState().then((state) => state.getToken()),
|
|
57
|
+
},
|
|
58
|
+
tenantId,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<AuthProvider provider={authProvider}>
|
|
63
|
+
<ApiConfigProvider config={apiConfig}>
|
|
64
|
+
<UrqlProvider value={client}>{children}</UrqlProvider>
|
|
65
|
+
</ApiConfigProvider>
|
|
66
|
+
</AuthProvider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test setup for Vitest.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { vi, afterEach } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// Mock localStorage
|
|
8
|
+
const localStorageMock = {
|
|
9
|
+
getItem: vi.fn(),
|
|
10
|
+
setItem: vi.fn(),
|
|
11
|
+
removeItem: vi.fn(),
|
|
12
|
+
clear: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
Object.defineProperty(window, 'localStorage', {
|
|
16
|
+
value: localStorageMock,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Mock fetch
|
|
20
|
+
global.fetch = vi.fn();
|
|
21
|
+
|
|
22
|
+
// Mock console methods to reduce noise in tests
|
|
23
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
24
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
25
|
+
|
|
26
|
+
// Reset all mocks after each test
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2020",
|
|
7
|
+
"DOM",
|
|
8
|
+
"DOM.Iterable"
|
|
9
|
+
],
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"rootDir": "./src",
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
"noImplicitReturns": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"esModuleInterop": true,
|
|
21
|
+
"skipLibCheck": true,
|
|
22
|
+
"forceConsistentCasingInFileNames": true,
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"moduleResolution": "node"
|
|
25
|
+
},
|
|
26
|
+
"include": [
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"exclude": [
|
|
30
|
+
"node_modules",
|
|
31
|
+
"dist",
|
|
32
|
+
"**/*.test.ts",
|
|
33
|
+
"**/__tests__/**/*",
|
|
34
|
+
"src/test-setup.ts",
|
|
35
|
+
"src/graphql/client.ts"
|
|
36
|
+
]
|
|
37
|
+
}
|
package/tsup.config.ts
ADDED