simple-ai-sdk 1.0.14 → 1.0.15
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/client/manager.d.ts +1 -0
- package/dist/client/manager.d.ts.map +1 -1
- package/dist/client/manager.js +141 -117
- package/dist/client/types.d.ts +12 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/useChatSession.d.ts.map +1 -1
- package/dist/client/useChatSession.js +5 -1
- package/package.json +1 -1
package/dist/client/manager.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI7D,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,IAAI,CAAC;IACvD,WAAW,IAAI,kBAAkB,CAAC;IAClC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;IACvD,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAClE;;;;OAIG;IACH,kBAAkB,CAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,GAAG,SAAS,GACnD,IAAI,CAAC;IACR;;OAEG;IACH,oBAAoB,CAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,GACnE,IAAI,CAAC;IACR;;OAEG;IACH,iBAAiB,CACf,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,SAAS,GAC5C,IAAI,CAAC;IACR;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;IACtE,UAAU,CACR,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;KAClC,GACA,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;;OAGG;IACH,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,cAAM,aAAc,YAAW,cAAc;IAC3C,OAAO,CAAC,KAAK,CAEX;IAEF,OAAO,CAAC,SAAS,CAAoC;IAGrD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,gBAAgB,CAA8C;IAEtE,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,MAAM;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"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"}
|
package/dist/client/manager.js
CHANGED
|
@@ -23,6 +23,9 @@ class StreamManager {
|
|
|
23
23
|
? g.cancelAnimationFrame
|
|
24
24
|
: undefined;
|
|
25
25
|
}
|
|
26
|
+
// 1フレーム内での messages 配列クローンをセッション単位で一度に抑制するためのフラグ
|
|
27
|
+
// 意図: チャンクが高頻度で到着するケースで、配列コピーをフレームあたり1回に抑える
|
|
28
|
+
pendingCloneSessions = new Set();
|
|
26
29
|
maxSessions;
|
|
27
30
|
debug;
|
|
28
31
|
ttlMs;
|
|
@@ -119,6 +122,8 @@ class StreamManager {
|
|
|
119
122
|
}
|
|
120
123
|
this.rafScheduled = false;
|
|
121
124
|
this.listeners.forEach((listener) => listener());
|
|
125
|
+
// 即時通知時はクローン抑制フラグをクリア(次フレームに持ち越さない)
|
|
126
|
+
this.pendingCloneSessions.clear();
|
|
122
127
|
}
|
|
123
128
|
else {
|
|
124
129
|
// ストリーミング中の頻繁な更新は rAF で 1 フレームにまとめる
|
|
@@ -131,6 +136,8 @@ class StreamManager {
|
|
|
131
136
|
this.rafId = null;
|
|
132
137
|
this.rafScheduled = false;
|
|
133
138
|
this.listeners.forEach((listener) => listener());
|
|
139
|
+
// フレームの終わりでクローン抑制フラグをクリア
|
|
140
|
+
this.pendingCloneSessions.clear();
|
|
134
141
|
});
|
|
135
142
|
}
|
|
136
143
|
else {
|
|
@@ -139,6 +146,8 @@ class StreamManager {
|
|
|
139
146
|
this.rafFallbackTimer = null;
|
|
140
147
|
this.rafScheduled = false;
|
|
141
148
|
this.listeners.forEach((listener) => listener());
|
|
149
|
+
// Fallbackでもフレーム相当のタイミングでクリア
|
|
150
|
+
this.pendingCloneSessions.clear();
|
|
142
151
|
}, 16);
|
|
143
152
|
}
|
|
144
153
|
}
|
|
@@ -159,8 +168,18 @@ class StreamManager {
|
|
|
159
168
|
}
|
|
160
169
|
}
|
|
161
170
|
initSession(sessionId, initialMessages = []) {
|
|
162
|
-
|
|
171
|
+
const existingSession = this.state.sessions.get(sessionId);
|
|
172
|
+
// streaming中のセッションが存在する場合は、initialMessagesで上書きしない
|
|
173
|
+
if (existingSession) {
|
|
174
|
+
if (existingSession.status === "streaming" || existingSession.status === "submitted") {
|
|
175
|
+
if (this.debug) {
|
|
176
|
+
console.log(`[SM] Skip initialMessages for streaming session: ${sessionId}`);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// streaming中でなければ既存のセッションでもそのまま終了
|
|
163
181
|
return;
|
|
182
|
+
}
|
|
164
183
|
// Check if we need to remove old sessions
|
|
165
184
|
if (this.state.sessions.size >= this.maxSessions) {
|
|
166
185
|
this.removeOldestSession();
|
|
@@ -168,6 +187,7 @@ class StreamManager {
|
|
|
168
187
|
this.state.sessions.set(sessionId, {
|
|
169
188
|
id: sessionId,
|
|
170
189
|
messages: initialMessages,
|
|
190
|
+
messagesVersion: 0,
|
|
171
191
|
status: "ready",
|
|
172
192
|
error: null,
|
|
173
193
|
abortController: null,
|
|
@@ -275,7 +295,16 @@ class StreamManager {
|
|
|
275
295
|
if (session) {
|
|
276
296
|
// Update last accessed time
|
|
277
297
|
session.lastAccessed = Date.now();
|
|
298
|
+
// 修正意図(日本語):
|
|
299
|
+
// - ストリーミング最適化のため、messages 配列の参照は再生成しない設計。
|
|
300
|
+
// そのため、useMemo 等の依存に参照だけを使う場合に更新が伝播しないことがある。
|
|
301
|
+
// - messages が更新されるたびに messagesVersion をインクリメントし、
|
|
302
|
+
// UI で依存として使えるフックを提供するため、この段階で明示的にインクリメントする。
|
|
278
303
|
Object.assign(session, updates);
|
|
304
|
+
if (Object.prototype.hasOwnProperty.call(updates, "messages")) {
|
|
305
|
+
// messages が指定されたタイミングでのみバージョンを進める
|
|
306
|
+
session.messagesVersion = (session.messagesVersion ?? 0) + 1;
|
|
307
|
+
}
|
|
279
308
|
// 再レンダリング抑制: 購読者が0のセッションに対する
|
|
280
309
|
// ストリーミング由来の message 更新では通知しない。
|
|
281
310
|
// (onFinish/onError等のイベントは immediate=true で通知される)
|
|
@@ -366,6 +395,9 @@ class StreamManager {
|
|
|
366
395
|
}
|
|
367
396
|
const decoder = new TextDecoder();
|
|
368
397
|
let buffer = "";
|
|
398
|
+
// SSEのパース状態(イベントをチャンク間でまたげるようループ外に配置)
|
|
399
|
+
let eventName = null;
|
|
400
|
+
let dataLines = [];
|
|
369
401
|
const assistantMessage = {
|
|
370
402
|
id: generateId({ prefix: "msg-" }),
|
|
371
403
|
role: "assistant",
|
|
@@ -383,140 +415,131 @@ class StreamManager {
|
|
|
383
415
|
// - 複数行の data: をサポート
|
|
384
416
|
// - コメント行(先頭 ':')は無視
|
|
385
417
|
// - event: が無い場合はデフォルトの 'message' を想定するが、本実装ではサーバ仕様に合わせ 'chunk' 等のみ処理
|
|
418
|
+
// - 末尾が改行で終わらない場合でも、ループ終了時に最後のイベントをフラッシュする
|
|
386
419
|
let failed = false; // 意図: error イベント処理後に通常完了経路や catch を踏まないためのフラグ
|
|
387
420
|
let shouldTerminate = false; // 意図: done/error 受信時にループ終了を指示
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (parsed && typeof parsed.text === "string") {
|
|
415
|
-
// stale stream を無視(state 変更前に判定)
|
|
416
|
-
const latestId = this.streamIds.get(sessionId);
|
|
417
|
-
if (latestId !== streamId)
|
|
418
|
-
return; // 意図: 中断済みのストリームからの遅延イベントを破棄
|
|
419
|
-
// 大きな文字列連結は O(n^2) になり得るが、まずは単純連結。
|
|
420
|
-
// 必要なら閾値超過で配列バッファ化する最適化を検討。
|
|
421
|
-
assistantMessage.content += parsed.text;
|
|
422
|
-
// 購読者がいる場合のみ通知(いない場合は状態は更新されるが通知は抑制)
|
|
423
|
-
const subCount = this.activeSubscribers.get(sessionId) ?? 0;
|
|
424
|
-
if (subCount > 0) {
|
|
425
|
-
// 配列の再構築を避け、同一配列参照で通知
|
|
421
|
+
const flushEvent = () => {
|
|
422
|
+
if (dataLines.length === 0 && !eventName)
|
|
423
|
+
return;
|
|
424
|
+
const dataStr = dataLines.join("\n");
|
|
425
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
426
|
+
let parsed;
|
|
427
|
+
try {
|
|
428
|
+
parsed = dataStr ? JSON.parse(dataStr) : null;
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
console.warn("[SM] Failed to parse JSON data:", dataStr, err);
|
|
432
|
+
parsed = null;
|
|
433
|
+
}
|
|
434
|
+
switch (eventName) {
|
|
435
|
+
case "chunk": {
|
|
436
|
+
if (parsed && typeof parsed.text === "string") {
|
|
437
|
+
const latestId = this.streamIds.get(sessionId);
|
|
438
|
+
if (latestId !== streamId)
|
|
439
|
+
return; // 中断済みのストリームからの遅延イベントを破棄
|
|
440
|
+
// NOTE: 連結は O(n^2) 化を避ける最適化余地がある(必要ならバッファ化)
|
|
441
|
+
assistantMessage.content += parsed.text;
|
|
442
|
+
const subCount = this.activeSubscribers.get(sessionId) ?? 0;
|
|
443
|
+
if (subCount > 0) {
|
|
444
|
+
// フレームあたり1回だけ配列参照を更新
|
|
445
|
+
if (!this.pendingCloneSessions.has(sessionId)) {
|
|
446
|
+
this.pendingCloneSessions.add(sessionId);
|
|
426
447
|
this.updateSession(sessionId, {
|
|
427
|
-
messages: updatedMessages,
|
|
428
|
-
}); //
|
|
448
|
+
messages: [...updatedMessages],
|
|
449
|
+
}); // rAFバッチで通知
|
|
429
450
|
}
|
|
430
|
-
else
|
|
431
|
-
|
|
451
|
+
else {
|
|
452
|
+
// 2回目以降は配列複製せず、rAFでの通知に任せる
|
|
453
|
+
// ここでは何もしない(不要なノーティファイを避ける)
|
|
432
454
|
}
|
|
433
455
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
case "fullContent": {
|
|
437
|
-
if (parsed &&
|
|
438
|
-
parsed.type === "replace" &&
|
|
439
|
-
typeof parsed.content === "string") {
|
|
440
|
-
// stale stream を無視(state 変更前に判定)
|
|
441
|
-
const latestId = this.streamIds.get(sessionId);
|
|
442
|
-
if (latestId !== streamId)
|
|
443
|
-
return; // 意図: 中断済みのストリームからの遅延イベントを破棄
|
|
444
|
-
// Replace message content with transformed version
|
|
445
|
-
assistantMessage.content = parsed.content;
|
|
446
|
-
this.updateSession(sessionId, {
|
|
447
|
-
messages: updatedMessages,
|
|
448
|
-
}, true); // Immediate update for full content
|
|
456
|
+
else if (this.debug) {
|
|
457
|
+
console.info(`[SM] chunk suppressed (no subscribers): ${sessionId}`);
|
|
449
458
|
}
|
|
450
|
-
break;
|
|
451
459
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
this.latestOnMetadata.get(sessionId)?.(parsed);
|
|
457
|
-
}
|
|
458
|
-
catch {
|
|
459
|
-
console.error("[SM] Failed to call onMetadata handler");
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
break;
|
|
463
|
-
}
|
|
464
|
-
case "error": {
|
|
465
|
-
const err = new Error(parsed?.message || "Stream error");
|
|
466
|
-
// ストリームを明示的に中断
|
|
467
|
-
try {
|
|
468
|
-
abortController.abort();
|
|
469
|
-
}
|
|
470
|
-
catch {
|
|
471
|
-
console.error("[SM] Failed to abort stream on error");
|
|
472
|
-
}
|
|
473
|
-
// stale stream を無視
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case "fullContent": {
|
|
463
|
+
if (parsed && parsed.type === "replace" && typeof parsed.content === "string") {
|
|
474
464
|
const latestId = this.streamIds.get(sessionId);
|
|
475
465
|
if (latestId !== streamId)
|
|
476
|
-
return; //
|
|
466
|
+
return; // 中断済みのストリームからの遅延イベントを破棄
|
|
467
|
+
assistantMessage.content = parsed.content;
|
|
477
468
|
this.updateSession(sessionId, {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
abortController: null,
|
|
481
|
-
}, true);
|
|
482
|
-
// セッションに登録された最新の onError を使用
|
|
483
|
-
try {
|
|
484
|
-
this.latestOnError.get(sessionId)?.(err);
|
|
485
|
-
}
|
|
486
|
-
catch {
|
|
487
|
-
console.error("[SM] Failed to call onError handler");
|
|
488
|
-
}
|
|
489
|
-
// 意図: catch に落とさず通常完了経路も避けるため、フラグを立ててループ終了
|
|
490
|
-
failed = true;
|
|
491
|
-
shouldTerminate = true;
|
|
492
|
-
break;
|
|
469
|
+
messages: [...updatedMessages],
|
|
470
|
+
}, true); // 即時更新
|
|
493
471
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return; // 意図: 中断済みのストリームからの遅延イベントを破棄
|
|
499
|
-
this.updateSession(sessionId, {
|
|
500
|
-
status: "ready",
|
|
501
|
-
abortController: null,
|
|
502
|
-
}, true);
|
|
503
|
-
// セッションに登録された最新の onFinish のみ呼ぶ
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "metadata": {
|
|
475
|
+
if (parsed) {
|
|
504
476
|
try {
|
|
505
|
-
this.
|
|
506
|
-
[]));
|
|
477
|
+
this.latestOnMetadata.get(sessionId)?.(parsed);
|
|
507
478
|
}
|
|
508
479
|
catch {
|
|
509
|
-
console.error("[SM] Failed to call
|
|
480
|
+
console.error("[SM] Failed to call onMetadata handler");
|
|
510
481
|
}
|
|
511
|
-
// 意図: 正常終了。外側の catch へは行かず処理を終了するためループ終了を指示
|
|
512
|
-
shouldTerminate = true;
|
|
513
|
-
break;
|
|
514
482
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case "error": {
|
|
486
|
+
const err = new Error(parsed?.message || "Stream error");
|
|
487
|
+
try {
|
|
488
|
+
abortController.abort();
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
console.error("[SM] Failed to abort stream on error");
|
|
492
|
+
}
|
|
493
|
+
const latestId = this.streamIds.get(sessionId);
|
|
494
|
+
if (latestId !== streamId)
|
|
495
|
+
return; // 中断済みのストリームからの遅延イベントを破棄
|
|
496
|
+
this.updateSession(sessionId, {
|
|
497
|
+
status: "error",
|
|
498
|
+
error: err,
|
|
499
|
+
abortController: null,
|
|
500
|
+
}, true);
|
|
501
|
+
try {
|
|
502
|
+
this.latestOnError.get(sessionId)?.(err);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
console.error("[SM] Failed to call onError handler");
|
|
506
|
+
}
|
|
507
|
+
failed = true;
|
|
508
|
+
shouldTerminate = true;
|
|
509
|
+
break;
|
|
518
510
|
}
|
|
519
|
-
|
|
511
|
+
case "done": {
|
|
512
|
+
const latestId = this.streamIds.get(sessionId);
|
|
513
|
+
if (latestId !== streamId)
|
|
514
|
+
return; // 中断済みのストリームからの遅延イベントを破棄
|
|
515
|
+
this.updateSession(sessionId, {
|
|
516
|
+
status: "ready",
|
|
517
|
+
abortController: null,
|
|
518
|
+
}, true);
|
|
519
|
+
try {
|
|
520
|
+
this.latestOnFinish.get(sessionId)?.((this.state.sessions.get(sessionId)?.messages || []));
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
console.error("[SM] Failed to call onFinish handler");
|
|
524
|
+
}
|
|
525
|
+
shouldTerminate = true;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
default:
|
|
529
|
+
// 未知イベントは無視
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
while (true) {
|
|
534
|
+
const { done, value } = await reader.read();
|
|
535
|
+
if (done) {
|
|
536
|
+
// ストリーム終了時に、末尾の未フラッシュイベントがあれば処理
|
|
537
|
+
flushEvent();
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
buffer += decoder.decode(value, { stream: true });
|
|
541
|
+
const lines = buffer.split("\n");
|
|
542
|
+
buffer = lines.pop() || ""; // 末尾の未完成行をバッファに残す
|
|
520
543
|
for (const line of lines) {
|
|
521
544
|
if (line === "") {
|
|
522
545
|
// 空行で一つのイベントをフラッシュ
|
|
@@ -681,6 +704,7 @@ class StreamManager {
|
|
|
681
704
|
this.latestOnMetadata.clear();
|
|
682
705
|
this.latestOnError.clear();
|
|
683
706
|
this.streamIds.clear();
|
|
707
|
+
this.pendingCloneSessions.clear();
|
|
684
708
|
}
|
|
685
709
|
}
|
|
686
710
|
// Export the StreamManager class for custom initialization
|
package/dist/client/types.d.ts
CHANGED
|
@@ -3,6 +3,13 @@ export type ChatStatus = "ready" | "submitted" | "streaming" | "error" | "aborte
|
|
|
3
3
|
export type ChatSession = {
|
|
4
4
|
id: string;
|
|
5
5
|
messages: Message[];
|
|
6
|
+
/**
|
|
7
|
+
* messages の内容が更新されるたびにインクリメントされるバージョン番号。
|
|
8
|
+
* - ストリーミング中は配列参照を再生成しない最適化を行っているため、
|
|
9
|
+
* useMemo 等で参照の変化だけに依存すると再計算されないことがある。
|
|
10
|
+
* - その場合はこの値を依存に含めることで、内容の更新を検知できる。
|
|
11
|
+
*/
|
|
12
|
+
messagesVersion: number;
|
|
6
13
|
status: ChatStatus;
|
|
7
14
|
error: Error | null;
|
|
8
15
|
abortController: AbortController | null;
|
|
@@ -42,6 +49,11 @@ export type ReloadMessage = (params?: {
|
|
|
42
49
|
}) => Promise<void>;
|
|
43
50
|
export type UseChatSessionReturn = {
|
|
44
51
|
messages: Message[];
|
|
52
|
+
/**
|
|
53
|
+
* messages の内容更新を検知するためのバージョン番号。
|
|
54
|
+
* useMemo の依存などに利用できる。
|
|
55
|
+
*/
|
|
56
|
+
messagesVersion: number;
|
|
45
57
|
status: ChatStatus;
|
|
46
58
|
error: Error | null;
|
|
47
59
|
sendMessage: (content: string, params?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE7D,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,WAAW,GACX,WAAW,GACX,OAAO,GACP,SAAS,CAAC;AAEd,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAChC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAC7D,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;AAE1B,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAEzD,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAEjE,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAE3D,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,EAAE;IACpC,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpB,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,CACX,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,kBAAkB,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE7D,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,WAAW,GACX,WAAW,GACX,OAAO,GACP,SAAS,CAAC;AAEd,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAChC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAC7D,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;AAE1B,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAEzD,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAEjE,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAE3D,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,EAAE;IACpC,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpB,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,CACX,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,kBAAkB,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useChatSession.d.ts","sourceRoot":"","sources":["../../src/client/useChatSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,
|
|
1
|
+
{"version":3,"file":"useChatSession.d.ts","sourceRoot":"","sources":["../../src/client/useChatSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAsB,MAAM,YAAY,CAAC;AAKlG,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CA+ItB"}
|
|
@@ -35,7 +35,9 @@ export function useChatSession(sessionId, options = {}) {
|
|
|
35
35
|
unsubscribe();
|
|
36
36
|
// アンマウント時にハンドラは消さない(完了時に呼ぶため残す)
|
|
37
37
|
};
|
|
38
|
-
}, [manager, sessionId]), useCallback(() => manager.getSnapshot(), [manager])
|
|
38
|
+
}, [manager, sessionId]), useCallback(() => manager.getSnapshot(), [manager]),
|
|
39
|
+
// SSR時は安定した空スナップショットを返し、Hydration差分を避ける
|
|
40
|
+
useCallback(() => ({ sessions: new Map() }), []));
|
|
39
41
|
// ハンドラが変わるたびに最新を登録
|
|
40
42
|
useEffect(() => {
|
|
41
43
|
// onFinish が undefined の場合は登録解除される
|
|
@@ -52,6 +54,7 @@ export function useChatSession(sessionId, options = {}) {
|
|
|
52
54
|
const session = state.sessions.get(sessionId) || {
|
|
53
55
|
id: sessionId,
|
|
54
56
|
messages: initialMessages,
|
|
57
|
+
messagesVersion: 0,
|
|
55
58
|
status: "ready",
|
|
56
59
|
error: null,
|
|
57
60
|
abortController: null,
|
|
@@ -94,6 +97,7 @@ export function useChatSession(sessionId, options = {}) {
|
|
|
94
97
|
}, [sessionId, manager]);
|
|
95
98
|
return {
|
|
96
99
|
messages: session.messages,
|
|
100
|
+
messagesVersion: session.messagesVersion ?? 0,
|
|
97
101
|
status: session.status,
|
|
98
102
|
error: session.error,
|
|
99
103
|
sendMessage,
|