@weirdfingers/boards 0.5.3 → 0.6.1

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.
@@ -51,6 +51,51 @@ export const GENERATION_FRAGMENT = gql`
51
51
  }
52
52
  `;
53
53
 
54
+ // Lineage fragments
55
+ export const ARTIFACT_LINEAGE_FRAGMENT = gql`
56
+ fragment ArtifactLineageFragment on ArtifactLineage {
57
+ generationId
58
+ role
59
+ artifactType
60
+ }
61
+ `;
62
+
63
+ export const ANCESTRY_NODE_FRAGMENT = gql`
64
+ ${GENERATION_FRAGMENT}
65
+ fragment AncestryNodeFragment on AncestryNode {
66
+ depth
67
+ role
68
+ generation {
69
+ ...GenerationFragment
70
+ }
71
+ parents {
72
+ depth
73
+ role
74
+ generation {
75
+ ...GenerationFragment
76
+ }
77
+ }
78
+ }
79
+ `;
80
+
81
+ export const DESCENDANT_NODE_FRAGMENT = gql`
82
+ ${GENERATION_FRAGMENT}
83
+ fragment DescendantNodeFragment on DescendantNode {
84
+ depth
85
+ role
86
+ generation {
87
+ ...GenerationFragment
88
+ }
89
+ children {
90
+ depth
91
+ role
92
+ generation {
93
+ ...GenerationFragment
94
+ }
95
+ }
96
+ }
97
+ `;
98
+
54
99
  // Auth queries
55
100
  export const GET_CURRENT_USER = gql`
56
101
  ${USER_FRAGMENT}
@@ -150,6 +195,40 @@ export const GET_GENERATION = gql`
150
195
  }
151
196
  `;
152
197
 
198
+ // Lineage queries
199
+ export const GET_ANCESTRY = gql`
200
+ ${ANCESTRY_NODE_FRAGMENT}
201
+ query GetAncestry($id: UUID!, $maxDepth: Int = 25) {
202
+ generation(id: $id) {
203
+ ancestry(maxDepth: $maxDepth) {
204
+ ...AncestryNodeFragment
205
+ }
206
+ }
207
+ }
208
+ `;
209
+
210
+ export const GET_DESCENDANTS = gql`
211
+ ${DESCENDANT_NODE_FRAGMENT}
212
+ query GetDescendants($id: UUID!, $maxDepth: Int = 25) {
213
+ generation(id: $id) {
214
+ descendants(maxDepth: $maxDepth) {
215
+ ...DescendantNodeFragment
216
+ }
217
+ }
218
+ }
219
+ `;
220
+
221
+ export const GET_INPUT_ARTIFACTS = gql`
222
+ ${ARTIFACT_LINEAGE_FRAGMENT}
223
+ query GetInputArtifacts($id: UUID!) {
224
+ generation(id: $id) {
225
+ inputArtifacts {
226
+ ...ArtifactLineageFragment
227
+ }
228
+ }
229
+ }
230
+ `;
231
+
153
232
  // Board mutations
154
233
  export const CREATE_BOARD = gql`
155
234
  ${BOARD_FRAGMENT}
@@ -243,6 +322,15 @@ export const RETRY_GENERATION = gql`
243
322
  }
244
323
  `;
245
324
 
325
+ export const UPLOAD_ARTIFACT_FROM_URL = gql`
326
+ ${GENERATION_FRAGMENT}
327
+ mutation UploadArtifactFromUrl($input: UploadArtifactInput!) {
328
+ uploadArtifact(input: $input) {
329
+ ...GenerationFragment
330
+ }
331
+ }
332
+ `;
333
+
246
334
  // Input types (these should match your backend GraphQL schema)
