@agent-native/core 0.128.0 → 0.128.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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/server/builder-design-systems.ts +299 -82
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/templates/analytics/_gitignore +2 -0
- package/corpus/templates/assets/_gitignore +2 -0
- package/corpus/templates/calendar/_gitignore +2 -0
- package/corpus/templates/clips/_gitignore +2 -0
- package/corpus/templates/content/_gitignore +2 -0
- package/corpus/templates/design/_gitignore +2 -0
- package/corpus/templates/design/app/i18n-data.ts +10 -0
- package/corpus/templates/design/app/lib/builder-design-system-upload.ts +280 -0
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +90 -54
- package/corpus/templates/design/changelog/2026-07-28-figma-fig-uploads-now-show-indexing-progress-and-an-open-in-.md +6 -0
- package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +49 -0
- package/corpus/templates/design/server/handlers/design-system-upload-start.ts +87 -0
- package/corpus/templates/design/server/handlers/index-design-system-sources.ts +82 -0
- package/corpus/templates/design/server/routes/api/design-system-decode-job-status.get.ts +1 -0
- package/corpus/templates/design/server/routes/api/design-system-upload-start.post.ts +1 -0
- package/corpus/templates/design/server/routes/api/index-design-system-sources.post.ts +1 -0
- package/corpus/templates/dispatch/_gitignore +2 -0
- package/corpus/templates/forms/_gitignore +2 -0
- package/corpus/templates/macros/_gitignore +2 -0
- package/corpus/templates/mail/_gitignore +2 -0
- package/corpus/templates/plan/_gitignore +2 -0
- package/corpus/templates/slides/_gitignore +2 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +126 -53
- package/corpus/templates/slides/app/components/design-system/builder-design-system-upload.ts +271 -0
- package/corpus/templates/slides/app/components/design-system/builder-index-response.ts +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +49 -0
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +87 -0
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +79 -0
- package/corpus/templates/slides/server/routes/api/design-system-decode-job-status.get.ts +1 -0
- package/corpus/templates/slides/server/routes/api/design-system-upload-start.post.ts +1 -0
- package/corpus/templates/slides/server/routes/api/index-design-system-sources.post.ts +1 -0
- package/corpus/templates/tasks/_gitignore +2 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-design-systems.d.ts +49 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +221 -69
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -1
- package/src/server/builder-design-systems.ts +299 -82
- package/src/server/index.ts +8 -0
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +0 -151
- package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +0 -1
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +0 -119
- package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +0 -1
|
@@ -1422,6 +1422,7 @@ const enUS = {
|
|
|
1422
1422
|
figmaParsingTitle: "Starting Builder DSI indexing...",
|
|
1423
1423
|
figmaParsingDescription:
|
|
1424
1424
|
"Builder will extract tokens, components, assets, and usage guidance",
|
|
1425
|
+
figmaDecodeFailed: "Decoding failed: {{error}}",
|
|
1425
1426
|
uploadFig: "Connect Figma with a .fig file",
|
|
1426
1427
|
figmaSaveLocalCopy:
|
|
1427
1428
|
"Upload a local copy from Figma: File -> Save local copy",
|
|
@@ -1891,6 +1892,7 @@ const designLocaleOverrides = {
|
|
|
1891
1892
|
"通过 Builder DSI 连接 Figma、代码和可选的 design.md 指引。上下文越多,代理得到的系统越准确。",
|
|
1892
1893
|
figmaParsingTitle: "正在启动 Builder DSI 索引...",
|
|
1893
1894
|
figmaParsingDescription: "Builder 会提取令牌、组件、资产和使用指引",
|
|
1895
|
+
figmaDecodeFailed: "解码失败:{{error}}",
|
|
1894
1896
|
uploadFig: "连接 Figma .fig 文件",
|
|
1895
1897
|
figmaSaveLocalCopy: "上传 Figma 本地副本:File -> Save local copy",
|
|
1896
1898
|
websiteUrl: "网站URL",
|
|
@@ -2295,6 +2297,7 @@ const designLocaleOverrides = {
|
|
|
2295
2297
|
figmaParsingTitle: "Iniciando indexación de Builder DSI...",
|
|
2296
2298
|
figmaParsingDescription:
|
|
2297
2299
|
"Builder extraerá tokens, componentes, recursos y guía de uso.",
|
|
2300
|
+
figmaDecodeFailed: "Error al decodificar: {{error}}",
|
|
2298
2301
|
uploadFig: "Conectar Figma con un archivo .fig",
|
|
2299
2302
|
figmaSaveLocalCopy:
|
|
2300
2303
|
"Sube una copia local de Figma: File -> Save local copy",
|
|
@@ -2720,6 +2723,7 @@ const designLocaleOverrides = {
|
|
|
2720
2723
|
figmaParsingTitle: "Démarrage de l’indexation Builder DSI...",
|
|
2721
2724
|
figmaParsingDescription:
|
|
2722
2725
|
"Builder extraira les jetons, composants, ressources et consignes d’utilisation",
|
|
2726
|
+
figmaDecodeFailed: "Échec du décodage : {{error}}",
|
|
2723
2727
|
uploadFig: "Connecter Figma avec un fichier .fig",
|
|
2724
2728
|
figmaSaveLocalCopy:
|
|
2725
2729
|
"Importez une copie locale Figma : File -> Save local copy",
|
|
@@ -3145,6 +3149,7 @@ const designLocaleOverrides = {
|
|
|
3145
3149
|
figmaParsingTitle: "Builder-DSI-Indizierung wird gestartet...",
|
|
3146
3150
|
figmaParsingDescription:
|
|
3147
3151
|
"Builder extrahiert Tokens, Komponenten, Assets und Nutzungsanweisungen",
|
|
3152
|
+
figmaDecodeFailed: "Dekodierung fehlgeschlagen: {{error}}",
|
|
3148
3153
|
uploadFig: "Figma mit einer .fig-Datei verbinden",
|
|
3149
3154
|
figmaSaveLocalCopy:
|
|
3150
3155
|
"Lade eine lokale Figma-Kopie hoch: File -> Save local copy",
|
|
@@ -3570,6 +3575,7 @@ const designLocaleOverrides = {
|
|
|
3570
3575
|
figmaParsingTitle: "Builder DSI のインデックス作成を開始しています...",
|
|
3571
3576
|
figmaParsingDescription:
|
|
3572
3577
|
"Builder がトークン、コンポーネント、アセット、利用ガイドを抽出します",
|
|
3578
|
+
figmaDecodeFailed: "デコードに失敗しました: {{error}}",
|
|
3573
3579
|
uploadFig: "Figma .fig ファイルを接続",
|
|
3574
3580
|
figmaSaveLocalCopy:
|
|
3575
3581
|
"Figma のローカルコピーをアップロード: File -> Save local copy",
|
|
@@ -3990,6 +3996,7 @@ const designLocaleOverrides = {
|
|
|
3990
3996
|
figmaParsingTitle: "Builder DSI 색인 생성을 시작하는 중...",
|
|
3991
3997
|
figmaParsingDescription:
|
|
3992
3998
|
"Builder가 토큰, 컴포넌트, 자산, 사용 지침을 추출합니다",
|
|
3999
|
+
figmaDecodeFailed: "디코딩에 실패했습니다: {{error}}",
|
|
3993
4000
|
uploadFig: "Figma .fig 파일 연결",
|
|
3994
4001
|
figmaSaveLocalCopy: "Figma 로컬 사본 업로드: File -> Save local copy",
|
|
3995
4002
|
websiteUrl: "웹사이트 URL",
|
|
@@ -4404,6 +4411,7 @@ const designLocaleOverrides = {
|
|
|
4404
4411
|
figmaParsingTitle: "Iniciando indexação do Builder DSI...",
|
|
4405
4412
|
figmaParsingDescription:
|
|
4406
4413
|
"Builder extrairá tokens, componentes, recursos e orientações de uso",
|
|
4414
|
+
figmaDecodeFailed: "Falha na decodificação: {{error}}",
|
|
4407
4415
|
uploadFig: "Conectar Figma com um arquivo .fig",
|
|
4408
4416
|
figmaSaveLocalCopy:
|
|
4409
4417
|
"Envie uma cópia local do Figma: File -> Save local copy",
|
|
@@ -4824,6 +4832,7 @@ const designLocaleOverrides = {
|
|
|
4824
4832
|
figmaParsingTitle: "Builder DSI indexing शुरू की जा रही है...",
|
|
4825
4833
|
figmaParsingDescription:
|
|
4826
4834
|
"Builder टोकन, कंपोनेंट, एसेट और उपयोग मार्गदर्शन निकालेगा",
|
|
4835
|
+
figmaDecodeFailed: "डिकोडिंग विफल: {{error}}",
|
|
4827
4836
|
uploadFig: "Figma .fig फ़ाइल कनेक्ट करें",
|
|
4828
4837
|
figmaSaveLocalCopy: "Figma स्थानीय कॉपी अपलोड करें: File -> Save local copy",
|
|
4829
4838
|
websiteUrl: "वेबसाइट URL",
|
|
@@ -5234,6 +5243,7 @@ const designLocaleOverrides = {
|
|
|
5234
5243
|
figmaParsingTitle: "جارٍ بدء فهرسة Builder DSI...",
|
|
5235
5244
|
figmaParsingDescription:
|
|
5236
5245
|
"سيستخرج Builder الرموز والمكونات والأصول وإرشادات الاستخدام",
|
|
5246
|
+
figmaDecodeFailed: "فشل فك الترميز: {{error}}",
|
|
5237
5247
|
uploadFig: "ربط Figma بملف .fig",
|
|
5238
5248
|
figmaSaveLocalCopy: "حمّل نسخة Figma محلية: File -> Save local copy",
|
|
5239
5249
|
websiteUrl: "موقع URL",
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { appApiPath } from "@agent-native/core/client/api-path";
|
|
2
|
+
|
|
3
|
+
// GCS resumable uploads require every chunk except the last to be a multiple
|
|
4
|
+
// of 256 KiB. 16 MiB is the recommended default and keeps very large `.fig`
|
|
5
|
+
// files off a single request body (the serverless host caps bodies well below
|
|
6
|
+
// Figma export sizes).
|
|
7
|
+
const GCS_CHUNK_SIZE = 16 * 1024 * 1024;
|
|
8
|
+
const MAX_CHUNK_RETRIES = 5;
|
|
9
|
+
|
|
10
|
+
interface UploadSlot {
|
|
11
|
+
idx: number;
|
|
12
|
+
uploadUrl: string;
|
|
13
|
+
uploadToken: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BuilderIndexResult {
|
|
17
|
+
designSystemId: string;
|
|
18
|
+
localDesignSystemId?: string;
|
|
19
|
+
builderUrl?: string;
|
|
20
|
+
suggestedTitle?: string | null;
|
|
21
|
+
status?: string;
|
|
22
|
+
uploadedFileCount?: number;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readJson(res: Response): Promise<any> {
|
|
27
|
+
try {
|
|
28
|
+
return await res.json();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function requestUploadSlots(files: File[]): Promise<UploadSlot[]> {
|
|
35
|
+
const res = await fetch(appApiPath("/api/design-system-upload-start"), {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
attachments: files.map((file) => ({
|
|
40
|
+
name: file.name,
|
|
41
|
+
mimetype: file.type || "application/octet-stream",
|
|
42
|
+
declaredSize: file.size,
|
|
43
|
+
})),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
const json = await readJson(res);
|
|
47
|
+
if (!res.ok || json?.error) {
|
|
48
|
+
throw new Error(json?.error || `Failed to start upload (${res.status})`);
|
|
49
|
+
}
|
|
50
|
+
const slots = Array.isArray(json?.uploads)
|
|
51
|
+
? ([...json.uploads] as UploadSlot[]).sort((a, b) => a.idx - b.idx)
|
|
52
|
+
: [];
|
|
53
|
+
if (slots.length !== files.length) {
|
|
54
|
+
throw new Error("Upload could not be started for all files.");
|
|
55
|
+
}
|
|
56
|
+
return slots;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function initiateResumableSession(
|
|
60
|
+
uploadUrl: string,
|
|
61
|
+
mimetype: string,
|
|
62
|
+
fileSize: number,
|
|
63
|
+
): Promise<string> {
|
|
64
|
+
const response = await fetch(uploadUrl, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"x-goog-resumable": "start",
|
|
68
|
+
// The signed URL commits to the exact declared size; echo it back
|
|
69
|
+
// byte-for-byte or GCS rejects the session.
|
|
70
|
+
"x-goog-content-length-range": `0,${fileSize}`,
|
|
71
|
+
"Content-Type": mimetype,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Failed to initiate upload session (${response.status})`);
|
|
76
|
+
}
|
|
77
|
+
const sessionUri = response.headers.get("Location");
|
|
78
|
+
if (!sessionUri) {
|
|
79
|
+
throw new Error("Upload session URI missing from storage response");
|
|
80
|
+
}
|
|
81
|
+
return sessionUri;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// GCS reports the highest committed byte in a `Range: bytes=0-<end>` header.
|
|
85
|
+
function committedOffsetFromRange(response: Response): number | null {
|
|
86
|
+
const match = response.headers.get("Range")?.match(/bytes=0-(\d+)/);
|
|
87
|
+
return match ? parseInt(match[1], 10) + 1 : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function queryCommittedOffset(
|
|
91
|
+
sessionUri: string,
|
|
92
|
+
total: number,
|
|
93
|
+
): Promise<number> {
|
|
94
|
+
const response = await fetch(sessionUri, {
|
|
95
|
+
method: "PUT",
|
|
96
|
+
headers: { "Content-Range": `bytes */${total}` },
|
|
97
|
+
});
|
|
98
|
+
if (response.status === 200 || response.status === 201) return total;
|
|
99
|
+
if (response.status === 308) return committedOffsetFromRange(response) ?? 0;
|
|
100
|
+
throw new Error(`Failed to query upload status (${response.status})`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
if (signal?.aborted) {
|
|
106
|
+
return reject(new DOMException("Aborted", "AbortError"));
|
|
107
|
+
}
|
|
108
|
+
const timer = setTimeout(resolve, ms);
|
|
109
|
+
signal?.addEventListener(
|
|
110
|
+
"abort",
|
|
111
|
+
() => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
114
|
+
},
|
|
115
|
+
{ once: true },
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function streamFileToStorage(
|
|
121
|
+
slot: UploadSlot,
|
|
122
|
+
file: File,
|
|
123
|
+
onProgress: (uploadedBytes: number) => void,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const mimetype = file.type || "application/octet-stream";
|
|
126
|
+
const sessionUri = await initiateResumableSession(
|
|
127
|
+
slot.uploadUrl,
|
|
128
|
+
mimetype,
|
|
129
|
+
file.size,
|
|
130
|
+
);
|
|
131
|
+
const total = file.size;
|
|
132
|
+
|
|
133
|
+
if (total === 0) {
|
|
134
|
+
const response = await fetch(sessionUri, {
|
|
135
|
+
method: "PUT",
|
|
136
|
+
headers: { "Content-Range": "bytes */0" },
|
|
137
|
+
body: new Uint8Array(0),
|
|
138
|
+
});
|
|
139
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Failed to finalize zero-byte upload (${response.status})`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
onProgress(0);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let offset = 0;
|
|
149
|
+
let retries = 0;
|
|
150
|
+
while (offset < total) {
|
|
151
|
+
const end = Math.min(offset + GCS_CHUNK_SIZE, total);
|
|
152
|
+
const isLast = end === total;
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(sessionUri, {
|
|
155
|
+
method: "PUT",
|
|
156
|
+
headers: { "Content-Range": `bytes ${offset}-${end - 1}/${total}` },
|
|
157
|
+
body: file.slice(offset, end),
|
|
158
|
+
});
|
|
159
|
+
if (response.status === 200 || response.status === 201) {
|
|
160
|
+
offset = total;
|
|
161
|
+
} else if (!isLast && response.status === 308) {
|
|
162
|
+
const nextOffset = committedOffsetFromRange(response) ?? offset;
|
|
163
|
+
if (nextOffset <= offset) {
|
|
164
|
+
throw new Error(`Upload stalled at byte ${offset}`);
|
|
165
|
+
}
|
|
166
|
+
offset = nextOffset;
|
|
167
|
+
} else {
|
|
168
|
+
throw new Error(`Unexpected upload status ${response.status}`);
|
|
169
|
+
}
|
|
170
|
+
retries = 0;
|
|
171
|
+
onProgress(offset);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (++retries > MAX_CHUNK_RETRIES) throw err;
|
|
174
|
+
await delay(500 * retries);
|
|
175
|
+
try {
|
|
176
|
+
offset = await queryCommittedOffset(sessionUri, total);
|
|
177
|
+
} catch {
|
|
178
|
+
// If the offset query also fails, retry from the last local offset —
|
|
179
|
+
// GCS's resumable PUT safely re-acknowledges bytes it already has.
|
|
180
|
+
}
|
|
181
|
+
onProgress(offset);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface UploadAndIndexOptions {
|
|
187
|
+
projectName?: string;
|
|
188
|
+
onProgress?: (fraction: number) => void;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Streams `.fig`/design files straight to storage in resumable chunks, then
|
|
193
|
+
* finalizes Builder DSI indexing with the resulting upload tokens. No file
|
|
194
|
+
* bytes pass through the app server, so arbitrarily large Figma files work.
|
|
195
|
+
*/
|
|
196
|
+
export async function uploadAndIndexFigmaFiles(
|
|
197
|
+
files: File[],
|
|
198
|
+
options: UploadAndIndexOptions = {},
|
|
199
|
+
): Promise<BuilderIndexResult> {
|
|
200
|
+
if (files.length === 0) throw new Error("No files to upload.");
|
|
201
|
+
|
|
202
|
+
const slots = await requestUploadSlots(files);
|
|
203
|
+
|
|
204
|
+
const totalBytes = files.reduce((sum, file) => sum + file.size, 0) || 1;
|
|
205
|
+
const uploadedPerFile = new Array<number>(files.length).fill(0);
|
|
206
|
+
for (let i = 0; i < files.length; i++) {
|
|
207
|
+
await streamFileToStorage(slots[i], files[i], (uploaded) => {
|
|
208
|
+
uploadedPerFile[i] = uploaded;
|
|
209
|
+
const done = uploadedPerFile.reduce((sum, n) => sum + n, 0);
|
|
210
|
+
options.onProgress?.(Math.min(done / totalBytes, 1));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const res = await fetch(appApiPath("/api/index-design-system-sources"), {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
projectName: options.projectName,
|
|
219
|
+
uploadTokens: slots.map((slot) => slot.uploadToken),
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
const json = await readJson(res);
|
|
223
|
+
if (!res.ok || json?.error) {
|
|
224
|
+
throw new Error(json?.error || `Indexing failed (${res.status})`);
|
|
225
|
+
}
|
|
226
|
+
return json as BuilderIndexResult;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface DecodeJobStatus {
|
|
230
|
+
status: "pending" | "processing" | "complete" | "error";
|
|
231
|
+
branchUrl: string | null;
|
|
232
|
+
error: string | null;
|
|
233
|
+
framesProcessed: number;
|
|
234
|
+
totalFrames: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const DECODE_JOB_POLL_INTERVAL_MS = 5_000;
|
|
238
|
+
const DECODE_JOB_MAX_POLLS = 120; // ~10 min at 5s, so a stuck job can't loop forever
|
|
239
|
+
|
|
240
|
+
export interface PollDecodeJobOptions {
|
|
241
|
+
signal?: AbortSignal;
|
|
242
|
+
onUpdate?: (status: DecodeJobStatus) => void;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* After indexing returns a jobId, the `.fig` decode job is still `pending` with
|
|
247
|
+
* no branchUrl. Poll until the branch appears or the job reaches a terminal
|
|
248
|
+
* state. A job that reports `status: "error"` resolves so the caller can read
|
|
249
|
+
* `status.error`; network failures, timeouts, and aborts reject.
|
|
250
|
+
*/
|
|
251
|
+
export async function pollDecodeJobStatus(
|
|
252
|
+
jobId: string,
|
|
253
|
+
options: PollDecodeJobOptions = {},
|
|
254
|
+
): Promise<DecodeJobStatus> {
|
|
255
|
+
const { signal, onUpdate } = options;
|
|
256
|
+
const path = appApiPath(
|
|
257
|
+
`/api/design-system-decode-job-status?jobId=${encodeURIComponent(jobId)}`,
|
|
258
|
+
);
|
|
259
|
+
for (let i = 0; i < DECODE_JOB_MAX_POLLS; i++) {
|
|
260
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
261
|
+
const res = await fetch(path, { signal });
|
|
262
|
+
const json = await readJson(res);
|
|
263
|
+
if (!res.ok || json?.error) {
|
|
264
|
+
throw new Error(json?.error || `Status check failed (${res.status})`);
|
|
265
|
+
}
|
|
266
|
+
const status = json as DecodeJobStatus;
|
|
267
|
+
onUpdate?.(status);
|
|
268
|
+
if (
|
|
269
|
+
status.branchUrl ||
|
|
270
|
+
status.status === "complete" ||
|
|
271
|
+
status.status === "error"
|
|
272
|
+
) {
|
|
273
|
+
return status;
|
|
274
|
+
}
|
|
275
|
+
await delay(DECODE_JOB_POLL_INTERVAL_MS, signal);
|
|
276
|
+
}
|
|
277
|
+
throw new Error(
|
|
278
|
+
"Timed out waiting for the design system to finish decoding.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { appApiPath } from "@agent-native/core/client/api-path";
|
|
2
1
|
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
3
2
|
import { useT } from "@agent-native/core/client/i18n";
|
|
4
3
|
import { openAgentSidebar } from "@agent-native/core/client/navigation";
|
|
@@ -30,6 +29,11 @@ import { Input } from "@/components/ui/input";
|
|
|
30
29
|
import { Spinner } from "@/components/ui/spinner";
|
|
31
30
|
import { Textarea } from "@/components/ui/textarea";
|
|
32
31
|
import { sendToDesignAgentChat } from "@/lib/agent-chat";
|
|
32
|
+
import {
|
|
33
|
+
uploadAndIndexFigmaFiles,
|
|
34
|
+
pollDecodeJobStatus,
|
|
35
|
+
type DecodeJobStatus,
|
|
36
|
+
} from "@/lib/builder-design-system-upload";
|
|
33
37
|
|
|
34
38
|
interface GitHubLink {
|
|
35
39
|
id: string;
|
|
@@ -58,20 +62,6 @@ interface BuilderIndexResult {
|
|
|
58
62
|
instructions?: string;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
async function readJsonResponse(res: Response): Promise<any> {
|
|
62
|
-
const text = await res.text();
|
|
63
|
-
if (!text.trim()) return {};
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(text);
|
|
66
|
-
} catch {
|
|
67
|
-
return {
|
|
68
|
-
error: res.ok
|
|
69
|
-
? "The server returned an invalid response."
|
|
70
|
-
: text.slice(0, 240),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
65
|
export default function DesignSystemSetup() {
|
|
76
66
|
const t = useT();
|
|
77
67
|
const navigate = useNavigate();
|
|
@@ -117,6 +107,62 @@ export default function DesignSystemSetup() {
|
|
|
117
107
|
const [builderIndexError, setBuilderIndexError] = useState<string | null>(
|
|
118
108
|
null,
|
|
119
109
|
);
|
|
110
|
+
const [decodeStatus, setDecodeStatus] = useState<DecodeJobStatus | null>(
|
|
111
|
+
null,
|
|
112
|
+
);
|
|
113
|
+
const decodePollRef = useRef<AbortController | null>(null);
|
|
114
|
+
|
|
115
|
+
const stopDecodePolling = useCallback(() => {
|
|
116
|
+
decodePollRef.current?.abort();
|
|
117
|
+
decodePollRef.current = null;
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
useEffect(() => stopDecodePolling, [stopDecodePolling]);
|
|
121
|
+
|
|
122
|
+
const startDecodePolling = useCallback(
|
|
123
|
+
(jobId: string, indexResult: BuilderIndexResult) => {
|
|
124
|
+
decodePollRef.current?.abort();
|
|
125
|
+
const controller = new AbortController();
|
|
126
|
+
decodePollRef.current = controller;
|
|
127
|
+
setDecodeStatus({
|
|
128
|
+
status: "pending",
|
|
129
|
+
branchUrl: null,
|
|
130
|
+
error: null,
|
|
131
|
+
framesProcessed: 0,
|
|
132
|
+
totalFrames: 0,
|
|
133
|
+
});
|
|
134
|
+
pollDecodeJobStatus(jobId, {
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
onUpdate: (status) => {
|
|
137
|
+
if (!controller.signal.aborted) setDecodeStatus(status);
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
.then((status) => {
|
|
141
|
+
if (controller.signal.aborted) return;
|
|
142
|
+
setDecodeStatus(status);
|
|
143
|
+
setBuilderIndexResult(
|
|
144
|
+
status.branchUrl
|
|
145
|
+
? { ...indexResult, builderUrl: status.branchUrl }
|
|
146
|
+
: indexResult,
|
|
147
|
+
);
|
|
148
|
+
setBuilderIndexing(false);
|
|
149
|
+
})
|
|
150
|
+
.catch((err: unknown) => {
|
|
151
|
+
if (controller.signal.aborted) return;
|
|
152
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
153
|
+
setDecodeStatus((prev) => ({
|
|
154
|
+
status: "error",
|
|
155
|
+
branchUrl: prev?.branchUrl ?? null,
|
|
156
|
+
error: err instanceof Error ? err.message : String(err),
|
|
157
|
+
framesProcessed: prev?.framesProcessed ?? 0,
|
|
158
|
+
totalFrames: prev?.totalFrames ?? 0,
|
|
159
|
+
}));
|
|
160
|
+
setBuilderIndexResult(indexResult);
|
|
161
|
+
setBuilderIndexing(false);
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
[],
|
|
165
|
+
);
|
|
120
166
|
|
|
121
167
|
const handleBuilderIndexUpload = useCallback(
|
|
122
168
|
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -129,33 +175,35 @@ export default function DesignSystemSetup() {
|
|
|
129
175
|
}
|
|
130
176
|
setBuilderIndexError(null);
|
|
131
177
|
setBuilderIndexResult(null);
|
|
178
|
+
stopDecodePolling();
|
|
179
|
+
setDecodeStatus(null);
|
|
132
180
|
setBuilderIndexing(true);
|
|
133
181
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
182
|
+
const suggestedTitle =
|
|
183
|
+
file.name
|
|
184
|
+
.replace(/\.fig$/i, "")
|
|
185
|
+
.replace(/[-_]+/g, " ")
|
|
186
|
+
.trim() || "Imported brand";
|
|
187
|
+
const json = await uploadAndIndexFigmaFiles([file], {
|
|
188
|
+
projectName: suggestedTitle,
|
|
189
|
+
});
|
|
190
|
+
const parsed = json as unknown as BuilderIndexResult;
|
|
191
|
+
if (parsed.jobId) {
|
|
192
|
+
startDecodePolling(parsed.jobId, parsed);
|
|
193
|
+
} else {
|
|
194
|
+
setBuilderIndexResult(parsed);
|
|
195
|
+
setBuilderIndexing(false);
|
|
146
196
|
}
|
|
147
|
-
setBuilderIndexResult(json as BuilderIndexResult);
|
|
148
197
|
} catch (err) {
|
|
149
198
|
setBuilderIndexError(
|
|
150
199
|
err instanceof Error
|
|
151
200
|
? err.message
|
|
152
201
|
: t("designSystemSetup.errors.parseFig"),
|
|
153
202
|
);
|
|
154
|
-
} finally {
|
|
155
203
|
setBuilderIndexing(false);
|
|
156
204
|
}
|
|
157
205
|
},
|
|
158
|
-
[t],
|
|
206
|
+
[t, startDecodePolling, stopDecodePolling],
|
|
159
207
|
);
|
|
160
208
|
|
|
161
209
|
useEffect(() => {
|
|
@@ -606,7 +654,10 @@ export default function DesignSystemSetup() {
|
|
|
606
654
|
) : (
|
|
607
655
|
<BuilderIndexPreview
|
|
608
656
|
result={builderIndexResult}
|
|
657
|
+
decodeStatus={decodeStatus}
|
|
609
658
|
onReset={() => {
|
|
659
|
+
stopDecodePolling();
|
|
660
|
+
setDecodeStatus(null);
|
|
610
661
|
setBuilderIndexResult(null);
|
|
611
662
|
setBuilderIndexError(null);
|
|
612
663
|
}}
|
|
@@ -1076,12 +1127,16 @@ function FileList({
|
|
|
1076
1127
|
|
|
1077
1128
|
function BuilderIndexPreview({
|
|
1078
1129
|
result,
|
|
1130
|
+
decodeStatus,
|
|
1079
1131
|
onReset,
|
|
1080
1132
|
}: {
|
|
1081
1133
|
result: BuilderIndexResult;
|
|
1134
|
+
decodeStatus: DecodeJobStatus | null;
|
|
1082
1135
|
onReset: () => void;
|
|
1083
1136
|
}) {
|
|
1084
1137
|
const t = useT();
|
|
1138
|
+
const decodeError =
|
|
1139
|
+
decodeStatus?.status === "error" ? decodeStatus.error : null;
|
|
1085
1140
|
|
|
1086
1141
|
return (
|
|
1087
1142
|
<div className="space-y-4 rounded-xl border border-border bg-card p-4">
|
|
@@ -1101,30 +1156,11 @@ function BuilderIndexPreview({
|
|
|
1101
1156
|
</div>
|
|
1102
1157
|
</div>
|
|
1103
1158
|
|
|
1104
|
-
|
|
1105
|
-
<
|
|
1106
|
-
{"
|
|
1107
|
-
</
|
|
1108
|
-
|
|
1109
|
-
<dt className="text-muted-foreground">
|
|
1110
|
-
{"Project" /* i18n-ignore Builder indexing field */}
|
|
1111
|
-
</dt>
|
|
1112
|
-
<dd className="truncate font-mono !text-[11px] text-foreground/80">
|
|
1113
|
-
{result.projectId}
|
|
1114
|
-
</dd>
|
|
1115
|
-
<dt className="text-muted-foreground">
|
|
1116
|
-
{"Job" /* i18n-ignore Builder indexing field */}
|
|
1117
|
-
</dt>
|
|
1118
|
-
<dd className="truncate font-mono !text-[11px] text-foreground/80">
|
|
1119
|
-
{result.jobId}
|
|
1120
|
-
</dd>
|
|
1121
|
-
<dt className="text-muted-foreground">
|
|
1122
|
-
{"Design system" /* i18n-ignore Builder indexing field */}
|
|
1123
|
-
</dt>
|
|
1124
|
-
<dd className="truncate font-mono !text-[11px] text-foreground/80">
|
|
1125
|
-
{result.designSystemId}
|
|
1126
|
-
</dd>
|
|
1127
|
-
</dl>
|
|
1159
|
+
{decodeError ? (
|
|
1160
|
+
<p role="alert" className="text-xs text-destructive">
|
|
1161
|
+
{t("designSystemSetup.figmaDecodeFailed", { error: decodeError })}
|
|
1162
|
+
</p>
|
|
1163
|
+
) : null}
|
|
1128
1164
|
|
|
1129
1165
|
<div className="flex items-center gap-2 border-t border-border pt-4">
|
|
1130
1166
|
{result.builderUrl ? (
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FeatureNotConfiguredError,
|
|
3
|
+
fetchBuilderDesignSystemDecodeJobStatus,
|
|
4
|
+
getSession,
|
|
5
|
+
runWithRequestContext,
|
|
6
|
+
} from "@agent-native/core/server";
|
|
7
|
+
import { defineEventHandler, getQuery, setResponseStatus } from "h3";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads a Builder design-system decode job's status. The UI polls this after
|
|
11
|
+
* `/api/index-design-system-sources` returns a jobId, until the `.fig` decode
|
|
12
|
+
* job leaves `pending` and exposes a `branchUrl` (or reports an `error`).
|
|
13
|
+
*/
|
|
14
|
+
export const designSystemDecodeJobStatus = defineEventHandler(async (event) => {
|
|
15
|
+
const session = await getSession(event).catch(() => null);
|
|
16
|
+
if (!session?.email) {
|
|
17
|
+
setResponseStatus(event, 401);
|
|
18
|
+
return { error: "Unauthorized" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const jobId = getQuery(event).jobId;
|
|
22
|
+
if (typeof jobId !== "string" || !jobId.trim()) {
|
|
23
|
+
setResponseStatus(event, 400);
|
|
24
|
+
return { error: "jobId is required." };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return await runWithRequestContext(
|
|
29
|
+
{ userEmail: session.email, orgId: session.orgId },
|
|
30
|
+
() => fetchBuilderDesignSystemDecodeJobStatus(jobId.trim()),
|
|
31
|
+
);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err instanceof FeatureNotConfiguredError) {
|
|
34
|
+
setResponseStatus(event, 412);
|
|
35
|
+
return {
|
|
36
|
+
error: err.message,
|
|
37
|
+
builderConnectUrl:
|
|
38
|
+
err.builderConnectUrl ?? "/_agent-native/builder/connect",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
setResponseStatus(event, 502);
|
|
42
|
+
return {
|
|
43
|
+
error:
|
|
44
|
+
err instanceof Error
|
|
45
|
+
? err.message
|
|
46
|
+
: "Failed to read decode job status.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|