@love-moon/app-sdk 0.4.2 → 0.5.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/CHANGELOG.md +11 -0
- package/dist/index.d.ts +12 -0
- package/dist/react/index.d.ts +66 -16
- package/dist/react/index.js +1069 -92
- package/dist/react/styles.css +351 -5
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +26 -0
- package/examples/02_bff/app/api/conductor/[...path]/route.ts +9 -0
- package/examples/02_bff/app/page.tsx +3 -0
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -113,6 +113,8 @@ function reducer(state, action) {
|
|
|
113
113
|
return { ...state, error: action.error };
|
|
114
114
|
case "TASK_FINISHED":
|
|
115
115
|
return { ...state, runtime: null };
|
|
116
|
+
case "RESET_TASK":
|
|
117
|
+
return { ...INITIAL_STATE, connectionState: state.connectionState, loadingHistory: true };
|
|
116
118
|
default:
|
|
117
119
|
return state;
|
|
118
120
|
}
|
|
@@ -197,7 +199,7 @@ function ChatProvider(props) {
|
|
|
197
199
|
void runCatchUp();
|
|
198
200
|
}, delayMs);
|
|
199
201
|
};
|
|
200
|
-
dispatch({ type: "
|
|
202
|
+
dispatch({ type: "RESET_TASK" });
|
|
201
203
|
activeAdapter.fetchHistory(taskId, { signal: abort.signal }).then((page) => {
|
|
202
204
|
if (cancelled) return;
|
|
203
205
|
dispatch({
|
|
@@ -336,7 +338,28 @@ function ChatProvider(props) {
|
|
|
336
338
|
dispatch({ type: "LOADING_HISTORY", loading: false });
|
|
337
339
|
}
|
|
338
340
|
};
|
|
339
|
-
|
|
341
|
+
const restart = async (opts) => {
|
|
342
|
+
const fn = adapterRef.current.restart;
|
|
343
|
+
if (!fn) return;
|
|
344
|
+
try {
|
|
345
|
+
await fn(taskIdRef.current, opts);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
onErrorRef.current?.(err);
|
|
348
|
+
dispatch({ type: "SET_ERROR", error: extractError(err) });
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
state,
|
|
353
|
+
taskId,
|
|
354
|
+
adapter,
|
|
355
|
+
send,
|
|
356
|
+
interrupt,
|
|
357
|
+
loadEarlier,
|
|
358
|
+
restart,
|
|
359
|
+
// `adapter` is in the dep array, so this re-derives whenever the adapter
|
|
360
|
+
// reference changes (the next subscribe also picks up the change).
|
|
361
|
+
restartSupported: typeof adapter.restart === "function"
|
|
362
|
+
};
|
|
340
363
|
}, [state, taskId, adapter]);
|
|
341
364
|
return /* @__PURE__ */ jsx(ChatContext.Provider, { value, children: props.children });
|
|
342
365
|
}
|
|
@@ -360,128 +383,1049 @@ function extractError(err) {
|
|
|
360
383
|
}
|
|
361
384
|
|
|
362
385
|
// src/react/components/MessageList.tsx
|
|
363
|
-
import {
|
|
386
|
+
import {
|
|
387
|
+
useCallback,
|
|
388
|
+
useEffect as useEffect4,
|
|
389
|
+
useLayoutEffect,
|
|
390
|
+
useMemo as useMemo2,
|
|
391
|
+
useRef as useRef4,
|
|
392
|
+
useState as useState2
|
|
393
|
+
} from "react";
|
|
394
|
+
|
|
395
|
+
// src/react/components/MessageBubble.tsx
|
|
396
|
+
import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
364
397
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
var TOUCH_DOUBLE_TAP_MS = 320;
|
|
399
|
+
var isInteractiveTarget = (target) => target instanceof HTMLElement && Boolean(target.closest("a, button, audio, video, summary, input, textarea, select"));
|
|
400
|
+
var prefersTapTimestamp = () => {
|
|
401
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
return window.matchMedia("(hover: none), (pointer: coarse)").matches;
|
|
405
|
+
};
|
|
406
|
+
var formatTime = (dateStr) => {
|
|
407
|
+
if (!dateStr) return "";
|
|
408
|
+
const date = new Date(dateStr);
|
|
409
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
410
|
+
return date.toLocaleString(void 0, {
|
|
411
|
+
month: "2-digit",
|
|
412
|
+
day: "2-digit",
|
|
413
|
+
hour: "2-digit",
|
|
414
|
+
minute: "2-digit"
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
var formatBytes = (value) => {
|
|
418
|
+
if (!Number.isFinite(value) || value < 1024) {
|
|
419
|
+
return `${Math.max(0, Math.round(value || 0))} B`;
|
|
420
|
+
}
|
|
421
|
+
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
|
|
422
|
+
if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
423
|
+
return `${(value / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
424
|
+
};
|
|
425
|
+
var attachmentKind = (att) => {
|
|
426
|
+
const mime = (att.mimeType || "").toLowerCase();
|
|
427
|
+
if (mime.startsWith("image/")) return "image";
|
|
428
|
+
if (mime.startsWith("video/")) return "video";
|
|
429
|
+
if (mime.startsWith("audio/")) return "audio";
|
|
430
|
+
return "file";
|
|
431
|
+
};
|
|
432
|
+
var getAppOriginName = (message) => {
|
|
433
|
+
const metadata = message.metadata;
|
|
434
|
+
if (!metadata || typeof metadata !== "object") return null;
|
|
435
|
+
const audit = metadata.audit;
|
|
436
|
+
if (!audit || typeof audit !== "object") return null;
|
|
437
|
+
const actor = audit.actor;
|
|
438
|
+
if (actor !== "app") return null;
|
|
439
|
+
const name = audit.appDisplayName;
|
|
440
|
+
return typeof name === "string" && name.trim() ? name.trim() : "app";
|
|
441
|
+
};
|
|
442
|
+
function MessageBubble({
|
|
443
|
+
message,
|
|
444
|
+
renderMessageContent,
|
|
445
|
+
onRequestMenu,
|
|
446
|
+
showAppOriginChip = false
|
|
447
|
+
}) {
|
|
448
|
+
const lastTouchEndAtRef = useRef2(0);
|
|
449
|
+
const [timestampVisible, setTimestampVisible] = useState(false);
|
|
450
|
+
useEffect2(() => {
|
|
451
|
+
setTimestampVisible(false);
|
|
452
|
+
}, [message.id]);
|
|
453
|
+
const content = renderMessageContent ? renderMessageContent(message) : message.content;
|
|
454
|
+
const attachments = message.attachments ?? [];
|
|
455
|
+
const appOriginName = showAppOriginChip ? getAppOriginName(message) : null;
|
|
456
|
+
const timeText = formatTime(message.createdAt);
|
|
457
|
+
return /* @__PURE__ */ jsxs(
|
|
458
|
+
"div",
|
|
459
|
+
{
|
|
460
|
+
className: "conductor-bubble-group" + (timestampVisible ? " conductor-bubble-group--timestamp-visible" : ""),
|
|
461
|
+
children: [
|
|
462
|
+
timeText ? /* @__PURE__ */ jsx2("span", { className: "conductor-bubble__time", children: timeText }) : null,
|
|
463
|
+
/* @__PURE__ */ jsxs(
|
|
464
|
+
"div",
|
|
465
|
+
{
|
|
466
|
+
className: "conductor-bubble",
|
|
467
|
+
role: "button",
|
|
468
|
+
tabIndex: 0,
|
|
469
|
+
onClick: (event) => {
|
|
470
|
+
if (isInteractiveTarget(event.target)) return;
|
|
471
|
+
if (prefersTapTimestamp()) {
|
|
472
|
+
setTimestampVisible((current) => !current);
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
onDoubleClick: (event) => {
|
|
476
|
+
if (isInteractiveTarget(event.target)) return;
|
|
477
|
+
onRequestMenu(message);
|
|
478
|
+
},
|
|
479
|
+
onTouchEnd: (event) => {
|
|
480
|
+
if (isInteractiveTarget(event.target)) {
|
|
481
|
+
lastTouchEndAtRef.current = 0;
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const now = Date.now();
|
|
485
|
+
if (now - lastTouchEndAtRef.current <= TOUCH_DOUBLE_TAP_MS) {
|
|
486
|
+
lastTouchEndAtRef.current = 0;
|
|
487
|
+
event.preventDefault();
|
|
488
|
+
onRequestMenu(message);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
lastTouchEndAtRef.current = now;
|
|
492
|
+
},
|
|
493
|
+
onKeyDown: (event) => {
|
|
494
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
495
|
+
event.preventDefault();
|
|
496
|
+
onRequestMenu(message);
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
children: [
|
|
500
|
+
appOriginName ? /* @__PURE__ */ jsxs("span", { className: "conductor-bubble__origin", children: [
|
|
501
|
+
"via ",
|
|
502
|
+
appOriginName
|
|
503
|
+
] }) : null,
|
|
504
|
+
content
|
|
505
|
+
]
|
|
506
|
+
}
|
|
507
|
+
),
|
|
508
|
+
attachments.length > 0 ? /* @__PURE__ */ jsx2("div", { className: "conductor-bubble__attachments", children: attachments.map((att) => {
|
|
509
|
+
const kind = attachmentKind(att);
|
|
510
|
+
return /* @__PURE__ */ jsxs("div", { className: "conductor-attachment", "data-kind": kind, children: [
|
|
511
|
+
kind === "image" ? /* @__PURE__ */ jsx2(
|
|
512
|
+
"a",
|
|
513
|
+
{
|
|
514
|
+
href: att.url,
|
|
515
|
+
target: "_blank",
|
|
516
|
+
rel: "noreferrer",
|
|
517
|
+
className: "conductor-attachment__media-link",
|
|
518
|
+
children: /* @__PURE__ */ jsx2("img", { src: att.url, alt: att.filename, className: "conductor-attachment__image" })
|
|
519
|
+
}
|
|
520
|
+
) : null,
|
|
521
|
+
kind === "video" ? /* @__PURE__ */ jsx2("video", { controls: true, preload: "metadata", className: "conductor-attachment__video", src: att.url }) : null,
|
|
522
|
+
kind === "audio" ? /* @__PURE__ */ jsx2("audio", { controls: true, className: "conductor-attachment__audio", src: att.url }) : null,
|
|
523
|
+
kind === "file" ? /* @__PURE__ */ jsxs("a", { href: att.url, target: "_blank", rel: "noreferrer", className: "conductor-attachment__file", children: [
|
|
524
|
+
/* @__PURE__ */ jsx2("span", { className: "conductor-attachment__name", children: att.filename }),
|
|
525
|
+
/* @__PURE__ */ jsx2("span", { className: "conductor-attachment__size", children: formatBytes(att.sizeBytes) })
|
|
526
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "conductor-attachment__meta", children: [
|
|
527
|
+
/* @__PURE__ */ jsx2("span", { className: "conductor-attachment__name", children: att.filename }),
|
|
528
|
+
/* @__PURE__ */ jsx2("span", { className: "conductor-attachment__size", children: formatBytes(att.sizeBytes) })
|
|
529
|
+
] })
|
|
530
|
+
] }, att.id);
|
|
531
|
+
}) }) : null
|
|
532
|
+
]
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/react/components/QuestionNav.tsx
|
|
538
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
539
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
540
|
+
function QuestionNav({
|
|
541
|
+
count,
|
|
542
|
+
activeIndex,
|
|
543
|
+
onJump,
|
|
544
|
+
visible = true,
|
|
545
|
+
label = "Jump to question"
|
|
546
|
+
}) {
|
|
547
|
+
const buttonRefs = useRef3(/* @__PURE__ */ new Map());
|
|
548
|
+
useEffect3(() => {
|
|
549
|
+
if (!visible) return;
|
|
550
|
+
const node = buttonRefs.current.get(activeIndex);
|
|
551
|
+
if (!node || typeof node.scrollIntoView !== "function") return;
|
|
552
|
+
node.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
553
|
+
}, [activeIndex, visible, count]);
|
|
554
|
+
if (count === 0) return null;
|
|
555
|
+
const visibilityClass = visible ? "conductor-question-nav--visible" : "conductor-question-nav--hidden";
|
|
556
|
+
return /* @__PURE__ */ jsx3(
|
|
557
|
+
"nav",
|
|
558
|
+
{
|
|
559
|
+
"aria-label": label,
|
|
560
|
+
"aria-hidden": !visible,
|
|
561
|
+
className: `conductor-question-nav ${visibilityClass}`,
|
|
562
|
+
children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsxs2("div", { className: "conductor-question-nav__group", children: [
|
|
563
|
+
i > 0 && /* @__PURE__ */ jsx3("span", { className: "conductor-question-nav__sep", "aria-hidden": "true" }),
|
|
564
|
+
/* @__PURE__ */ jsx3(
|
|
565
|
+
"button",
|
|
566
|
+
{
|
|
567
|
+
type: "button",
|
|
568
|
+
ref: (node) => {
|
|
569
|
+
if (node) {
|
|
570
|
+
buttonRefs.current.set(i, node);
|
|
571
|
+
} else {
|
|
572
|
+
buttonRefs.current.delete(i);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
tabIndex: visible ? 0 : -1,
|
|
576
|
+
"aria-label": `${label} ${i + 1}`,
|
|
577
|
+
title: `${i + 1}`,
|
|
578
|
+
onClick: () => onJump(i),
|
|
579
|
+
className: "conductor-question-nav__btn",
|
|
580
|
+
children: /* @__PURE__ */ jsx3(
|
|
581
|
+
"span",
|
|
582
|
+
{
|
|
583
|
+
className: "conductor-question-nav__dot" + (activeIndex === i ? " conductor-question-nav__dot--active" : "")
|
|
584
|
+
}
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
)
|
|
588
|
+
] }, i))
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/react/components/MessageList.tsx
|
|
594
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
595
|
+
var SCROLL_STORAGE_PREFIX = "conductor-sdk-task-scroll:";
|
|
596
|
+
var SCROLL_BOTTOM_THRESHOLD_PX = 40;
|
|
597
|
+
var SCROLL_TOP_LOAD_THRESHOLD_PX = 24;
|
|
598
|
+
var SCROLL_TO_BOTTOM_BUTTON_MIN_OVERFLOW_PX = 40;
|
|
599
|
+
var SCROLL_DIRECTION_THRESHOLD_PX = 4;
|
|
600
|
+
var QUESTION_ACTIVE_OFFSET_PX = 80;
|
|
601
|
+
var QUESTION_JUMP_TOP_PADDING_PX = 12;
|
|
602
|
+
var COPY_FEEDBACK_MS = 1500;
|
|
603
|
+
var SCROLL_PERSIST_DEBOUNCE_MS = 150;
|
|
604
|
+
var getScrollStorageKey = (taskId) => `${SCROLL_STORAGE_PREFIX}${taskId}`;
|
|
605
|
+
var getMaxScrollTop = (el) => Math.max(0, el.scrollHeight - el.clientHeight);
|
|
606
|
+
var clampScrollTop = (el, scrollTop) => Math.min(Math.max(scrollTop, 0), getMaxScrollTop(el));
|
|
607
|
+
var isNearBottom = (el) => getMaxScrollTop(el) - el.scrollTop <= SCROLL_BOTTOM_THRESHOLD_PX;
|
|
608
|
+
var isUserSide = (m) => m.role === "user" || m.role === "sdk";
|
|
609
|
+
var readStoredScrollState = (taskId) => {
|
|
610
|
+
if (typeof window === "undefined") return null;
|
|
611
|
+
try {
|
|
612
|
+
const raw = window.sessionStorage.getItem(getScrollStorageKey(taskId));
|
|
613
|
+
if (!raw) return null;
|
|
614
|
+
const parsed = JSON.parse(raw);
|
|
615
|
+
if (typeof parsed === "object" && parsed !== null && Number.isFinite(parsed.scrollTop) && typeof parsed.stickToBottom === "boolean") {
|
|
616
|
+
return { scrollTop: Math.max(0, parsed.scrollTop), stickToBottom: parsed.stickToBottom };
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
};
|
|
622
|
+
var writeStoredScrollState = (taskId, state) => {
|
|
623
|
+
if (typeof window === "undefined") return;
|
|
624
|
+
try {
|
|
625
|
+
window.sessionStorage.setItem(getScrollStorageKey(taskId), JSON.stringify(state));
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
async function copyText(text) {
|
|
630
|
+
try {
|
|
631
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
632
|
+
await navigator.clipboard.writeText(text);
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const textarea = document.createElement("textarea");
|
|
639
|
+
textarea.value = text;
|
|
640
|
+
textarea.setAttribute("readonly", "");
|
|
641
|
+
textarea.style.position = "fixed";
|
|
642
|
+
textarea.style.top = "-1000px";
|
|
643
|
+
textarea.style.left = "-1000px";
|
|
644
|
+
document.body.appendChild(textarea);
|
|
645
|
+
textarea.select();
|
|
646
|
+
const ok = typeof document.execCommand === "function" && document.execCommand("copy");
|
|
647
|
+
document.body.removeChild(textarea);
|
|
648
|
+
return Boolean(ok);
|
|
649
|
+
} catch {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function MessageList({
|
|
654
|
+
labels,
|
|
655
|
+
renderMessageContent,
|
|
656
|
+
showAppOriginChip,
|
|
657
|
+
readOnly = false
|
|
658
|
+
}) {
|
|
659
|
+
const { state, loadEarlier, send, interrupt, restart, restartSupported, taskId } = useChat();
|
|
660
|
+
const containerRef = useRef4(null);
|
|
661
|
+
const [showScrollToBottom, setShowScrollToBottom] = useState2(false);
|
|
662
|
+
const [showQuestionNav, setShowQuestionNav] = useState2(false);
|
|
663
|
+
const [activeQuestion, setActiveQuestion] = useState2(0);
|
|
664
|
+
const [menuMessage, setMenuMessage] = useState2(null);
|
|
665
|
+
const [copied, setCopied] = useState2(false);
|
|
666
|
+
const [restartPending, setRestartPending] = useState2(false);
|
|
667
|
+
const questionRefs = useRef4(/* @__PURE__ */ new Map());
|
|
668
|
+
const isJumpingRef = useRef4(false);
|
|
669
|
+
const lastScrollTopRef = useRef4(0);
|
|
670
|
+
const activeQuestionRafRef = useRef4(null);
|
|
671
|
+
const copyTimeoutRef = useRef4(null);
|
|
672
|
+
const scrollWriteTimerRef = useRef4(null);
|
|
673
|
+
const pendingScrollStateRef = useRef4(null);
|
|
674
|
+
const previousMessageCountRef = useRef4(state.messages.length);
|
|
675
|
+
const pendingRestoreScrollStateRef = useRef4(null);
|
|
676
|
+
const pendingPrependAnchorRef = useRef4(null);
|
|
677
|
+
const autoLoadUntilFilledRef = useRef4(false);
|
|
678
|
+
const shouldRestoreScrollRef = useRef4(true);
|
|
679
|
+
const shouldStickToBottomRef = useRef4(true);
|
|
680
|
+
const messages = state.messages;
|
|
681
|
+
const hasMoreBefore = state.hasMoreBefore;
|
|
682
|
+
const oldestMessageId = state.oldestMessageId;
|
|
683
|
+
const loadingHistory = state.loadingHistory;
|
|
684
|
+
const replyInProgress = state.runtime?.replyInProgress === true;
|
|
685
|
+
const canInterrupt = replyInProgress && Boolean(state.latestReplyId);
|
|
686
|
+
const questionIndexById = useMemo2(() => {
|
|
687
|
+
const map = /* @__PURE__ */ new Map();
|
|
688
|
+
let q = 0;
|
|
689
|
+
for (const m of messages) {
|
|
690
|
+
if (isUserSide(m)) {
|
|
691
|
+
map.set(m.id, q);
|
|
692
|
+
q += 1;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return map;
|
|
696
|
+
}, [messages]);
|
|
697
|
+
const userQuestionCount = questionIndexById.size;
|
|
698
|
+
const writeScrollNow = useCallback(
|
|
699
|
+
(override) => {
|
|
700
|
+
if (scrollWriteTimerRef.current !== null) {
|
|
701
|
+
window.clearTimeout(scrollWriteTimerRef.current);
|
|
702
|
+
scrollWriteTimerRef.current = null;
|
|
703
|
+
}
|
|
704
|
+
const el = containerRef.current;
|
|
705
|
+
const next = override ?? pendingScrollStateRef.current ?? (el ? { scrollTop: clampScrollTop(el, el.scrollTop), stickToBottom: isNearBottom(el) } : null);
|
|
706
|
+
pendingScrollStateRef.current = null;
|
|
707
|
+
if (next) writeStoredScrollState(taskId, next);
|
|
708
|
+
},
|
|
709
|
+
[taskId]
|
|
710
|
+
);
|
|
711
|
+
const persistScroll = useCallback(
|
|
712
|
+
(scrollTop) => {
|
|
713
|
+
const el = containerRef.current;
|
|
714
|
+
if (!el) return;
|
|
715
|
+
const next = typeof scrollTop === "number" ? clampScrollTop(el, scrollTop) : clampScrollTop(el, el.scrollTop);
|
|
716
|
+
const stick = isNearBottom(el);
|
|
717
|
+
const canScroll = getMaxScrollTop(el) > SCROLL_TO_BOTTOM_BUTTON_MIN_OVERFLOW_PX;
|
|
718
|
+
shouldStickToBottomRef.current = stick;
|
|
719
|
+
setShowScrollToBottom(canScroll && !stick);
|
|
720
|
+
pendingScrollStateRef.current = { scrollTop: next, stickToBottom: stick };
|
|
721
|
+
if (scrollWriteTimerRef.current === null) {
|
|
722
|
+
scrollWriteTimerRef.current = window.setTimeout(() => {
|
|
723
|
+
scrollWriteTimerRef.current = null;
|
|
724
|
+
if (pendingScrollStateRef.current) {
|
|
725
|
+
writeStoredScrollState(taskId, pendingScrollStateRef.current);
|
|
726
|
+
pendingScrollStateRef.current = null;
|
|
727
|
+
}
|
|
728
|
+
}, SCROLL_PERSIST_DEBOUNCE_MS);
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
[taskId]
|
|
732
|
+
);
|
|
733
|
+
const scrollToBottom = useCallback(() => {
|
|
370
734
|
const el = containerRef.current;
|
|
371
735
|
if (!el) return;
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
}, [
|
|
380
|
-
|
|
736
|
+
const next = getMaxScrollTop(el);
|
|
737
|
+
lastScrollTopRef.current = next;
|
|
738
|
+
el.scrollTop = next;
|
|
739
|
+
shouldStickToBottomRef.current = true;
|
|
740
|
+
setShowScrollToBottom(false);
|
|
741
|
+
setShowQuestionNav(false);
|
|
742
|
+
writeScrollNow({ scrollTop: next, stickToBottom: true });
|
|
743
|
+
}, [writeScrollNow]);
|
|
744
|
+
const handleJumpToQuestion = useCallback((questionIndex) => {
|
|
745
|
+
const node = questionRefs.current.get(questionIndex);
|
|
746
|
+
const el = containerRef.current;
|
|
747
|
+
if (!node || !el) return;
|
|
748
|
+
isJumpingRef.current = true;
|
|
749
|
+
const containerTop = el.getBoundingClientRect().top;
|
|
750
|
+
const elTop = node.getBoundingClientRect().top;
|
|
751
|
+
const next = clampScrollTop(
|
|
752
|
+
el,
|
|
753
|
+
el.scrollTop + (elTop - containerTop) - QUESTION_JUMP_TOP_PADDING_PX
|
|
754
|
+
);
|
|
755
|
+
lastScrollTopRef.current = next;
|
|
756
|
+
el.scrollTop = next;
|
|
757
|
+
setActiveQuestion(questionIndex);
|
|
758
|
+
window.setTimeout(() => {
|
|
759
|
+
isJumpingRef.current = false;
|
|
760
|
+
}, 120);
|
|
761
|
+
}, []);
|
|
762
|
+
const loadOlder = useCallback(
|
|
763
|
+
async (options) => {
|
|
764
|
+
if (!oldestMessageId || loadingHistory) return;
|
|
765
|
+
if (options?.continueUntilFilled) autoLoadUntilFilledRef.current = true;
|
|
766
|
+
const el = containerRef.current;
|
|
767
|
+
if (el) {
|
|
768
|
+
pendingPrependAnchorRef.current = {
|
|
769
|
+
previousScrollHeight: el.scrollHeight,
|
|
770
|
+
previousScrollTop: el.scrollTop
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
await loadEarlier();
|
|
774
|
+
},
|
|
775
|
+
[oldestMessageId, loadingHistory, loadEarlier]
|
|
776
|
+
);
|
|
777
|
+
const maybeContinueAutoLoad = useCallback(() => {
|
|
778
|
+
const el = containerRef.current;
|
|
779
|
+
if (!autoLoadUntilFilledRef.current || !el || loadingHistory) return;
|
|
780
|
+
if (!hasMoreBefore || !oldestMessageId) {
|
|
781
|
+
autoLoadUntilFilledRef.current = false;
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (el.scrollHeight > el.clientHeight + SCROLL_TOP_LOAD_THRESHOLD_PX) {
|
|
785
|
+
autoLoadUntilFilledRef.current = false;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
void loadOlder({ continueUntilFilled: true });
|
|
789
|
+
}, [loadingHistory, hasMoreBefore, oldestMessageId, loadOlder]);
|
|
790
|
+
const handleScroll = useCallback(() => {
|
|
791
|
+
persistScroll();
|
|
792
|
+
const el = containerRef.current;
|
|
793
|
+
if (!el) return;
|
|
794
|
+
const current = el.scrollTop;
|
|
795
|
+
const delta = current - lastScrollTopRef.current;
|
|
796
|
+
if (!isJumpingRef.current && userQuestionCount > 1) {
|
|
797
|
+
if (delta <= -SCROLL_DIRECTION_THRESHOLD_PX) {
|
|
798
|
+
setShowQuestionNav(true);
|
|
799
|
+
} else if (delta >= SCROLL_DIRECTION_THRESHOLD_PX) {
|
|
800
|
+
setShowQuestionNav(false);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (!isJumpingRef.current && questionRefs.current.size > 0 && activeQuestionRafRef.current === null) {
|
|
804
|
+
activeQuestionRafRef.current = window.requestAnimationFrame(() => {
|
|
805
|
+
activeQuestionRafRef.current = null;
|
|
806
|
+
if (isJumpingRef.current) return;
|
|
807
|
+
const c = containerRef.current;
|
|
808
|
+
if (!c) return;
|
|
809
|
+
const containerTop = c.getBoundingClientRect().top;
|
|
810
|
+
let closest = 0;
|
|
811
|
+
let closestDist = Infinity;
|
|
812
|
+
questionRefs.current.forEach((node, idx) => {
|
|
813
|
+
const dist = Math.abs(
|
|
814
|
+
node.getBoundingClientRect().top - containerTop - QUESTION_ACTIVE_OFFSET_PX
|
|
815
|
+
);
|
|
816
|
+
if (dist < closestDist) {
|
|
817
|
+
closestDist = dist;
|
|
818
|
+
closest = idx;
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
setActiveQuestion((cur) => cur === closest ? cur : closest);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
lastScrollTopRef.current = current;
|
|
825
|
+
if (!hasMoreBefore || loadingHistory || !oldestMessageId) {
|
|
826
|
+
if (!hasMoreBefore || !oldestMessageId) autoLoadUntilFilledRef.current = false;
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (current <= SCROLL_TOP_LOAD_THRESHOLD_PX) {
|
|
830
|
+
void loadOlder({ continueUntilFilled: true });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
autoLoadUntilFilledRef.current = false;
|
|
834
|
+
}, [persistScroll, userQuestionCount, hasMoreBefore, loadingHistory, oldestMessageId, loadOlder]);
|
|
835
|
+
useLayoutEffect(() => {
|
|
836
|
+
pendingRestoreScrollStateRef.current = readStoredScrollState(taskId);
|
|
837
|
+
pendingPrependAnchorRef.current = null;
|
|
838
|
+
autoLoadUntilFilledRef.current = false;
|
|
839
|
+
shouldRestoreScrollRef.current = true;
|
|
840
|
+
shouldStickToBottomRef.current = true;
|
|
841
|
+
previousMessageCountRef.current = messages.length;
|
|
842
|
+
questionRefs.current = /* @__PURE__ */ new Map();
|
|
843
|
+
isJumpingRef.current = false;
|
|
844
|
+
lastScrollTopRef.current = 0;
|
|
845
|
+
if (activeQuestionRafRef.current !== null) {
|
|
846
|
+
window.cancelAnimationFrame(activeQuestionRafRef.current);
|
|
847
|
+
activeQuestionRafRef.current = null;
|
|
848
|
+
}
|
|
849
|
+
setShowScrollToBottom(false);
|
|
850
|
+
setShowQuestionNav(false);
|
|
851
|
+
setActiveQuestion(0);
|
|
852
|
+
setMenuMessage(null);
|
|
853
|
+
setCopied(false);
|
|
854
|
+
}, [taskId]);
|
|
855
|
+
useLayoutEffect(() => {
|
|
381
856
|
const el = containerRef.current;
|
|
382
857
|
if (!el) return;
|
|
383
|
-
|
|
858
|
+
if (pendingPrependAnchorRef.current) {
|
|
859
|
+
const { previousScrollHeight, previousScrollTop } = pendingPrependAnchorRef.current;
|
|
860
|
+
const delta = el.scrollHeight - previousScrollHeight;
|
|
861
|
+
const next = clampScrollTop(el, previousScrollTop + delta);
|
|
862
|
+
lastScrollTopRef.current = next;
|
|
863
|
+
el.scrollTop = next;
|
|
864
|
+
persistScroll(next);
|
|
865
|
+
pendingPrependAnchorRef.current = null;
|
|
866
|
+
previousMessageCountRef.current = messages.length;
|
|
867
|
+
maybeContinueAutoLoad();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (shouldRestoreScrollRef.current) {
|
|
871
|
+
const belongsToTask = messages.length === 0 || messages[0].taskId === taskId;
|
|
872
|
+
if (!belongsToTask) return;
|
|
873
|
+
if (loadingHistory && messages.length === 0) return;
|
|
874
|
+
const stored = pendingRestoreScrollStateRef.current;
|
|
875
|
+
if (stored?.stickToBottom) {
|
|
876
|
+
scrollToBottom();
|
|
877
|
+
} else if (stored) {
|
|
878
|
+
const next = clampScrollTop(el, stored.scrollTop);
|
|
879
|
+
lastScrollTopRef.current = next;
|
|
880
|
+
el.scrollTop = next;
|
|
881
|
+
persistScroll(next);
|
|
882
|
+
} else {
|
|
883
|
+
scrollToBottom();
|
|
884
|
+
}
|
|
885
|
+
shouldRestoreScrollRef.current = false;
|
|
886
|
+
pendingRestoreScrollStateRef.current = null;
|
|
887
|
+
previousMessageCountRef.current = messages.length;
|
|
888
|
+
maybeContinueAutoLoad();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const prevCount = previousMessageCountRef.current;
|
|
892
|
+
if (messages.length > prevCount) {
|
|
893
|
+
const lastMsg = messages[messages.length - 1];
|
|
894
|
+
const localSend = lastMsg ? isUserSide(lastMsg) && lastMsg.id.startsWith("pending:") : false;
|
|
895
|
+
if (shouldStickToBottomRef.current || localSend) {
|
|
896
|
+
scrollToBottom();
|
|
897
|
+
} else {
|
|
898
|
+
persistScroll();
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
persistScroll();
|
|
902
|
+
}
|
|
903
|
+
previousMessageCountRef.current = messages.length;
|
|
904
|
+
maybeContinueAutoLoad();
|
|
905
|
+
}, [loadingHistory, messages.length, taskId, persistScroll, scrollToBottom, maybeContinueAutoLoad]);
|
|
906
|
+
useEffect4(
|
|
907
|
+
() => () => {
|
|
908
|
+
writeScrollNow();
|
|
909
|
+
},
|
|
910
|
+
[taskId, writeScrollNow]
|
|
911
|
+
);
|
|
912
|
+
useEffect4(
|
|
913
|
+
() => () => {
|
|
914
|
+
if (activeQuestionRafRef.current !== null) {
|
|
915
|
+
window.cancelAnimationFrame(activeQuestionRafRef.current);
|
|
916
|
+
activeQuestionRafRef.current = null;
|
|
917
|
+
}
|
|
918
|
+
if (copyTimeoutRef.current !== null) {
|
|
919
|
+
window.clearTimeout(copyTimeoutRef.current);
|
|
920
|
+
copyTimeoutRef.current = null;
|
|
921
|
+
}
|
|
922
|
+
if (scrollWriteTimerRef.current !== null) {
|
|
923
|
+
window.clearTimeout(scrollWriteTimerRef.current);
|
|
924
|
+
scrollWriteTimerRef.current = null;
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
[]
|
|
928
|
+
);
|
|
929
|
+
const closeMenu = useCallback(() => {
|
|
930
|
+
if (copyTimeoutRef.current !== null) {
|
|
931
|
+
window.clearTimeout(copyTimeoutRef.current);
|
|
932
|
+
copyTimeoutRef.current = null;
|
|
933
|
+
}
|
|
934
|
+
setCopied(false);
|
|
935
|
+
setMenuMessage(null);
|
|
936
|
+
}, []);
|
|
937
|
+
const openMenu = useCallback((message) => {
|
|
938
|
+
if (copyTimeoutRef.current !== null) {
|
|
939
|
+
window.clearTimeout(copyTimeoutRef.current);
|
|
940
|
+
copyTimeoutRef.current = null;
|
|
941
|
+
}
|
|
942
|
+
setCopied(false);
|
|
943
|
+
setMenuMessage(message);
|
|
384
944
|
}, []);
|
|
385
|
-
|
|
386
|
-
|
|
945
|
+
useEffect4(() => {
|
|
946
|
+
if (!menuMessage) return;
|
|
947
|
+
const onKeyDown = (event) => {
|
|
948
|
+
if (event.key === "Escape") closeMenu();
|
|
949
|
+
};
|
|
950
|
+
window.addEventListener("keydown", onKeyDown);
|
|
951
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
952
|
+
}, [menuMessage, closeMenu]);
|
|
953
|
+
const handleCopy = useCallback(async () => {
|
|
954
|
+
if (!menuMessage) return;
|
|
955
|
+
const ok = await copyText(menuMessage.content);
|
|
956
|
+
if (!ok) {
|
|
957
|
+
closeMenu();
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
setCopied(true);
|
|
961
|
+
copyTimeoutRef.current = window.setTimeout(() => {
|
|
962
|
+
closeMenu();
|
|
963
|
+
}, COPY_FEEDBACK_MS);
|
|
964
|
+
}, [menuMessage, closeMenu]);
|
|
965
|
+
const handleResend = useCallback(() => {
|
|
966
|
+
if (!menuMessage || !menuMessage.content.trim()) return;
|
|
967
|
+
void send(menuMessage.content);
|
|
968
|
+
closeMenu();
|
|
969
|
+
}, [menuMessage, send, closeMenu]);
|
|
970
|
+
const handleInterrupt = useCallback(() => {
|
|
971
|
+
void interrupt();
|
|
972
|
+
closeMenu();
|
|
973
|
+
}, [interrupt, closeMenu]);
|
|
974
|
+
const handleRestart = useCallback(async () => {
|
|
975
|
+
if (!restartSupported || restartPending) return;
|
|
976
|
+
setRestartPending(true);
|
|
977
|
+
closeMenu();
|
|
978
|
+
try {
|
|
979
|
+
await restart({ restartMode: "refresh_session" });
|
|
980
|
+
} finally {
|
|
981
|
+
setRestartPending(false);
|
|
982
|
+
}
|
|
983
|
+
}, [restartSupported, restartPending, restart, closeMenu]);
|
|
984
|
+
const menuIsUserSide = menuMessage ? isUserSide(menuMessage) : false;
|
|
985
|
+
const showEmptyState = messages.length === 0 && !loadingHistory;
|
|
986
|
+
return /* @__PURE__ */ jsxs3("div", { className: "conductor-message-list-viewport", children: [
|
|
987
|
+
/* @__PURE__ */ jsxs3(
|
|
988
|
+
"div",
|
|
989
|
+
{
|
|
990
|
+
ref: containerRef,
|
|
991
|
+
className: "conductor-message-list",
|
|
992
|
+
role: "log",
|
|
993
|
+
"aria-live": "polite",
|
|
994
|
+
onScroll: handleScroll,
|
|
995
|
+
children: [
|
|
996
|
+
hasMoreBefore && /* @__PURE__ */ jsx4("div", { className: "conductor-load-earlier", children: /* @__PURE__ */ jsx4(
|
|
997
|
+
"button",
|
|
998
|
+
{
|
|
999
|
+
type: "button",
|
|
1000
|
+
onClick: () => {
|
|
1001
|
+
void loadOlder({ continueUntilFilled: true });
|
|
1002
|
+
},
|
|
1003
|
+
disabled: loadingHistory,
|
|
1004
|
+
children: loadingHistory ? "\u2026" : labels.loadEarlier
|
|
1005
|
+
}
|
|
1006
|
+
) }),
|
|
1007
|
+
showEmptyState ? /* @__PURE__ */ jsxs3("div", { className: "conductor-empty", children: [
|
|
1008
|
+
/* @__PURE__ */ jsx4(
|
|
1009
|
+
"svg",
|
|
1010
|
+
{
|
|
1011
|
+
className: "conductor-empty__icon",
|
|
1012
|
+
fill: "none",
|
|
1013
|
+
stroke: "currentColor",
|
|
1014
|
+
strokeWidth: 1.5,
|
|
1015
|
+
viewBox: "0 0 24 24",
|
|
1016
|
+
"aria-hidden": "true",
|
|
1017
|
+
children: /* @__PURE__ */ jsx4(
|
|
1018
|
+
"path",
|
|
1019
|
+
{
|
|
1020
|
+
strokeLinecap: "round",
|
|
1021
|
+
strokeLinejoin: "round",
|
|
1022
|
+
d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
1023
|
+
}
|
|
1024
|
+
)
|
|
1025
|
+
}
|
|
1026
|
+
),
|
|
1027
|
+
/* @__PURE__ */ jsx4("p", { className: "conductor-empty__title", children: labels.emptyTitle }),
|
|
1028
|
+
/* @__PURE__ */ jsx4("p", { className: "conductor-empty__body", children: labels.emptyBody }),
|
|
1029
|
+
restartSupported && !readOnly ? /* @__PURE__ */ jsx4(
|
|
1030
|
+
"button",
|
|
1031
|
+
{
|
|
1032
|
+
type: "button",
|
|
1033
|
+
className: "conductor-button conductor-empty__restart",
|
|
1034
|
+
"data-testid": "conductor-empty-restart",
|
|
1035
|
+
disabled: restartPending,
|
|
1036
|
+
onClick: () => {
|
|
1037
|
+
void handleRestart();
|
|
1038
|
+
},
|
|
1039
|
+
children: restartPending ? labels.restartPending : labels.restart
|
|
1040
|
+
}
|
|
1041
|
+
) : null
|
|
1042
|
+
] }) : null,
|
|
1043
|
+
messages.map((m) => {
|
|
1044
|
+
const isUser = isUserSide(m);
|
|
1045
|
+
const isPending = m.id.startsWith("pending:");
|
|
1046
|
+
const qIdx = questionIndexById.get(m.id);
|
|
1047
|
+
return /* @__PURE__ */ jsx4(
|
|
1048
|
+
"div",
|
|
1049
|
+
{
|
|
1050
|
+
ref: (node) => {
|
|
1051
|
+
if (qIdx == null) return;
|
|
1052
|
+
if (node) {
|
|
1053
|
+
questionRefs.current.set(qIdx, node);
|
|
1054
|
+
} else {
|
|
1055
|
+
questionRefs.current.delete(qIdx);
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
className: "conductor-message " + (isUser ? "conductor-message--user" : "conductor-message--assistant") + (isPending ? " conductor-message--pending" : ""),
|
|
1059
|
+
"data-role": m.role,
|
|
1060
|
+
"data-message-id": m.id,
|
|
1061
|
+
children: /* @__PURE__ */ jsx4(
|
|
1062
|
+
MessageBubble,
|
|
1063
|
+
{
|
|
1064
|
+
message: m,
|
|
1065
|
+
renderMessageContent,
|
|
1066
|
+
onRequestMenu: openMenu,
|
|
1067
|
+
showAppOriginChip
|
|
1068
|
+
}
|
|
1069
|
+
)
|
|
1070
|
+
},
|
|
1071
|
+
m.id
|
|
1072
|
+
);
|
|
1073
|
+
})
|
|
1074
|
+
]
|
|
1075
|
+
}
|
|
1076
|
+
),
|
|
1077
|
+
/* @__PURE__ */ jsx4(
|
|
1078
|
+
QuestionNav,
|
|
1079
|
+
{
|
|
1080
|
+
count: userQuestionCount,
|
|
1081
|
+
activeIndex: activeQuestion,
|
|
1082
|
+
visible: showQuestionNav && userQuestionCount > 1,
|
|
1083
|
+
onJump: handleJumpToQuestion,
|
|
1084
|
+
label: labels.jumpToQuestion
|
|
1085
|
+
}
|
|
1086
|
+
),
|
|
1087
|
+
showScrollToBottom && /* @__PURE__ */ jsx4(
|
|
387
1088
|
"button",
|
|
388
1089
|
{
|
|
389
1090
|
type: "button",
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
children:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1091
|
+
className: "conductor-scroll-to-bottom",
|
|
1092
|
+
"aria-label": labels.scrollToBottom,
|
|
1093
|
+
"data-testid": "conductor-scroll-to-bottom",
|
|
1094
|
+
onClick: scrollToBottom,
|
|
1095
|
+
children: /* @__PURE__ */ jsxs3(
|
|
1096
|
+
"svg",
|
|
1097
|
+
{
|
|
1098
|
+
viewBox: "0 0 24 24",
|
|
1099
|
+
fill: "none",
|
|
1100
|
+
stroke: "currentColor",
|
|
1101
|
+
strokeWidth: 2,
|
|
1102
|
+
strokeLinecap: "round",
|
|
1103
|
+
strokeLinejoin: "round",
|
|
1104
|
+
"aria-hidden": "true",
|
|
1105
|
+
children: [
|
|
1106
|
+
/* @__PURE__ */ jsx4("path", { d: "M12 5v14" }),
|
|
1107
|
+
/* @__PURE__ */ jsx4("path", { d: "m19 12-7 7-7-7" })
|
|
1108
|
+
]
|
|
1109
|
+
}
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
),
|
|
1113
|
+
menuMessage && /* @__PURE__ */ jsxs3(
|
|
1114
|
+
"div",
|
|
1115
|
+
{
|
|
1116
|
+
className: "conductor-bubble-menu-overlay",
|
|
1117
|
+
onClick: closeMenu,
|
|
1118
|
+
"data-testid": "conductor-bubble-menu",
|
|
1119
|
+
children: [
|
|
1120
|
+
/* @__PURE__ */ jsx4("div", { className: "conductor-bubble-menu-backdrop" }),
|
|
1121
|
+
/* @__PURE__ */ jsxs3(
|
|
1122
|
+
"div",
|
|
1123
|
+
{
|
|
1124
|
+
className: "conductor-bubble-menu-sheet",
|
|
1125
|
+
role: "menu",
|
|
1126
|
+
onClick: (event) => event.stopPropagation(),
|
|
1127
|
+
children: [
|
|
1128
|
+
/* @__PURE__ */ jsx4("div", { className: "conductor-bubble-menu-handle", "aria-hidden": "true" }),
|
|
1129
|
+
/* @__PURE__ */ jsxs3("div", { className: "conductor-bubble-menu-actions", children: [
|
|
1130
|
+
/* @__PURE__ */ jsx4(
|
|
1131
|
+
"button",
|
|
1132
|
+
{
|
|
1133
|
+
type: "button",
|
|
1134
|
+
className: "conductor-bubble-menu-item",
|
|
1135
|
+
"data-testid": "conductor-bubble-menu-copy",
|
|
1136
|
+
onClick: () => {
|
|
1137
|
+
void handleCopy();
|
|
1138
|
+
},
|
|
1139
|
+
children: copied ? labels.copied : labels.copy
|
|
1140
|
+
}
|
|
1141
|
+
),
|
|
1142
|
+
menuIsUserSide && !readOnly && /* @__PURE__ */ jsx4(
|
|
1143
|
+
"button",
|
|
1144
|
+
{
|
|
1145
|
+
type: "button",
|
|
1146
|
+
className: "conductor-bubble-menu-item",
|
|
1147
|
+
"data-testid": "conductor-bubble-menu-resend",
|
|
1148
|
+
disabled: !menuMessage.content.trim(),
|
|
1149
|
+
onClick: handleResend,
|
|
1150
|
+
children: labels.resend
|
|
1151
|
+
}
|
|
1152
|
+
),
|
|
1153
|
+
canInterrupt && !readOnly && /* @__PURE__ */ jsx4(
|
|
1154
|
+
"button",
|
|
1155
|
+
{
|
|
1156
|
+
type: "button",
|
|
1157
|
+
className: "conductor-bubble-menu-item",
|
|
1158
|
+
"data-testid": "conductor-bubble-menu-interrupt",
|
|
1159
|
+
onClick: handleInterrupt,
|
|
1160
|
+
children: labels.interrupt
|
|
1161
|
+
}
|
|
1162
|
+
),
|
|
1163
|
+
restartSupported && !readOnly && /* @__PURE__ */ jsx4(
|
|
1164
|
+
"button",
|
|
1165
|
+
{
|
|
1166
|
+
type: "button",
|
|
1167
|
+
className: "conductor-bubble-menu-item",
|
|
1168
|
+
"data-testid": "conductor-bubble-menu-restart",
|
|
1169
|
+
disabled: restartPending,
|
|
1170
|
+
onClick: () => {
|
|
1171
|
+
void handleRestart();
|
|
1172
|
+
},
|
|
1173
|
+
children: restartPending ? labels.restartPending : labels.restart
|
|
1174
|
+
}
|
|
1175
|
+
)
|
|
1176
|
+
] })
|
|
1177
|
+
]
|
|
1178
|
+
}
|
|
1179
|
+
)
|
|
1180
|
+
]
|
|
1181
|
+
}
|
|
1182
|
+
)
|
|
413
1183
|
] });
|
|
414
1184
|
}
|
|
415
1185
|
|
|
416
1186
|
// src/react/components/MessageInput.tsx
|
|
417
|
-
import {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
1187
|
+
import {
|
|
1188
|
+
useCallback as useCallback2,
|
|
1189
|
+
useEffect as useEffect5,
|
|
1190
|
+
useLayoutEffect as useLayoutEffect2,
|
|
1191
|
+
useMemo as useMemo3,
|
|
1192
|
+
useRef as useRef5,
|
|
1193
|
+
useState as useState3
|
|
1194
|
+
} from "react";
|
|
1195
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1196
|
+
var DRAFT_STORAGE_PREFIX = "conductor-sdk-task-draft:";
|
|
1197
|
+
var MAX_HISTORY_ITEMS = 200;
|
|
1198
|
+
var INPUT_SCROLL_THRESHOLD_RATIO = 0.75;
|
|
1199
|
+
var INTERRUPT_PENDING_TIMEOUT_MS = 5e3;
|
|
1200
|
+
var getDraftStorageKey = (taskId) => `${DRAFT_STORAGE_PREFIX}${taskId}`;
|
|
1201
|
+
var deriveSentHistory = (messages) => {
|
|
1202
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1203
|
+
const reversed = [];
|
|
1204
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
1205
|
+
const message = messages[i];
|
|
1206
|
+
if (!message || message.role !== "user") continue;
|
|
1207
|
+
const trimmed = (message.content ?? "").trim();
|
|
1208
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
1209
|
+
seen.add(trimmed);
|
|
1210
|
+
reversed.push(trimmed);
|
|
1211
|
+
if (reversed.length >= MAX_HISTORY_ITEMS) break;
|
|
1212
|
+
}
|
|
1213
|
+
reversed.reverse();
|
|
1214
|
+
return reversed;
|
|
1215
|
+
};
|
|
1216
|
+
function MessageInput({ labels, disabled, autoFocus = false }) {
|
|
1217
|
+
const { state, send, interrupt, taskId } = useChat();
|
|
1218
|
+
const [value, setValue] = useState3("");
|
|
1219
|
+
const [interruptPending, setInterruptPending] = useState3(false);
|
|
1220
|
+
const taRef = useRef5(null);
|
|
1221
|
+
const isComposingRef = useRef5(false);
|
|
1222
|
+
const skipDraftSaveRef = useRef5(true);
|
|
1223
|
+
const historyCursorRef = useRef5(null);
|
|
1224
|
+
const historyDraftRef = useRef5("");
|
|
1225
|
+
const interruptTimeoutRef = useRef5(null);
|
|
1226
|
+
const sentHistory = useMemo3(() => deriveSentHistory(state.messages), [state.messages]);
|
|
1227
|
+
const replyInProgress = state.runtime?.replyInProgress === true;
|
|
1228
|
+
const canInterrupt = replyInProgress && Boolean(state.latestReplyId);
|
|
1229
|
+
useEffect5(() => {
|
|
1230
|
+
if (typeof window === "undefined") return;
|
|
1231
|
+
skipDraftSaveRef.current = true;
|
|
1232
|
+
historyCursorRef.current = null;
|
|
1233
|
+
historyDraftRef.current = "";
|
|
1234
|
+
try {
|
|
1235
|
+
setValue(window.sessionStorage.getItem(getDraftStorageKey(taskId)) ?? "");
|
|
1236
|
+
} catch {
|
|
1237
|
+
setValue("");
|
|
1238
|
+
}
|
|
1239
|
+
}, [taskId]);
|
|
1240
|
+
useEffect5(() => {
|
|
1241
|
+
if (typeof window === "undefined") return;
|
|
1242
|
+
if (skipDraftSaveRef.current) {
|
|
1243
|
+
skipDraftSaveRef.current = false;
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
const key = getDraftStorageKey(taskId);
|
|
1248
|
+
if (!value) window.sessionStorage.removeItem(key);
|
|
1249
|
+
else window.sessionStorage.setItem(key, value);
|
|
1250
|
+
} catch {
|
|
1251
|
+
}
|
|
1252
|
+
}, [value, taskId]);
|
|
424
1253
|
useLayoutEffect2(() => {
|
|
425
1254
|
const el = taRef.current;
|
|
426
1255
|
if (!el) return;
|
|
427
1256
|
el.style.height = "auto";
|
|
428
|
-
|
|
1257
|
+
const computed = window.getComputedStyle(el);
|
|
1258
|
+
const lineHeight = Number.parseFloat(computed.lineHeight) || 20;
|
|
1259
|
+
const maxHeight = Math.max(
|
|
1260
|
+
lineHeight * 2,
|
|
1261
|
+
Math.floor(window.innerHeight * INPUT_SCROLL_THRESHOLD_RATIO)
|
|
1262
|
+
);
|
|
1263
|
+
const shouldScroll = el.scrollHeight > maxHeight;
|
|
1264
|
+
el.style.height = `${shouldScroll ? maxHeight : el.scrollHeight}px`;
|
|
1265
|
+
el.style.overflowY = shouldScroll ? "auto" : "hidden";
|
|
429
1266
|
}, [value]);
|
|
430
|
-
|
|
1267
|
+
useEffect5(() => {
|
|
1268
|
+
if (!autoFocus || disabled) return;
|
|
1269
|
+
const el = taRef.current;
|
|
1270
|
+
if (!el) return;
|
|
1271
|
+
el.focus({ preventScroll: true });
|
|
1272
|
+
const end = el.value.length;
|
|
1273
|
+
el.setSelectionRange(end, end);
|
|
1274
|
+
}, [autoFocus, disabled, taskId]);
|
|
1275
|
+
useEffect5(() => {
|
|
1276
|
+
if (!replyInProgress && interruptPending) {
|
|
1277
|
+
setInterruptPending(false);
|
|
1278
|
+
if (interruptTimeoutRef.current !== null) {
|
|
1279
|
+
window.clearTimeout(interruptTimeoutRef.current);
|
|
1280
|
+
interruptTimeoutRef.current = null;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}, [replyInProgress, interruptPending]);
|
|
1284
|
+
useEffect5(
|
|
1285
|
+
() => () => {
|
|
1286
|
+
if (interruptTimeoutRef.current !== null) {
|
|
1287
|
+
window.clearTimeout(interruptTimeoutRef.current);
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
[]
|
|
1291
|
+
);
|
|
1292
|
+
const moveCaretToEnd = useCallback2((next) => {
|
|
1293
|
+
const el = taRef.current;
|
|
1294
|
+
if (!el) return;
|
|
1295
|
+
const pos = (next ?? el.value).length;
|
|
1296
|
+
requestAnimationFrame(() => {
|
|
1297
|
+
el.focus({ preventScroll: true });
|
|
1298
|
+
el.setSelectionRange(pos, pos);
|
|
1299
|
+
});
|
|
1300
|
+
}, []);
|
|
1301
|
+
const handleSend = useCallback2(() => {
|
|
431
1302
|
if (!value.trim() || disabled) return;
|
|
432
1303
|
void send(value);
|
|
1304
|
+
historyCursorRef.current = null;
|
|
1305
|
+
historyDraftRef.current = "";
|
|
433
1306
|
setValue("");
|
|
434
1307
|
}, [send, value, disabled]);
|
|
435
|
-
const
|
|
1308
|
+
const triggerInterrupt = useCallback2(() => {
|
|
1309
|
+
if (!canInterrupt || interruptPending || disabled) return;
|
|
1310
|
+
setInterruptPending(true);
|
|
1311
|
+
void interrupt();
|
|
1312
|
+
if (interruptTimeoutRef.current !== null) window.clearTimeout(interruptTimeoutRef.current);
|
|
1313
|
+
interruptTimeoutRef.current = window.setTimeout(() => {
|
|
1314
|
+
interruptTimeoutRef.current = null;
|
|
1315
|
+
setInterruptPending(false);
|
|
1316
|
+
}, INTERRUPT_PENDING_TIMEOUT_MS);
|
|
1317
|
+
}, [canInterrupt, interruptPending, interrupt, disabled]);
|
|
1318
|
+
const handleKeyDown = useCallback2(
|
|
436
1319
|
(e) => {
|
|
437
1320
|
const nativeComposing = e.nativeEvent.isComposing === true;
|
|
438
|
-
|
|
439
|
-
if (e.key === "
|
|
1321
|
+
const composing = isComposingRef.current || nativeComposing;
|
|
1322
|
+
if (e.key === "ArrowUp") {
|
|
1323
|
+
if (composing || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
1324
|
+
if (sentHistory.length === 0) return;
|
|
1325
|
+
e.preventDefault();
|
|
1326
|
+
let next;
|
|
1327
|
+
if (historyCursorRef.current === null) {
|
|
1328
|
+
historyDraftRef.current = value;
|
|
1329
|
+
next = sentHistory[sentHistory.length - 1];
|
|
1330
|
+
} else {
|
|
1331
|
+
const idx = sentHistory.lastIndexOf(historyCursorRef.current);
|
|
1332
|
+
if (idx === -1) next = sentHistory[sentHistory.length - 1];
|
|
1333
|
+
else if (idx > 0) next = sentHistory[idx - 1];
|
|
1334
|
+
else next = sentHistory[0];
|
|
1335
|
+
}
|
|
1336
|
+
historyCursorRef.current = next;
|
|
1337
|
+
setValue(next);
|
|
1338
|
+
moveCaretToEnd(next);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (e.key === "ArrowDown") {
|
|
1342
|
+
if (composing || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
1343
|
+
if (historyCursorRef.current === null) return;
|
|
1344
|
+
e.preventDefault();
|
|
1345
|
+
const idx = sentHistory.lastIndexOf(historyCursorRef.current);
|
|
1346
|
+
if (idx === -1 || idx >= sentHistory.length - 1) {
|
|
1347
|
+
historyCursorRef.current = null;
|
|
1348
|
+
setValue(historyDraftRef.current);
|
|
1349
|
+
moveCaretToEnd(historyDraftRef.current);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const next = sentHistory[idx + 1];
|
|
1353
|
+
historyCursorRef.current = next;
|
|
1354
|
+
setValue(next);
|
|
1355
|
+
moveCaretToEnd(next);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (e.key === "Escape") {
|
|
1359
|
+
if (composing || value.trim() || !canInterrupt || interruptPending) return;
|
|
440
1360
|
e.preventDefault();
|
|
441
|
-
|
|
1361
|
+
triggerInterrupt();
|
|
1362
|
+
return;
|
|
442
1363
|
}
|
|
1364
|
+
if (e.key !== "Enter" || composing) return;
|
|
1365
|
+
if (e.shiftKey) return;
|
|
1366
|
+
if (e.ctrlKey || e.metaKey) {
|
|
1367
|
+
e.preventDefault();
|
|
1368
|
+
const el = taRef.current;
|
|
1369
|
+
if (!el) return;
|
|
1370
|
+
const start = el.selectionStart;
|
|
1371
|
+
const end = el.selectionEnd;
|
|
1372
|
+
const next = value.slice(0, start) + "\n" + value.slice(end);
|
|
1373
|
+
setValue(next);
|
|
1374
|
+
window.setTimeout(() => {
|
|
1375
|
+
el.selectionStart = el.selectionEnd = start + 1;
|
|
1376
|
+
}, 0);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
e.preventDefault();
|
|
1380
|
+
handleSend();
|
|
443
1381
|
},
|
|
444
|
-
[
|
|
1382
|
+
[
|
|
1383
|
+
sentHistory,
|
|
1384
|
+
value,
|
|
1385
|
+
canInterrupt,
|
|
1386
|
+
interruptPending,
|
|
1387
|
+
triggerInterrupt,
|
|
1388
|
+
handleSend,
|
|
1389
|
+
moveCaretToEnd
|
|
1390
|
+
]
|
|
445
1391
|
);
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const handleCompositionEnd = useCallback(() => {
|
|
450
|
-
isComposingRef.current = false;
|
|
451
|
-
}, []);
|
|
452
|
-
const replyInProgress = state.runtime?.replyInProgress === true;
|
|
453
|
-
const canInterrupt = replyInProgress && Boolean(state.latestReplyId);
|
|
454
|
-
return /* @__PURE__ */ jsxs2("div", { className: "conductor-message-input", children: [
|
|
455
|
-
/* @__PURE__ */ jsx3(
|
|
1392
|
+
const canSend = Boolean(value.trim()) && !disabled;
|
|
1393
|
+
return /* @__PURE__ */ jsxs4("div", { className: "conductor-message-input", children: [
|
|
1394
|
+
/* @__PURE__ */ jsx5(
|
|
456
1395
|
"textarea",
|
|
457
1396
|
{
|
|
458
1397
|
ref: taRef,
|
|
459
1398
|
value,
|
|
460
1399
|
onChange: (e) => setValue(e.target.value),
|
|
461
1400
|
onKeyDown: handleKeyDown,
|
|
462
|
-
onCompositionStart:
|
|
463
|
-
|
|
1401
|
+
onCompositionStart: () => {
|
|
1402
|
+
isComposingRef.current = true;
|
|
1403
|
+
},
|
|
1404
|
+
onCompositionEnd: () => {
|
|
1405
|
+
isComposingRef.current = false;
|
|
1406
|
+
},
|
|
464
1407
|
placeholder: labels.inputPlaceholder,
|
|
465
1408
|
rows: 1,
|
|
466
1409
|
className: "conductor-message-input__textarea",
|
|
467
1410
|
disabled
|
|
468
1411
|
}
|
|
469
1412
|
),
|
|
470
|
-
/* @__PURE__ */
|
|
1413
|
+
/* @__PURE__ */ jsx5("div", { className: "conductor-message-input__actions", children: canInterrupt && !disabled ? /* @__PURE__ */ jsx5(
|
|
471
1414
|
"button",
|
|
472
1415
|
{
|
|
473
1416
|
type: "button",
|
|
474
1417
|
className: "conductor-button conductor-button--interrupt",
|
|
475
|
-
onClick:
|
|
476
|
-
|
|
1418
|
+
onClick: triggerInterrupt,
|
|
1419
|
+
disabled: interruptPending,
|
|
1420
|
+
children: interruptPending ? "\u2026" : labels.interrupt
|
|
477
1421
|
}
|
|
478
|
-
) : /* @__PURE__ */
|
|
1422
|
+
) : /* @__PURE__ */ jsx5(
|
|
479
1423
|
"button",
|
|
480
1424
|
{
|
|
481
1425
|
type: "button",
|
|
482
1426
|
className: "conductor-button conductor-button--send",
|
|
483
1427
|
onClick: handleSend,
|
|
484
|
-
disabled: !
|
|
1428
|
+
disabled: !canSend,
|
|
485
1429
|
children: labels.send
|
|
486
1430
|
}
|
|
487
1431
|
) })
|
|
@@ -489,28 +1433,34 @@ function MessageInput({ labels, disabled }) {
|
|
|
489
1433
|
}
|
|
490
1434
|
|
|
491
1435
|
// src/react/components/RuntimeStatusBar.tsx
|
|
492
|
-
import { jsx as
|
|
1436
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
493
1437
|
function RuntimeStatusBar({ labels }) {
|
|
494
1438
|
const { state } = useChat();
|
|
495
1439
|
const runtime = state.runtime;
|
|
496
1440
|
const isThinking = runtime?.replyInProgress === true || runtime?.state === "thinking";
|
|
497
|
-
const text = pickStatusText(
|
|
1441
|
+
const text = pickStatusText(
|
|
1442
|
+
runtime?.state,
|
|
1443
|
+
runtime?.statusLine,
|
|
1444
|
+
runtime?.statusDoneLine,
|
|
1445
|
+
labels
|
|
1446
|
+
);
|
|
498
1447
|
const showConnection = state.connectionState !== "connected" && state.connectionState !== "offline";
|
|
499
|
-
return /* @__PURE__ */
|
|
500
|
-
/* @__PURE__ */
|
|
1448
|
+
return /* @__PURE__ */ jsxs5("div", { className: "conductor-runtime-status", role: "status", "aria-live": "polite", children: [
|
|
1449
|
+
/* @__PURE__ */ jsx6(
|
|
501
1450
|
"span",
|
|
502
1451
|
{
|
|
503
1452
|
className: "conductor-runtime-indicator" + (isThinking ? " conductor-runtime-indicator--active" : ""),
|
|
504
1453
|
"aria-hidden": "true"
|
|
505
1454
|
}
|
|
506
1455
|
),
|
|
507
|
-
/* @__PURE__ */
|
|
508
|
-
state.error && /* @__PURE__ */
|
|
509
|
-
showConnection && /* @__PURE__ */
|
|
1456
|
+
/* @__PURE__ */ jsx6("span", { className: "conductor-runtime-text", children: text }),
|
|
1457
|
+
state.error && /* @__PURE__ */ jsx6("span", { className: "conductor-runtime-error", role: "alert", children: state.error.message }),
|
|
1458
|
+
showConnection && /* @__PURE__ */ jsx6("span", { className: "conductor-runtime-connection", children: state.connectionState === "reconnecting" ? "\u2026" : "" })
|
|
510
1459
|
] });
|
|
511
1460
|
}
|
|
512
|
-
function pickStatusText(state, statusLine, labels) {
|
|
513
|
-
if (statusLine) return statusLine;
|
|
1461
|
+
function pickStatusText(state, statusLine, statusDoneLine, labels) {
|
|
1462
|
+
if (statusLine && statusLine.trim()) return statusLine;
|
|
1463
|
+
if (statusDoneLine && statusDoneLine.trim()) return statusDoneLine;
|
|
514
1464
|
switch (state) {
|
|
515
1465
|
case "thinking":
|
|
516
1466
|
return labels.statusThinking;
|
|
@@ -526,7 +1476,7 @@ function pickStatusText(state, statusLine, labels) {
|
|
|
526
1476
|
}
|
|
527
1477
|
|
|
528
1478
|
// src/react/ChatView.tsx
|
|
529
|
-
import { jsx as
|
|
1479
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
530
1480
|
var DEFAULT_LABELS = {
|
|
531
1481
|
statusThinking: "Thinking\u2026",
|
|
532
1482
|
statusToolCall: "Calling tool\u2026",
|
|
@@ -536,29 +1486,39 @@ var DEFAULT_LABELS = {
|
|
|
536
1486
|
send: "Send",
|
|
537
1487
|
interrupt: "Stop",
|
|
538
1488
|
restart: "Restart",
|
|
539
|
-
loadEarlier: "Load earlier messages"
|
|
1489
|
+
loadEarlier: "Load earlier messages",
|
|
1490
|
+
copy: "Copy",
|
|
1491
|
+
copied: "Copied",
|
|
1492
|
+
resend: "Resend",
|
|
1493
|
+
scrollToBottom: "Scroll to latest message",
|
|
1494
|
+
jumpToQuestion: "Jump to question",
|
|
1495
|
+
emptyTitle: "No messages yet",
|
|
1496
|
+
emptyBody: "Send a message to get started \u2014 the conversation will appear here.",
|
|
1497
|
+
restartPending: "Restarting\u2026"
|
|
540
1498
|
};
|
|
541
1499
|
function ChatView(props) {
|
|
542
1500
|
const labels = { ...DEFAULT_LABELS, ...props.labels ?? {} };
|
|
543
1501
|
const themeStyle = props.theme ? toCssVariableStyle(props.theme) : void 0;
|
|
544
1502
|
const className = ["conductor-chat-view", props.className].filter(Boolean).join(" ");
|
|
545
|
-
return /* @__PURE__ */
|
|
1503
|
+
return /* @__PURE__ */ jsx7(
|
|
546
1504
|
"div",
|
|
547
1505
|
{
|
|
548
1506
|
className,
|
|
549
1507
|
"data-task-id": props.taskId,
|
|
550
1508
|
"data-layout": props.layout ?? "auto",
|
|
551
1509
|
style: themeStyle,
|
|
552
|
-
children: /* @__PURE__ */
|
|
553
|
-
/* @__PURE__ */
|
|
554
|
-
/* @__PURE__ */
|
|
1510
|
+
children: /* @__PURE__ */ jsxs6(ChatProvider, { taskId: props.taskId, adapter: props.adapter, onError: props.onError, children: [
|
|
1511
|
+
/* @__PURE__ */ jsx7(RuntimeStatusBar, { labels }),
|
|
1512
|
+
/* @__PURE__ */ jsx7(
|
|
555
1513
|
MessageList,
|
|
556
1514
|
{
|
|
557
1515
|
labels,
|
|
558
|
-
renderMessageContent: props.renderMessageContent
|
|
1516
|
+
renderMessageContent: props.renderMessageContent,
|
|
1517
|
+
showAppOriginChip: props.showAppOriginChip,
|
|
1518
|
+
readOnly: props.readOnly
|
|
559
1519
|
}
|
|
560
1520
|
),
|
|
561
|
-
/* @__PURE__ */
|
|
1521
|
+
/* @__PURE__ */ jsx7(MessageInput, { labels, disabled: props.readOnly, autoFocus: props.autoFocus })
|
|
562
1522
|
] })
|
|
563
1523
|
}
|
|
564
1524
|
);
|
|
@@ -711,7 +1671,7 @@ function createRestAdapter(options) {
|
|
|
711
1671
|
if (!text) return void 0;
|
|
712
1672
|
return JSON.parse(text);
|
|
713
1673
|
}
|
|
714
|
-
|
|
1674
|
+
const adapter = {
|
|
715
1675
|
async fetchHistory(taskId, opts) {
|
|
716
1676
|
if (!taskId) {
|
|
717
1677
|
throw new ConductorAppError({
|
|
@@ -824,6 +1784,23 @@ function createRestAdapter(options) {
|
|
|
824
1784
|
);
|
|
825
1785
|
}
|
|
826
1786
|
};
|
|
1787
|
+
if (options.enableRestart) {
|
|
1788
|
+
adapter.restart = async (taskId, opts) => {
|
|
1789
|
+
if (!taskId) {
|
|
1790
|
+
throw new ConductorAppError({
|
|
1791
|
+
code: "invalid_input",
|
|
1792
|
+
message: "restart requires a taskId"
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
const restartMode = opts?.restartMode?.trim();
|
|
1796
|
+
await jsonFetch(
|
|
1797
|
+
"POST",
|
|
1798
|
+
`/tasks/${encodeURIComponent(taskId)}/restart`,
|
|
1799
|
+
{ body: restartMode ? { restart_mode: restartMode } : {} }
|
|
1800
|
+
);
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
return adapter;
|
|
827
1804
|
}
|
|
828
1805
|
function buildUrl(base, path, query) {
|
|
829
1806
|
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|