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