bleam 0.0.10 → 0.0.12

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 (90) hide show
  1. package/dist/ai.cjs +1872 -1182
  2. package/dist/ai.d.cts +267 -234
  3. package/dist/ai.d.ts +267 -234
  4. package/dist/ai.js +1859 -1167
  5. package/dist/cli.cjs +3 -183
  6. package/dist/cli.d.cts +3 -47
  7. package/dist/cli.d.ts +3 -47
  8. package/dist/cli.js +3 -183
  9. package/dist/config.d.cts +1 -1
  10. package/dist/config.d.ts +1 -1
  11. package/dist/{files-Ds1wT8C2.js → files-BXVkPrPN.js} +6 -1
  12. package/dist/{files-Bo7h9fik.cjs → files-DxaQ-Nv0.cjs} +11 -0
  13. package/dist/files.cjs +1 -1
  14. package/dist/files.js +1 -1
  15. package/dist/index.d.cts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/platform.cjs +1 -13
  18. package/dist/platform.d.cts +1 -28
  19. package/dist/platform.d.ts +1 -28
  20. package/dist/platform.js +1 -10
  21. package/dist/schema-B5BfdswF.js +226 -0
  22. package/dist/schema-BnVZOXfu.cjs +286 -0
  23. package/dist/schema-D5eImHxu.d.cts +125 -0
  24. package/dist/schema-SSjokbtw.d.ts +125 -0
  25. package/dist/schema.cjs +10 -2
  26. package/dist/schema.d.cts +2 -2
  27. package/dist/schema.d.ts +2 -2
  28. package/dist/schema.js +2 -2
  29. package/dist/state-Dh3HLixb.js +874 -0
  30. package/dist/state-LssDgpff.cjs +973 -0
  31. package/dist/state.cjs +16 -12
  32. package/dist/state.d.cts +144 -140
  33. package/dist/state.d.ts +145 -140
  34. package/dist/state.js +3 -3
  35. package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
  36. package/dist/{ui-Dd7SXdbg.d.cts → ui-D7bRLYee.d.ts} +7 -7
  37. package/dist/ui.d.cts +1 -1
  38. package/dist/ui.d.ts +1 -1
  39. package/dist/window.d.cts +1 -1
  40. package/dist/window.d.ts +1 -1
  41. package/package.json +3 -1
  42. package/templates/foundation-models/app/index.tsx +77 -15
  43. package/templates/image-generation/app/index.tsx +4 -2
  44. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  45. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  46. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  47. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  48. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  49. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  50. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  51. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  52. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  53. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  54. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  55. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  56. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  57. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  58. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  59. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  60. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  61. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  62. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  63. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  64. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  65. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  66. package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
  67. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +135 -129
  68. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  69. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  70. package/templates/native/ios/GenerationService/main.swift +174 -48
  71. package/templates/native/ios/Podfile.lock +173 -173
  72. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  73. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  74. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +239 -341
  75. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  76. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
  77. package/templates/text-generation/app/index.tsx +124 -0
  78. package/templates/text-generation/app.config.ts +5 -0
  79. package/templates/updates/README.md +43 -199
  80. package/templates/updates/src/index.ts +5 -98
  81. package/templates/updates/src/schema.ts +18 -173
  82. package/dist/schema-Bo5Jvqus.js +0 -90
  83. package/dist/schema-DOOjfXvs.d.ts +0 -58
  84. package/dist/schema-ENSMX_1t.d.cts +0 -58
  85. package/dist/schema-rQ13mrpD.cjs +0 -102
  86. package/dist/state-Bx0VlTlO.cjs +0 -852
  87. package/dist/state-CAwe-Vw1.js +0 -767
  88. package/templates/native/ios/PlatformHelper/main.swift +0 -726
  89. /package/dist/{config-Cms0rvqg.d.ts → config-COcRnn5a.d.cts} +0 -0
  90. /package/dist/{config-CufOVJV3.d.cts → config-Chi-flpJ.d.ts} +0 -0
package/dist/ai.cjs CHANGED
@@ -1,16 +1,61 @@
1
1
  const require_chunk = require('./chunk-CUT6urMc.cjs');
2
2
  require('./native-runtime-CsXnXkQn.cjs');
3
3
  require('./crypto-CT3VZ7lF.cjs');
4
- const require_files = require('./files-Bo7h9fik.cjs');
5
- const require_state = require('./state-Bx0VlTlO.cjs');
6
- require('./schema-rQ13mrpD.cjs');
4
+ const require_files = require('./files-DxaQ-Nv0.cjs');
5
+ const require_schema = require('./schema-BnVZOXfu.cjs');
6
+ const require_state = require('./state-LssDgpff.cjs');
7
7
  let expo = require("expo");
8
8
  expo = require_chunk.__toESM(expo);
9
9
  let react = require("react");
10
10
  react = require_chunk.__toESM(react);
11
- let react_native = require("react-native");
12
- react_native = require_chunk.__toESM(react_native);
13
11
 
12
+ //#region src/ai/bonsai.ts
13
+ const internalTextModelCatalog = [
14
+ {
15
+ id: "ternary-bonsai-1.7b",
16
+ repo: "prism-ml/Ternary-Bonsai-1.7B-mlx-2bit",
17
+ revision: "5f3e306330f636cfc6c6241b4850fae6711c5985",
18
+ estimatedDownloadBytes: 495528947,
19
+ contextLength: 32768,
20
+ architecture: "qwen3"
21
+ },
22
+ {
23
+ id: "ternary-bonsai-4b",
24
+ repo: "prism-ml/Ternary-Bonsai-4B-mlx-2bit",
25
+ revision: "e1374ad6bf9b1b56afd743936b8faa33c409a75f",
26
+ estimatedDownloadBytes: 1143060044,
27
+ contextLength: 32768,
28
+ architecture: "qwen3"
29
+ },
30
+ {
31
+ id: "ternary-bonsai-8b",
32
+ repo: "prism-ml/Ternary-Bonsai-8B-mlx-2bit",
33
+ revision: "9260b24298e4211e804663e9f519962cf59f34be",
34
+ estimatedDownloadBytes: 2315155948,
35
+ contextLength: 65536,
36
+ architecture: "qwen3"
37
+ },
38
+ {
39
+ id: "ternary-bonsai-27b",
40
+ repo: "prism-ml/Ternary-Bonsai-27B-mlx-2bit",
41
+ revision: "70f75f3ad081ab840a42f3304c02c27e7f89bfb7",
42
+ estimatedDownloadBytes: 8510972679,
43
+ contextLength: 262144,
44
+ architecture: "qwen3.5"
45
+ }
46
+ ];
47
+ const bonsaiTextModelCatalog = Object.freeze(internalTextModelCatalog.map(({ architecture: _,...metadata }) => Object.freeze(metadata)));
48
+ function bonsai(id) {
49
+ const text = internalTextModelCatalog.some((model) => model.id === id);
50
+ if (!text && id !== "ternary-bonsai-image-4b") throw new TypeError(`Unsupported Bonsai model: ${id}`);
51
+ return Object.freeze({
52
+ provider: "bonsai",
53
+ kind: text ? "text" : "image",
54
+ id
55
+ });
56
+ }
57
+
58
+ //#endregion
14
59
  //#region src/ai/errors.ts
