@umituz/web-ai 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ABTesting-DDqNhHtP.d.ts → AIErrors-B2ogd1PB.d.ts} +14 -1
- package/dist/{ABTesting-DWBtKTMQ.d.cts → AIErrors-C5EasV_c.d.cts} +14 -1
- package/dist/{ProviderConfig-a55tLhWT.d.cts → ProviderConfig-DIFa9nmG.d.cts} +10 -1
- package/dist/{ProviderConfig-a55tLhWT.d.ts → ProviderConfig-DIFa9nmG.d.ts} +10 -1
- package/dist/chat/index.cjs +114 -23
- package/dist/chat/index.cjs.map +1 -1
- package/dist/chat/index.d.cts +4 -4
- package/dist/chat/index.d.ts +4 -4
- package/dist/chat/index.js +114 -23
- package/dist/chat/index.js.map +1 -1
- package/dist/{chat.service-DBKq9I2c.d.cts → chat.service-CL1qtYxe.d.cts} +7 -7
- package/dist/{chat.service-RQn8UTXD.d.ts → chat.service-q05GnhEm.d.ts} +7 -7
- package/dist/content/index.cjs +393 -186
- package/dist/content/index.cjs.map +1 -1
- package/dist/content/index.d.cts +7 -8
- package/dist/content/index.d.ts +7 -8
- package/dist/content/index.js +393 -186
- package/dist/content/index.js.map +1 -1
- package/dist/core/index.cjs +112 -15
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +5 -33
- package/dist/core/index.d.ts +5 -33
- package/dist/core/index.js +111 -16
- package/dist/core/index.js.map +1 -1
- package/dist/{create-ai-DGZFBOA1.d.cts → create-ai-DOI9jD3v.d.cts} +4 -21
- package/dist/{create-ai-BhNaz86i.d.ts → create-ai-DXEoke8-.d.ts} +4 -21
- package/dist/hooks/index.cjs +359 -169
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +6 -6
- package/dist/hooks/index.d.ts +6 -6
- package/dist/hooks/index.js +359 -169
- package/dist/hooks/index.js.map +1 -1
- package/dist/index-DIHz8_OJ.d.ts +48 -0
- package/dist/index-DKHNcMbl.d.cts +48 -0
- package/dist/index.cjs +245 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +244 -95
- package/dist/index.js.map +1 -1
- package/dist/{provider.factory-COsnprLH.d.cts → provider.factory-BxsfK4WW.d.cts} +1 -1
- package/dist/{provider.factory-CpWFyjbw.d.ts → provider.factory-DfDfk0dK.d.ts} +1 -1
- package/dist/providers/index.cjs +281 -124
- package/dist/providers/index.cjs.map +1 -1
- package/dist/providers/index.d.cts +11 -12
- package/dist/providers/index.d.ts +11 -12
- package/dist/providers/index.js +281 -124
- package/dist/providers/index.js.map +1 -1
- package/dist/wizard/index.d.cts +1 -1
- package/dist/wizard/index.d.ts +1 -1
- package/package.json +4 -1
- package/dist/index-DjX9ZKuM.d.cts +0 -14
- package/dist/index-DxLvE588.d.ts +0 -14
package/dist/hooks/index.cjs
CHANGED
|
@@ -5,6 +5,17 @@ var react = require('react');
|
|
|
5
5
|
// src/hooks/useAIContent.ts
|
|
6
6
|
|
|
7
7
|
// src/core/errors/ai-error-types.ts
|
|
8
|
+
var AIErrorType = /* @__PURE__ */ ((AIErrorType2) => {
|
|
9
|
+
AIErrorType2["INVALID_API_KEY"] = "INVALID_API_KEY";
|
|
10
|
+
AIErrorType2["MISSING_CONFIG"] = "MISSING_CONFIG";
|
|
11
|
+
AIErrorType2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
12
|
+
AIErrorType2["ABORT_ERROR"] = "ABORT_ERROR";
|
|
13
|
+
AIErrorType2["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
|
|
14
|
+
AIErrorType2["SERVER_ERROR"] = "SERVER_ERROR";
|
|
15
|
+
AIErrorType2["CLIENT_ERROR"] = "CLIENT_ERROR";
|
|
16
|
+
AIErrorType2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
17
|
+
return AIErrorType2;
|
|
18
|
+
})(AIErrorType || {});
|
|
8
19
|
var USER_FRIENDLY_ERROR_MESSAGES = {
|
|
9
20
|
["INVALID_API_KEY" /* INVALID_API_KEY */]: "Please check your API key and try again.",
|
|
10
21
|
["MISSING_CONFIG" /* MISSING_CONFIG */]: "The provider is not configured. Please create a provider with createAI first.",
|
|
@@ -17,17 +28,80 @@ var USER_FRIENDLY_ERROR_MESSAGES = {
|
|
|
17
28
|
};
|
|
18
29
|
|
|
19
30
|
// src/core/errors/ai-error.ts
|
|
20
|
-
var
|
|
31
|
+
var KNOWN_ERROR_TYPES = new Set(Object.values(AIErrorType));
|
|
32
|
+
function mapCodeToType(code) {
|
|
33
|
+
switch (code) {
|
|
34
|
+
case "VALIDATION_ERROR":
|
|
35
|
+
case "CLIENT_ERROR":
|
|
36
|
+
return "CLIENT_ERROR" /* CLIENT_ERROR */;
|
|
37
|
+
case "INVALID_API_KEY":
|
|
38
|
+
return "INVALID_API_KEY" /* INVALID_API_KEY */;
|
|
39
|
+
case "MISSING_CONFIG":
|
|
40
|
+
case "PROVIDER_UNAVAILABLE":
|
|
41
|
+
return "MISSING_CONFIG" /* MISSING_CONFIG */;
|
|
42
|
+
case "PROVIDER_QUOTA_EXHAUSTED":
|
|
43
|
+
case "RATE_LIMIT_ERROR":
|
|
44
|
+
return "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */;
|
|
45
|
+
case "NETWORK_ERROR":
|
|
46
|
+
case "TIMEOUT":
|
|
47
|
+
return "NETWORK_ERROR" /* NETWORK_ERROR */;
|
|
48
|
+
case "ABORT_ERROR":
|
|
49
|
+
case "ABORTED":
|
|
50
|
+
return "ABORT_ERROR" /* ABORT_ERROR */;
|
|
51
|
+
case "SERVER_ERROR":
|
|
52
|
+
case "PROVIDER_ERROR":
|
|
53
|
+
case "NO_RESULT":
|
|
54
|
+
return "SERVER_ERROR" /* SERVER_ERROR */;
|
|
55
|
+
default:
|
|
56
|
+
return "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function mapMessageToCode(message, fallback = "UNKNOWN") {
|
|
60
|
+
const lower = message.toLowerCase();
|
|
61
|
+
if (lower.includes("validation")) return "VALIDATION_ERROR";
|
|
62
|
+
if (lower.includes("quota")) return "PROVIDER_QUOTA_EXHAUSTED";
|
|
63
|
+
if (lower.includes("unavailable")) return "PROVIDER_UNAVAILABLE";
|
|
64
|
+
if (lower.includes("timeout") || lower.includes("aborted")) {
|
|
65
|
+
return lower.includes("aborted") ? "ABORTED" : "TIMEOUT";
|
|
66
|
+
}
|
|
67
|
+
if (lower.includes("parse") || lower.includes("json")) return "PARSE_ERROR";
|
|
68
|
+
if (lower.includes("no result")) return "NO_RESULT";
|
|
69
|
+
if (lower.includes("network") || lower.includes("fetch")) return "NETWORK_ERROR";
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
var AIError = class _AIError extends Error {
|
|
21
73
|
type;
|
|
74
|
+
code;
|
|
22
75
|
cause;
|
|
23
76
|
metadata;
|
|
24
|
-
constructor(
|
|
25
|
-
|
|
77
|
+
constructor(typeOrMessage, messageOrCode, cause, metadata = {}) {
|
|
78
|
+
let effectiveType;
|
|
79
|
+
let effectiveMessage;
|
|
80
|
+
let effectiveCode;
|
|
81
|
+
if (KNOWN_ERROR_TYPES.has(typeOrMessage)) {
|
|
82
|
+
effectiveType = typeOrMessage;
|
|
83
|
+
effectiveMessage = messageOrCode ?? "";
|
|
84
|
+
effectiveCode = effectiveType;
|
|
85
|
+
} else {
|
|
86
|
+
effectiveMessage = typeOrMessage;
|
|
87
|
+
effectiveCode = messageOrCode ?? "UNKNOWN";
|
|
88
|
+
effectiveType = mapCodeToType(effectiveCode);
|
|
89
|
+
}
|
|
90
|
+
super(effectiveMessage);
|
|
26
91
|
this.name = "AIError";
|
|
27
|
-
this.type =
|
|
92
|
+
this.type = effectiveType;
|
|
93
|
+
this.code = effectiveCode;
|
|
28
94
|
this.cause = cause;
|
|
29
95
|
this.metadata = metadata;
|
|
30
96
|
}
|
|
97
|
+
static from(error, fallbackCode = "UNKNOWN") {
|
|
98
|
+
if (error instanceof _AIError) return error;
|
|
99
|
+
if (error instanceof Error) {
|
|
100
|
+
const code = mapMessageToCode(error.message, fallbackCode);
|
|
101
|
+
return new _AIError(error.message, code, error);
|
|
102
|
+
}
|
|
103
|
+
return new _AIError(String(error), fallbackCode, error);
|
|
104
|
+
}
|
|
31
105
|
toString() {
|
|
32
106
|
const statusPart = typeof this.metadata.status === "number" ? ` (HTTP ${this.metadata.status})` : "";
|
|
33
107
|
return `${this.name} [${this.type}]: ${this.message}${statusPart}`;
|
|
@@ -36,6 +110,7 @@ var AIError = class extends Error {
|
|
|
36
110
|
return {
|
|
37
111
|
name: this.name,
|
|
38
112
|
type: this.type,
|
|
113
|
+
code: this.code,
|
|
39
114
|
message: this.message,
|
|
40
115
|
metadata: this.metadata
|
|
41
116
|
};
|
|
@@ -224,12 +299,16 @@ var ProviderFactory = class {
|
|
|
224
299
|
let wake = null;
|
|
225
300
|
let finished = false;
|
|
226
301
|
let failure;
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
const onAbort = () => controller.abort();
|
|
304
|
+
request.signal?.addEventListener("abort", onAbort);
|
|
305
|
+
const effectiveRequest = { ...request, signal: controller.signal };
|
|
227
306
|
const notify = () => {
|
|
228
307
|
const resolve = wake;
|
|
229
308
|
wake = null;
|
|
230
309
|
resolve?.();
|
|
231
310
|
};
|
|
232
|
-
const completion = this.chatStream(
|
|
311
|
+
const completion = this.chatStream(effectiveRequest, {
|
|
233
312
|
onChunk: (chunk) => {
|
|
234
313
|
queue.push(chunk);
|
|
235
314
|
notify();
|
|
@@ -245,18 +324,23 @@ var ProviderFactory = class {
|
|
|
245
324
|
notify();
|
|
246
325
|
}
|
|
247
326
|
);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
327
|
+
try {
|
|
328
|
+
while (!finished || queue.length > 0) {
|
|
329
|
+
if (queue.length === 0 && !finished) {
|
|
330
|
+
await new Promise((resolve) => {
|
|
331
|
+
wake = resolve;
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
while (queue.length > 0) {
|
|
335
|
+
yield queue.shift();
|
|
336
|
+
}
|
|
337
|
+
if (failure) throw failure;
|
|
256
338
|
}
|
|
257
|
-
|
|
339
|
+
await completion;
|
|
340
|
+
} finally {
|
|
341
|
+
controller.abort();
|
|
342
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
258
343
|
}
|
|
259
|
-
await completion;
|
|
260
344
|
}
|
|
261
345
|
async generateJson(request) {
|
|
262
346
|
const effective = request.model || request.tier ? request : { ...request, tier: this.defaultTier };
|
|
@@ -403,15 +487,22 @@ var ProviderFactory = class {
|
|
|
403
487
|
// src/core/constants/protocol.constants.ts
|
|
404
488
|
var SSE_PREFIX = "data: ";
|
|
405
489
|
var SSE_DONE_MARKER = "[DONE]";
|
|
406
|
-
var JSON_CODE_BLOCK_PATTERN = /```json
|
|
490
|
+
var JSON_CODE_BLOCK_PATTERN = /```(?:json)?\s*\n?([\s\S]*?)\n?```/;
|
|
407
491
|
var JSON_OBJECT_PATTERN = /\{[\s\S]*\}/;
|
|
492
|
+
var JSON_ARRAY_PATTERN = /\[[\s\S]*\]/;
|
|
408
493
|
|
|
409
494
|
// src/core/parsers/sse-lines.ts
|
|
410
495
|
function isSseDataLine(line) {
|
|
411
|
-
return line.startsWith(SSE_PREFIX);
|
|
496
|
+
return line.startsWith(SSE_PREFIX) || line.startsWith("data:");
|
|
412
497
|
}
|
|
413
498
|
function extractSseDataPayload(line) {
|
|
414
|
-
|
|
499
|
+
if (line.startsWith(SSE_PREFIX)) {
|
|
500
|
+
return line.slice(SSE_PREFIX.length);
|
|
501
|
+
}
|
|
502
|
+
if (line.startsWith("data:")) {
|
|
503
|
+
return line.slice(5);
|
|
504
|
+
}
|
|
505
|
+
return line;
|
|
415
506
|
}
|
|
416
507
|
function isSseDoneMarker(payload) {
|
|
417
508
|
return payload === SSE_DONE_MARKER;
|
|
@@ -420,7 +511,7 @@ function isSseDoneMarker(payload) {
|
|
|
420
511
|
// src/core/parsers/json-content-extractor.ts
|
|
421
512
|
function extractJsonFromCodeFence(content) {
|
|
422
513
|
const match = content.match(JSON_CODE_BLOCK_PATTERN);
|
|
423
|
-
if (match && match[1]) return match[1];
|
|
514
|
+
if (match && match[1]) return match[1].trim();
|
|
424
515
|
return null;
|
|
425
516
|
}
|
|
426
517
|
function extractJsonObject(content) {
|
|
@@ -428,8 +519,27 @@ function extractJsonObject(content) {
|
|
|
428
519
|
if (match && match[0]) return match[0];
|
|
429
520
|
return null;
|
|
430
521
|
}
|
|
522
|
+
function extractJsonArray(content) {
|
|
523
|
+
const match = content.match(JSON_ARRAY_PATTERN);
|
|
524
|
+
if (match && match[0]) return match[0];
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
function repairCommonJsonDefects(text) {
|
|
528
|
+
return text.replace(/(:\s*-?\d+(?:\.\d+)?)\s*"/g, "$1").replace(/(:\s*"[^"]*")"/g, "$1");
|
|
529
|
+
}
|
|
431
530
|
function extractJsonCandidate(content) {
|
|
432
|
-
|
|
531
|
+
const fromFence = extractJsonFromCodeFence(content);
|
|
532
|
+
if (fromFence) return fromFence;
|
|
533
|
+
const objectIndex = content.indexOf("{");
|
|
534
|
+
const arrayIndex = content.indexOf("[");
|
|
535
|
+
if (objectIndex !== -1 && (arrayIndex === -1 || objectIndex < arrayIndex)) {
|
|
536
|
+
const obj = extractJsonObject(content);
|
|
537
|
+
if (obj) return obj;
|
|
538
|
+
} else if (arrayIndex !== -1) {
|
|
539
|
+
const arr = extractJsonArray(content);
|
|
540
|
+
if (arr) return arr;
|
|
541
|
+
}
|
|
542
|
+
return content.trim();
|
|
433
543
|
}
|
|
434
544
|
|
|
435
545
|
// src/core/parsers/json-response.parser.ts
|
|
@@ -438,12 +548,17 @@ var JsonResponseParser = class {
|
|
|
438
548
|
const candidate = extractJsonCandidate(rawContent);
|
|
439
549
|
try {
|
|
440
550
|
return JSON.parse(candidate);
|
|
441
|
-
} catch
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
551
|
+
} catch {
|
|
552
|
+
try {
|
|
553
|
+
return JSON.parse(repairCommonJsonDefects(candidate));
|
|
554
|
+
} catch (error) {
|
|
555
|
+
throw new AIError(
|
|
556
|
+
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
|
|
557
|
+
"Failed to parse structured response as JSON",
|
|
558
|
+
error instanceof Error ? error : void 0,
|
|
559
|
+
{ rawContent }
|
|
560
|
+
);
|
|
561
|
+
}
|
|
447
562
|
}
|
|
448
563
|
}
|
|
449
564
|
};
|
|
@@ -757,11 +872,14 @@ async function consumeAnthropicStream(stream, callbacks) {
|
|
|
757
872
|
break;
|
|
758
873
|
case "message_stop":
|
|
759
874
|
break;
|
|
760
|
-
case "error":
|
|
875
|
+
case "error": {
|
|
876
|
+
const errType = event.error.type;
|
|
877
|
+
const mappedType = errType === "rate_limit_error" ? "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */ : errType === "authentication_error" ? "INVALID_API_KEY" /* INVALID_API_KEY */ : errType === "invalid_request_error" ? "CLIENT_ERROR" /* CLIENT_ERROR */ : "SERVER_ERROR" /* SERVER_ERROR */;
|
|
761
878
|
throw new AIError(
|
|
762
|
-
|
|
879
|
+
mappedType,
|
|
763
880
|
event.error.message || "Anthropic stream failed mid-flight"
|
|
764
881
|
);
|
|
882
|
+
}
|
|
765
883
|
}
|
|
766
884
|
};
|
|
767
885
|
try {
|
|
@@ -1553,6 +1671,13 @@ async function consumeGroqStream(stream, callbacks) {
|
|
|
1553
1671
|
} catch {
|
|
1554
1672
|
return;
|
|
1555
1673
|
}
|
|
1674
|
+
const errorBody = chunk.error;
|
|
1675
|
+
if (errorBody) {
|
|
1676
|
+
throw new AIError(
|
|
1677
|
+
errorBody.message?.toLowerCase().includes("rate limit") ? "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */ : "SERVER_ERROR" /* SERVER_ERROR */,
|
|
1678
|
+
errorBody.message || "Groq stream failed mid-flight"
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1556
1681
|
if (chunk.model) model = chunk.model;
|
|
1557
1682
|
const choice = chunk.choices?.[0];
|
|
1558
1683
|
const delta = choice?.delta?.content;
|
|
@@ -1567,13 +1692,6 @@ async function consumeGroqStream(stream, callbacks) {
|
|
|
1567
1692
|
if (tailUsage) {
|
|
1568
1693
|
usage = mapGroqUsage(tailUsage);
|
|
1569
1694
|
}
|
|
1570
|
-
const errorBody = chunk.error;
|
|
1571
|
-
if (errorBody) {
|
|
1572
|
-
throw new AIError(
|
|
1573
|
-
"SERVER_ERROR" /* SERVER_ERROR */,
|
|
1574
|
-
errorBody.message || "Groq stream failed mid-flight"
|
|
1575
|
-
);
|
|
1576
|
-
}
|
|
1577
1695
|
};
|
|
1578
1696
|
try {
|
|
1579
1697
|
while (true) {
|
|
@@ -1774,37 +1892,7 @@ function findLastIndex2(items, predicate) {
|
|
|
1774
1892
|
}
|
|
1775
1893
|
|
|
1776
1894
|
// src/content/domain/errors/AIErrors.ts
|
|
1777
|
-
var
|
|
1778
|
-
code;
|
|
1779
|
-
cause;
|
|
1780
|
-
constructor(message, code, cause) {
|
|
1781
|
-
super(message);
|
|
1782
|
-
this.name = "AIError";
|
|
1783
|
-
this.code = code;
|
|
1784
|
-
this.cause = cause;
|
|
1785
|
-
}
|
|
1786
|
-
static from(error, fallbackCode = "UNKNOWN") {
|
|
1787
|
-
if (error instanceof _AIError) return error;
|
|
1788
|
-
if (error instanceof Error) {
|
|
1789
|
-
return new _AIError(error.message, mapMessageToCode(error.message, fallbackCode), error);
|
|
1790
|
-
}
|
|
1791
|
-
return new _AIError(String(error), fallbackCode, error);
|
|
1792
|
-
}
|
|
1793
|
-
};
|
|
1794
|
-
function mapMessageToCode(message, fallback) {
|
|
1795
|
-
const lower = message.toLowerCase();
|
|
1796
|
-
if (lower.includes("validation")) return "VALIDATION_ERROR";
|
|
1797
|
-
if (lower.includes("quota")) return "PROVIDER_QUOTA_EXHAUSTED";
|
|
1798
|
-
if (lower.includes("unavailable")) return "PROVIDER_UNAVAILABLE";
|
|
1799
|
-
if (lower.includes("timeout") || lower.includes("aborted")) {
|
|
1800
|
-
return lower.includes("aborted") ? "ABORTED" : "TIMEOUT";
|
|
1801
|
-
}
|
|
1802
|
-
if (lower.includes("parse") || lower.includes("json")) return "PARSE_ERROR";
|
|
1803
|
-
if (lower.includes("no result")) return "NO_RESULT";
|
|
1804
|
-
if (lower.includes("network") || lower.includes("fetch")) return "NETWORK_ERROR";
|
|
1805
|
-
return fallback;
|
|
1806
|
-
}
|
|
1807
|
-
var ValidationError = class extends AIError2 {
|
|
1895
|
+
var ValidationError = class extends AIError {
|
|
1808
1896
|
constructor(message) {
|
|
1809
1897
|
super(message, "VALIDATION_ERROR");
|
|
1810
1898
|
this.name = "ValidationError";
|
|
@@ -1935,7 +2023,9 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1935
2023
|
headers: this.authHeaders()
|
|
1936
2024
|
}
|
|
1937
2025
|
);
|
|
1938
|
-
|
|
2026
|
+
if (response.ok) return "healthy";
|
|
2027
|
+
if (response.status === 401 || response.status === 403) return "unavailable";
|
|
2028
|
+
return "degraded";
|
|
1939
2029
|
} catch {
|
|
1940
2030
|
return "unavailable";
|
|
1941
2031
|
}
|
|
@@ -1946,6 +2036,8 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1946
2036
|
async generate(request) {
|
|
1947
2037
|
try {
|
|
1948
2038
|
const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
|
|
2039
|
+
const systemMessages = request.messages.filter((m) => m.role === "system");
|
|
2040
|
+
const systemPrompt = request.systemPrompt && request.systemPrompt.length > 0 ? request.systemPrompt : systemMessages.length > 0 ? systemMessages.map((m) => m.content).join("\n") : void 0;
|
|
1949
2041
|
const response = await this.withRetry(async () => {
|
|
1950
2042
|
return await this.fetchWithTimeout(
|
|
1951
2043
|
`${this.config.baseUrl}/models/${model}:generateContent`,
|
|
@@ -1953,22 +2045,28 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1953
2045
|
method: "POST",
|
|
1954
2046
|
headers: this.authHeaders(),
|
|
1955
2047
|
body: JSON.stringify({
|
|
1956
|
-
systemInstruction:
|
|
2048
|
+
systemInstruction: systemPrompt && systemPrompt.length > 0 ? { parts: [{ text: systemPrompt }] } : void 0,
|
|
1957
2049
|
contents: toGeminiContents(request.messages),
|
|
1958
2050
|
generationConfig: {
|
|
1959
|
-
temperature: request.temperature
|
|
1960
|
-
maxOutputTokens: request.maxTokens
|
|
1961
|
-
topP: request.topP
|
|
2051
|
+
temperature: request.temperature ?? 0.7,
|
|
2052
|
+
maxOutputTokens: request.maxTokens ?? 2048,
|
|
2053
|
+
topP: request.topP ?? 0.9
|
|
1962
2054
|
}
|
|
1963
2055
|
})
|
|
1964
2056
|
}
|
|
1965
2057
|
);
|
|
1966
2058
|
});
|
|
1967
2059
|
if (!response.ok) {
|
|
1968
|
-
|
|
2060
|
+
const message = await extractErrorMessageFromResponse(response);
|
|
2061
|
+
throw new AIError(
|
|
2062
|
+
classifyHttpStatus(response.status),
|
|
2063
|
+
`Gemini API error (${response.status}): ${message}`,
|
|
2064
|
+
void 0,
|
|
2065
|
+
{ status: response.status, model }
|
|
2066
|
+
);
|
|
1969
2067
|
}
|
|
1970
2068
|
const data = await response.json();
|
|
1971
|
-
const text = data.candidates[0]?.content?.parts?.map((part) => part.text ?? "").join("") ?? "";
|
|
2069
|
+
const text = data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("") ?? "";
|
|
1972
2070
|
if (text.length === 0) {
|
|
1973
2071
|
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No content generated from Gemini API", void 0, { model });
|
|
1974
2072
|
}
|
|
@@ -1981,7 +2079,7 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1981
2079
|
} : void 0,
|
|
1982
2080
|
model,
|
|
1983
2081
|
providerId: this.id,
|
|
1984
|
-
finishReason: mapGeminiFinishReason(data.candidates[0]?.finishReason)
|
|
2082
|
+
finishReason: mapGeminiFinishReason(data.candidates?.[0]?.finishReason)
|
|
1985
2083
|
};
|
|
1986
2084
|
} catch (error) {
|
|
1987
2085
|
if (error instanceof AIError) throw error;
|
|
@@ -2033,12 +2131,18 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
2033
2131
|
);
|
|
2034
2132
|
});
|
|
2035
2133
|
if (!response.ok) {
|
|
2036
|
-
|
|
2134
|
+
const message = await extractErrorMessageFromResponse(response);
|
|
2135
|
+
throw new AIError(
|
|
2136
|
+
classifyHttpStatus(response.status),
|
|
2137
|
+
`Gemini image generation error (${response.status}): ${message}`,
|
|
2138
|
+
void 0,
|
|
2139
|
+
{ status: response.status }
|
|
2140
|
+
);
|
|
2037
2141
|
}
|
|
2038
2142
|
const data = await response.json();
|
|
2039
|
-
const imageData = data.candidates[0]?.content?.parts[0]?.inlineData;
|
|
2143
|
+
const imageData = data.candidates?.[0]?.content?.parts?.[0]?.inlineData;
|
|
2040
2144
|
if (!imageData) {
|
|
2041
|
-
throw new
|
|
2145
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No image data in Gemini response");
|
|
2042
2146
|
}
|
|
2043
2147
|
const url = `data:${imageData.mimeType};base64,${imageData.data}`;
|
|
2044
2148
|
return {
|
|
@@ -2467,17 +2571,70 @@ var PrunaProvider = class {
|
|
|
2467
2571
|
async estimateCost(_request) {
|
|
2468
2572
|
return 1;
|
|
2469
2573
|
}
|
|
2470
|
-
async generateImage(
|
|
2471
|
-
|
|
2574
|
+
async generateImage(request) {
|
|
2575
|
+
let finalUrl;
|
|
2576
|
+
const model = this.config.models?.textToImage;
|
|
2577
|
+
for await (const stage of generateWithPruna(
|
|
2578
|
+
{ prompt: request.prompt, model, quality: request.quality },
|
|
2579
|
+
this.config.apiKey
|
|
2580
|
+
)) {
|
|
2581
|
+
if (stage.error) throw stage.error;
|
|
2582
|
+
if (stage.result?.url) finalUrl = stage.result.url;
|
|
2583
|
+
}
|
|
2584
|
+
if (!finalUrl) {
|
|
2585
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No image generated by Pruna AI");
|
|
2586
|
+
}
|
|
2587
|
+
return {
|
|
2588
|
+
id: Date.now().toString(),
|
|
2589
|
+
type: "image",
|
|
2590
|
+
url: finalUrl,
|
|
2591
|
+
metadata: { provider: this.id, model: model ?? "p-image" },
|
|
2592
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2593
|
+
};
|
|
2472
2594
|
}
|
|
2473
|
-
async generateVideo(
|
|
2474
|
-
|
|
2595
|
+
async generateVideo(request) {
|
|
2596
|
+
let finalUrl;
|
|
2597
|
+
const model = this.config.models?.textToVideo;
|
|
2598
|
+
for await (const stage of generateWithPruna(
|
|
2599
|
+
{ prompt: request.prompt, model, duration: request.duration, aspectRatio: request.aspectRatio },
|
|
2600
|
+
this.config.apiKey
|
|
2601
|
+
)) {
|
|
2602
|
+
if (stage.error) throw stage.error;
|
|
2603
|
+
if (stage.result?.url) finalUrl = stage.result.url;
|
|
2604
|
+
}
|
|
2605
|
+
if (!finalUrl) {
|
|
2606
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No video generated by Pruna AI");
|
|
2607
|
+
}
|
|
2608
|
+
return {
|
|
2609
|
+
id: Date.now().toString(),
|
|
2610
|
+
type: "video",
|
|
2611
|
+
url: finalUrl,
|
|
2612
|
+
metadata: { provider: this.id, model: model ?? "p-video" },
|
|
2613
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2614
|
+
};
|
|
2475
2615
|
}
|
|
2476
|
-
async imageToVideo(
|
|
2477
|
-
|
|
2616
|
+
async imageToVideo(request) {
|
|
2617
|
+
let finalUrl;
|
|
2618
|
+
for await (const stage of generateWithPruna(
|
|
2619
|
+
{ image: request.imageUrl, prompt: request.prompt, duration: request.duration, motion: request.motion },
|
|
2620
|
+
this.config.apiKey
|
|
2621
|
+
)) {
|
|
2622
|
+
if (stage.error) throw stage.error;
|
|
2623
|
+
if (stage.result?.url) finalUrl = stage.result.url;
|
|
2624
|
+
}
|
|
2625
|
+
if (!finalUrl) {
|
|
2626
|
+
throw new AIError("SERVER_ERROR" /* SERVER_ERROR */, "No video generated from image by Pruna AI");
|
|
2627
|
+
}
|
|
2628
|
+
return {
|
|
2629
|
+
id: Date.now().toString(),
|
|
2630
|
+
type: "video",
|
|
2631
|
+
url: finalUrl,
|
|
2632
|
+
metadata: { provider: this.id, model: "p-image-to-video" },
|
|
2633
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2634
|
+
};
|
|
2478
2635
|
}
|
|
2479
2636
|
async videoToVideo(_request) {
|
|
2480
|
-
throw new AIError("
|
|
2637
|
+
throw new AIError("CLIENT_ERROR" /* CLIENT_ERROR */, "Pruna AI provider does not support video-to-video conversion");
|
|
2481
2638
|
}
|
|
2482
2639
|
};
|
|
2483
2640
|
|
|
@@ -3084,11 +3241,12 @@ var buildScriptResult = (parsed, duration) => {
|
|
|
3084
3241
|
if (!script) {
|
|
3085
3242
|
throw new Error("AI response missing script field");
|
|
3086
3243
|
}
|
|
3244
|
+
const wordCount = script.trim().length === 0 ? 0 : script.trim().split(/\s+/).length;
|
|
3087
3245
|
return {
|
|
3088
3246
|
...parsed,
|
|
3089
3247
|
script,
|
|
3090
3248
|
duration,
|
|
3091
|
-
wordCount
|
|
3249
|
+
wordCount
|
|
3092
3250
|
};
|
|
3093
3251
|
};
|
|
3094
3252
|
var VideoScriptService = class {
|
|
@@ -3283,7 +3441,6 @@ var SeoService = class {
|
|
|
3283
3441
|
analyzeKeywords(content, keywords) {
|
|
3284
3442
|
const validatedContent = validateContentBody(content);
|
|
3285
3443
|
const validatedKeywords = validateKeywordList(keywords);
|
|
3286
|
-
void countWordsInContent(validatedContent);
|
|
3287
3444
|
return validatedKeywords.map((keyword) => {
|
|
3288
3445
|
const count = countKeywordOccurrences(validatedContent, keyword);
|
|
3289
3446
|
const density = calculateKeywordDensity(validatedContent, keyword);
|
|
@@ -3378,68 +3535,20 @@ var ABTestService = class {
|
|
|
3378
3535
|
});
|
|
3379
3536
|
const scoreA = predictions[0]?.predictedEngagement ?? FALLBACK_ENGAGEMENT;
|
|
3380
3537
|
const scoreB = predictions[1]?.predictedEngagement ?? FALLBACK_ENGAGEMENT;
|
|
3538
|
+
const isTie = scoreA === scoreB;
|
|
3381
3539
|
const winner = selectHigherScoreVariantId(scoreA, scoreB, VARIANT_A_ID, VARIANT_B_ID);
|
|
3382
3540
|
const improvement = calculateImprovementPercentage(scoreA, scoreB);
|
|
3383
3541
|
return {
|
|
3384
3542
|
winner,
|
|
3385
3543
|
confidence: AB_TEST_DEFAULT_CONFIDENCE,
|
|
3386
|
-
improvement: `+${improvement}% engagement`,
|
|
3387
|
-
reasoning: winner === VARIANT_A_ID ? "Variant A has more engaging content" : "Variant B has more engaging content",
|
|
3544
|
+
improvement: isTie ? "0%" : `+${improvement}% engagement`,
|
|
3545
|
+
reasoning: isTie ? "Both variants have equal predicted engagement" : winner === VARIANT_A_ID ? "Variant A has more engaging content" : "Variant B has more engaging content",
|
|
3388
3546
|
recommendations: [...DEFAULT_RECOMMENDATIONS]
|
|
3389
3547
|
};
|
|
3390
3548
|
}
|
|
3391
3549
|
};
|
|
3392
3550
|
|
|
3393
|
-
// src/content/infrastructure/utils/jsonParser.ts
|
|
3394
|
-
function repairCommonJsonDefects(text) {
|
|
3395
|
-
return text.replace(/(:\s*-?\d+(?:\.\d+)?)\s*"/g, "$1").replace(/(:\s*"[^"]*")"/g, "$1");
|
|
3396
|
-
}
|
|
3397
|
-
function safeJSONParse(text, fallback, onError) {
|
|
3398
|
-
try {
|
|
3399
|
-
return JSON.parse(text);
|
|
3400
|
-
} catch (error) {
|
|
3401
|
-
try {
|
|
3402
|
-
return JSON.parse(repairCommonJsonDefects(text));
|
|
3403
|
-
} catch {
|
|
3404
|
-
}
|
|
3405
|
-
if (error instanceof SyntaxError) {
|
|
3406
|
-
const positionMatch = error.message.match(/position (\d+)/);
|
|
3407
|
-
const position = positionMatch ? parseInt(positionMatch[1], 10) : -1;
|
|
3408
|
-
if (position > 0) {
|
|
3409
|
-
text.substring(Math.max(0, position - 50), position + 50);
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
return fallback;
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
function extractJSON(text) {
|
|
3416
|
-
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
3417
|
-
if (codeBlockMatch && codeBlockMatch[1]) {
|
|
3418
|
-
const candidate = codeBlockMatch[1].trim();
|
|
3419
|
-
if (candidate.startsWith("{") || candidate.startsWith("[")) {
|
|
3420
|
-
return candidate;
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
const objectMatch = text.match(/\{[\s\S]*\}/);
|
|
3424
|
-
if (objectMatch) {
|
|
3425
|
-
return objectMatch[0];
|
|
3426
|
-
}
|
|
3427
|
-
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
|
3428
|
-
if (arrayMatch) {
|
|
3429
|
-
return arrayMatch[0];
|
|
3430
|
-
}
|
|
3431
|
-
return null;
|
|
3432
|
-
}
|
|
3433
|
-
function parseAIResponse(response, fallback, onError) {
|
|
3434
|
-
const json = extractJSON(response);
|
|
3435
|
-
if (!json) {
|
|
3436
|
-
return fallback;
|
|
3437
|
-
}
|
|
3438
|
-
return safeJSONParse(json, fallback);
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
3551
|
// src/content/application/services/ImagePromptService.ts
|
|
3442
|
-
var JSON_ARRAY_PATTERN = /\[[\s\S]*\]/;
|
|
3443
3552
|
var ImagePromptService = class {
|
|
3444
3553
|
constructor(executor) {
|
|
3445
3554
|
this.executor = executor;
|
|
@@ -3453,10 +3562,11 @@ var ImagePromptService = class {
|
|
|
3453
3562
|
async generateImagePromptsForBlog(blogContent) {
|
|
3454
3563
|
const truncated = truncateForBlogPromptPreview(validateContentBody(blogContent));
|
|
3455
3564
|
const prompt = PromptBuilder.blogImagePrompts(truncated);
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3565
|
+
const prompts = await this.executor.runJson(prompt, [], {
|
|
3566
|
+
maxTokens: 500,
|
|
3567
|
+
temperature: 0.8
|
|
3568
|
+
});
|
|
3569
|
+
return Array.isArray(prompts) ? prompts : [];
|
|
3460
3570
|
}
|
|
3461
3571
|
};
|
|
3462
3572
|
|
|
@@ -3500,22 +3610,27 @@ var ContentCalendarService = class {
|
|
|
3500
3610
|
const validatedNiche = validateTopicText(niche);
|
|
3501
3611
|
const validatedDays = validateCalendarDays(days);
|
|
3502
3612
|
const prompt = PromptBuilder.contentCalendar(validatedNiche, validatedDays);
|
|
3503
|
-
const data = await this.executor.runJson(
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3613
|
+
const data = await this.executor.runJson(
|
|
3614
|
+
prompt,
|
|
3615
|
+
[],
|
|
3616
|
+
{
|
|
3617
|
+
maxTokens: estimateTokenBudgetForCalendarDays(validatedDays),
|
|
3618
|
+
temperature: 0.8
|
|
3619
|
+
}
|
|
3620
|
+
);
|
|
3621
|
+
const items = Array.isArray(data) ? data : Array.isArray(data?.calendar) ? data.calendar : Array.isArray(data?.entries) ? data.entries : [];
|
|
3622
|
+
return items.map(toCalendarEntry);
|
|
3508
3623
|
}
|
|
3509
3624
|
};
|
|
3510
3625
|
|
|
3511
3626
|
// src/content/application/services/ProviderServices.ts
|
|
3512
3627
|
var wrapProviderError = (operation, error) => {
|
|
3513
3628
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3514
|
-
return new
|
|
3629
|
+
return new AIError(`${operation} failed: ${message}`, "PROVIDER_ERROR", error);
|
|
3515
3630
|
};
|
|
3516
3631
|
var ensureFactory = (factory, operation) => {
|
|
3517
3632
|
if (factory.getAllProviders().length === 0) {
|
|
3518
|
-
throw new
|
|
3633
|
+
throw new AIError(
|
|
3519
3634
|
`${operation} failed: No providers are registered. Provide a ProviderConfig with at least one enabled provider when constructing AIContentService.`,
|
|
3520
3635
|
"PROVIDER_UNAVAILABLE"
|
|
3521
3636
|
);
|
|
@@ -3552,6 +3667,54 @@ var MediaGenerationService = class {
|
|
|
3552
3667
|
}
|
|
3553
3668
|
};
|
|
3554
3669
|
|
|
3670
|
+
// src/content/infrastructure/utils/jsonParser.ts
|
|
3671
|
+
function repairCommonJsonDefects2(text) {
|
|
3672
|
+
return text.replace(/(:\s*-?\d+(?:\.\d+)?)\s*"/g, "$1").replace(/(:\s*"[^"]*")"/g, "$1");
|
|
3673
|
+
}
|
|
3674
|
+
function safeJSONParse(text, fallback, onError) {
|
|
3675
|
+
try {
|
|
3676
|
+
return JSON.parse(text);
|
|
3677
|
+
} catch (error) {
|
|
3678
|
+
try {
|
|
3679
|
+
return JSON.parse(repairCommonJsonDefects2(text));
|
|
3680
|
+
} catch {
|
|
3681
|
+
}
|
|
3682
|
+
if (error instanceof SyntaxError) {
|
|
3683
|
+
const positionMatch = error.message.match(/position (\d+)/);
|
|
3684
|
+
const position = positionMatch ? parseInt(positionMatch[1], 10) : -1;
|
|
3685
|
+
if (position > 0) {
|
|
3686
|
+
text.substring(Math.max(0, position - 50), position + 50);
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return fallback;
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
function extractJSON(text) {
|
|
3693
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
3694
|
+
if (codeBlockMatch && codeBlockMatch[1]) {
|
|
3695
|
+
const candidate = codeBlockMatch[1].trim();
|
|
3696
|
+
if (candidate.startsWith("{") || candidate.startsWith("[")) {
|
|
3697
|
+
return candidate;
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
const objectMatch = text.match(/\{[\s\S]*\}/);
|
|
3701
|
+
if (objectMatch) {
|
|
3702
|
+
return objectMatch[0];
|
|
3703
|
+
}
|
|
3704
|
+
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
|
3705
|
+
if (arrayMatch) {
|
|
3706
|
+
return arrayMatch[0];
|
|
3707
|
+
}
|
|
3708
|
+
return null;
|
|
3709
|
+
}
|
|
3710
|
+
function parseAIResponse(response, fallback, onError) {
|
|
3711
|
+
const json = extractJSON(response);
|
|
3712
|
+
if (!json) {
|
|
3713
|
+
return fallback;
|
|
3714
|
+
}
|
|
3715
|
+
return safeJSONParse(json, fallback);
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3555
3718
|
// src/content/application/services/GenerationExecutor.ts
|
|
3556
3719
|
var GenerationExecutor = class {
|
|
3557
3720
|
constructor(generator) {
|
|
@@ -3568,7 +3731,7 @@ var GenerationExecutor = class {
|
|
|
3568
3731
|
/** Usage-complete pass-through; requires a ProviderFactory engine. */
|
|
3569
3732
|
runChat(request) {
|
|
3570
3733
|
if (!(this.generator instanceof ProviderFactory)) {
|
|
3571
|
-
throw new
|
|
3734
|
+
throw new AIError(
|
|
3572
3735
|
"runChat requires a ProviderFactory engine \u2014 construct AIContentService with a provider config, not a custom ITextGenerator",
|
|
3573
3736
|
"PROVIDER_UNAVAILABLE"
|
|
3574
3737
|
);
|
|
@@ -3582,6 +3745,13 @@ var isTextGenerator = (value) => typeof value === "object" && value !== null &&
|
|
|
3582
3745
|
function toAIProviderConfig(config) {
|
|
3583
3746
|
const legacy = config;
|
|
3584
3747
|
const modern = config;
|
|
3748
|
+
const anthropic = modern.anthropic ?? (legacy.anthropic?.enabled ? {
|
|
3749
|
+
apiKey: legacy.anthropic.apiKey,
|
|
3750
|
+
baseUrl: legacy.anthropic.baseUrl,
|
|
3751
|
+
timeoutMs: legacy.anthropic.timeout,
|
|
3752
|
+
models: legacy.anthropic.models,
|
|
3753
|
+
effort: legacy.anthropic.effort
|
|
3754
|
+
} : void 0);
|
|
3585
3755
|
const groq = modern.groq ?? (legacy.groq?.enabled ? {
|
|
3586
3756
|
apiKey: legacy.groq.apiKey,
|
|
3587
3757
|
baseUrl: legacy.groq.baseUrl,
|
|
@@ -3596,7 +3766,7 @@ function toAIProviderConfig(config) {
|
|
|
3596
3766
|
priority: modern.priority,
|
|
3597
3767
|
fallbackEnabled: modern.fallbackEnabled,
|
|
3598
3768
|
defaultTier: modern.defaultTier,
|
|
3599
|
-
anthropic:
|
|
3769
|
+
...anthropic ? { anthropic } : {},
|
|
3600
3770
|
...groq ? { groq } : {},
|
|
3601
3771
|
...gemini ? { gemini } : {},
|
|
3602
3772
|
...fal ? { fal } : {},
|
|
@@ -3762,8 +3932,8 @@ function useAIContentContext(options) {
|
|
|
3762
3932
|
callbacksRef.current?.onProgress?.(value);
|
|
3763
3933
|
}, []);
|
|
3764
3934
|
const reportError = react.useCallback((err, fallbackMessage) => {
|
|
3765
|
-
const aiError =
|
|
3766
|
-
const finalError = aiError.message ? aiError : new
|
|
3935
|
+
const aiError = AIError.from(err, "UNKNOWN");
|
|
3936
|
+
const finalError = aiError.message ? aiError : new AIError(fallbackMessage, "UNKNOWN", err);
|
|
3767
3937
|
if (mountedRef.current) {
|
|
3768
3938
|
setError(finalError.message);
|
|
3769
3939
|
setErrorCode(finalError.code);
|
|
@@ -3789,7 +3959,7 @@ function useAIContentContext(options) {
|
|
|
3789
3959
|
}
|
|
3790
3960
|
return result;
|
|
3791
3961
|
} catch (err) {
|
|
3792
|
-
const aiError =
|
|
3962
|
+
const aiError = AIError.from(err, "UNKNOWN");
|
|
3793
3963
|
if (aiError.message) {
|
|
3794
3964
|
reportError(aiError, fallbackMessage);
|
|
3795
3965
|
} else {
|
|
@@ -4353,10 +4523,13 @@ function useWizardFlow(options) {
|
|
|
4353
4523
|
if (steps.length <= 1) return 0;
|
|
4354
4524
|
return Math.round(currentIndex / (steps.length - 1) * 100);
|
|
4355
4525
|
}, [currentIndex, steps.length]);
|
|
4526
|
+
const completedRef = react.useRef(false);
|
|
4356
4527
|
const navigateTo = react.useCallback((to) => {
|
|
4357
4528
|
setCurrentStep((prev) => {
|
|
4358
4529
|
if (prev === to) return prev;
|
|
4359
|
-
|
|
4530
|
+
queueMicrotask(() => {
|
|
4531
|
+
optionsRef.current.onStepChange?.(prev, to);
|
|
4532
|
+
});
|
|
4360
4533
|
return to;
|
|
4361
4534
|
});
|
|
4362
4535
|
}, []);
|
|
@@ -4397,13 +4570,17 @@ function useWizardFlow(options) {
|
|
|
4397
4570
|
[currentIndex, steps.length]
|
|
4398
4571
|
);
|
|
4399
4572
|
const reset = react.useCallback(() => {
|
|
4573
|
+
completedRef.current = false;
|
|
4400
4574
|
setCurrentStep(optionsRef.current.initialStep);
|
|
4401
4575
|
setWizardData({});
|
|
4402
4576
|
setIsProcessing(false);
|
|
4403
4577
|
}, []);
|
|
4404
4578
|
react.useEffect(() => {
|
|
4405
|
-
if (currentIndex === steps.length - 1 && steps.length > 0) {
|
|
4579
|
+
if (currentIndex === steps.length - 1 && steps.length > 0 && !completedRef.current) {
|
|
4580
|
+
completedRef.current = true;
|
|
4406
4581
|
optionsRef.current.onComplete?.(wizardData);
|
|
4582
|
+
} else if (currentIndex < steps.length - 1) {
|
|
4583
|
+
completedRef.current = false;
|
|
4407
4584
|
}
|
|
4408
4585
|
}, [currentIndex, steps.length, wizardData]);
|
|
4409
4586
|
return {
|
|
@@ -4632,21 +4809,17 @@ function useText(options = {}, deps) {
|
|
|
4632
4809
|
]
|
|
4633
4810
|
);
|
|
4634
4811
|
react.useEffect(() => {
|
|
4635
|
-
const controller = new AbortController();
|
|
4636
|
-
abortControllerRef.current = controller;
|
|
4637
4812
|
return () => {
|
|
4638
|
-
|
|
4813
|
+
abortControllerRef.current?.abort();
|
|
4639
4814
|
abortControllerRef.current = null;
|
|
4640
4815
|
};
|
|
4641
4816
|
}, []);
|
|
4642
4817
|
const runWithState = react.useCallback(
|
|
4643
4818
|
async (task) => {
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
}
|
|
4819
|
+
abortControllerRef.current?.abort();
|
|
4820
|
+
const controller = new AbortController();
|
|
4821
|
+
abortControllerRef.current = controller;
|
|
4648
4822
|
const { signal } = controller;
|
|
4649
|
-
if (signal.aborted) return null;
|
|
4650
4823
|
setIsLoading(true);
|
|
4651
4824
|
setError(null);
|
|
4652
4825
|
setResult(null);
|
|
@@ -4667,6 +4840,9 @@ function useText(options = {}, deps) {
|
|
|
4667
4840
|
if (!signal.aborted) {
|
|
4668
4841
|
setIsLoading(false);
|
|
4669
4842
|
}
|
|
4843
|
+
if (abortControllerRef.current === controller) {
|
|
4844
|
+
abortControllerRef.current = null;
|
|
4845
|
+
}
|
|
4670
4846
|
}
|
|
4671
4847
|
},
|
|
4672
4848
|
[]
|
|
@@ -4731,6 +4907,7 @@ function useText(options = {}, deps) {
|
|
|
4731
4907
|
);
|
|
4732
4908
|
const cancel = react.useCallback(() => {
|
|
4733
4909
|
abortControllerRef.current?.abort();
|
|
4910
|
+
abortControllerRef.current = null;
|
|
4734
4911
|
setIsLoading(false);
|
|
4735
4912
|
}, []);
|
|
4736
4913
|
const reset = react.useCallback(() => {
|
|
@@ -4873,10 +5050,8 @@ function useChat(options, deps) {
|
|
|
4873
5050
|
};
|
|
4874
5051
|
}, [conversationId, surfaceError]);
|
|
4875
5052
|
react.useEffect(() => {
|
|
4876
|
-
const controller = new AbortController();
|
|
4877
|
-
abortControllerRef.current = controller;
|
|
4878
5053
|
return () => {
|
|
4879
|
-
|
|
5054
|
+
abortControllerRef.current?.abort();
|
|
4880
5055
|
abortControllerRef.current = null;
|
|
4881
5056
|
};
|
|
4882
5057
|
}, []);
|
|
@@ -4917,9 +5092,11 @@ function useChat(options, deps) {
|
|
|
4917
5092
|
});
|
|
4918
5093
|
void persist(userMessage);
|
|
4919
5094
|
callbacksRef.current.onMessageSent?.(userMessage);
|
|
5095
|
+
let streamingPlaceholderId = null;
|
|
4920
5096
|
try {
|
|
4921
5097
|
if (streamEnabled) {
|
|
4922
5098
|
const placeholderId = chatIdGenerator.next("assistant");
|
|
5099
|
+
streamingPlaceholderId = placeholderId;
|
|
4923
5100
|
const placeholder = {
|
|
4924
5101
|
id: placeholderId,
|
|
4925
5102
|
sender: "assistant",
|
|
@@ -4938,7 +5115,8 @@ function useChat(options, deps) {
|
|
|
4938
5115
|
(m) => m.id === placeholderId ? { ...m, content: m.content + chunk } : m
|
|
4939
5116
|
)
|
|
4940
5117
|
);
|
|
4941
|
-
}
|
|
5118
|
+
},
|
|
5119
|
+
controller.signal
|
|
4942
5120
|
);
|
|
4943
5121
|
const finalMessage = {
|
|
4944
5122
|
...placeholder,
|
|
@@ -4955,13 +5133,18 @@ function useChat(options, deps) {
|
|
|
4955
5133
|
const aiResponse = await chatService.generateAIResponse(
|
|
4956
5134
|
conversationId,
|
|
4957
5135
|
trimmed,
|
|
4958
|
-
contextForAI
|
|
5136
|
+
contextForAI,
|
|
5137
|
+
controller.signal
|
|
4959
5138
|
);
|
|
4960
5139
|
setMessages((prev) => [...prev, aiResponse]);
|
|
4961
5140
|
void persist(aiResponse);
|
|
4962
5141
|
callbacksRef.current.onAIResponse?.(aiResponse);
|
|
4963
5142
|
return aiResponse;
|
|
4964
5143
|
} catch (err) {
|
|
5144
|
+
if (streamingPlaceholderId) {
|
|
5145
|
+
const failedId = streamingPlaceholderId;
|
|
5146
|
+
setMessages((prev) => prev.filter((m) => m.id !== failedId));
|
|
5147
|
+
}
|
|
4965
5148
|
surfaceError(err);
|
|
4966
5149
|
return null;
|
|
4967
5150
|
} finally {
|
|
@@ -4987,6 +5170,9 @@ function useChat(options, deps) {
|
|
|
4987
5170
|
const lastUserMessage = [...currentMessages].reverse().find((m) => m.sender === "user");
|
|
4988
5171
|
if (!lastUserMessage) return null;
|
|
4989
5172
|
const trimmed = currentMessages.length > 0 && currentMessages[currentMessages.length - 1].sender === "assistant" ? currentMessages.slice(0, -1) : currentMessages;
|
|
5173
|
+
const controller = new AbortController();
|
|
5174
|
+
abortControllerRef.current?.abort();
|
|
5175
|
+
abortControllerRef.current = controller;
|
|
4990
5176
|
setIsLoading(true);
|
|
4991
5177
|
setError(null);
|
|
4992
5178
|
setIsTyping(true);
|
|
@@ -4994,7 +5180,8 @@ function useChat(options, deps) {
|
|
|
4994
5180
|
const aiResponse = await chatService.generateAIResponse(
|
|
4995
5181
|
conversationId,
|
|
4996
5182
|
lastUserMessage.content,
|
|
4997
|
-
trimmed
|
|
5183
|
+
trimmed,
|
|
5184
|
+
controller.signal
|
|
4998
5185
|
);
|
|
4999
5186
|
setMessages((prev) => {
|
|
5000
5187
|
const lastIsAI = prev.length > 0 && prev[prev.length - 1].sender === "assistant";
|
|
@@ -5010,6 +5197,9 @@ function useChat(options, deps) {
|
|
|
5010
5197
|
} finally {
|
|
5011
5198
|
setIsLoading(false);
|
|
5012
5199
|
setIsTyping(false);
|
|
5200
|
+
if (abortControllerRef.current === controller) {
|
|
5201
|
+
abortControllerRef.current = null;
|
|
5202
|
+
}
|
|
5013
5203
|
}
|
|
5014
5204
|
}, [chatService, conversationId, isLoading, messages, persist, surfaceError]);
|
|
5015
5205
|
const clearMessages = react.useCallback(() => {
|