247
335
  export interface CreateBoardInput {
248
336
  title: string;
@@ -268,6 +356,15 @@ export interface CreateGenerationInput {
268
356
  metadata?: Record<string, unknown>;
269
357
  }
270
358
 
359
+ export interface UploadArtifactInput {
360
+ boardId: string;
361
+ artifactType: ArtifactType;
362
+ fileUrl?: string;
363
+ originalFilename?: string;
364
+ userDescription?: string;
365
+ parentGenerationId?: string;
366
+ }
367
+
271
368
  // Enums (should match backend)
272
369
  export enum BoardRole {
273
370
  VIEWER = "VIEWER",
@@ -284,10 +381,10 @@ export enum GenerationStatus {
284
381
  }
285
382
 
286
383
  export enum ArtifactType {
287
- IMAGE = "image",
288
- VIDEO = "video",
289
- AUDIO = "audio",
290
- TEXT = "text",
291
- LORA = "lora",
292
- MODEL = "model",
384
+ IMAGE = "IMAGE",
385
+ VIDEO = "VIDEO",
386
+ AUDIO = "AUDIO",
387
+ TEXT = "TEXT",
388
+ LORA = "LORA",
389
+ MODEL = "MODEL",
293
390
  }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Hook for accessing generation lineage (ancestry and descendants).
3
+ */
4
+
5
+ import { useMemo } from "react";
6
+ import { useQuery } from "urql";
7
+ import {
8
+ GET_ANCESTRY,
9
+ GET_DESCENDANTS,
10
+ GET_INPUT_ARTIFACTS,
11
+ } from "../graphql/operations";
12
+
13
+ export interface ArtifactLineage {
14
+ generationId: string;
15
+ role: string;
16
+ artifactType: string;
17
+ }
18
+
19
+ export interface AncestryNode {
20
+ generation: {
21
+ id: string;
22
+ generatorName: string;
23
+ artifactType: string;
24
+ status: string;
25
+ storageUrl?: string;
26
+ thumbnailUrl?: string;
27
+ createdAt: string;
28
+ [key: string]: unknown;
29
+ };
30
+ depth: number;
31
+ role: string | null;
32
+ parents: AncestryNode[];
33
+ }
34
+
35
+ export interface DescendantNode {
36
+ generation: {
37
+ id: string;
38
+ generatorName: string;
39
+ artifactType: string;
40
+ status: string;
41
+ storageUrl?: string;
42
+ thumbnailUrl?: string;
43
+ createdAt: string;
44
+ [key: string]: unknown;
45
+ };
46
+ depth: number;
47
+ role: string | null;
48
+ children: DescendantNode[];
49
+ }
50
+
51
+ interface UseAncestryOptions {
52
+ maxDepth?: number;
53
+ pause?: boolean;
54
+ }
55
+
56
+ interface UseAncestryHook {
57
+ ancestry: AncestryNode | null;
58
+ loading: boolean;
59
+ error: Error | null;
60
+ }
61
+
62
+ /**
63
+ * Hook for fetching the ancestry tree of a generation.
64
+ * @param generationId - The ID of the generation to fetch ancestry for
65
+ * @param options - Optional configuration (maxDepth, pause)
66
+ */
67
+ export function useAncestry(
68
+ generationId: string,
69
+ options: UseAncestryOptions = {}
70
+ ): UseAncestryHook {
71
+ const { maxDepth = 25, pause = false } = options;
72
+
73
+ const [{ data, fetching, error }] = useQuery({
74
+ query: GET_ANCESTRY,
75
+ variables: { id: generationId, maxDepth },
76
+ pause,
77
+ });
78
+
79
+ const ancestry = useMemo(
80
+ () => data?.generation?.ancestry || null,
81
+ [data?.generation?.ancestry]
82
+ );
83
+
84
+ return {
85
+ ancestry,
86
+ loading: fetching,
87
+ error: error ? new Error(error.message) : null,
88
+ };
89
+ }
90
+
91
+ interface UseDescendantsOptions {
92
+ maxDepth?: number;
93
+ pause?: boolean;
94
+ }
95
+
96
+ interface UseDescendantsHook {
97
+ descendants: DescendantNode | null;
98
+ loading: boolean;
99
+ error: Error | null;
100
+ }
101
+
102
+ /**
103
+ * Hook for fetching the descendants tree of a generation.
104
+ * @param generationId - The ID of the generation to fetch descendants for
105
+ * @param options - Optional configuration (maxDepth, pause)
106
+ */
107
+ export function useDescendants(
108
+ generationId: string,
109
+ options: UseDescendantsOptions = {}
110
+ ): UseDescendantsHook {
111
+ const { maxDepth = 25, pause = false } = options;
112
+
113
+ const [{ data, fetching, error }] = useQuery({
114
+ query: GET_DESCENDANTS,
115
+ variables: { id: generationId, maxDepth },
116
+ pause,
117
+ });
118
+
119
+ const descendants = useMemo(
120
+ () => data?.generation?.descendants || null,
121
+ [data?.generation?.descendants]
122
+ );
123
+
124
+ return {
125
+ descendants,
126
+ loading: fetching,
127
+ error: error ? new Error(error.message) : null,
128
+ };
129
+ }
130
+
131
+ interface UseInputArtifactsOptions {
132
+ pause?: boolean;
133
+ }
134
+
135
+ interface UseInputArtifactsHook {
136
+ inputArtifacts: ArtifactLineage[];
137
+ loading: boolean;
138
+ error: Error | null;
139
+ }
140
+
141
+ /**
142
+ * Hook for fetching the input artifacts of a generation.
143
+ * @param generationId - The ID of the generation to fetch input artifacts for
144
+ * @param options - Optional configuration (pause)
145
+ */
146
+ export function useInputArtifacts(
147
+ generationId: string,
148
+ options: UseInputArtifactsOptions = {}
149
+ ): UseInputArtifactsHook {
150
+ const { pause = false } = options;
151
+
152
+ const [{ data, fetching, error }] = useQuery({
153
+ query: GET_INPUT_ARTIFACTS,
154
+ variables: { id: generationId },
155
+ pause,
156
+ });
157
+
158
+ const inputArtifacts = useMemo(
159
+ () => data?.generation?.inputArtifacts || [],
160
+ [data?.generation?.inputArtifacts]
161
+ );
162
+
163
+ return {
164
+ inputArtifacts,
165
+ loading: fetching,
166
+ error: error ? new Error(error.message) : null,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Combined hook for fetching both ancestry and descendants.
172
+ * Useful for lineage explorer pages that show both trees.
173
+ */
174
+ interface UseLineageOptions {
175
+ maxDepth?: number;
176
+ pause?: boolean;
177
+ }
178
+
179
+ interface UseLineageHook {
180
+ ancestry: AncestryNode | null;
181
+ descendants: DescendantNode | null;
182
+ inputArtifacts: ArtifactLineage[];
183
+ loading: boolean;
184
+ error: Error | null;
185
+ }
186
+
187
+ export function useLineage(
188
+ generationId: string,
189
+ options: UseLineageOptions = {}
190
+ ): UseLineageHook {
191
+ const ancestryResult = useAncestry(generationId, options);
192
+ const descendantsResult = useDescendants(generationId, options);
193
+ const inputArtifactsResult = useInputArtifacts(generationId, options);
194
+
195
+ const loading =
196
+ ancestryResult.loading ||
197
+ descendantsResult.loading ||
198
+ inputArtifactsResult.loading;
199
+
200
+ const error =
201
+ ancestryResult.error || descendantsResult.error || inputArtifactsResult.error;
202
+
203
+ return {
204
+ ancestry: ancestryResult.ancestry,
205
+ descendants: descendantsResult.descendants,
206
+ inputArtifacts: inputArtifactsResult.inputArtifacts,
207
+ loading,
208
+ error,
209
+ };
210
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Hook for uploading artifacts (images, videos, audio, text).
3
+ */
4
+
5
+ import { useCallback, useState } from "react";
6
+ import { useMutation } from "urql";
7
+ import { UPLOAD_ARTIFACT_FROM_URL, ArtifactType } from "../graphql/operations";
8
+ import { useAuth } from "../auth/context";
9
+ import { useApiConfig } from "../config/ApiConfigContext";
10
+
11
+ export interface UploadRequest {
12
+ boardId: string;
13
+ artifactType: ArtifactType;
14
+ source: File | string; // File object or URL string
15
+ userDescription?: string;
16
+ parentGenerationId?: string;
17
+ }
18
+
19
+ export interface UploadResult {
20
+ id: string;
21
+ storageUrl: string;
22
+ thumbnailUrl?: string;
23
+ status: "completed" | "failed";
24
+ artifactType: ArtifactType;
25
+ generatorName: string;
26
+ }
27
+
28
+ export interface UploadHook {
29
+ upload: (request: UploadRequest) => Promise<UploadResult>;
30
+ isUploading: boolean;
31
+ progress: number; // 0-100
32
+ error: Error | null;
33
+ }
34
+
35
+ export function useUpload(): UploadHook {
36
+ const [isUploading, setIsUploading] = useState(false);
37
+ const [progress, setProgress] = useState(0);
38
+ const [error, setError] = useState<Error | null>(null);
39
+
40
+ const { apiUrl } = useApiConfig();
41
+ const auth = useAuth();
42
+
43
+ const [, uploadFromUrlMutation] = useMutation(UPLOAD_ARTIFACT_FROM_URL);
44
+
45
+ const upload = useCallback(
46
+ async (request: UploadRequest): Promise<UploadResult> => {
47
+ setError(null);
48
+ setProgress(0);
49
+ setIsUploading(true);
50
+
51
+ try {
52
+ // Handle URL upload via GraphQL
53
+ if (typeof request.source === "string") {
54
+ const result = await uploadFromUrlMutation({
55
+ input: {
56
+ boardId: request.boardId,
57
+ artifactType: request.artifactType,
58
+ fileUrl: request.source,
59
+ userDescription: request.userDescription,
60
+ parentGenerationId: request.parentGenerationId,
61
+ },
62
+ });
63
+
64
+ if (result.error) {
65
+ throw new Error(result.error.message);
66
+ }
67
+
68
+ if (!result.data?.uploadArtifact) {
69
+ throw new Error("Upload failed");
70
+ }
71
+
72
+ setProgress(100);
73
+ setIsUploading(false);
74
+
75
+ return {
76
+ id: result.data.uploadArtifact.id,
77
+ storageUrl: result.data.uploadArtifact.storageUrl,
78
+ thumbnailUrl: result.data.uploadArtifact.thumbnailUrl,
79
+ status: "completed",
80
+ artifactType: result.data.uploadArtifact.artifactType,
81
+ generatorName: result.data.uploadArtifact.generatorName,
82
+ };
83
+ }
84
+
85
+ // Handle file upload via REST API
86
+ const formData = new FormData();
87
+ formData.append("board_id", request.boardId);
88
+ formData.append("artifact_type", request.artifactType.toLowerCase());
89
+ formData.append("file", request.source);
90
+ if (request.userDescription) {
91
+ formData.append("user_description", request.userDescription);
92
+ }
93
+ if (request.parentGenerationId) {
94
+ formData.append("parent_generation_id", request.parentGenerationId);
95
+ }
96
+
97
+ const token = await auth.getToken();
98
+ const headers: Record<string, string> = {};
99
+ if (token) {
100
+ headers.Authorization = `Bearer ${token}`;
101
+ }
102
+
103
+ // Use XMLHttpRequest for progress tracking
104
+ const result = await new Promise<UploadResult>((resolve, reject) => {
105
+ const xhr = new XMLHttpRequest();
106
+
107
+ xhr.upload.addEventListener("progress", (e) => {
108
+ if (e.lengthComputable) {
109
+ const percentComplete = (e.loaded / e.total) * 100;
110
+ setProgress(percentComplete);
111
+ }
112
+ });
113
+
114
+ xhr.addEventListener("load", () => {
115
+ if (xhr.status === 200) {
116
+ try {
117
+ const data = JSON.parse(xhr.responseText);
118
+ resolve({
119
+ id: data.id,
120
+ storageUrl: data.storageUrl,
121
+ thumbnailUrl: data.thumbnailUrl,
122
+ status: "completed",
123
+ artifactType: data.artifactType as ArtifactType,
124
+ generatorName: data.generatorName,
125
+ });
126
+ } catch (err) {
127
+ reject(new Error("Failed to parse response"));
128
+ }
129
+ } else {
130
+ try {
131
+ const errorData = JSON.parse(xhr.responseText);
132
+ reject(new Error(errorData.detail || `Upload failed: ${xhr.statusText}`));
133
+ } catch {
134
+ reject(new Error(`Upload failed: ${xhr.statusText}`));
135
+ }
136
+ }
137
+ });
138
+
139
+ xhr.addEventListener("error", () => {
140
+ reject(new Error("Upload failed: Network error"));
141
+ });
142
+
143
+ xhr.addEventListener("abort", () => {
144
+ reject(new Error("Upload cancelled"));
145
+ });
146
+
147
+ xhr.open("POST", `${apiUrl}/api/uploads/artifact`);
148
+ Object.entries(headers).forEach(([key, value]) => {
149
+ xhr.setRequestHeader(key, value);
150
+ });
151
+ xhr.send(formData);
152
+ });
153
+
154
+ setProgress(100);
155
+ setIsUploading(false);
156
+ return result;
157
+ } catch (err) {
158
+ const uploadError =
159
+ err instanceof Error ? err : new Error("Upload failed");
160
+ setError(uploadError);
161
+ setIsUploading(false);
162
+ setProgress(0);
163
+ throw uploadError;
164
+ }
165
+ },
166
+ [uploadFromUrlMutation, apiUrl, auth]
167
+ );
168
+
169
+ return {
170
+ upload,
171
+ isUploading,
172
+ progress,
173
+ error,
174
+ };
175
+ }
package/src/index.ts CHANGED
@@ -12,8 +12,16 @@ export { useApiConfig } from "./config/ApiConfigContext";
12
12
  export type { ApiConfig } from "./config/ApiConfigContext";
13
13
 
14
14
  // Generator selection context
15
- export { GeneratorSelectionProvider, useGeneratorSelection } from "./config/GeneratorSelectionContext";
16
- export type { GeneratorInfo, GeneratorSelectionContextValue, ArtifactSlotInfo, Artifact } from "./config/GeneratorSelectionContext";
15
+ export {
16
+ GeneratorSelectionProvider,
17
+ useGeneratorSelection,
18
+ } from "./config/GeneratorSelectionContext";
19
+ export type {
20
+ GeneratorInfo,
21
+ GeneratorSelectionContextValue,
22
+ ArtifactSlotInfo,
23
+ Artifact,
24
+ } from "./config/GeneratorSelectionContext";
17
25
 
18
26
  // GraphQL exports
19
27
  export { createGraphQLClient } from "./graphql/client";
@@ -24,7 +32,24 @@ export { useBoards } from "./hooks/useBoards";
24
32
  export { useBoard } from "./hooks/useBoard";
25
33
  export { useGeneration } from "./hooks/useGeneration";
26
34
  export { useGenerators } from "./hooks/useGenerators";
35
+ export { useUpload } from "./hooks/useUpload";
27
36
  export type { Generator, JSONSchema7 } from "./hooks/useGenerators";
37
+ export {
38
+ useAncestry,
39
+ useDescendants,
40
+ useInputArtifacts,
41
+ useLineage,
42
+ } from "./hooks/useLineage";
43
+ export type {
44
+ ArtifactLineage,
45
+ AncestryNode,
46
+ DescendantNode,
47
+ } from "./hooks/useLineage";
48
+ export type {
49
+ UploadRequest,
50
+ UploadResult,
51
+ UploadHook,
52
+ } from "./hooks/useUpload";
28
53
 
29
54
  // Generator schema utilities
30
55
  export * from "./types/generatorSchema";