15
60
  var AIError = class extends Error {
16
61
  constructor(code, message, cause) {
@@ -51,312 +96,500 @@ function mapNativeError(error) {
51
96
  }
52
97
 
53
98
  //#endregion
54
- //#region src/ai/image-batch.ts
55
- function batchImageSeeds(count, seed) {
56
- if (!Number.isInteger(count) || count < 1 || count > 64) throw new AIError("invalid_message", "Batch count must be an integer from 1 to 64");
57
- const first = seed ?? Math.floor(Math.random() * 4294967296);
58
- return Array.from({ length: count }, (_, index) => first + index >>> 0);
99
+ //#region src/ai/bonsai-text.ts
100
+ const bonsaiTextModelFiles = [
101
+ "config.json",
102
+ "tokenizer.json",
103
+ "tokenizer_config.json",
104
+ "chat_template.jinja",
105
+ "model.safetensors",
106
+ "model.safetensors.index.json"
107
+ ];
108
+ function modelId(model) {
109
+ return typeof model === "string" ? model : model.id;
110
+ }
111
+ function metadataFor(model) {
112
+ const id = modelId(model);
113
+ const metadata = bonsaiTextModelCatalog.find((candidate) => candidate.id === id);
114
+ if (!metadata) throw new AIError("invalid_model", `Unsupported Bonsai text model: ${id}`);
115
+ return metadata;
116
+ }
117
+ function bonsaiTextModelDirectory(model) {
118
+ return require_files.files.directory(require_files.files.paths.models, "bonsai", "text", modelId(model));
119
+ }
120
+ function modelDirectory(model, override) {
121
+ return override ? require_files.files.directory(override) : bonsaiTextModelDirectory(model);
122
+ }
123
+ function modelDirectoryIn(model, directory) {
124
+ return directory ? require_files.files.directory(directory, modelId(model)) : bonsaiTextModelDirectory(model);
125
+ }
126
+ function resolveUrl(repo, revision, file) {
127
+ return `https://huggingface.co/${repo.split("/").map(encodeURIComponent).join("/")}/resolve/${encodeURIComponent(revision)}/${encodeURIComponent(file)}`;
128
+ }
129
+ function throwIfAborted$1(signal) {
130
+ if (signal?.aborted) throw new AIError("generation_canceled", "Bonsai model preparation canceled");
131
+ }
132
+ const fileSizes = {
133
+ "ternary-bonsai-1.7b": {
134
+ "config.json": 2939,
135
+ "tokenizer.json": 11422650,
136
+ "tokenizer_config.json": 348,
137
+ "chat_template.jinja": 4063,
138
+ "model.safetensors": 484049216,
139
+ "model.safetensors.index.json": 49731
140
+ },
141
+ "ternary-bonsai-4b": {
142
+ "config.json": 3115,
143
+ "tokenizer.json": 11422650,
144
+ "tokenizer_config.json": 348,
145
+ "chat_template.jinja": 4063,
146
+ "model.safetensors": 1131565944,
147
+ "model.safetensors.index.json": 63924
148
+ },
149
+ "ternary-bonsai-8b": {
150
+ "config.json": 3118,
151
+ "tokenizer.json": 11422650,
152
+ "tokenizer_config.json": 348,
153
+ "chat_template.jinja": 4063,
154
+ "model.safetensors": 2303661704,
155
+ "model.safetensors.index.json": 64065
156
+ },
157
+ "ternary-bonsai-27b": {
158
+ "config.json": 3790,
159
+ "tokenizer.json": 19989343,
160
+ "tokenizer_config.json": 1098,
161
+ "chat_template.jinja": 7764,
162
+ "model.safetensors": 8490785104,
163
+ "model.safetensors.index.json": 185580
164
+ }
165
+ };
166
+ function validFile(model, file, size) {
167
+ return size === fileSizes[model][file];
168
+ }
169
+ async function stateFor(model, rootOverride) {
170
+ const metadata = metadataFor(model);
171
+ const root = modelDirectory(model, rootOverride);
172
+ const missingFiles = [];
173
+ const invalidFiles = [];
174
+ let bytesPresent = 0;
175
+ for (const file of bonsaiTextModelFiles) {
176
+ const stat = await root.file(file).stat();
177
+ if (stat.type !== "file") missingFiles.push(file);
178
+ else {
179
+ bytesPresent += stat.size ?? 0;
180
+ if (!validFile(metadata.id, file, stat.size)) invalidFiles.push(file);
181
+ }
182
+ }
183
+ const expectedRevision = metadata.revision;
184
+ const manifestFile = root.file("manifest.json");
185
+ if ((await manifestFile.stat()).type !== "file") missingFiles.push("manifest.json");
186
+ else try {
187
+ const manifest = await manifestFile.json();
188
+ if (!isRecord(manifest) || manifest.formatVersion !== 1 || manifest.model !== metadata.id || manifest.revision !== expectedRevision) invalidFiles.push("manifest.json");
189
+ } catch {
190
+ invalidFiles.push("manifest.json");
191
+ }
192
+ if (!missingFiles.includes("config.json")) try {
193
+ const config = await root.file("config.json").json();
194
+ if (!validConfig(metadata.id, config)) invalidFiles.push("config.json");
195
+ } catch {
196
+ invalidFiles.push("config.json");
197
+ }
198
+ const installed = missingFiles.length === 0;
199
+ return {
200
+ model: bonsai(metadata.id),
201
+ metadata,
202
+ path: root.path,
203
+ installed,
204
+ verified: installed && invalidFiles.length === 0,
205
+ bytesPresent,
206
+ estimatedTotalBytes: metadata.estimatedDownloadBytes,
207
+ missingFiles,
208
+ invalidFiles
209
+ };
210
+ }
211
+ function validConfig(model, value) {
212
+ if (!isRecord(value)) return false;
213
+ const expected = {
214
+ "ternary-bonsai-1.7b": [
215
+ 2048,
216
+ 28,
217
+ 32768
218
+ ],
219
+ "ternary-bonsai-4b": [
220
+ 2560,
221
+ 36,
222
+ 32768
223
+ ],
224
+ "ternary-bonsai-8b": [
225
+ 4096,
226
+ 36,
227
+ 65536
228
+ ],
229
+ "ternary-bonsai-27b": [
230
+ 5120,
231
+ 64,
232
+ 262144
233
+ ]
234
+ }[model];
235
+ const config = model === "ternary-bonsai-27b" ? value.text_config : value;
236
+ if (!isRecord(config)) return false;
237
+ const quantization = config.quantization;
238
+ return isRecord(quantization) && quantization.bits === 2 && quantization.group_size === 128 && config.hidden_size === expected[0] && config.num_hidden_layers === expected[1] && config.max_position_embeddings === expected[2];
239
+ }
240
+ async function prepareBonsaiTextModel(model, options = {}) {
241
+ const metadata = metadataFor(model);
242
+ const root = modelDirectoryIn(model, options.directory);
243
+ throwIfAborted$1(options.signal);
244
+ await root.create({ recursive: true });
245
+ const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
246
+ const completedBytes = /* @__PURE__ */ new Map();
247
+ const emit = (file, index, progress) => {
248
+ completedBytes.set(file, progress.bytesWritten);
249
+ const modelBytesWritten = Array.from(completedBytes.values()).reduce((total, bytes) => total + bytes, 0);
250
+ options.onProgress?.({
251
+ model: metadata.id,
252
+ file,
253
+ index,
254
+ total: bonsaiTextModelFiles.length,
255
+ ...progress,
256
+ modelBytesWritten,
257
+ estimatedModelBytes: metadata.estimatedDownloadBytes,
258
+ modelFraction: Math.min(1, modelBytesWritten / metadata.estimatedDownloadBytes)
259
+ });
260
+ };
261
+ for (const [fileIndex, file] of bonsaiTextModelFiles.entries()) {
262
+ throwIfAborted$1(options.signal);
263
+ const index = fileIndex + 1;
264
+ const destination = root.file(file);
265
+ const expectedBytes = fileSizes[metadata.id][file];
266
+ const stat = await destination.stat();
267
+ if (stat.type === "file" && validFile(metadata.id, file, stat.size)) {
268
+ emit(file, index, {
269
+ bytesWritten: stat.size ?? 0,
270
+ totalBytes: stat.size,
271
+ fraction: 1
272
+ });
273
+ continue;
274
+ }
275
+ await destination.download(resolveUrl(metadata.repo, metadata.revision, file), {
276
+ headers,
277
+ expectedBytes,
278
+ atomic: true,
279
+ resume: true,
280
+ signal: options.signal,
281
+ onProgress(progress) {
282
+ emit(file, index, progress);
283
+ }
284
+ }).result;
285
+ throwIfAborted$1(options.signal);
286
+ }
287
+ await root.file("manifest.json").write(JSON.stringify({
288
+ formatVersion: 1,
289
+ model: metadata.id,
290
+ revision: metadata.revision
291
+ }));
292
+ const state = await stateFor(metadata.id, root.path);
293
+ if (!state.verified) throw new AIError("invalid_model", `Downloaded Bonsai model is incomplete: ${[...state.missingFiles, ...state.invalidFiles].join(", ")}`);
294
+ return {
295
+ model: state.model,
296
+ path: root.path
297
+ };
298
+ }
299
+ function verifyBonsaiTextModel(model, options = {}) {
300
+ return stateFor(model, options.path);
301
+ }
302
+ function listBonsaiTextModels(options = {}) {
303
+ return Promise.all(bonsaiTextModelCatalog.map((metadata) => stateFor(metadata.id, options.directory ? require_files.files.directory(options.directory, metadata.id).path : void 0)));
304
+ }
305
+ async function deleteBonsaiTextModel(model, options = {}) {
306
+ await modelDirectory(model, options.path).delete({ recursive: true });
59
307
  }
60
308
 
61
309
  //#endregion
62
- //#region src/ai/image-jobs.ts
63
- var ImageGenerationQueue = class {
64
- jobs = /* @__PURE__ */ new Map();
65
- pending = [];
66
- running;
67
- constructor(executor) {
310
+ //#region src/ai/chat-manager.ts
311
+ const missingSnapshot = {
312
+ chat: null,
313
+ messages: []
314
+ };
315
+ var ChatManager = class {
316
+ snapshots = /* @__PURE__ */ new Map();
317
+ chatListeners = /* @__PURE__ */ new Map();
318
+ listListeners = /* @__PURE__ */ new Set();
319
+ controllers = /* @__PURE__ */ new Map();
320
+ listSnapshot = [];
321
+ operation = Promise.resolve();
322
+ ready;
323
+ constructor(repository, executor) {
324
+ this.repository = repository;
68
325
  this.executor = executor;
326
+ this.ready = this.initialize();
69
327
  }
70
328
  setExecutor(executor) {
71
329
  this.executor = executor;
72
330
  }
73
- schedule(options) {
74
- const id = crypto.randomUUID();
75
- let resolve;
76
- let reject;
77
- const promise = new Promise((onResolve, onReject) => {
78
- resolve = onResolve;
79
- reject = onReject;
331
+ async create(input = {}) {
332
+ await this.ready;
333
+ return this.mutate(async () => {
334
+ const chat$1 = await this.repository.create({ title: validTitle(input.title ?? "New Chat") });
335
+ this.set({
336
+ chat: chat$1,
337
+ messages: []
338
+ });
339
+ return chat$1;
80
340
  });
81
- const record = {
82
- snapshot: {
83
- id,
84
- status: "queued",
85
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
86
- },
87
- options,
88
- controller: new AbortController(),
89
- listeners: /* @__PURE__ */ new Set(),
90
- promise,
91
- resolve,
92
- reject
93
- };
94
- this.jobs.set(id, record);
95
- this.pending.push(record);
96
- if (options.signal) {
97
- const cancel = () => this.cancel(id);
98
- options.signal.addEventListener("abort", cancel, { once: true });
99
- record.removeSignalListener = () => options.signal?.removeEventListener("abort", cancel);
100
- if (options.signal.aborted) cancel();
101
- }
102
- this.runNext();
103
- return this.handle(record);
104
- }
105
- get(id) {
106
- const record = this.jobs.get(id);
107
- return record ? this.handle(record) : void 0;
108
341
  }
109
- list() {
110
- return [...this.jobs.values()].map(({ snapshot }) => ({ ...snapshot }));
342
+ async get(id) {
343
+ await this.ready;
344
+ return this.snapshots.get(id)?.chat ?? null;
345
+ }
346
+ async list() {
347
+ await this.ready;
348
+ return this.listSnapshot;
349
+ }
350
+ async send(id, content, options = {}, callbacks = {}) {
351
+ await this.ready;
352
+ if (options.signal?.aborted) throw canceledException();
353
+ const value = content.trim();
354
+ if (!value) throw new AIError("invalid_message", "Message must not be empty");
355
+ const controller = new AbortController();
356
+ const accepted = await this.mutate(async () => {
357
+ if (this.controllers.has(id)) throw new AIError("concurrent_request", "This chat already has an active generation");
358
+ const next = await this.repository.acceptSend(id, value);
359
+ this.controllers.set(id, controller);
360
+ this.setAccepted(next);
361
+ return next;
362
+ });
363
+ this.run(accepted, options, callbacks, controller);
364
+ return resultFromAccepted(accepted);
365
+ }
366
+ async cancel(id) {
367
+ await this.ready;
368
+ return this.mutate(async () => {
369
+ const controller = this.controllers.get(id);
370
+ const assistant = this.snapshots.get(id)?.messages.findLast((message) => message.role === "assistant");
371
+ if (!controller || !assistant || !isActiveMessage(assistant)) return false;
372
+ const next = await this.repository.markCanceling(id, assistant.id);
373
+ this.setAccepted(next);
374
+ controller.abort();
375
+ return true;
376
+ });
111
377
  }
112
- cancel(id) {
113
- const record = this.jobs.get(id);
114
- if (!record || !["queued", "running"].includes(record.snapshot.status)) return false;
115
- record.controller.abort();
116
- if (record.snapshot.status === "queued") {
117
- const index = this.pending.indexOf(record);
118
- if (index >= 0) this.pending.splice(index, 1);
119
- const error = canceledError();
120
- this.update(record, {
121
- status: "canceled",
122
- error
378
+ async retry(id, messageId, options = {}, callbacks = {}) {
379
+ await this.ready;
380
+ if (options.signal?.aborted) throw canceledException();
381
+ const controller = new AbortController();
382
+ const accepted = await this.mutate(async () => {
383
+ if (this.controllers.has(id)) throw new AIError("concurrent_request", "This chat already has an active generation");
384
+ const next = await this.repository.retry(id, messageId);
385
+ this.controllers.set(id, controller);
386
+ this.setAccepted(next);
387
+ return next;
388
+ });
389
+ this.run(accepted, options, callbacks, controller);
390
+ return resultFromAccepted(accepted);
391
+ }
392
+ async setTitle(id, title) {
393
+ await this.ready;
394
+ return this.mutate(async () => {
395
+ const chat$1 = await this.repository.setTitle(id, validTitle(title));
396
+ this.set({
397
+ chat: chat$1,
398
+ messages: this.snapshots.get(id)?.messages ?? []
123
399
  });
124
- record.removeSignalListener?.();
125
- record.reject(error);
126
- }
127
- return true;
400
+ return chat$1;
401
+ });
128
402
  }
129
- handle(record) {
130
- return {
131
- id: record.snapshot.id,
132
- promise: record.promise,
133
- cancel: () => this.cancel(record.snapshot.id),
134
- getSnapshot: () => ({ ...record.snapshot }),
135
- subscribe: (listener) => {
136
- record.listeners.add(listener);
137
- listener({ ...record.snapshot });
138
- return () => record.listeners.delete(listener);
403
+ async remove(id) {
404
+ await this.ready;
405
+ return this.mutate(async () => {
406
+ if (this.controllers.has(id)) throw new AIError("concurrent_request", "Cancel the generation before removing this chat");
407
+ const removed = await this.repository.remove(id);
408
+ if (removed) {
409
+ this.snapshots.delete(id);
410
+ this.refreshList();
411
+ this.emit(id);
139
412
  }
413
+ return removed;
414
+ });
415
+ }
416
+ getChatSnapshot(id) {
417
+ return this.snapshots.get(id) ?? missingSnapshot;
418
+ }
419
+ getListSnapshot() {
420
+ return this.listSnapshot;
421
+ }
422
+ subscribeChat(id, listener) {
423
+ const listeners = this.chatListeners.get(id) ?? /* @__PURE__ */ new Set();
424
+ listeners.add(listener);
425
+ this.chatListeners.set(id, listeners);
426
+ return () => {
427
+ listeners.delete(listener);
428
+ if (listeners.size === 0) this.chatListeners.delete(id);
140
429
  };
141
430
  }
142
- async runNext() {
143
- if (this.running) return;
144
- const record = this.pending.shift();
145
- if (!record) return;
146
- this.running = record;
147
- this.update(record, {
148
- status: "running",
149
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
431
+ subscribeList(listener) {
432
+ this.listListeners.add(listener);
433
+ return () => this.listListeners.delete(listener);
434
+ }
435
+ async initialize() {
436
+ await this.repository.recoverActive();
437
+ const chats$1 = await this.repository.list();
438
+ const transcripts = await Promise.all(chats$1.map((chat$1) => this.repository.messages(chat$1.id)));
439
+ chats$1.forEach((chat$1, index) => {
440
+ this.snapshots.set(chat$1.id, {
441
+ chat: chat$1,
442
+ messages: transcripts[index] ?? []
443
+ });
150
444
  });
445
+ this.refreshList();
446
+ }
447
+ run(accepted, options, callbacks, controller) {
448
+ const externalAbort = () => void this.cancel(accepted.chatId);
449
+ options.signal?.addEventListener("abort", externalAbort, { once: true });
450
+ if (options.signal?.aborted) externalAbort();
451
+ this.generate(accepted, options, callbacks, controller).finally(() => {
452
+ options.signal?.removeEventListener("abort", externalAbort);
453
+ }).catch(() => void 0);
454
+ }
455
+ async generate(accepted, options, callbacks, controller) {
456
+ let current = accepted.assistantMessage;
457
+ let content = "";
458
+ let lastPersistedAt = 0;
151
459
  try {
152
- const result = await this.executor({
153
- ...record.options,
154
- signal: record.controller.signal,
155
- onProgress: (progress) => {
156
- record.options.onProgress?.(progress);
157
- this.progress(record, progress);
158
- }
460
+ const started = await this.mutate(async () => {
461
+ const next = await this.repository.start(accepted.chatId, accepted.assistantMessageId);
462
+ this.setAccepted(next);
463
+ return next;
159
464
  });
160
- this.update(record, {
161
- status: "completed",
162
- result,
163
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
465
+ current = started.assistantMessage;
466
+ const messages = historyBefore(started.messages, current, options.system);
467
+ const executorOptions = {
468
+ ...options,
469
+ messages,
470
+ signal: controller.signal
471
+ };
472
+ delete executorOptions.system;
473
+ for await (const snapshot of this.executor.stream(executorOptions)) {
474
+ content = snapshot;
475
+ await this.mutate(async () => {
476
+ current = {
477
+ ...current,
478
+ content,
479
+ updatedAt: now$1()
480
+ };
481
+ this.replaceMessage(started.chatId, current);
482
+ const timestamp$1 = Date.now();
483
+ if (timestamp$1 - lastPersistedAt >= 100) {
484
+ current = await this.repository.updateContent(current);
485
+ lastPersistedAt = timestamp$1;
486
+ }
487
+ });
488
+ }
489
+ await this.mutate(async () => {
490
+ const next = await this.repository.finish(started.chatId, current.id, controller.signal.aborted ? "canceled" : "completed", content, controller.signal.aborted ? canceledError$1() : void 0);
491
+ this.controllers.delete(started.chatId);
492
+ this.setAccepted(next);
493
+ if (next.assistantMessage.status === "completed") notify(callbacks.onFinish, next.assistantMessage);
494
+ else notify(callbacks.onError, next.assistantMessage.error);
164
495
  });
165
- record.resolve(result);
166
496
  } catch (error) {
167
- const canceled = record.controller.signal.aborted;
168
- const failure = canceled ? canceledError(error) : error;
169
- this.update(record, {
170
- status: canceled ? "canceled" : "failed",
171
- error: failure,
172
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
497
+ const serialized = controller.signal.aborted ? canceledError$1() : serializeError(error);
498
+ await this.mutate(async () => {
499
+ const next = await this.repository.finish(accepted.chatId, accepted.assistantMessageId, controller.signal.aborted ? "canceled" : "failed", content, serialized);
500
+ this.controllers.delete(accepted.chatId);
501
+ this.setAccepted(next);
502
+ notify(callbacks.onError, serialized);
173
503
  });
174
- record.reject(failure);
175
504
  } finally {
176
- record.removeSignalListener?.();
177
- this.running = void 0;
178
- this.runNext();
505
+ if (this.controllers.get(accepted.chatId) === controller) this.controllers.delete(accepted.chatId);
179
506
  }
180
507
  }
181
- progress(record, progress) {
182
- this.update(record, { progress });
508
+ mutate(action) {
509
+ const result = this.operation.then(action, action);
510
+ this.operation = result.then(() => void 0, () => void 0);
511
+ return result;
183
512
  }
184
- update(record, update) {
185
- record.snapshot = {
186
- ...record.snapshot,
187
- ...update
188
- };
189
- for (const listener of record.listeners) listener({ ...record.snapshot });
513
+ setAccepted(value) {
514
+ this.set({
515
+ chat: value.chat,
516
+ messages: value.messages
517
+ });
190
518
  }
191
- };
192
- function canceledError(cause) {
193
- return new AIError("generation_canceled", "Image generation canceled", cause);
194
- }
195
-
196
- //#endregion
197
- //#region src/ai/image-models.ts
198
- const flux2Klein4B8BitAbliterated = {
199
- formatVersion: 1,
200
- model: "flux2-klein-4b-8bit-abliterated",
201
- directory: "flux2-klein-4b-8bit-abliterated",
202
- metadata: {
203
- displayName: "Flux.2 Klein 4B Abliterated",
204
- family: "flux2-klein",
205
- precision: "8-bit",
206
- estimatedMemoryGB: 12,
207
- quality: "high",
208
- speed: "fast",
209
- capabilities: [
210
- "text-to-image",
211
- "image-to-image",
212
- "inpainting",
213
- "outpainting",
214
- "references",
215
- "lora"
216
- ]
217
- },
218
- components: {
219
- textEncoder: {
220
- repo: "mlx-community/Josiefied-Qwen3-4B-abliterated-v1-8bit",
221
- revision: "main",
222
- path: "text-encoder",
223
- files: [
224
- "config.json",
225
- "tokenizer.json",
226
- "tokenizer_config.json",
227
- "*.safetensors",
228
- "*.safetensors.index.json"
229
- ]
230
- },
231
- transformer: {
232
- repo: "moxin-org/FLUX.2-klein-4B-8bit-mlx",
233
- revision: "main",
234
- path: "transformer",
235
- files: ["config.json", "quantized_8bit.safetensors"]
236
- },
237
- vae: {
238
- repo: "black-forest-labs/FLUX.2-klein-4B",
239
- revision: "main",
240
- path: "vae",
241
- files: [
242
- "config.json",
243
- "diffusion_pytorch_model.safetensors",
244
- "diffusion_pytorch_model.safetensors.index.json",
245
- "diffusion_pytorch_model-*.safetensors"
246
- ]
247
- }
519
+ replaceMessage(chatId, message) {
520
+ const snapshot = this.snapshots.get(chatId);
521
+ if (!snapshot) return;
522
+ this.set({
523
+ chat: snapshot.chat,
524
+ messages: snapshot.messages.map((item) => item.id === message.id ? message : item)
525
+ });
526
+ }
527
+ set(snapshot) {
528
+ if (!snapshot.chat) return;
529
+ const previous = this.snapshots.get(snapshot.chat.id);
530
+ this.snapshots.set(snapshot.chat.id, snapshot);
531
+ if (previous?.chat !== snapshot.chat) this.refreshList();
532
+ this.emit(snapshot.chat.id);
533
+ }
534
+ refreshList() {
535
+ this.listSnapshot = [...this.snapshots.values()].flatMap((snapshot) => snapshot.chat ? [snapshot.chat] : []).sort((left, right) => left.updatedAt === right.updatedAt ? right.id.localeCompare(left.id) : right.updatedAt.localeCompare(left.updatedAt));
536
+ for (const listener of this.listListeners) listener();
537
+ }
538
+ emit(id) {
539
+ for (const listener of this.chatListeners.get(id) ?? []) listener();
248
540
  }
249
541
  };
250
- const imageModelManifests = [flux2Klein4B8BitAbliterated];
251
- function imageModelManifest(model) {
252
- return imageModelManifests.find((manifest) => manifest.model === model);
542
+ function historyBefore(messages, target, system) {
543
+ return [...system?.trim() ? [{
544
+ role: "system",
545
+ content: system
546
+ }] : [], ...messages.filter((message) => message.sequence < target.sequence && (message.role === "user" || message.status === "completed")).map((message) => ({
547
+ role: message.role,
548
+ content: message.content
549
+ }))];
253
550
  }
254
- const flux2KleinModelFiles = [
255
- {
256
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
257
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
258
- source: "config.json",
259
- destination: "text-encoder/config.json",
260
- bytes: 937
261
- },
262
- {
263
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
264
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
265
- source: "tokenizer.json",
266
- destination: "text-encoder/tokenizer.json",
267
- bytes: 11422654
268
- },
269
- {
270
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
271
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
272
- source: "tokenizer_config.json",
273
- destination: "text-encoder/tokenizer_config.json",
274
- bytes: 9706
275
- },
276
- {
277
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
278
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
279
- source: "model.safetensors",
280
- destination: "text-encoder/model.safetensors",
281
- bytes: 4274158989
282
- },
283
- {
284
- repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
285
- revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
286
- source: "model.safetensors.index.json",
287
- destination: "text-encoder/model.safetensors.index.json",
288
- bytes: 63924
289
- },
290
- {
291
- repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
292
- revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
293
- source: "transformer/config.json",
294
- destination: "transformer/config.json",
295
- bytes: 541
296
- },
297
- {
298
- repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
299
- revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
300
- source: "transformer/quantized_8bit.safetensors",
301
- destination: "transformer/quantized_8bit.safetensors",
302
- bytes: 4360050983
303
- },
304
- {
305
- repo: flux2Klein4B8BitAbliterated.components.vae.repo,
306
- revision: flux2Klein4B8BitAbliterated.components.vae.revision,
307
- source: "vae/config.json",
308
- destination: "vae/config.json",
309
- bytes: 821
310
- },
311
- {
312
- repo: flux2Klein4B8BitAbliterated.components.vae.repo,
313
- revision: flux2Klein4B8BitAbliterated.components.vae.revision,
314
- source: "vae/diffusion_pytorch_model.safetensors",
315
- destination: "vae/diffusion_pytorch_model.safetensors",
316
- bytes: 168120878
317
- }
318
- ];
319
- const flux2KleinTotalBytes = flux2KleinModelFiles.reduce((total, file) => total + file.bytes, 0);
320
- function joinModelPath(root, component) {
321
- return `${root.replace(/\/+$/, "")}/${component.replace(/^\/+/, "")}`;
551
+ function isActiveMessage(message) {
552
+ return message.role === "assistant" && (message.status === "queued" || message.status === "generating" || message.status === "canceling");
553
+ }
554
+ function resultFromAccepted(value) {
555
+ return {
556
+ chatId: value.chatId,
557
+ userMessageId: value.userMessageId,
558
+ assistantMessageId: value.assistantMessageId
559
+ };
560
+ }
561
+ function validTitle(title) {
562
+ const value = title.trim();
563
+ if (!value) throw new AIError("invalid_message", "Title must not be empty");
564
+ return value;
322
565
  }
323
- function fluxImageModelPaths(modelRoot, manifest = flux2Klein4B8BitAbliterated) {
566
+ function serializeError(error) {
567
+ const value = error instanceof Error ? error : new Error(String(error));
568
+ const details = value;
324
569
  return {
325
- modelRoot,
326
- textEncoderPath: joinModelPath(modelRoot, manifest.components.textEncoder.path),
327
- transformerPath: joinModelPath(modelRoot, manifest.components.transformer.path),
328
- vaePath: joinModelPath(modelRoot, manifest.components.vae.path)
570
+ name: value.name,
571
+ message: value.message,
572
+ code: typeof details.code === "string" ? details.code : void 0,
573
+ stage: typeof details.stage === "string" ? details.stage : void 0
329
574
  };
330
575
  }
331
-
332
- //#endregion
333
- //#region src/ai/image-schedule.ts
334
- function imageSigmaSchedule(schedules) {
335
- const configured = schedules.filter((schedule) => schedule !== void 0);
336
- if (configured.length === 0) return void 0;
337
- const first = configured[0];
338
- if (first.length === 0 || first.some((sigma, index) => !Number.isFinite(sigma) || sigma <= 0 || sigma > 1 || index > 0 && sigma >= first[index - 1])) throw new AIError("invalid_model", "LoRA sigmaSchedule must contain descending values greater than 0 and at most 1");
339
- if (configured.some((schedule) => schedule.length !== first.length || schedule.some((sigma, index) => sigma !== first[index]))) throw new AIError("invalid_model", "Selected LoRAs require conflicting sigma schedules");
340
- return [...first];
341
- }
342
-
343
- //#endregion
344
- //#region src/ai/image-upscale.ts
345
- function normalizeUpscaleScale(value) {
346
- const scale = value ?? 2;
347
- if (!Number.isFinite(scale) || scale <= 1 || scale > 4) throw new AIError("invalid_model", "Upscale scale must be greater than 1 and at most 4");
348
- return scale;
349
- }
350
-
351
- //#endregion
352
- //#region src/ai/native.ts
353
- function bridge() {
354
- try {
355
- return (0, expo.requireNativeModule)("AI");
356
- } catch (error) {
357
- throw new AIError("unsupported_runtime", "bleam/ai requires the Bleam native runtime", error);
358
- }
359
- }
576
+ function canceledError$1() {
577
+ return {
578
+ name: "AIError",
579
+ code: "generation_canceled",
580
+ message: "Generation canceled"
581
+ };
582
+ }
583
+ function canceledException() {
584
+ return new AIError("generation_canceled", "Generation canceled");
585
+ }
586
+ function notify(callback, value) {
587
+ if (!callback) return;
588
+ Promise.resolve().then(() => callback(value)).catch(() => void 0);
589
+ }
590
+ function now$1() {
591
+ return (/* @__PURE__ */ new Date()).toISOString();
592
+ }
360
593
 
361
594
  //#endregion
362
595
  //#region src/ai/tokenizers.mjs
@@ -496,7 +729,7 @@ var create_pattern = (pattern, invert = true) => {
496
729
  return null;
497
730
  }
498
731
  };
499
- var escape_reg_exp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
732
+ var escape_reg_exp = (string$1) => string$1.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
500
733
  var fuse_unk = (arr, tokens_to_ids, unk_token_id) => {
501
734
  const fused = [];
502
735
  let i = 0;
@@ -2218,16 +2451,16 @@ var ByteFallback = class extends Decoder_default {
2218
2451
  if (bytes !== null) previous_byte_tokens.push(bytes);
2219
2452
  else {
2220
2453
  if (previous_byte_tokens.length > 0) {
2221
- const string = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
2222
- new_tokens.push(string);
2454
+ const string$1 = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
2455
+ new_tokens.push(string$1);
2223
2456
  previous_byte_tokens = [];
2224
2457
  }
2225
2458
  new_tokens.push(token);
2226
2459
  }
2227
2460
  }
2228
2461
  if (previous_byte_tokens.length > 0) {
2229
- const string = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
2230
- new_tokens.push(string);
2462
+ const string$1 = this.text_decoder.decode(Uint8Array.from(previous_byte_tokens));
2463
+ new_tokens.push(string$1);
2231
2464
  previous_byte_tokens = [];
2232
2465
  }
2233
2466
  return new_tokens;
@@ -2422,16 +2655,1266 @@ var Tokenizer = class {
2422
2655
  };
2423
2656
  var Tokenizer_default = Tokenizer;
2424
2657
 
2658
+ //#endregion
2659
+ //#region src/ai/bonsai-generation.ts
2660
+ async function prepareBonsaiGeneration(input) {
2661
+ const state = await verifyBonsaiTextModel(input.model, { path: input.path });
2662
+ if (!state.verified) throw new AIError("model_unavailable", `${input.model.id} is not prepared. Call prepareTextModel() before generation.`);
2663
+ const tokenizer = new Tokenizer_default(await require_files.files.file(state.path, "tokenizer.json").json(), await require_files.files.file(state.path, "tokenizer_config.json").json());
2664
+ const inputIds = tokenizer.encode(formatMessages(messagesFromInput(input))).ids.map(Number);
2665
+ if (!inputIds.length || inputIds.some((token) => !Number.isInteger(token))) throw new AIError("invalid_message", "Tokenizer returned invalid token ids");
2666
+ return {
2667
+ request: {
2668
+ id: input.id,
2669
+ model: input.model.id,
2670
+ modelPath: state.path,
2671
+ inputIds,
2672
+ endTokenIds: endTokenIds(input.model.id),
2673
+ maxTokens: input.maxTokens,
2674
+ temperature: input.temperature,
2675
+ seed: input.seed
2676
+ },
2677
+ decode(tokenIds) {
2678
+ return tokenIds.length ? tokenizer.decode(tokenIds, { skip_special_tokens: true }) : "";
2679
+ }
2680
+ };
2681
+ }
2682
+ function messagesFromInput(input) {
2683
+ if (input.messages) {
2684
+ if (input.system !== void 0 || input.prompt !== void 0) throw new AIError("invalid_message", "messages cannot be combined with system or prompt");
2685
+ if (!input.messages.length) throw new AIError("invalid_message", "Expected at least one message");
2686
+ return input.messages;
2687
+ }
2688
+ if (!input.prompt?.trim()) throw new AIError("invalid_message", "Expected prompt");
2689
+ return [...input.system ? [{
2690
+ role: "system",
2691
+ content: input.system
2692
+ }] : [], {
2693
+ role: "user",
2694
+ content: input.prompt
2695
+ }];
2696
+ }
2697
+ function formatMessages(messages) {
2698
+ if (messages[0]?.role === "assistant") throw new AIError("invalid_message", "Conversation cannot begin with assistant");
2699
+ const parts = messages.map((message, index) => {
2700
+ if (!message.content.trim()) throw new AIError("invalid_message", `Message ${index + 1} is empty`);
2701
+ if (message.role === "system" && index !== 0) throw new AIError("invalid_message", "System message must be first");
2702
+ return `<|im_start|>${message.role}\n${message.content}<|im_end|>\n`;
2703
+ });
2704
+ parts.push("<|im_start|>assistant\n<think>\n\n</think>\n\n");
2705
+ return parts.join("");
2706
+ }
2707
+ function endTokenIds(model) {
2708
+ return model === "ternary-bonsai-27b" ? [248044, 248046] : [151643, 151645];
2709
+ }
2710
+
2711
+ //#endregion
2712
+ //#region src/ai/native.ts
2713
+ function bridge() {
2714
+ try {
2715
+ return (0, expo.requireNativeModule)("AI");
2716
+ } catch (error) {
2717
+ throw new AIError("unsupported_runtime", "bleam/ai requires the Bleam native runtime", error);
2718
+ }
2719
+ }
2720
+
2721
+ //#endregion
2722
+ //#region src/ai/chat-executor.ts
2723
+ function generationId() {
2724
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
2725
+ }
2726
+ function foundationRequest(options, id) {
2727
+ if (options.temperature !== void 0 && (!Number.isFinite(options.temperature) || options.temperature < 0)) throw new AIError("invalid_message", "temperature must be a non-negative number");
2728
+ if (options.maxTokens !== void 0 && (!Number.isInteger(options.maxTokens) || options.maxTokens <= 0)) throw new AIError("invalid_message", "maxTokens must be a positive integer");
2729
+ if (options.messages.length === 0) throw new AIError("invalid_message", "Expected at least one message");
2730
+ let system;
2731
+ const conversation = [];
2732
+ for (const [index, message] of options.messages.entries()) {
2733
+ if (!message.content.trim()) throw new AIError("invalid_message", `Message ${index + 1} is empty`);
2734
+ if (message.role === "system") {
2735
+ if (index !== 0 || system !== void 0) throw new AIError("invalid_message", "System message must be first");
2736
+ system = message.content;
2737
+ } else conversation.push({
2738
+ role: message.role,
2739
+ content: message.content
2740
+ });
2741
+ }
2742
+ if (conversation[0]?.role === "assistant") throw new AIError("invalid_message", "Conversation cannot begin with assistant");
2743
+ const final = conversation.at(-1);
2744
+ if (final?.role !== "user") throw new AIError("invalid_message", "Conversation must end with a user message");
2745
+ return {
2746
+ id,
2747
+ system,
2748
+ prompt: final.content,
2749
+ messages: conversation.slice(0, -1),
2750
+ temperature: options.temperature,
2751
+ maxTokens: options.maxTokens
2752
+ };
2753
+ }
2754
+ async function* streamChat(options) {
2755
+ if (options.model) {
2756
+ yield* streamBonsaiChat(options);
2757
+ return;
2758
+ }
2759
+ const id = generationId();
2760
+ const native = bridge();
2761
+ const updates = [];
2762
+ let wake;
2763
+ let settled = false;
2764
+ let failure;
2765
+ const updateSubscription = native.addListener?.("textGenerationUpdate", (event) => {
2766
+ if (event.id !== id) return;
2767
+ updates.push(event.content);
2768
+ wake?.();
2769
+ wake = void 0;
2770
+ });
2771
+ const abort = () => void native.cancelTextGeneration?.(id);
2772
+ options.signal.addEventListener("abort", abort, { once: true });
2773
+ const completion = native.streamGenerate?.(foundationRequest(options, id)).catch((error) => {
2774
+ failure = error;
2775
+ }).finally(() => {
2776
+ settled = true;
2777
+ wake?.();
2778
+ wake = void 0;
2779
+ });
2780
+ try {
2781
+ if (options.signal.aborted) {
2782
+ abort();
2783
+ throw new AIError("generation_canceled", "Generation canceled");
2784
+ }
2785
+ if (!completion) throw new AIError("unsupported_runtime", "Streaming is unavailable in this runtime");
2786
+ while (!settled || updates.length > 0) if (updates.length > 0) yield updates.shift();
2787
+ else await new Promise((resolve) => {
2788
+ wake = resolve;
2789
+ });
2790
+ await completion;
2791
+ if (failure) throw mapNativeError(failure);
2792
+ } finally {
2793
+ if (!settled) await native.cancelTextGeneration?.(id);
2794
+ options.signal.removeEventListener("abort", abort);
2795
+ updateSubscription?.remove();
2796
+ }
2797
+ }
2798
+ async function* streamBonsaiChat(options) {
2799
+ const id = generationId();
2800
+ const native = bridge();
2801
+ if (options.signal.aborted) throw new AIError("generation_canceled", "Generation canceled");
2802
+ const prepared = await prepareBonsaiGeneration({
2803
+ id,
2804
+ model: options.model,
2805
+ path: options.path,
2806
+ messages: options.messages,
2807
+ temperature: options.temperature,
2808
+ maxTokens: options.maxTokens,
2809
+ seed: options.seed
2810
+ });
2811
+ if (options.signal.aborted) throw new AIError("generation_canceled", "Generation canceled");
2812
+ const updates = [];
2813
+ let wake;
2814
+ let settled = false;
2815
+ let failure;
2816
+ const updateSubscription = native.addListener?.("localTextGenerationUpdate", (event) => {
2817
+ if (event.id !== id) return;
2818
+ updates.push(event.tokenIds);
2819
+ wake?.();
2820
+ wake = void 0;
2821
+ });
2822
+ const abort = () => void native.cancelTextGeneration?.(id);
2823
+ options.signal.addEventListener("abort", abort, { once: true });
2824
+ const completion = native.streamLocalText?.(prepared.request).catch((error) => {
2825
+ failure = error;
2826
+ return [];
2827
+ }).finally(() => {
2828
+ settled = true;
2829
+ wake?.();
2830
+ wake = void 0;
2831
+ });
2832
+ try {
2833
+ if (!completion) throw new AIError("unsupported_runtime", "Local text streaming is unavailable in this runtime");
2834
+ while (!settled || updates.length > 0) if (updates.length > 0) yield prepared.decode(updates.shift());
2835
+ else await new Promise((resolve) => {
2836
+ wake = resolve;
2837
+ });
2838
+ await completion;
2839
+ if (failure) throw mapNativeError(failure);
2840
+ } finally {
2841
+ if (!settled) await native.cancelTextGeneration?.(id);
2842
+ options.signal.removeEventListener("abort", abort);
2843
+ updateSubscription?.remove();
2844
+ }
2845
+ }
2846
+
2847
+ //#endregion
2848
+ //#region src/ai/chat.tsx
2849
+ const chatShape = {
2850
+ title: require_schema.string({ default: "New Chat" }),
2851
+ status: require_schema.string({ default: "ready" })
2852
+ };
2853
+ const errorShape = {
2854
+ name: require_schema.string(),
2855
+ message: require_schema.string(),
2856
+ code: require_schema.string(),
2857
+ stage: require_schema.string()
2858
+ };
2859
+ const messageShape = {
2860
+ chatId: require_schema.string(),
2861
+ role: require_schema.string(),
2862
+ status: require_schema.string(),
2863
+ content: require_schema.string({ default: "" }),
2864
+ attempt: require_schema.number({ default: 1 }),
2865
+ sequence: require_schema.number(),
2866
+ error: require_schema.object(errorShape),
2867
+ startedAt: require_schema.string(),
2868
+ completedAt: require_schema.string()
2869
+ };
2870
+ const chats = require_state.atomCollection("__bleam_ai_chats", chatShape);
2871
+ const chatMessages = require_state.atomCollection("__bleam_ai_chat_messages", messageShape);
2872
+ var CollectionChatRepository = class {
2873
+ async create(input) {
2874
+ const insert = require_state.mutateAtom(chats, (mutation) => mutation.insert());
2875
+ const id = await require_state.internalStore.set(require_state.internalJotaiWritableAtom(insert), {
2876
+ title: input.title ?? "New Chat",
2877
+ status: "ready"
2878
+ });
2879
+ return this.requireChat(id);
2880
+ }
2881
+ async get(id) {
2882
+ const row = require_state.queryAtom(chats, (query) => query.row(id));
2883
+ return chatFromRow(await require_state.internalStore.get(require_state.internalJotaiAtom(row)));
2884
+ }
2885
+ async list() {
2886
+ const ids = require_state.queryAtom(chats, (query) => query.sortBy("updatedAt", "desc").ids());
2887
+ const values = await require_state.internalStore.get(require_state.internalJotaiAtom(ids));
2888
+ return Promise.all(values.map((id) => this.requireChat(id)));
2889
+ }
2890
+ async messages(chatId) {
2891
+ const ids = require_state.queryAtom(chatMessages, (query) => query.where({ chatId }).sortBy("sequence").ids());
2892
+ const values = await require_state.internalStore.get(require_state.internalJotaiAtom(ids));
2893
+ return (await Promise.all(values.map((id) => this.messageRow(id)))).flatMap((row) => row ? [messageFromRow(row)] : []);
2894
+ }
2895
+ async acceptSend(chatId, content) {
2896
+ const transaction = require_state.transactionAtom(async (tx) => {
2897
+ const chat$1 = await tx.get(chats, chatId);
2898
+ if (!chat$1) throw new Error(`Chat not found: ${chatId}`);
2899
+ if (isActiveStatus(chat$1.data.status)) throw new Error("This chat already has an active generation");
2900
+ const previousIds = await tx.query(chatMessages, (query) => query.where({ chatId }).sortBy("sequence", "desc").take(1).ids());
2901
+ const sequence = ((previousIds[0] ? await tx.get(chatMessages, previousIds[0]) : null)?.data.sequence ?? 0) + 1;
2902
+ const userId = await tx.insert(chatMessages, {
2903
+ chatId,
2904
+ role: "user",
2905
+ status: "completed",
2906
+ content,
2907
+ attempt: 1,
2908
+ sequence,
2909
+ completedAt: timestamp()
2910
+ });
2911
+ const assistantId = await tx.insert(chatMessages, {
2912
+ chatId,
2913
+ role: "assistant",
2914
+ status: "queued",
2915
+ content: "",
2916
+ attempt: 1,
2917
+ sequence: sequence + 1
2918
+ });
2919
+ return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "queued" }), assistantId, userId);
2920
+ });
2921
+ return require_state.internalStore.set(require_state.internalJotaiWritableAtom(transaction));
2922
+ }
2923
+ async start(chatId, messageId) {
2924
+ return this.transition(chatId, messageId, async (tx, _chat, message) => {
2925
+ if (message.data.status !== "queued") throw new Error("Only queued messages can start");
2926
+ const startedAt = timestamp();
2927
+ const assistant = await tx.update(chatMessages, messageId, {
2928
+ status: "generating",
2929
+ startedAt
2930
+ });
2931
+ return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "generating" }), assistant.id);
2932
+ });
2933
+ }
2934
+ async updateContent(message) {
2935
+ const update = require_state.mutateAtom(chatMessages, (mutation) => mutation.update(message.id));
2936
+ return messageFromRow(await require_state.internalStore.set(require_state.internalJotaiWritableAtom(update), { content: message.content }));
2937
+ }
2938
+ async markCanceling(chatId, messageId) {
2939
+ return this.transition(chatId, messageId, async (tx, _chat, message) => {
2940
+ if (!isActiveStatus(message.data.status ?? "")) throw new Error("Message is not active");
2941
+ const assistant = await tx.update(chatMessages, messageId, { status: "canceling" });
2942
+ return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: "canceling" }), assistant.id);
2943
+ });
2944
+ }
2945
+ async finish(chatId, messageId, status, content, error) {
2946
+ return this.transition(chatId, messageId, async (tx, _chat) => {
2947
+ const completedAt = timestamp();
2948
+ const assistant = await tx.update(chatMessages, messageId, {
2949
+ status,
2950
+ content,
2951
+ error,
2952
+ completedAt
2953
+ });
2954
+ return acceptedFromTransaction(tx, await tx.update(chats, chatId, { status: status === "completed" ? "ready" : status }), assistant.id);
2955
+ });
2956
+ }
2957
+ async retry(chatId, messageId) {
2958
+ return this.transition(chatId, messageId, async (tx, _chat, message) => {
2959
+ const ids = await tx.query(chatMessages, (query) => query.where({ chatId }).sortBy("sequence", "desc").ids());
2960
+ const rows = await Promise.all(ids.map((id) => tx.get(chatMessages, id)));
2961
+ if (rows.find((row) => row?.data.role === "assistant")?.id !== messageId) throw new Error("Only the latest assistant message can be retried");
2962
+ if (message.data.status !== "failed" && message.data.status !== "canceled") throw new Error("Only failed or canceled messages can be retried");
2963
+ const assistant = await tx.update(chatMessages, messageId, {
2964
+ status: "queued",
2965
+ content: "",
2966
+ attempt: message.data.attempt + 1,
2967
+ error: void 0,
2968
+ startedAt: void 0,
2969
+ completedAt: void 0
2970
+ });
2971
+ const nextChat = await tx.update(chats, chatId, { status: "queued" });
2972
+ const user = rows.find((row) => row?.data.role === "user" && (row.data.sequence ?? 0) < (assistant.data.sequence ?? 0));
2973
+ return acceptedFromTransaction(tx, nextChat, assistant.id, user?.id);
2974
+ });
2975
+ }
2976
+ async setTitle(id, title) {
2977
+ const update = require_state.mutateAtom(chats, (mutation) => mutation.update(id));
2978
+ return chatFromRow(await require_state.internalStore.set(require_state.internalJotaiWritableAtom(update), { title }));
2979
+ }
2980
+ async remove(id) {
2981
+ const transaction = require_state.transactionAtom(async (tx) => {
2982
+ const chat$1 = await tx.get(chats, id);
2983
+ if (!chat$1) return false;
2984
+ if (isActiveStatus(chat$1.data.status)) throw new Error("Active chats cannot be removed");
2985
+ const ids = await tx.query(chatMessages, (query) => query.where({ chatId: id }).ids());
2986
+ for (const messageId of ids) await tx.remove(chatMessages, messageId);
2987
+ return tx.remove(chats, id);
2988
+ });
2989
+ return require_state.internalStore.set(require_state.internalJotaiWritableAtom(transaction));
2990
+ }
2991
+ async recoverActive() {
2992
+ const transaction = require_state.transactionAtom(async (tx) => {
2993
+ const chatIds = await tx.query(chats, (query) => query.ids());
2994
+ for (const chatId of chatIds) {
2995
+ const chat$1 = await tx.get(chats, chatId);
2996
+ if (!chat$1) continue;
2997
+ const messageIds = await tx.query(chatMessages, (query) => query.where({ chatId }).ids());
2998
+ let recoveredMessage = false;
2999
+ for (const messageId of messageIds) {
3000
+ const message = await tx.get(chatMessages, messageId);
3001
+ if (message?.data.role === "assistant" && isActiveStatus(message.data.status ?? "")) {
3002
+ await tx.update(chatMessages, messageId, {
3003
+ status: "canceled",
3004
+ error: recoveryError(),
3005
+ completedAt: timestamp()
3006
+ });
3007
+ recoveredMessage = true;
3008
+ }
3009
+ }
3010
+ if (recoveredMessage || isActiveStatus(chat$1.data.status)) await tx.update(chats, chatId, { status: "canceled" });
3011
+ }
3012
+ });
3013
+ await require_state.internalStore.set(require_state.internalJotaiWritableAtom(transaction));
3014
+ }
3015
+ async requireChat(id) {
3016
+ const chat$1 = await this.get(id);
3017
+ if (!chat$1) throw new Error(`Chat not found: ${id}`);
3018
+ return chat$1;
3019
+ }
3020
+ async messageRow(id) {
3021
+ const row = require_state.queryAtom(chatMessages, (query) => query.row(id));
3022
+ return require_state.internalStore.get(require_state.internalJotaiAtom(row));
3023
+ }
3024
+ async transition(chatId, messageId, action) {
3025
+ const transaction = require_state.transactionAtom(async (tx) => {
3026
+ const chat$1 = await tx.get(chats, chatId);
3027
+ const message = await tx.get(chatMessages, messageId);
3028
+ if (!chat$1) throw new Error(`Chat not found: ${chatId}`);
3029
+ if (!message || message.data.chatId !== chatId) throw new Error(`Message not found: ${messageId}`);
3030
+ return action(tx, chat$1, message);
3031
+ });
3032
+ return require_state.internalStore.set(require_state.internalJotaiWritableAtom(transaction));
3033
+ }
3034
+ };
3035
+ const managerKey = Symbol.for("bleam.ai.chatManager.v1");
3036
+ function managedChats() {
3037
+ const scope = globalThis;
3038
+ scope[managerKey] ??= new ChatManager(new CollectionChatRepository(), { stream(options) {
3039
+ return streamChat(options);
3040
+ } });
3041
+ return scope[managerKey];
3042
+ }
3043
+ const chat = {
3044
+ create(input = {}) {
3045
+ return managedChats().create(input);
3046
+ },
3047
+ get(id) {
3048
+ return managedChats().get(id);
3049
+ },
3050
+ list() {
3051
+ return managedChats().list();
3052
+ },
3053
+ send(id, content, options) {
3054
+ return managedChats().send(id, content, options);
3055
+ },
3056
+ cancel(id) {
3057
+ return managedChats().cancel(id);
3058
+ },
3059
+ retry(id, messageId, options) {
3060
+ return managedChats().retry(id, messageId, options);
3061
+ },
3062
+ setTitle(id, title) {
3063
+ return managedChats().setTitle(id, title);
3064
+ },
3065
+ remove(id) {
3066
+ return managedChats().remove(id);
3067
+ }
3068
+ };
3069
+ function useChat(id, options = {}) {
3070
+ const manager = managedChats();
3071
+ (0, react.use)(manager.ready);
3072
+ const snapshot = (0, react.useSyncExternalStore)((listener) => manager.subscribeChat(id, listener), () => manager.getChatSnapshot(id));
3073
+ const generationOptions = optionsWithoutCallbacks(options);
3074
+ const callbacks = {
3075
+ onFinish: options.onFinish,
3076
+ onError: options.onError
3077
+ };
3078
+ const status = snapshot.chat?.status ?? "not-found";
3079
+ const active = status === "queued" || status === "generating" || status === "canceling";
3080
+ const error = snapshot.messages.findLast((message) => message.role === "assistant")?.error;
3081
+ return {
3082
+ chat: snapshot.chat,
3083
+ messages: snapshot.messages,
3084
+ status,
3085
+ error: error ?? null,
3086
+ isGenerating: active,
3087
+ canSend: snapshot.chat !== null && !active,
3088
+ canCancel: status === "queued" || status === "generating",
3089
+ send: (content, invocation) => manager.send(id, content, mergeOptions(generationOptions, invocation), callbacks),
3090
+ cancel: () => manager.cancel(id),
3091
+ retry: (messageId, invocation) => manager.retry(id, messageId, mergeOptions(generationOptions, invocation), callbacks),
3092
+ setTitle: (title) => manager.setTitle(id, title),
3093
+ remove: () => manager.remove(id)
3094
+ };
3095
+ }
3096
+ function useChatList() {
3097
+ const manager = managedChats();
3098
+ (0, react.use)(manager.ready);
3099
+ return (0, react.useSyncExternalStore)((listener) => manager.subscribeList(listener), () => manager.getListSnapshot());
3100
+ }
3101
+ async function acceptedFromTransaction(tx, chatRow, assistantId, userId) {
3102
+ const ids = await tx.query(chatMessages, (query) => query.where({ chatId: chatRow.id }).sortBy("sequence").ids());
3103
+ const messages = (await Promise.all(ids.map((id) => tx.get(chatMessages, id)))).flatMap((row) => row ? [messageFromRow(row)] : []);
3104
+ const assistantMessage = messages.find((message) => message.id === assistantId);
3105
+ if (assistantMessage?.role !== "assistant") throw new Error(`Assistant message not found: ${assistantId}`);
3106
+ const userMessage = userId ? messages.find((message) => message.id === userId) : void 0;
3107
+ return {
3108
+ chatId: chatRow.id,
3109
+ userMessageId: userMessage?.id ?? "",
3110
+ assistantMessageId: assistantId,
3111
+ chat: chatFromRow(chatRow),
3112
+ userMessage,
3113
+ assistantMessage,
3114
+ messages
3115
+ };
3116
+ }
3117
+ function chatFromRow(row) {
3118
+ if (!row) return null;
3119
+ return {
3120
+ id: row.id,
3121
+ title: row.data.title,
3122
+ status: row.data.status,
3123
+ createdAt: row.createdAt,
3124
+ updatedAt: row.updatedAt
3125
+ };
3126
+ }
3127
+ function messageFromRow(row) {
3128
+ return {
3129
+ id: row.id,
3130
+ chatId: row.data.chatId,
3131
+ role: row.data.role,
3132
+ status: row.data.status,
3133
+ content: row.data.content,
3134
+ attempt: row.data.attempt,
3135
+ sequence: row.data.sequence,
3136
+ error: row.data.error,
3137
+ createdAt: row.createdAt,
3138
+ updatedAt: row.updatedAt,
3139
+ startedAt: row.data.startedAt,
3140
+ completedAt: row.data.completedAt
3141
+ };
3142
+ }
3143
+ function optionsWithoutCallbacks(options) {
3144
+ const { onFinish: _onFinish, onError: _onError,...generation } = options;
3145
+ return generation;
3146
+ }
3147
+ function mergeOptions(defaults, invocation) {
3148
+ return {
3149
+ ...defaults,
3150
+ ...invocation
3151
+ };
3152
+ }
3153
+ function isActiveStatus(status) {
3154
+ return status === "queued" || status === "generating" || status === "canceling";
3155
+ }
3156
+ function recoveryError() {
3157
+ return {
3158
+ name: "AIError",
3159
+ code: "generation_canceled",
3160
+ message: "Generation was interrupted when the app exited"
3161
+ };
3162
+ }
3163
+ function timestamp() {
3164
+ return (/* @__PURE__ */ new Date()).toISOString();
3165
+ }
3166
+
3167
+ //#endregion
3168
+ //#region src/ai/image-batch.ts
3169
+ function batchImageSeeds(count, seed) {
3170
+ if (!Number.isInteger(count) || count < 1 || count > 64) throw new AIError("invalid_message", "Batch count must be an integer from 1 to 64");
3171
+ const first = seed ?? Math.floor(Math.random() * 4294967296);
3172
+ return Array.from({ length: count }, (_, index) => first + index >>> 0);
3173
+ }
3174
+
3175
+ //#endregion
3176
+ //#region src/ai/image-jobs.ts
3177
+ const jobShape = {
3178
+ status: require_schema.string({ default: "paused" }),
3179
+ request: require_schema.string({ default: "{}" }),
3180
+ progress: require_schema.string({ default: "" }),
3181
+ result: require_schema.string({ default: "" }),
3182
+ error: require_schema.string({ default: "" }),
3183
+ attempt: require_schema.number({ default: 1 }),
3184
+ startedAt: require_schema.string({ default: "" }),
3185
+ completedAt: require_schema.string({ default: "" })
3186
+ };
3187
+ var CollectionImageJobRepository = class {
3188
+ records = require_state.atomCollection("__bleam_ai_jobs", jobShape);
3189
+ insertRecord = require_state.mutateAtom(this.records, (mutation) => mutation.insert());
3190
+ async list() {
3191
+ const ids = await require_state.internalStore.get(require_state.internalJotaiAtom(this.records));
3192
+ return (await Promise.all(ids.map((id) => this.getRow(id)))).flatMap((row) => row ? [jobFromRow(row)] : []);
3193
+ }
3194
+ async insert(job) {
3195
+ const id = await require_state.internalStore.set(require_state.internalJotaiWritableAtom(this.insertRecord), dataFromJob(job));
3196
+ const row = await this.getRow(id);
3197
+ if (!row) throw new Error(`Could not load inserted image job ${id}`);
3198
+ return jobFromRow(row);
3199
+ }
3200
+ async update(job) {
3201
+ const updateRecord = require_state.mutateAtom(this.records, (mutation) => mutation.update(job.id));
3202
+ return jobFromRow(await require_state.internalStore.set(require_state.internalJotaiWritableAtom(updateRecord), dataFromJob(job)));
3203
+ }
3204
+ async remove(id) {
3205
+ const removeRecord = require_state.mutateAtom(this.records, (mutation) => mutation.remove(id));
3206
+ await require_state.internalStore.set(require_state.internalJotaiWritableAtom(removeRecord));
3207
+ }
3208
+ async getRow(id) {
3209
+ const row = require_state.queryAtom(this.records, (query) => query.row(id));
3210
+ return require_state.internalStore.get(require_state.internalJotaiAtom(row));
3211
+ }
3212
+ };
3213
+ var ImageJobManager = class {
3214
+ records = /* @__PURE__ */ new Map();
3215
+ jobListeners = /* @__PURE__ */ new Map();
3216
+ listListeners = /* @__PURE__ */ new Set();
3217
+ listSnapshot = [];
3218
+ operation = Promise.resolve();
3219
+ pumping = false;
3220
+ runningId;
3221
+ controller;
3222
+ ready;
3223
+ constructor(repository, executor, jobFiles = nativeImageJobFiles) {
3224
+ this.repository = repository;
3225
+ this.executor = executor;
3226
+ this.jobFiles = jobFiles;
3227
+ this.ready = this.initialize();
3228
+ }
3229
+ setExecutor(executor) {
3230
+ this.executor = executor;
3231
+ }
3232
+ async queue(options) {
3233
+ await this.ready;
3234
+ const request = normalizeRequest(options);
3235
+ const provisional = await this.repository.insert({
3236
+ kind: "image",
3237
+ status: "paused",
3238
+ request,
3239
+ attempt: 0
3240
+ });
3241
+ try {
3242
+ const preparedRequest = await this.jobFiles.prepare(provisional.id, request);
3243
+ const queued = await this.repository.update({
3244
+ ...provisional,
3245
+ status: "queued",
3246
+ request: preparedRequest,
3247
+ attempt: 1
3248
+ });
3249
+ this.set(queued);
3250
+ this.pump();
3251
+ return queued;
3252
+ } catch (error) {
3253
+ await this.repository.remove(provisional.id);
3254
+ await this.jobFiles.remove(provisional).catch(() => void 0);
3255
+ throw error;
3256
+ }
3257
+ }
3258
+ async get(id) {
3259
+ await this.ready;
3260
+ return this.records.get(id);
3261
+ }
3262
+ async list() {
3263
+ await this.ready;
3264
+ return this.listSnapshot;
3265
+ }
3266
+ async cancel(id) {
3267
+ await this.ready;
3268
+ return this.mutate(async () => {
3269
+ const job = this.records.get(id);
3270
+ if (!job) return false;
3271
+ if (job.status === "queued" || job.status === "paused") {
3272
+ await this.replace(job, {
3273
+ status: "canceled",
3274
+ error: canceledError(),
3275
+ completedAt: now()
3276
+ });
3277
+ return true;
3278
+ }
3279
+ if (job.status !== "running") return false;
3280
+ await this.replace(job, { status: "canceling" });
3281
+ if (this.runningId === id) this.controller?.abort();
3282
+ return true;
3283
+ });
3284
+ }
3285
+ async retry(id) {
3286
+ await this.ready;
3287
+ const resumed = await this.mutate(async () => {
3288
+ const job = this.records.get(id);
3289
+ if (!job || job.status !== "failed" && job.status !== "canceled") return false;
3290
+ await this.replace(job, {
3291
+ status: "queued",
3292
+ attempt: job.attempt + 1,
3293
+ progress: void 0,
3294
+ result: void 0,
3295
+ error: void 0,
3296
+ startedAt: void 0,
3297
+ completedAt: void 0
3298
+ });
3299
+ return true;
3300
+ });
3301
+ if (resumed) this.pump();
3302
+ return resumed;
3303
+ }
3304
+ async resume(id) {
3305
+ await this.ready;
3306
+ const resumed = await this.mutate(async () => {
3307
+ const job = this.records.get(id);
3308
+ if (job?.status !== "paused") return false;
3309
+ await this.replace(job, { status: "queued" });
3310
+ return true;
3311
+ });
3312
+ if (resumed) this.pump();
3313
+ return resumed;
3314
+ }
3315
+ async remove(id) {
3316
+ await this.ready;
3317
+ return this.mutate(async () => {
3318
+ const job = this.records.get(id);
3319
+ if (!job) return false;
3320
+ if (job.status === "queued" || job.status === "running" || job.status === "canceling") throw new AIError("concurrent_request", "Cancel the image job before removing it");
3321
+ await this.jobFiles.remove(job);
3322
+ await this.repository.remove(id);
3323
+ this.records.delete(id);
3324
+ this.refreshList();
3325
+ this.emit(id);
3326
+ return true;
3327
+ });
3328
+ }
3329
+ getJobSnapshot(id) {
3330
+ return this.records.get(id);
3331
+ }
3332
+ getListSnapshot() {
3333
+ return this.listSnapshot;
3334
+ }
3335
+ subscribeJob(id, listener) {
3336
+ const listeners = this.jobListeners.get(id) ?? /* @__PURE__ */ new Set();
3337
+ listeners.add(listener);
3338
+ this.jobListeners.set(id, listeners);
3339
+ return () => {
3340
+ listeners.delete(listener);
3341
+ if (listeners.size === 0) this.jobListeners.delete(id);
3342
+ };
3343
+ }
3344
+ subscribeList(listener) {
3345
+ this.listListeners.add(listener);
3346
+ return () => this.listListeners.delete(listener);
3347
+ }
3348
+ async initialize() {
3349
+ const jobs$1 = await this.repository.list();
3350
+ for (const job of jobs$1) {
3351
+ if (job.status === "paused" && job.attempt === 0) {
3352
+ await this.repository.remove(job.id);
3353
+ await this.jobFiles.remove(job).catch(() => void 0);
3354
+ continue;
3355
+ }
3356
+ let recovered = job;
3357
+ if (job.status === "running" || job.status === "canceling") recovered = await this.repository.update({
3358
+ ...job,
3359
+ status: "canceled",
3360
+ error: canceledError("Generation was interrupted when the app exited"),
3361
+ completedAt: now()
3362
+ });
3363
+ else if (job.status === "queued") recovered = await this.repository.update({
3364
+ ...job,
3365
+ status: "paused"
3366
+ });
3367
+ this.records.set(recovered.id, recovered);
3368
+ }
3369
+ this.refreshList();
3370
+ }
3371
+ mutate(action) {
3372
+ const result = this.operation.then(action, action);
3373
+ this.operation = result.then(() => void 0, () => void 0);
3374
+ return result;
3375
+ }
3376
+ pump() {
3377
+ if (this.pumping) return;
3378
+ this.pumping = true;
3379
+ this.runLoop().finally(() => {
3380
+ this.pumping = false;
3381
+ if (this.nextQueued()) this.pump();
3382
+ });
3383
+ }
3384
+ async runLoop() {
3385
+ while (true) {
3386
+ const job = await this.mutate(async () => {
3387
+ const next = this.nextQueued();
3388
+ if (!next) return void 0;
3389
+ this.runningId = next.id;
3390
+ this.controller = new AbortController();
3391
+ return this.replace(next, {
3392
+ status: "running",
3393
+ startedAt: now(),
3394
+ completedAt: void 0,
3395
+ error: void 0,
3396
+ progress: void 0
3397
+ });
3398
+ });
3399
+ if (!job) return;
3400
+ const controller = this.controller;
3401
+ try {
3402
+ const result = await this.executor({
3403
+ ...job.request,
3404
+ signal: controller.signal,
3405
+ onProgress: (progress) => {
3406
+ this.mutate(async () => {
3407
+ const current = this.records.get(job.id);
3408
+ if (current?.status === "running") await this.replace(current, { progress });
3409
+ });
3410
+ }
3411
+ });
3412
+ await this.mutate(async () => {
3413
+ const current = this.records.get(job.id);
3414
+ if (!current) return;
3415
+ if (controller.signal.aborted || current.status === "canceling") {
3416
+ await this.replace(current, {
3417
+ status: "canceled",
3418
+ error: canceledError(),
3419
+ completedAt: now()
3420
+ });
3421
+ return;
3422
+ }
3423
+ await this.replace(current, {
3424
+ status: "completed",
3425
+ progress: {
3426
+ currentStep: 1,
3427
+ totalSteps: 1,
3428
+ fraction: 1
3429
+ },
3430
+ result,
3431
+ completedAt: now()
3432
+ });
3433
+ });
3434
+ } catch (error) {
3435
+ await this.mutate(async () => {
3436
+ const current = this.records.get(job.id);
3437
+ if (!current) return;
3438
+ const canceled = controller.signal.aborted || current.status === "canceling";
3439
+ await this.replace(current, {
3440
+ status: canceled ? "canceled" : "failed",
3441
+ error: canceled ? canceledError() : serializedError(error),
3442
+ completedAt: now()
3443
+ });
3444
+ });
3445
+ } finally {
3446
+ this.runningId = void 0;
3447
+ this.controller = void 0;
3448
+ }
3449
+ }
3450
+ }
3451
+ nextQueued() {
3452
+ return [...this.records.values()].filter((job) => job.status === "queued").sort((left, right) => left.createdAt === right.createdAt ? left.id.localeCompare(right.id) : left.createdAt.localeCompare(right.createdAt))[0];
3453
+ }
3454
+ async replace(job, update) {
3455
+ const persisted = await this.repository.update({
3456
+ ...job,
3457
+ ...update
3458
+ });
3459
+ this.set(persisted);
3460
+ return persisted;
3461
+ }
3462
+ set(job) {
3463
+ this.records.set(job.id, job);
3464
+ this.refreshList();
3465
+ this.emit(job.id);
3466
+ }
3467
+ refreshList() {
3468
+ this.listSnapshot = [...this.records.values()].sort((left, right) => left.createdAt === right.createdAt ? right.id.localeCompare(left.id) : right.createdAt.localeCompare(left.createdAt)).map(summaryFromJob);
3469
+ for (const listener of this.listListeners) listener();
3470
+ }
3471
+ emit(id) {
3472
+ for (const listener of this.jobListeners.get(id) ?? []) listener();
3473
+ }
3474
+ };
3475
+ function useManagedJob(manager, id) {
3476
+ (0, react.use)(manager.ready);
3477
+ return (0, react.useSyncExternalStore)((listener) => manager.subscribeJob(id, listener), () => manager.getJobSnapshot(id));
3478
+ }
3479
+ function useManagedJobList(manager) {
3480
+ (0, react.use)(manager.ready);
3481
+ return (0, react.useSyncExternalStore)((listener) => manager.subscribeList(listener), () => manager.getListSnapshot());
3482
+ }
3483
+ const nativeImageJobFiles = {
3484
+ async prepare(id, request) {
3485
+ const inputs = jobRoot(id).directory("Inputs");
3486
+ await inputs.create();
3487
+ return {
3488
+ ...request,
3489
+ image: request.image ? await copyInput(request.image, inputs.path, "Image") : void 0,
3490
+ mask: request.mask ? await copyInput(request.mask, inputs.path, "Mask") : void 0,
3491
+ references: request.references ? await Promise.all(request.references.map((reference, index) => copyInput(reference, inputs.path, `Reference-${index + 1}`))) : void 0,
3492
+ loras: request.loras ? await Promise.all(request.loras.map(async (lora, index) => lora.source.type === "local" ? {
3493
+ ...lora,
3494
+ source: {
3495
+ ...lora.source,
3496
+ path: await copyInput(lora.source.path, inputs.path, `LoRA-${index + 1}`, ".safetensors")
3497
+ }
3498
+ } : lora)) : void 0
3499
+ };
3500
+ },
3501
+ async remove(job) {
3502
+ await jobRoot(job.id).delete().catch(() => void 0);
3503
+ if (job.result?.uri) await require_files.files.file(pathFromUri(job.result.uri)).delete().catch(() => void 0);
3504
+ }
3505
+ };
3506
+ function dataFromJob(job) {
3507
+ return {
3508
+ status: job.status,
3509
+ request: JSON.stringify(job.request),
3510
+ progress: encode(job.progress),
3511
+ result: encode(job.result),
3512
+ error: encode(job.error),
3513
+ attempt: job.attempt,
3514
+ startedAt: job.startedAt ?? "",
3515
+ completedAt: job.completedAt ?? ""
3516
+ };
3517
+ }
3518
+ function jobFromRow(row) {
3519
+ return {
3520
+ id: row.id,
3521
+ kind: "image",
3522
+ status: row.data.status,
3523
+ request: decode(row.data.request),
3524
+ progress: decode(row.data.progress),
3525
+ result: decode(row.data.result),
3526
+ error: decode(row.data.error),
3527
+ attempt: row.data.attempt,
3528
+ createdAt: row.createdAt,
3529
+ updatedAt: row.updatedAt,
3530
+ startedAt: row.data.startedAt || void 0,
3531
+ completedAt: row.data.completedAt || void 0
3532
+ };
3533
+ }
3534
+ function encode(value) {
3535
+ return value === void 0 ? "" : JSON.stringify(value);
3536
+ }
3537
+ function decode(value) {
3538
+ return value ? JSON.parse(value) : void 0;
3539
+ }
3540
+ function normalizeRequest(options) {
3541
+ const runtimeOptions = options;
3542
+ for (const key of [
3543
+ "accessToken",
3544
+ "onModelProgress",
3545
+ "onProgress",
3546
+ "signal",
3547
+ "tokenizer"
3548
+ ]) if (runtimeOptions[key] !== void 0) throw new AIError("invalid_message", `${key} is not supported for persistent image jobs`);
3549
+ if (!options.prompt.trim()) throw new AIError("invalid_message", "Expected prompt");
3550
+ assertSerializable(options);
3551
+ return {
3552
+ ...options,
3553
+ model: typeof options.model === "string" ? options.model : options.model?.id,
3554
+ seed: options.seed ?? Math.floor(Math.random() * 4294967296)
3555
+ };
3556
+ }
3557
+ function assertSerializable(value, path = "options") {
3558
+ if (typeof value === "number" && !Number.isFinite(value)) throw new AIError("invalid_message", `${path} must be finite`);
3559
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") throw new AIError("invalid_message", `${path} must be serializable`);
3560
+ if (Array.isArray(value)) value.forEach((next, index) => {
3561
+ assertSerializable(next, `${path}[${index}]`);
3562
+ });
3563
+ else if (value && typeof value === "object") for (const [key, next] of Object.entries(value)) assertSerializable(next, `${path}.${key}`);
3564
+ }
3565
+ function serializedError(error) {
3566
+ const value = error instanceof Error ? error : new Error(String(error));
3567
+ const record = value;
3568
+ return {
3569
+ name: value.name,
3570
+ message: value.message,
3571
+ code: typeof record.code === "string" ? record.code : void 0,
3572
+ stage: typeof record.stage === "string" ? record.stage : void 0
3573
+ };
3574
+ }
3575
+ function canceledError(message = "Image generation canceled") {
3576
+ return {
3577
+ name: "AIError",
3578
+ code: "generation_canceled",
3579
+ message
3580
+ };
3581
+ }
3582
+ function summaryFromJob(job) {
3583
+ return {
3584
+ id: job.id,
3585
+ kind: job.kind,
3586
+ status: job.status,
3587
+ prompt: job.request.prompt,
3588
+ progress: job.progress,
3589
+ attempt: job.attempt,
3590
+ createdAt: job.createdAt,
3591
+ updatedAt: job.updatedAt,
3592
+ startedAt: job.startedAt,
3593
+ completedAt: job.completedAt
3594
+ };
3595
+ }
3596
+ function jobRoot(id) {
3597
+ return require_files.files.directory(require_files.files.paths.shared, "ImageJobs", require_state.currentAppStorageKey(), id);
3598
+ }
3599
+ async function copyInput(source, directory, name, fallbackExtension = ".png") {
3600
+ const path = pathFromUri(source);
3601
+ const destination = `${directory}/${name}${path.match(/\.[A-Za-z0-9]+$/)?.[0] ?? fallbackExtension}`;
3602
+ await require_files.nativeCopy(path, destination);
3603
+ return destination;
3604
+ }
3605
+ function pathFromUri(value) {
3606
+ if (!value.startsWith("file://")) return value;
3607
+ try {
3608
+ return decodeURIComponent(new URL(value).pathname);
3609
+ } catch {
3610
+ return value.slice(7);
3611
+ }
3612
+ }
3613
+ function now() {
3614
+ return (/* @__PURE__ */ new Date()).toISOString();
3615
+ }
3616
+
3617
+ //#endregion
3618
+ //#region src/ai/image-models.ts
3619
+ const flux2Klein4B8BitAbliterated = {
3620
+ formatVersion: 1,
3621
+ model: "flux2-klein-4b-8bit-abliterated",
3622
+ directory: "flux2-klein-4b-8bit-abliterated",
3623
+ metadata: {
3624
+ displayName: "Flux.2 Klein 4B Abliterated",
3625
+ family: "flux2-klein",
3626
+ precision: "8-bit",
3627
+ estimatedMemoryGB: 12,
3628
+ quality: "high",
3629
+ speed: "fast",
3630
+ capabilities: [
3631
+ "text-to-image",
3632
+ "image-to-image",
3633
+ "inpainting",
3634
+ "outpainting",
3635
+ "references",
3636
+ "lora"
3637
+ ]
3638
+ },
3639
+ components: {
3640
+ textEncoder: {
3641
+ repo: "mlx-community/Josiefied-Qwen3-4B-abliterated-v1-8bit",
3642
+ revision: "main",
3643
+ path: "text-encoder",
3644
+ files: [
3645
+ "config.json",
3646
+ "tokenizer.json",
3647
+ "tokenizer_config.json",
3648
+ "*.safetensors",
3649
+ "*.safetensors.index.json"
3650
+ ]
3651
+ },
3652
+ transformer: {
3653
+ repo: "moxin-org/FLUX.2-klein-4B-8bit-mlx",
3654
+ revision: "main",
3655
+ path: "transformer",
3656
+ files: ["config.json", "quantized_8bit.safetensors"]
3657
+ },
3658
+ vae: {
3659
+ repo: "black-forest-labs/FLUX.2-klein-4B",
3660
+ revision: "main",
3661
+ path: "vae",
3662
+ files: [
3663
+ "config.json",
3664
+ "diffusion_pytorch_model.safetensors",
3665
+ "diffusion_pytorch_model.safetensors.index.json",
3666
+ "diffusion_pytorch_model-*.safetensors"
3667
+ ]
3668
+ }
3669
+ }
3670
+ };
3671
+ const ternaryBonsaiImage4B = {
3672
+ formatVersion: 1,
3673
+ model: "ternary-bonsai-image-4b",
3674
+ directory: "ternary-bonsai-image-4b",
3675
+ metadata: {
3676
+ displayName: "Ternary Bonsai Image 4B",
3677
+ family: "bonsai-image",
3678
+ precision: "2-bit",
3679
+ estimatedMemoryGB: 8,
3680
+ quality: "high",
3681
+ speed: "fast",
3682
+ capabilities: ["text-to-image"]
3683
+ },
3684
+ components: {
3685
+ textEncoder: {
3686
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
3687
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
3688
+ path: "text-encoder",
3689
+ files: [
3690
+ "text_encoder-mlx-4bit/config.json",
3691
+ "text_encoder-mlx-4bit/tokenizer.json",
3692
+ "text_encoder-mlx-4bit/tokenizer_config.json",
3693
+ "text_encoder-mlx-4bit/model.safetensors",
3694
+ "text_encoder-mlx-4bit/model.safetensors.index.json"
3695
+ ]
3696
+ },
3697
+ transformer: {
3698
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
3699
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
3700
+ path: "transformer",
3701
+ files: [
3702
+ "transformer-packed-mflux/config.json",
3703
+ "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
3704
+ "transformer-packed-mflux/quantization_config.json"
3705
+ ]
3706
+ },
3707
+ vae: {
3708
+ repo: "prism-ml/bonsai-image-ternary-4B-mlx-2bit",
3709
+ revision: "2c24c81b934a658ba5590cf39088ba929985b4a8",
3710
+ path: "vae",
3711
+ files: ["vae/config.json", "vae/diffusion_pytorch_model.safetensors"]
3712
+ }
3713
+ }
3714
+ };
3715
+ const imageModelManifests = [flux2Klein4B8BitAbliterated, ternaryBonsaiImage4B];
3716
+ function imageModelManifest(model) {
3717
+ return imageModelManifests.find((manifest) => manifest.model === model);
3718
+ }
3719
+ const flux2KleinModelFiles = [
3720
+ {
3721
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3722
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3723
+ source: "config.json",
3724
+ destination: "text-encoder/config.json",
3725
+ bytes: 937
3726
+ },
3727
+ {
3728
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3729
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3730
+ source: "tokenizer.json",
3731
+ destination: "text-encoder/tokenizer.json",
3732
+ bytes: 11422654
3733
+ },
3734
+ {
3735
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3736
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3737
+ source: "tokenizer_config.json",
3738
+ destination: "text-encoder/tokenizer_config.json",
3739
+ bytes: 9706
3740
+ },
3741
+ {
3742
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3743
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3744
+ source: "model.safetensors",
3745
+ destination: "text-encoder/model.safetensors",
3746
+ bytes: 4274158989
3747
+ },
3748
+ {
3749
+ repo: flux2Klein4B8BitAbliterated.components.textEncoder.repo,
3750
+ revision: flux2Klein4B8BitAbliterated.components.textEncoder.revision,
3751
+ source: "model.safetensors.index.json",
3752
+ destination: "text-encoder/model.safetensors.index.json",
3753
+ bytes: 63924
3754
+ },
3755
+ {
3756
+ repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
3757
+ revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
3758
+ source: "transformer/config.json",
3759
+ destination: "transformer/config.json",
3760
+ bytes: 541
3761
+ },
3762
+ {
3763
+ repo: flux2Klein4B8BitAbliterated.components.transformer.repo,
3764
+ revision: flux2Klein4B8BitAbliterated.components.transformer.revision,
3765
+ source: "transformer/quantized_8bit.safetensors",
3766
+ destination: "transformer/quantized_8bit.safetensors",
3767
+ bytes: 4360050983
3768
+ },
3769
+ {
3770
+ repo: flux2Klein4B8BitAbliterated.components.vae.repo,
3771
+ revision: flux2Klein4B8BitAbliterated.components.vae.revision,
3772
+ source: "vae/config.json",
3773
+ destination: "vae/config.json",
3774
+ bytes: 821
3775
+ },
3776
+ {
3777
+ repo: flux2Klein4B8BitAbliterated.components.vae.repo,
3778
+ revision: flux2Klein4B8BitAbliterated.components.vae.revision,
3779
+ source: "vae/diffusion_pytorch_model.safetensors",
3780
+ destination: "vae/diffusion_pytorch_model.safetensors",
3781
+ bytes: 168120878
3782
+ }
3783
+ ];
3784
+ const flux2KleinTotalBytes = flux2KleinModelFiles.reduce((total, file) => total + file.bytes, 0);
3785
+ const bonsaiRepo = ternaryBonsaiImage4B.components.textEncoder.repo;
3786
+ const bonsaiRevision = ternaryBonsaiImage4B.components.textEncoder.revision;
3787
+ const ternaryBonsaiImageModelFiles = [
3788
+ {
3789
+ repo: bonsaiRepo,
3790
+ revision: bonsaiRevision,
3791
+ source: "text_encoder-mlx-4bit/config.json",
3792
+ destination: "text-encoder/config.json",
3793
+ bytes: 937
3794
+ },
3795
+ {
3796
+ repo: bonsaiRepo,
3797
+ revision: bonsaiRevision,
3798
+ source: "text_encoder-mlx-4bit/tokenizer.json",
3799
+ destination: "text-encoder/tokenizer.json",
3800
+ bytes: 11422654
3801
+ },
3802
+ {
3803
+ repo: bonsaiRepo,
3804
+ revision: bonsaiRevision,
3805
+ source: "text_encoder-mlx-4bit/tokenizer_config.json",
3806
+ destination: "text-encoder/tokenizer_config.json",
3807
+ bytes: 9706
3808
+ },
3809
+ {
3810
+ repo: bonsaiRepo,
3811
+ revision: bonsaiRevision,
3812
+ source: "text_encoder-mlx-4bit/model.safetensors",
3813
+ destination: "text-encoder/model.safetensors",
3814
+ bytes: 2263022529
3815
+ },
3816
+ {
3817
+ repo: bonsaiRepo,
3818
+ revision: bonsaiRevision,
3819
+ source: "text_encoder-mlx-4bit/model.safetensors.index.json",
3820
+ destination: "text-encoder/model.safetensors.index.json",
3821
+ bytes: 63924
3822
+ },
3823
+ {
3824
+ repo: bonsaiRepo,
3825
+ revision: bonsaiRevision,
3826
+ source: "transformer-packed-mflux/config.json",
3827
+ destination: "transformer/config.json",
3828
+ bytes: 619
3829
+ },
3830
+ {
3831
+ repo: bonsaiRepo,
3832
+ revision: bonsaiRevision,
3833
+ source: "transformer-packed-mflux/diffusion_pytorch_model.safetensors",
3834
+ destination: "transformer/diffusion_pytorch_model.safetensors",
3835
+ bytes: 1425271472
3836
+ },
3837
+ {
3838
+ repo: bonsaiRepo,
3839
+ revision: bonsaiRevision,
3840
+ source: "transformer-packed-mflux/quantization_config.json",
3841
+ destination: "transformer/quantization_config.json",
3842
+ bytes: 5054
3843
+ },
3844
+ {
3845
+ repo: bonsaiRepo,
3846
+ revision: bonsaiRevision,
3847
+ source: "vae/config.json",
3848
+ destination: "vae/config.json",
3849
+ bytes: 821
3850
+ },
3851
+ {
3852
+ repo: bonsaiRepo,
3853
+ revision: bonsaiRevision,
3854
+ source: "vae/diffusion_pytorch_model.safetensors",
3855
+ destination: "vae/diffusion_pytorch_model.safetensors",
3856
+ bytes: 168120878
3857
+ }
3858
+ ];
3859
+ const ternaryBonsaiImageTotalBytes = ternaryBonsaiImageModelFiles.reduce((total, file) => total + file.bytes, 0);
3860
+ function joinModelPath(root, component) {
3861
+ return `${root.replace(/\/+$/, "")}/${component.replace(/^\/+/, "")}`;
3862
+ }
3863
+ function fluxImageModelPaths(path, manifest = flux2Klein4B8BitAbliterated) {
3864
+ return {
3865
+ path,
3866
+ textEncoderPath: joinModelPath(path, manifest.components.textEncoder.path),
3867
+ transformerPath: joinModelPath(path, manifest.components.transformer.path),
3868
+ vaePath: joinModelPath(path, manifest.components.vae.path)
3869
+ };
3870
+ }
3871
+
3872
+ //#endregion
3873
+ //#region src/ai/image-schedule.ts
3874
+ function imageSigmaSchedule(schedules) {
3875
+ const configured = schedules.filter((schedule) => schedule !== void 0);
3876
+ if (configured.length === 0) return void 0;
3877
+ const first = configured[0];
3878
+ if (first.length === 0 || first.some((sigma, index) => !Number.isFinite(sigma) || sigma <= 0 || sigma > 1 || index > 0 && sigma >= first[index - 1])) throw new AIError("invalid_model", "LoRA sigmaSchedule must contain descending values greater than 0 and at most 1");
3879
+ if (configured.some((schedule) => schedule.length !== first.length || schedule.some((sigma, index) => sigma !== first[index]))) throw new AIError("invalid_model", "Selected LoRAs require conflicting sigma schedules");
3880
+ return [...first];
3881
+ }
3882
+
3883
+ //#endregion
3884
+ //#region src/ai/image-upscale.ts
3885
+ function normalizeUpscaleScale(value) {
3886
+ const scale = value ?? 2;
3887
+ if (!Number.isFinite(scale) || scale <= 1 || scale > 4) throw new AIError("invalid_model", "Upscale scale must be greater than 1 and at most 4");
3888
+ return scale;
3889
+ }
3890
+
2425
3891
  //#endregion
2426
3892
  //#region src/ai/image.ts
2427
- function modelCacheRoot(manifest = flux2Klein4B8BitAbliterated) {
3893
+ function imageModelId(model) {
3894
+ return typeof model === "string" ? model : model.id;
3895
+ }
3896
+ function imageModelDefinition(model) {
3897
+ const id = imageModelId(model);
3898
+ if (id === flux2Klein4B8BitAbliterated.model) return {
3899
+ manifest: flux2Klein4B8BitAbliterated,
3900
+ files: flux2KleinModelFiles,
3901
+ totalBytes: flux2KleinTotalBytes
3902
+ };
3903
+ if (id === ternaryBonsaiImage4B.model) return {
3904
+ manifest: ternaryBonsaiImage4B,
3905
+ files: ternaryBonsaiImageModelFiles,
3906
+ totalBytes: ternaryBonsaiImageTotalBytes
3907
+ };
3908
+ throw new AIError("invalid_model", `Unsupported image model: ${id}`);
3909
+ }
3910
+ function modelCacheRoot(manifest) {
2428
3911
  return require_files.files.directory(require_files.files.paths.models, manifest.directory);
2429
3912
  }
2430
3913
  function assertImageModel(model) {
2431
- if (model !== flux2Klein4B8BitAbliterated.model) throw new AIError("invalid_model", `Unsupported image model: ${model}`);
3914
+ imageModelDefinition(model);
2432
3915
  }
2433
- function modelRootFromOptions(modelRoot) {
2434
- return modelRoot ?? modelCacheRoot().path;
3916
+ function modelPathFromOptions(definition, path) {
3917
+ return path ?? modelCacheRoot(definition.manifest).path;
2435
3918
  }
2436
3919
  function downloadConcurrency(value) {
2437
3920
  if (value === void 0) return 2;
@@ -2598,11 +4081,11 @@ function cachedLoRAFile(lora) {
2598
4081
  return imageLoRARoot().directory(id).file(safeCacheName(sourceFileName(lora), "lora filename"));
2599
4082
  }
2600
4083
  async function ensureModelFile(input) {
2601
- const destination = require_files.files.file(input.modelRoot, input.file.destination);
4084
+ const destination = require_files.files.file(input.path, input.file.destination);
2602
4085
  const stat = await destination.stat();
2603
4086
  if (stat.type === "file" && stat.size === input.file.bytes) {
2604
4087
  input.onProgress?.({
2605
- model: flux2Klein4B8BitAbliterated.model,
4088
+ model: input.model,
2606
4089
  file: input.file.destination,
2607
4090
  index: input.index,
2608
4091
  total: input.total,
@@ -2619,7 +4102,7 @@ async function ensureModelFile(input) {
2619
4102
  resume: true,
2620
4103
  onProgress(progress) {
2621
4104
  input.onProgress?.({
2622
- model: flux2Klein4B8BitAbliterated.model,
4105
+ model: input.model,
2623
4106
  file: input.file.destination,
2624
4107
  index: input.index,
2625
4108
  total: input.total,
@@ -2628,16 +4111,16 @@ async function ensureModelFile(input) {
2628
4111
  }
2629
4112
  }).result;
2630
4113
  }
2631
- async function writePreparedManifest(modelRoot) {
2632
- await require_files.files.file(modelRoot, "manifest.json").write(JSON.stringify(flux2Klein4B8BitAbliterated, null, 2));
4114
+ async function writePreparedManifest(path, manifest) {
4115
+ await require_files.files.file(path, "manifest.json").write(JSON.stringify(manifest, null, 2));
2633
4116
  }
2634
- async function imageModelState(model, modelRoot) {
2635
- assertImageModel(model);
4117
+ async function imageModelState(model, path) {
4118
+ const definition = imageModelDefinition(model);
2636
4119
  let bytesPresent = 0;
2637
4120
  const missingFiles = [];
2638
4121
  const invalidFiles = [];
2639
- for (const file of flux2KleinModelFiles) {
2640
- const stat = await require_files.files.file(modelRoot, file.destination).stat();
4122
+ for (const file of definition.files) {
4123
+ const stat = await require_files.files.file(path, file.destination).stat();
2641
4124
  if (stat.type !== "file") {
2642
4125
  missingFiles.push(file.destination);
2643
4126
  continue;
@@ -2645,17 +4128,17 @@ async function imageModelState(model, modelRoot) {
2645
4128
  bytesPresent += Math.min(stat.size ?? 0, file.bytes);
2646
4129
  if (stat.size !== file.bytes) invalidFiles.push(file.destination);
2647
4130
  }
2648
- const installed = missingFiles.length === 0;
2649
- const manifestStat = await require_files.files.file(modelRoot, "manifest.json").stat();
4131
+ const manifestStat = await require_files.files.file(path, "manifest.json").stat();
2650
4132
  if (manifestStat.type !== "file") missingFiles.push("manifest.json");
4133
+ const installed = missingFiles.length === 0;
2651
4134
  return {
2652
4135
  model,
2653
- modelRoot,
2654
- paths: fluxImageModelPaths(modelRoot),
4136
+ path,
4137
+ paths: fluxImageModelPaths(path, definition.manifest),
2655
4138
  installed,
2656
4139
  verified: installed && invalidFiles.length === 0 && manifestStat.type === "file",
2657
4140
  bytesPresent,
2658
- totalBytes: flux2KleinTotalBytes,
4141
+ totalBytes: definition.totalBytes,
2659
4142
  missingFiles,
2660
4143
  invalidFiles
2661
4144
  };
@@ -2665,13 +4148,13 @@ function tokenArray(value, name) {
2665
4148
  const values = Array.isArray(candidate) ? candidate : ArrayBuffer.isView(candidate) ? Array.from(candidate) : void 0;
2666
4149
  if (!values) throw new AIError("invalid_message", `Tokenizer did not return ${name}`);
2667
4150
  return values.map((token) => {
2668
- const number = typeof token === "bigint" ? Number(token) : token;
2669
- if (!Number.isInteger(number) || number < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
2670
- return number;
4151
+ const number$1 = typeof token === "bigint" ? Number(token) : token;
4152
+ if (!Number.isInteger(number$1) || number$1 < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
4153
+ return number$1;
2671
4154
  });
2672
4155
  }
2673
- async function createFluxTokenizer(modelRoot) {
2674
- const paths = fluxImageModelPaths(modelRoot);
4156
+ async function createFluxTokenizer(path, manifest) {
4157
+ const paths = fluxImageModelPaths(path, manifest);
2675
4158
  const tokenizer = new Tokenizer_default(await require_files.files.file(paths.textEncoderPath, "tokenizer.json").json(), await require_files.files.file(paths.textEncoderPath, "tokenizer_config.json").json());
2676
4159
  const padTokenId = 151643;
2677
4160
  return ({ prompt, maxLength }) => {
@@ -2718,18 +4201,18 @@ function assertTokenArray(value, name) {
2718
4201
  if (value.length === 0) throw new AIError("invalid_message", `${name} must not be empty`);
2719
4202
  for (const token of value) if (!Number.isInteger(token) || token < 0) throw new AIError("invalid_message", `${name} must contain token ids`);
2720
4203
  }
2721
- function imageResult(value, fallbackMetadata) {
2722
- if (!isRecord(value) || typeof value.uri !== "string" || typeof value.width !== "number" || typeof value.height !== "number" || value.model !== "flux2-klein-4b-8bit-abliterated") throw new AIError("image_generation_failed", "Expected generated image URI");
4204
+ function imageResult(value, model, fallbackMetadata) {
4205
+ if (!isRecord(value) || typeof value.uri !== "string" || typeof value.width !== "number" || typeof value.height !== "number" || value.model !== model) throw new AIError("image_generation_failed", "Expected generated image URI");
2723
4206
  const result = value;
2724
4207
  const metadata = isRecord(result.metadata) ? result.metadata : {};
2725
4208
  return {
2726
4209
  uri: value.uri,
2727
4210
  width: value.width,
2728
4211
  height: value.height,
2729
- model: value.model,
4212
+ model,
2730
4213
  metadata: {
2731
4214
  ...fallbackMetadata,
2732
- model: metadata.model === value.model ? metadata.model : fallbackMetadata.model,
4215
+ model: metadata.model === model ? model : fallbackMetadata.model,
2733
4216
  width: typeof metadata.width === "number" ? metadata.width : value.width,
2734
4217
  height: typeof metadata.height === "number" ? metadata.height : value.height,
2735
4218
  steps: typeof metadata.steps === "number" ? metadata.steps : fallbackMetadata.steps,
@@ -2778,15 +4261,18 @@ function nativeErrorMessage(error) {
2778
4261
  return (error instanceof Error ? error.message : String(error)).replace(/^\[(download|tokenize|encode|denoise|decode|write)]\s*/, "");
2779
4262
  }
2780
4263
  async function prepareImageModel(options = {}) {
2781
- const model = options.model ?? flux2Klein4B8BitAbliterated.model;
2782
- assertImageModel(model);
2783
- const root = modelCacheRoot();
4264
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
4265
+ return prepareImageModelAtPath(definition, options, options.directory ? require_files.files.directory(options.directory, definition.manifest.directory).path : modelCacheRoot(definition.manifest).path);
4266
+ }
4267
+ async function prepareImageModelAtPath(definition, options, path) {
4268
+ const model = definition.manifest.model;
4269
+ const root = require_files.files.directory(path);
2784
4270
  await root.create({ recursive: true });
2785
4271
  const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : void 0;
2786
- const total = flux2KleinModelFiles.length;
4272
+ const total = definition.files.length;
2787
4273
  const bytesByFile = /* @__PURE__ */ new Map();
2788
4274
  const emitProgress = (progress) => {
2789
- const file = flux2KleinModelFiles.find((candidate) => candidate.destination === progress.file);
4275
+ const file = definition.files.find((candidate) => candidate.destination === progress.file);
2790
4276
  const bytesWritten = file ? Math.min(progress.bytesWritten, file.bytes) : progress.bytesWritten;
2791
4277
  bytesByFile.set(progress.file, bytesWritten);
2792
4278
  const modelBytesWritten = Array.from(bytesByFile.values()).reduce((sum, bytes) => sum + bytes, 0);
@@ -2794,12 +4280,12 @@ async function prepareImageModel(options = {}) {
2794
4280
  ...progress,
2795
4281
  bytesWritten,
2796
4282
  modelBytesWritten,
2797
- totalModelBytes: flux2KleinTotalBytes,
2798
- modelFraction: modelBytesWritten / flux2KleinTotalBytes
4283
+ totalModelBytes: definition.totalBytes,
4284
+ modelFraction: modelBytesWritten / definition.totalBytes
2799
4285
  });
2800
4286
  };
2801
4287
  const missingFiles = [];
2802
- for (const [index, file] of flux2KleinModelFiles.entries()) {
4288
+ for (const [index, file] of definition.files.entries()) {
2803
4289
  const stat = await require_files.files.file(root.path, file.destination).stat();
2804
4290
  if (stat.type === "file" && stat.size === file.bytes) emitProgress({
2805
4291
  model,
@@ -2817,7 +4303,8 @@ async function prepareImageModel(options = {}) {
2817
4303
  }
2818
4304
  await runConcurrent(missingFiles, downloadConcurrency(options.concurrency), async ({ file, index }) => {
2819
4305
  await ensureModelFile({
2820
- modelRoot: root.path,
4306
+ model,
4307
+ path: root.path,
2821
4308
  file,
2822
4309
  headers,
2823
4310
  index,
@@ -2825,22 +4312,26 @@ async function prepareImageModel(options = {}) {
2825
4312
  onProgress: emitProgress
2826
4313
  });
2827
4314
  });
2828
- await writePreparedManifest(root.path);
4315
+ await writePreparedManifest(root.path, definition.manifest);
2829
4316
  return {
2830
4317
  model,
2831
- ...fluxImageModelPaths(root.path),
2832
- tokenizer: await createFluxTokenizer(root.path)
4318
+ ...fluxImageModelPaths(root.path, definition.manifest),
4319
+ tokenizer: await createFluxTokenizer(root.path, definition.manifest)
2833
4320
  };
2834
4321
  }
2835
4322
  async function verifyImageModel(options = {}) {
2836
- return imageModelState(options.model ?? flux2Klein4B8BitAbliterated.model, modelRootFromOptions(options.modelRoot));
4323
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
4324
+ return imageModelState(definition.manifest.model, modelPathFromOptions(definition, options.path));
2837
4325
  }
2838
4326
  async function listImageModels(options = {}) {
2839
- return [await imageModelState(flux2Klein4B8BitAbliterated.model, modelRootFromOptions(options.modelRoot))];
4327
+ return [...await Promise.all([flux2Klein4B8BitAbliterated.model, ternaryBonsaiImage4B.model].map((model) => {
4328
+ const definition = imageModelDefinition(model);
4329
+ return imageModelState(model, options.directory ? require_files.files.directory(options.directory, definition.manifest.directory).path : modelPathFromOptions(definition, void 0));
4330
+ }))];
2840
4331
  }
2841
4332
  async function deleteImageModel(options = {}) {
2842
- assertImageModel(options.model ?? flux2Klein4B8BitAbliterated.model);
2843
- await require_files.files.directory(modelRootFromOptions(options.modelRoot)).delete({ recursive: true });
4333
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
4334
+ await require_files.files.directory(modelPathFromOptions(definition, options.path)).delete({ recursive: true });
2844
4335
  }
2845
4336
  async function imageMemoryTelemetry() {
2846
4337
  const value = await bridge().imageMemoryTelemetry?.();
@@ -2978,8 +4469,8 @@ async function prepareSelectedImageLoRAs(loras, model, options = {}) {
2978
4469
  }
2979
4470
  async function generateImage(options) {
2980
4471
  throwIfAborted(options.signal);
2981
- const model = options.model ?? flux2Klein4B8BitAbliterated.model;
2982
- assertImageModel(model);
4472
+ const definition = imageModelDefinition(options.model ?? flux2Klein4B8BitAbliterated.model);
4473
+ const model = definition.manifest.model;
2983
4474
  const prompt = options.prompt.trim();
2984
4475
  if (!prompt) throw new AIError("invalid_message", "Expected prompt");
2985
4476
  const presetSize = options.aspectRatio ? imageSizeForPreset(options.aspectRatio) : void 0;
@@ -3038,16 +4529,16 @@ async function generateImage(options) {
3038
4529
  const sigmaSchedule = imageSigmaSchedule(selectedLoRAs.map((lora) => lora.metadata?.sigmaSchedule));
3039
4530
  if (sigmaSchedule && options.steps !== void 0 && options.steps !== sigmaSchedule.length) throw new AIError("invalid_model", `steps must equal the LoRA sigma schedule length (${sigmaSchedule.length})`);
3040
4531
  const generationSteps = sigmaSchedule?.length ?? steps;
3041
- const prepared = options.modelRoot && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModel({
4532
+ const prepared = options.path && options.tokenizer ? void 0 : await withImageStage("download", () => prepareImageModelAtPath(definition, {
3042
4533
  model,
3043
4534
  accessToken: options.accessToken,
3044
4535
  concurrency: options.concurrency,
3045
4536
  onProgress: options.onModelProgress
3046
- }));
4537
+ }, options.path ?? modelCacheRoot(definition.manifest).path));
3047
4538
  throwIfAborted(options.signal);
3048
4539
  const tokenizer = options.tokenizer ?? prepared?.tokenizer;
3049
- const modelRoot = options.modelRoot ?? prepared?.modelRoot;
3050
- if (!tokenizer || !modelRoot) throw new AIError("invalid_model", "Expected image model root and tokenizer");
4540
+ const modelPath = options.path ?? prepared?.path;
4541
+ if (!tokenizer || !modelPath) throw new AIError("invalid_model", "Expected image model path and tokenizer");
3051
4542
  const tokenized = await withImageStage("tokenize", () => tokenizer({
3052
4543
  prompt,
3053
4544
  maxLength: 512
@@ -3058,7 +4549,7 @@ async function generateImage(options) {
3058
4549
  assertTokenArray(tokenized.attentionMask, "attentionMask");
3059
4550
  if (tokenized.inputIds.length !== tokenized.attentionMask.length) throw new AIError("invalid_message", "inputIds and attentionMask must have the same length");
3060
4551
  });
3061
- const paths = fluxImageModelPaths(modelRoot);
4552
+ const paths = fluxImageModelPaths(modelPath, definition.manifest);
3062
4553
  const request = {
3063
4554
  model,
3064
4555
  prompt,
@@ -3088,7 +4579,10 @@ async function generateImage(options) {
3088
4579
  seed,
3089
4580
  inputIds: tokenized.inputIds,
3090
4581
  attentionMask: tokenized.attentionMask,
3091
- ...paths
4582
+ modelRoot: paths.path,
4583
+ textEncoderPath: paths.textEncoderPath,
4584
+ transformerPath: paths.transformerPath,
4585
+ vaePath: paths.vaePath
3092
4586
  };
3093
4587
  const fallbackMetadata = {
3094
4588
  model,
@@ -3119,7 +4613,7 @@ async function generateImage(options) {
3119
4613
  throwIfAborted(options.signal);
3120
4614
  const value = await native.generateImage?.(request);
3121
4615
  throwIfAborted(options.signal);
3122
- return imageResult(value, fallbackMetadata);
4616
+ return imageResult(value, model, fallbackMetadata);
3123
4617
  } finally {
3124
4618
  subscription?.remove();
3125
4619
  options.signal?.removeEventListener("abort", cancelNative);
@@ -3145,22 +4639,36 @@ async function generateImages(options) {
3145
4639
  }
3146
4640
  return results;
3147
4641
  }
3148
- const imageGenerationQueueKey = Symbol.for("bleam.ai.imageGenerationQueue");
3149
- function imageGenerationQueue() {
4642
+ const imageJobManagerKey = Symbol.for("bleam.ai.imageJobManager.v2");
4643
+ function imageJobManager() {
3150
4644
  const scope = globalThis;
3151
- const queue = scope[imageGenerationQueueKey] ?? new ImageGenerationQueue(generateImage);
3152
- queue.setExecutor(generateImage);
3153
- scope[imageGenerationQueueKey] = queue;
3154
- return queue;
4645
+ const manager = scope[imageJobManagerKey] ?? new ImageJobManager(new CollectionImageJobRepository(), generateImage);
4646
+ manager.setExecutor(generateImage);
4647
+ scope[imageJobManagerKey] = manager;
4648
+ return manager;
3155
4649
  }
3156
- function scheduleImageGeneration(options) {
3157
- return imageGenerationQueue().schedule(options);
4650
+ async function queueImage(options) {
4651
+ return imageJobManager().queue(options);
3158
4652
  }
3159
- function getImageGenerationJob(id) {
3160
- return imageGenerationQueue().get(id);
4653
+ const image = Object.freeze({
4654
+ generate: generateImage,
4655
+ generateMany: generateImages,
4656
+ queue: queueImage,
4657
+ upscale: upscaleImage
4658
+ });
4659
+ const jobs = Object.freeze({
4660
+ get: (id) => imageJobManager().get(id),
4661
+ list: () => imageJobManager().list(),
4662
+ cancel: (id) => imageJobManager().cancel(id),
4663
+ retry: (id) => imageJobManager().retry(id),
4664
+ resume: (id) => imageJobManager().resume(id),
4665
+ remove: (id) => imageJobManager().remove(id)
4666
+ });
4667
+ function useJob(id) {
4668
+ return useManagedJob(imageJobManager(), id);
3161
4669
  }
3162
- function listImageGenerationJobs() {
3163
- return imageGenerationQueue().list();
4670
+ function useJobList() {
4671
+ return useManagedJobList(imageJobManager());
3164
4672
  }
3165
4673
 
3166
4674
  //#endregion
@@ -3230,7 +4738,7 @@ function buildImageGenerationRequest(input, options = {}) {
3230
4738
  ...options
3231
4739
  };
3232
4740
  if (input.mode === "text") {
3233
- const references = (input.refs ?? []).map((image) => image.path);
4741
+ const references = (input.refs ?? []).map((image$1) => image$1.path);
3234
4742
  return {
3235
4743
  ...base,
3236
4744
  references: references.length > 0 ? references : void 0,
@@ -3243,7 +4751,7 @@ function buildImageGenerationRequest(input, options = {}) {
3243
4751
  if (input.mode === "subject") {
3244
4752
  if (!input.subject) throw new Error("Subject mode requires a subject image");
3245
4753
  const refs = input.refs ?? [];
3246
- const references = [input.subject, ...refs].map((image) => image.path);
4754
+ const references = [input.subject, ...refs].map((image$1) => image$1.path);
3247
4755
  const size = imageSizeForAspectRatio(input.subject.size);
3248
4756
  return {
3249
4757
  ...base,
@@ -3256,7 +4764,7 @@ function buildImageGenerationRequest(input, options = {}) {
3256
4764
  }
3257
4765
  if (input.mode === "edit") {
3258
4766
  if (!input.source) throw new Error("Edit requires a source image");
3259
- const references = (input.refs ?? []).map((image) => image.path);
4767
+ const references = (input.refs ?? []).map((image$1) => image$1.path);
3260
4768
  const size = roundedImageSize(input.source.size);
3261
4769
  return {
3262
4770
  ...base,
@@ -3284,863 +4792,45 @@ function buildImageGenerationRequest(input, options = {}) {
3284
4792
  height: defaultImageSize
3285
4793
  };
3286
4794
  }
3287
- function useImageComposer(modeOrOptions, options = {}) {
3288
- if (typeof modeOrOptions !== "string") return useImageComposerController(modeOrOptions ?? {});
3289
- const mode = modeOrOptions;
3290
- if (mode === "text") return useTextComposer(options);
3291
- if (mode === "subject") return useSubjectComposer(options);
3292
- if (mode === "edit") return useEditComposer(options);
3293
- return useOutpaintComposer(options);
3294
- }
3295
- function useImageComposerController(options) {
3296
- const [mode, setMode] = require_state.useAtom(useComposerAtom(options.initialMode ?? "text"));
3297
- const text = useTextComposer(options);
3298
- const subject = useSubjectComposer(options);
3299
- const edit = useEditComposer(options);
3300
- const outpaint = useOutpaintComposer(options);
3301
- const active = mode === "text" ? text : mode === "subject" ? subject : mode === "edit" ? edit : outpaint;
3302
- const error = active.error ?? text.error ?? subject.error ?? edit.error ?? outpaint.error;
3303
- return {
3304
- mode,
3305
- setMode,
3306
- active,
3307
- text,
3308
- subject,
3309
- edit,
3310
- outpaint,
3311
- isPicking: text.isPicking || subject.isPicking || edit.isPicking || outpaint.isPicking,
3312
- error,
3313
- request: (prompt) => active.request(prompt),
3314
- clear() {
3315
- text.clear();
3316
- subject.clear();
3317
- edit.clear();
3318
- outpaint.clear();
3319
- }
3320
- };
3321
- }
3322
- function useTextComposer(options) {
3323
- const store = useComposerAtom({
3324
- refs: [],
3325
- isPicking: false,
3326
- error: null
3327
- });
3328
- const [state, setState] = require_state.useAtom(store);
3329
- const maxRefs = options.maxRefs ?? 3;
3330
- async function pickRefs() {
3331
- setState((current) => ({
3332
- ...current,
3333
- isPicking: true,
3334
- error: null
3335
- }));
3336
- try {
3337
- const refs = [...store.get().refs, ...await pickImages(options.inputRoot)].slice(0, maxRefs);
3338
- setState((current) => ({
3339
- ...current,
3340
- refs,
3341
- isPicking: false
3342
- }));
3343
- } catch (error) {
3344
- setState((current) => ({
3345
- ...current,
3346
- error: errorFromUnknown(error),
3347
- isPicking: false
3348
- }));
3349
- }
3350
- }
3351
- return {
3352
- mode: "text",
3353
- ...state,
3354
- pickRefs,
3355
- removeRef: (index) => setState((current) => removeRefFromState(current, index)),
3356
- request: (prompt) => ({
3357
- mode: "text",
3358
- prompt,
3359
- refs: store.get().refs
3360
- }),
3361
- clear: () => setState({
3362
- refs: [],
3363
- isPicking: false,
3364
- error: null
3365
- })
3366
- };
3367
- }
3368
- function useSubjectComposer(options) {
3369
- const store = useComposerAtom({
3370
- subject: null,
3371
- refs: [],
3372
- isPicking: false,
3373
- error: null
3374
- });
3375
- const [state, setState] = require_state.useAtom(store);
3376
- const maxRefs = options.maxRefs ?? 2;
3377
- async function pickSubject() {
3378
- setState((current) => ({
3379
- ...current,
3380
- isPicking: true,
3381
- error: null
3382
- }));
3383
- try {
3384
- const subject = await pickOneImage(options.inputRoot);
3385
- setState((current) => ({
3386
- ...current,
3387
- subject: subject ?? current.subject,
3388
- isPicking: false
3389
- }));
3390
- } catch (error) {
3391
- setState((current) => ({
3392
- ...current,
3393
- error: errorFromUnknown(error),
3394
- isPicking: false
3395
- }));
3396
- }
3397
- }
3398
- async function pickRefs() {
3399
- setState((current) => ({
3400
- ...current,
3401
- isPicking: true,
3402
- error: null
3403
- }));
3404
- try {
3405
- const refs = [...store.get().refs, ...await pickImages(options.inputRoot)].slice(0, maxRefs);
3406
- setState((current) => ({
3407
- ...current,
3408
- refs,
3409
- isPicking: false
3410
- }));
3411
- } catch (error) {
3412
- setState((current) => ({
3413
- ...current,
3414
- error: errorFromUnknown(error),
3415
- isPicking: false
3416
- }));
3417
- }
3418
- }
3419
- return {
3420
- mode: "subject",
3421
- ...state,
3422
- pickSubject,
3423
- removeSubject: () => setState((current) => ({
3424
- ...current,
3425
- subject: null
3426
- })),
3427
- pickRefs,
3428
- removeRef: (index) => setState((current) => removeRefFromState(current, index)),
3429
- request: (prompt) => {
3430
- const current = store.get();
3431
- return {
3432
- mode: "subject",
3433
- prompt,
3434
- subject: current.subject,
3435
- refs: current.refs
3436
- };
3437
- },
3438
- clear: () => setState({
3439
- subject: null,
3440
- refs: [],
3441
- isPicking: false,
3442
- error: null
3443
- })
3444
- };
3445
- }
3446
- function useEditComposer(options) {
3447
- const store = useComposerAtom({
3448
- source: null,
3449
- refs: [],
3450
- isPicking: false,
3451
- error: null
3452
- });
3453
- const [state, setState] = require_state.useAtom(store);
3454
- const maxRefs = options.maxRefs ?? 3;
3455
- async function pickSource() {
3456
- setState((current) => ({
3457
- ...current,
3458
- isPicking: true,
3459
- error: null
3460
- }));
3461
- try {
3462
- const source = await pickOneImage(options.inputRoot);
3463
- setState((current) => ({
3464
- ...current,
3465
- source: source ?? current.source,
3466
- isPicking: false
3467
- }));
3468
- } catch (error) {
3469
- setState((current) => ({
3470
- ...current,
3471
- error: errorFromUnknown(error),
3472
- isPicking: false
3473
- }));
3474
- }
3475
- }
3476
- async function pickRefs() {
3477
- setState((current) => ({
3478
- ...current,
3479
- isPicking: true,
3480
- error: null
3481
- }));
3482
- try {
3483
- const refs = [...store.get().refs, ...await pickImages(options.inputRoot)].slice(0, maxRefs);
3484
- setState((current) => ({
3485
- ...current,
3486
- refs,
3487
- isPicking: false
3488
- }));
3489
- } catch (error) {
3490
- setState((current) => ({
3491
- ...current,
3492
- error: errorFromUnknown(error),
3493
- isPicking: false
3494
- }));
3495
- }
3496
- }
3497
- return {
3498
- mode: "edit",
3499
- ...state,
3500
- pickSource,
3501
- removeSource: () => setState((current) => ({
3502
- ...current,
3503
- source: null
3504
- })),
3505
- pickRefs,
3506
- removeRef: (index) => setState((current) => removeRefFromState(current, index)),
3507
- request: (prompt) => {
3508
- const current = store.get();
3509
- return {
3510
- mode: "edit",
3511
- prompt,
3512
- source: current.source,
3513
- refs: current.refs
3514
- };
3515
- },
3516
- clear: () => setState({
3517
- source: null,
3518
- refs: [],
3519
- isPicking: false,
3520
- error: null
3521
- })
3522
- };
3523
- }
3524
- function useOutpaintComposer(options) {
3525
- const store = useComposerAtom({
3526
- source: null,
3527
- rect: null,
3528
- isPicking: false,
3529
- error: null
3530
- });
3531
- const [state, setState] = require_state.useAtom(store);
3532
- async function pickSource() {
3533
- setState((current) => ({
3534
- ...current,
3535
- isPicking: true,
3536
- error: null
3537
- }));
3538
- try {
3539
- const source = await pickOneImage(options.inputRoot);
3540
- setState((current) => ({
3541
- ...current,
3542
- source: source ?? current.source,
3543
- rect: source ? initialOutpaintRect(source.size) : current.rect,
3544
- isPicking: false
3545
- }));
3546
- } catch (error) {
3547
- setState((current) => ({
3548
- ...current,
3549
- error: errorFromUnknown(error),
3550
- isPicking: false
3551
- }));
3552
- }
3553
- }
3554
- return {
3555
- mode: "outpaint",
3556
- ...state,
3557
- pickSource,
3558
- removeSource: () => setState((current) => ({
3559
- ...current,
3560
- source: null,
3561
- rect: null
3562
- })),
3563
- setRect: (rect) => setState((current) => ({
3564
- ...current,
3565
- rect
3566
- })),
3567
- request: (prompt) => {
3568
- const current = store.get();
3569
- return {
3570
- mode: "outpaint",
3571
- prompt,
3572
- source: current.source,
3573
- rect: current.rect
3574
- };
3575
- },
3576
- clear: () => setState({
3577
- source: null,
3578
- rect: null,
3579
- isPicking: false,
3580
- error: null
3581
- })
3582
- };
3583
- }
3584
- function useImageGeneration() {
3585
- const [state, setState] = require_state.useAtom(useComposerAtom({
3586
- imageUri: null,
3587
- result: null,
3588
- progress: 0,
3589
- status: "",
3590
- isGenerating: false,
3591
- error: null
3592
- }));
3593
- const abortControllerRef = useStableRef(null);
3594
- async function run(input, options = {}) {
3595
- if (abortControllerRef.current) return null;
3596
- const abortController = new AbortController();
3597
- abortControllerRef.current = abortController;
3598
- setState((current) => ({
3599
- ...current,
3600
- imageUri: null,
3601
- result: null,
3602
- progress: 0,
3603
- status: "Generating...",
3604
- isGenerating: true,
3605
- error: null
3606
- }));
3607
- try {
3608
- const result = await generateImage(buildImageGenerationRequest(input, {
3609
- ...options,
3610
- signal: abortController.signal,
3611
- onModelProgress(progress) {
3612
- setState((current) => ({
3613
- ...current,
3614
- progress: progress.modelFraction >= 1 ? .15 : progress.modelFraction * .15,
3615
- status: progress.modelFraction >= 1 ? "Generating..." : modelProgressStatus(progress)
3616
- }));
3617
- },
3618
- onProgress(progress) {
3619
- setState((current) => ({
3620
- ...current,
3621
- progress: .15 + progress.currentStep / progress.totalSteps * .85,
3622
- status: `Step ${progress.currentStep}/${progress.totalSteps}`
3623
- }));
3624
- }
3625
- }));
3626
- setState((current) => ({
3627
- ...current,
3628
- imageUri: result.uri,
3629
- result,
3630
- progress: 1,
3631
- status: `${result.metadata.width}×${result.metadata.height} • ${result.metadata.steps} steps`,
3632
- isGenerating: false
3633
- }));
3634
- return result;
3635
- } catch (error) {
3636
- setState((current) => ({
3637
- ...current,
3638
- status: error instanceof Error ? error.message : String(error),
3639
- error: errorFromUnknown(error),
3640
- isGenerating: false
3641
- }));
3642
- return null;
3643
- } finally {
3644
- if (abortControllerRef.current === abortController) abortControllerRef.current = null;
3645
- }
3646
- }
3647
- function cancel() {
3648
- setState((current) => ({
3649
- ...current,
3650
- status: "Canceling..."
3651
- }));
3652
- abortControllerRef.current?.abort();
3653
- }
3654
- function clear() {
3655
- setState({
3656
- imageUri: null,
3657
- result: null,
3658
- progress: 0,
3659
- status: "",
3660
- isGenerating: false,
3661
- error: null
3662
- });
3663
- }
3664
- return {
3665
- ...state,
3666
- run,
3667
- cancel,
3668
- clear
3669
- };
3670
- }
3671
- function useImageModel() {
3672
- const [state, setState] = require_state.useAtom(useComposerAtom({
3673
- checking: false,
3674
- state: null,
3675
- error: null
3676
- }));
3677
- async function check() {
3678
- setState((current) => ({
3679
- ...current,
3680
- checking: true,
3681
- error: null
3682
- }));
3683
- try {
3684
- const [modelState] = await listImageModels();
3685
- setState({
3686
- checking: false,
3687
- state: modelState ?? null,
3688
- error: null
3689
- });
3690
- return modelState ?? null;
3691
- } catch (error) {
3692
- setState((current) => ({
3693
- ...current,
3694
- checking: false,
3695
- error: errorFromUnknown(error)
3696
- }));
3697
- return null;
3698
- }
3699
- }
3700
- return {
3701
- ...state,
3702
- installed: state.state?.installed ?? false,
3703
- verified: state.state?.verified ?? false,
3704
- missingFiles: state.state?.missingFiles ?? [],
3705
- bytesPresent: state.state?.bytesPresent ?? 0,
3706
- totalBytes: state.state?.totalBytes ?? 0,
3707
- check
3708
- };
3709
- }
3710
- function useComposerAtom(initialValue) {
3711
- const ref = useStableRef(null);
3712
- ref.current ??= require_state.atom(initialValue);
3713
- return ref.current;
3714
- }
3715
- function useStableRef(initialValue) {
3716
- return (0, react.useRef)(initialValue);
3717
- }
3718
- function removeRefFromState(state, index) {
3719
- return {
3720
- ...state,
3721
- refs: state.refs.filter((_, i) => i !== index)
3722
- };
3723
- }
3724
- function inputDestination(inputRoot) {
3725
- return require_files.files.directory(inputRoot ?? require_files.files.paths.shared, "ImageGenerationInputs", String(Date.now()));
3726
- }
3727
- async function pickOneImage(inputRoot) {
3728
- const [picked] = await require_files.files.pick({
3729
- type: "image",
3730
- destination: inputDestination(inputRoot)
3731
- });
3732
- if (!picked) return null;
3733
- return Object.assign(picked, { size: await getImageSize(picked.uri) });
3734
- }
3735
- async function pickImages(inputRoot) {
3736
- const picked = await require_files.files.pick({
3737
- type: "image",
3738
- multiple: true,
3739
- destination: inputDestination(inputRoot)
3740
- });
3741
- return Promise.all(picked.map(async (image) => Object.assign(image, { size: await getImageSize(image.uri) })));
3742
- }
3743
- function getImageSize(uri) {
3744
- return new Promise((resolve, reject) => {
3745
- react_native.Image.getSize(uri, (width, height) => resolve({
3746
- width,
3747
- height
3748
- }), reject);
3749
- });
3750
- }
3751
- function errorFromUnknown(error) {
3752
- return error instanceof Error ? error : new Error(String(error));
3753
- }
3754
- function modelProgressStatus(progress) {
3755
- return `Model ${Math.round(progress.modelFraction * 100)}% • ${formatBytes(progress.modelBytesWritten)} / ${formatBytes(progress.totalModelBytes)}`;
3756
- }
3757
- function formatBytes(value) {
3758
- if (value < 1024) return `${value} B`;
3759
- const units = [
3760
- "KB",
3761
- "MB",
3762
- "GB",
3763
- "TB"
3764
- ];
3765
- let size = value / 1024;
3766
- let index = 0;
3767
- while (size >= 1024 && index < units.length - 1) {
3768
- size /= 1024;
3769
- index += 1;
3770
- }
3771
- return `${size.toFixed(size >= 10 ? 1 : 2)} ${units[index]}`;
3772
- }
3773
-
3774
- //#endregion
3775
- //#region src/ai/shape.ts
3776
- var ShapeError = class extends Error {
3777
- constructor(code, message, issues = []) {
3778
- super(message);
3779
- this.code = code;
3780
- this.issues = issues;
3781
- this.name = "ShapeError";
3782
- }
3783
- };
3784
- function issue(message, path = []) {
3785
- return {
3786
- message,
3787
- path
3788
- };
3789
- }
3790
- function validateDefinition(definition, value, path = []) {
3791
- if (definition === "string" || definition === "number" || definition === "boolean") {
3792
- if (typeof value === definition) return { value };
3793
- return { issues: [issue(`Expected ${path.join(".") || "value"} to be ${definition}`, path)] };
3794
- }
3795
- if (Array.isArray(definition)) {
3796
- if (!Array.isArray(value)) return { issues: [issue(`Expected ${path.join(".") || "value"} to be array`, path)] };
3797
- const itemDefinition = definition[0];
3798
- const issues$1 = [];
3799
- const output$1 = [];
3800
- for (const [index, item] of value.entries()) {
3801
- const result = validateDefinition(itemDefinition, item, [...path, index]);
3802
- if (result.issues !== void 0) issues$1.push(...result.issues);
3803
- else output$1.push(result.value);
3804
- }
3805
- if (issues$1.length > 0) return { issues: issues$1 };
3806
- return { value: output$1 };
3807
- }
3808
- if (!isRecord(value)) return { issues: [issue(`Expected ${path.join(".") || "value"} to be object`, path)] };
3809
- const output = {};
3810
- const issues = [];
3811
- for (const [key, field] of Object.entries(definition)) {
3812
- const result = validateDefinition(field, value[key], [...path, key]);
3813
- if (result.issues !== void 0) issues.push(...result.issues);
3814
- else output[key] = result.value;
3815
- }
3816
- if (issues.length > 0) return { issues };
3817
- return { value: output };
3818
- }
3819
- function bridgeSchema(definition) {
3820
- if (definition === "string" || definition === "number" || definition === "boolean") return { type: definition };
3821
- if (Array.isArray(definition)) return {
3822
- type: "array",
3823
- items: bridgeSchema(definition[0])
3824
- };
3825
- const properties = {};
3826
- const required = [];
3827
- for (const [key, value] of Object.entries(definition)) {
3828
- properties[key] = bridgeSchema(value);
3829
- required.push(key);
3830
- }
3831
- return {
3832
- type: "object",
3833
- properties,
3834
- required
3835
- };
3836
- }
3837
- function assertValidShape(definition, path = "shape") {
3838
- if (definition === "string" || definition === "number" || definition === "boolean") return;
3839
- if (Array.isArray(definition)) {
3840
- if (definition.length !== 1) throw new ShapeError("invalid_shape", `${path} arrays must contain exactly one item shape`);
3841
- assertValidShape(definition[0], `${path}[0]`);
3842
- return;
3843
- }
3844
- if (!isRecord(definition)) throw new ShapeError("invalid_shape", `${path} must be a shape object`);
3845
- for (const [key, value] of Object.entries(definition)) assertValidShape(value, `${path}.${key}`);
3846
- }
3847
- function createShape(definition) {
3848
- assertValidShape(definition);
3849
- const api = {
3850
- "~standard": {
3851
- version: 1,
3852
- vendor: "bleam",
3853
- validate(value) {
3854
- return api.safeParse(value);
3855
- }
3856
- },
3857
- parse(value) {
3858
- const result = api.safeParse(value);
3859
- if (result.issues !== void 0) throw new ShapeError("validation_failed", result.issues.map((nextIssue) => nextIssue.message).join("\n"), result.issues);
3860
- return result.value;
3861
- },
3862
- safeParse(value) {
3863
- return validateDefinition(definition, value);
3864
- },
3865
- toJSONSchema() {
3866
- return bridgeSchema(definition);
3867
- }
3868
- };
3869
- return api;
3870
- }
3871
- function shape(definition) {
3872
- return createShape(definition);
3873
- }
3874
- function schemaToBridgeSchema(schema) {
3875
- const schemaWithBridge = schema;
3876
- if (typeof schemaWithBridge.toJSONSchema === "function") return schemaWithBridge.toJSONSchema();
3877
- throw new AIError("invalid_schema", "Structured output schemas must implement toJSONSchema()");
3878
- }
3879
- function validateStructuredInput(input) {
3880
- const result = input.shape["~standard"].validate(input.value);
3881
- if (result.issues !== void 0) throw new AIError("validation_failed", result.issues.map((nextIssue) => nextIssue.message).join("\n"), result.issues);
3882
- return result.value;
3883
- }
3884
- function stableStringify(value) {
3885
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
3886
- if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
3887
- return JSON.stringify(value) ?? "undefined";
3888
- }
3889
-
3890
- //#endregion
3891
- //#region src/ai/text.ts
3892
- function promptFromInput(options) {
3893
- const parts = [];
3894
- if (options.prompt) parts.push(options.prompt);
3895
- if (typeof options.input === "string") parts.push(options.input);
3896
- else if (options.input) parts.push(stableStringify(validateStructuredInput(options.input)));
3897
- if (parts.length === 0) throw new AIError("invalid_message", "Expected prompt or input");
3898
- return parts.join("\n\n");
3899
- }
3900
- async function isAvailable() {
3901
- try {
3902
- return await bridge().isAvailable?.() ?? false;
3903
- } catch {
3904
- return false;
3905
- }
3906
- }
3907
- async function availability() {
3908
- try {
3909
- return await bridge().availability?.() ?? {
3910
- available: false,
3911
- reason: "unknown"
3912
- };
3913
- } catch {
3914
- return {
3915
- available: false,
3916
- reason: "unknown"
3917
- };
3918
- }
3919
- }
3920
- function generationId() {
3921
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3922
- }
3923
- function nativeRequest(options, id) {
3924
- if (options.temperature !== void 0 && (!Number.isFinite(options.temperature) || options.temperature < 0)) throw new AIError("invalid_message", "temperature must be a non-negative number");
3925
- if (options.maxTokens !== void 0 && (!Number.isInteger(options.maxTokens) || options.maxTokens <= 0)) throw new AIError("invalid_message", "maxTokens must be a positive integer");
3926
- const names = /* @__PURE__ */ new Set();
3927
- for (const tool of options.tools ?? []) {
3928
- if (!tool.name.trim()) throw new AIError("invalid_schema", "Tool names must not be empty");
3929
- if (names.has(tool.name)) throw new AIError("invalid_schema", `Duplicate tool name: ${tool.name}`);
3930
- names.add(tool.name);
3931
- }
3932
- return {
3933
- id,
3934
- system: options.system,
3935
- prompt: promptFromInput(options),
3936
- temperature: options.temperature,
3937
- maxTokens: options.maxTokens,
3938
- outputSchema: options.output ? schemaToBridgeSchema(options.output) : void 0,
3939
- tools: options.tools?.map((tool) => ({
3940
- name: tool.name,
3941
- description: tool.description,
3942
- inputSchema: schemaToBridgeSchema(tool.input)
3943
- }))
3944
- };
3945
- }
3946
- function listenForTools(id, tools) {
3947
- if (!tools?.length) return void 0;
3948
- const native = bridge();
3949
- return native.addListener?.("toolCall", (event) => {
3950
- if (event.generationId !== id) return;
3951
- const tool = tools.find((candidate) => candidate.name === event.name);
3952
- (async () => {
3953
- try {
3954
- if (!tool) throw new Error(`Unknown tool: ${event.name}`);
3955
- const validation = tool.input["~standard"].validate(event.arguments);
3956
- if (validation.issues !== void 0) throw new Error(validation.issues.map((issue$1) => issue$1.message).join("\n"));
3957
- const output = await tool.execute(validation.value);
3958
- await native.resolveToolCall?.({
3959
- generationId: id,
3960
- callId: event.callId,
3961
- output: typeof output === "string" ? output : stableStringify(output)
3962
- });
3963
- } catch (error) {
3964
- await native.resolveToolCall?.({
3965
- generationId: id,
3966
- callId: event.callId,
3967
- error: error instanceof Error ? error.message : String(error)
3968
- });
3969
- }
3970
- })();
3971
- });
3972
- }
3973
- async function generate(options) {
3974
- const output = options.output;
3975
- const id = generationId();
3976
- const native = bridge();
3977
- const toolSubscription = listenForTools(id, options.tools);
3978
- const abort = () => void native.cancelTextGeneration?.(id);
3979
- options.signal?.addEventListener("abort", abort, { once: true });
3980
- try {
3981
- if (options.signal?.aborted) throw new AIError("generation_canceled", "Generation canceled");
3982
- const value = await native.generate?.(nativeRequest(options, id));
3983
- if (output) {
3984
- const result = output["~standard"].validate(value);
3985
- if (result.issues !== void 0) throw new AIError("validation_failed", result.issues.map((nextIssue) => nextIssue.message).join("\n"), result.issues);
3986
- return result.value;
3987
- }
3988
- if (typeof value !== "string") throw new AIError("generation_failed", "Expected generated text");
3989
- return value;
3990
- } catch (error) {
3991
- if (error instanceof AIError) throw error;
3992
- throw mapNativeError(error);
3993
- } finally {
3994
- options.signal?.removeEventListener("abort", abort);
3995
- toolSubscription?.remove();
3996
- }
3997
- }
3998
- async function* stream(options) {
3999
- const id = generationId();
4000
- const native = bridge();
4001
- const updates = [];
4002
- let wake;
4003
- let settled = false;
4004
- let failure;
4005
- const updateSubscription = native.addListener?.("textGenerationUpdate", (event) => {
4006
- if (event.id !== id) return;
4007
- updates.push(event.content);
4008
- wake?.();
4009
- wake = void 0;
4010
- });
4011
- const toolSubscription = listenForTools(id, options.tools);
4012
- const abort = () => void native.cancelTextGeneration?.(id);
4013
- options.signal?.addEventListener("abort", abort, { once: true });
4014
- const completion = native.streamGenerate?.(nativeRequest(options, id)).catch((error) => {
4015
- failure = error;
4016
- }).finally(() => {
4017
- settled = true;
4018
- wake?.();
4019
- wake = void 0;
4020
- });
4021
- try {
4022
- if (!completion) throw new AIError("unsupported_runtime", "Streaming is unavailable in this runtime");
4023
- while (!settled || updates.length > 0) if (updates.length > 0) yield updates.shift();
4024
- else await new Promise((resolve) => {
4025
- wake = resolve;
4026
- });
4027
- await completion;
4028
- if (failure) throw mapNativeError(failure);
4029
- } finally {
4030
- if (!settled) await native.cancelTextGeneration?.(id);
4031
- options.signal?.removeEventListener("abort", abort);
4032
- updateSubscription?.remove();
4033
- toolSubscription?.remove();
4034
- }
4035
- }
4036
-
4037
- //#endregion
4038
- //#region src/ai/text-hook.ts
4039
- const initialState = {
4040
- text: "",
4041
- isGenerating: false,
4042
- error: null
4043
- };
4044
- function useTextGeneration() {
4045
- const [state, setState] = (0, react.useState)(initialState);
4046
- const abortControllerRef = (0, react.useRef)(null);
4047
- async function run(options) {
4048
- if (abortControllerRef.current) return null;
4049
- const abortController = new AbortController();
4050
- const abort = () => abortController.abort();
4051
- abortControllerRef.current = abortController;
4052
- options.signal?.addEventListener("abort", abort, { once: true });
4053
- if (options.signal?.aborted) abortController.abort();
4054
- setState({
4055
- text: "",
4056
- isGenerating: true,
4057
- error: null
4058
- });
4059
- let text = "";
4060
- try {
4061
- for await (const snapshot of stream({
4062
- ...options,
4063
- signal: abortController.signal
4064
- })) {
4065
- text = snapshot;
4066
- setState({
4067
- text,
4068
- isGenerating: true,
4069
- error: null
4070
- });
4071
- }
4072
- setState({
4073
- text,
4074
- isGenerating: false,
4075
- error: null
4076
- });
4077
- return text;
4078
- } catch (error) {
4079
- setState({
4080
- text,
4081
- isGenerating: false,
4082
- error: error instanceof Error ? error : new Error(String(error))
4083
- });
4084
- return null;
4085
- } finally {
4086
- options.signal?.removeEventListener("abort", abort);
4087
- if (abortControllerRef.current === abortController) abortControllerRef.current = null;
4088
- }
4089
- }
4090
- function cancel() {
4091
- abortControllerRef.current?.abort();
4092
- }
4093
- function clear() {
4094
- if (abortControllerRef.current) return;
4095
- setState(initialState);
4096
- }
4097
- return {
4098
- ...state,
4099
- run,
4100
- cancel,
4101
- clear
4102
- };
4103
- }
4104
4795
 
4105
4796
  //#endregion
4106
4797
  exports.AIError = AIError;
4107
4798
  exports.ImageGenerationError = ImageGenerationError;
4108
- exports.availability = availability;
4799
+ exports.bonsai = bonsai;
4800
+ exports.bonsaiTextModelCatalog = bonsaiTextModelCatalog;
4109
4801
  exports.buildImageGenerationRequest = buildImageGenerationRequest;
4802
+ exports.chat = chat;
4110
4803
  exports.clampOutpaintRect = clampOutpaintRect;
4111
4804
  exports.defaultImageLongSide = defaultImageLongSide;
4112
4805
  exports.deleteImageLoRA = deleteImageLoRA;
4113
4806
  exports.deleteImageModel = deleteImageModel;
4807
+ exports.deleteTextModel = deleteBonsaiTextModel;
4114
4808
  exports.flux2Klein4B8BitAbliterated = flux2Klein4B8BitAbliterated;
4115
4809
  exports.fluxImageModelPaths = fluxImageModelPaths;
4116
- exports.generate = generate;
4117
- exports.generateImage = generateImage;
4118
- exports.generateImages = generateImages;
4119
- exports.getImageGenerationJob = getImageGenerationJob;
4810
+ exports.image = image;
4120
4811
  exports.imageMemoryTelemetry = imageMemoryTelemetry;
4121
4812
  exports.imageModelManifest = imageModelManifest;
4122
4813
  exports.imageModelManifests = imageModelManifests;
4123
4814
  exports.imageSizeForAspectRatio = imageSizeForAspectRatio;
4124
4815
  exports.imageSizeForPreset = imageSizeForPreset;
4125
4816
  exports.initialOutpaintRect = initialOutpaintRect;
4126
- exports.isAvailable = isAvailable;
4127
- exports.listImageGenerationJobs = listImageGenerationJobs;
4817
+ exports.jobs = jobs;
4128
4818
  exports.listImageLoRAs = listImageLoRAs;
4129
4819
  exports.listImageModels = listImageModels;
4820
+ exports.listTextModels = listBonsaiTextModels;
4130
4821
  exports.normalizeImageSize = normalizeImageSize;
4131
4822
  exports.prepareImageLoRA = prepareImageLoRA;
4132
4823
  exports.prepareImageModel = prepareImageModel;
4824
+ exports.prepareTextModel = prepareBonsaiTextModel;
4133
4825
  exports.rectForSourceScale = rectForSourceScale;
4134
4826
  exports.roundImageDimension = roundImageDimension;
4135
4827
  exports.roundedImageSize = roundedImageSize;
4136
- exports.scheduleImageGeneration = scheduleImageGeneration;
4137
- exports.shape = shape;
4138
- exports.stream = stream;
4828
+ exports.ternaryBonsaiImage4B = ternaryBonsaiImage4B;
4139
4829
  exports.unloadImageModel = unloadImageModel;
4140
- exports.upscaleImage = upscaleImage;
4141
- exports.useImageComposer = useImageComposer;
4142
- exports.useImageGeneration = useImageGeneration;
4143
- exports.useImageModel = useImageModel;
4144
- exports.useTextGeneration = useTextGeneration;
4830
+ exports.useChat = useChat;
4831
+ exports.useChatList = useChatList;
4832
+ exports.useJob = useJob;
4833
+ exports.useJobList = useJobList;
4145
4834
  exports.verifyImageLoRA = verifyImageLoRA;
4146
- exports.verifyImageModel = verifyImageModel;
4835
+ exports.verifyImageModel = verifyImageModel;
4836
+ exports.verifyTextModel = verifyBonsaiTextModel;