@runtypelabs/persona 1.47.0 → 2.0.0
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/README.md +140 -8
- package/dist/index.cjs +90 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1093 -25
- package/dist/index.d.ts +1093 -25
- package/dist/index.global.js +111 -60
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +90 -39
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/widget.css +852 -505
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +80 -0
- package/src/client.test.ts +20 -21
- package/src/client.ts +153 -4
- package/src/components/approval-bubble.ts +45 -42
- package/src/components/artifact-card.ts +91 -0
- package/src/components/artifact-pane.ts +501 -0
- package/src/components/composer-builder.ts +32 -27
- package/src/components/event-stream-view.ts +40 -40
- package/src/components/feedback.ts +36 -36
- package/src/components/forms.ts +11 -11
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +55 -36
- package/src/components/header-layouts.ts +58 -125
- package/src/components/launcher.ts +36 -21
- package/src/components/message-bubble.ts +92 -65
- package/src/components/messages.ts +2 -2
- package/src/components/panel.ts +42 -11
- package/src/components/reasoning-bubble.ts +23 -23
- package/src/components/registry.ts +4 -0
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +32 -32
- package/src/defaults.ts +30 -4
- package/src/index.ts +80 -2
- package/src/install.ts +22 -0
- package/src/plugins/types.ts +23 -0
- package/src/postprocessors.ts +2 -2
- package/src/runtime/host-layout.ts +174 -0
- package/src/runtime/init.test.ts +236 -0
- package/src/runtime/init.ts +114 -55
- package/src/session.ts +173 -7
- package/src/styles/tailwind.css +1 -1
- package/src/styles/widget.css +852 -505
- package/src/types/theme.ts +354 -0
- package/src/types.ts +348 -16
- package/src/ui.docked.test.ts +104 -0
- package/src/ui.ts +1093 -244
- package/src/utils/artifact-gate.test.ts +255 -0
- package/src/utils/artifact-gate.ts +142 -0
- package/src/utils/artifact-resize.test.ts +64 -0
- package/src/utils/artifact-resize.ts +67 -0
- package/src/utils/attachment-manager.ts +10 -10
- package/src/utils/code-generators.test.ts +52 -0
- package/src/utils/code-generators.ts +40 -36
- package/src/utils/dock.ts +17 -0
- package/src/utils/dom-context.test.ts +504 -0
- package/src/utils/dom-context.ts +896 -0
- package/src/utils/dom.ts +12 -1
- package/src/utils/message-fingerprint.test.ts +187 -0
- package/src/utils/message-fingerprint.ts +105 -0
- package/src/utils/migration.ts +179 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/plugins.ts +175 -0
- package/src/utils/positioning.ts +4 -4
- package/src/utils/theme.test.ts +125 -0
- package/src/utils/theme.ts +216 -60
- package/src/utils/tokens.ts +682 -0
- package/src/voice/audio-playback-manager.ts +187 -0
- package/src/voice/runtype-voice-provider.ts +305 -69
- package/src/voice/voice-activity-detector.ts +90 -0
- package/src/voice/voice.test.ts +6 -5
package/src/ui.ts
CHANGED
|
@@ -20,15 +20,19 @@ import {
|
|
|
20
20
|
InjectSystemMessageOptions,
|
|
21
21
|
LoadingIndicatorRenderContext,
|
|
22
22
|
IdleIndicatorRenderContext,
|
|
23
|
-
VoiceStatus
|
|
23
|
+
VoiceStatus,
|
|
24
|
+
PersonaArtifactRecord,
|
|
25
|
+
PersonaArtifactManualUpsert
|
|
24
26
|
} from "./types";
|
|
25
27
|
import { AttachmentManager } from "./utils/attachment-manager";
|
|
26
28
|
import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
|
|
27
29
|
import { applyThemeVariables, createThemeObserver } from "./utils/theme";
|
|
28
30
|
import { renderLucideIcon } from "./utils/icons";
|
|
29
|
-
import { createElement } from "./utils/dom";
|
|
31
|
+
import { createElement, createElementInDocument } from "./utils/dom";
|
|
30
32
|
import { morphMessages } from "./utils/morph";
|
|
33
|
+
import { computeMessageFingerprint, createMessageCache, getCachedWrapper, setCachedWrapper, pruneCache } from "./utils/message-fingerprint";
|
|
31
34
|
import { statusCopy } from "./utils/constants";
|
|
35
|
+
import { isDockedMountMode } from "./utils/dock";
|
|
32
36
|
import { createLauncherButton } from "./components/launcher";
|
|
33
37
|
import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
|
|
34
38
|
import { buildHeaderWithLayout } from "./components/header-layouts";
|
|
@@ -43,6 +47,14 @@ import { createSuggestions } from "./components/suggestions";
|
|
|
43
47
|
import { EventStreamBuffer } from "./utils/event-stream-buffer";
|
|
44
48
|
import { EventStreamStore } from "./utils/event-stream-store";
|
|
45
49
|
import { createEventStreamView } from "./components/event-stream-view";
|
|
50
|
+
import { createArtifactPane, type ArtifactPaneApi } from "./components/artifact-pane";
|
|
51
|
+
import {
|
|
52
|
+
artifactsSidebarEnabled,
|
|
53
|
+
applyArtifactLayoutCssVars,
|
|
54
|
+
applyArtifactPaneAppearance,
|
|
55
|
+
shouldExpandLauncherForArtifacts
|
|
56
|
+
} from "./utils/artifact-gate";
|
|
57
|
+
import { readFlexGapPx, resolveArtifactPaneWidthPx } from "./utils/artifact-resize";
|
|
46
58
|
import { enhanceWithForms } from "./components/forms";
|
|
47
59
|
import { pluginRegistry } from "./plugins/registry";
|
|
48
60
|
import { mergeWithDefaults } from "./defaults";
|
|
@@ -277,6 +289,14 @@ type Controller = {
|
|
|
277
289
|
hideEventStream: () => void;
|
|
278
290
|
/** Returns current visibility state of the event stream panel */
|
|
279
291
|
isEventStreamVisible: () => boolean;
|
|
292
|
+
/** Show artifact sidebar (no-op if features.artifacts.enabled is false) */
|
|
293
|
+
showArtifacts: () => void;
|
|
294
|
+
/** Hide artifact sidebar */
|
|
295
|
+
hideArtifacts: () => void;
|
|
296
|
+
/** Upsert an artifact programmatically */
|
|
297
|
+
upsertArtifact: (manual: PersonaArtifactManualUpsert) => PersonaArtifactRecord | null;
|
|
298
|
+
selectArtifact: (id: string) => void;
|
|
299
|
+
clearArtifacts: () => void;
|
|
280
300
|
/**
|
|
281
301
|
* Focus the chat input. Returns true if focus succeeded, false if panel is closed
|
|
282
302
|
* (launcher mode) or textarea is unavailable.
|
|
@@ -467,6 +487,7 @@ export const createAgentExperience = (
|
|
|
467
487
|
let prevAutoExpand = autoExpand;
|
|
468
488
|
let prevLauncherEnabled = launcherEnabled;
|
|
469
489
|
let prevHeaderLayout = config.layout?.header?.layout;
|
|
490
|
+
let wasMobileFullscreen = false;
|
|
470
491
|
let open = launcherEnabled ? autoExpand : true;
|
|
471
492
|
|
|
472
493
|
// Track pending resubmit state for injection-triggered resubmit
|
|
@@ -591,20 +612,11 @@ export const createAgentExperience = (
|
|
|
591
612
|
let attachmentInput: HTMLInputElement | null = panelElements.attachmentInput;
|
|
592
613
|
let attachmentPreviewsContainer: HTMLElement | null = panelElements.attachmentPreviewsContainer;
|
|
593
614
|
|
|
594
|
-
//
|
|
615
|
+
// Initialized after composer plugins rebind footer DOM (see `bindComposerRefsFromFooter`)
|
|
595
616
|
let attachmentManager: AttachmentManager | null = null;
|
|
596
|
-
if (config.attachments?.enabled && attachmentInput && attachmentPreviewsContainer) {
|
|
597
|
-
attachmentManager = AttachmentManager.fromConfig(config.attachments);
|
|
598
|
-
attachmentManager.setPreviewsContainer(attachmentPreviewsContainer);
|
|
599
617
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const target = e.target as HTMLInputElement;
|
|
603
|
-
attachmentManager?.handleFileSelect(target.files);
|
|
604
|
-
// Reset input so same file can be selected again
|
|
605
|
-
target.value = "";
|
|
606
|
-
});
|
|
607
|
-
}
|
|
618
|
+
/** Wired after `handleMicButtonClick` is defined; used by `renderComposer` `onVoiceToggle`. */
|
|
619
|
+
let composerVoiceBridge: (() => void) | null = null;
|
|
608
620
|
|
|
609
621
|
// Plugin hook: renderHeader - allow plugins to provide custom header
|
|
610
622
|
const headerPlugin = plugins.find(p => p.renderHeader);
|
|
@@ -620,7 +632,7 @@ export const createAgentExperience = (
|
|
|
620
632
|
});
|
|
621
633
|
if (customHeader) {
|
|
622
634
|
// Replace the default header with custom header
|
|
623
|
-
const existingHeader = container.querySelector('.
|
|
635
|
+
const existingHeader = container.querySelector('.persona-border-b-persona-divider');
|
|
624
636
|
if (existingHeader) {
|
|
625
637
|
existingHeader.replaceWith(customHeader);
|
|
626
638
|
header = customHeader;
|
|
@@ -647,9 +659,9 @@ export const createAgentExperience = (
|
|
|
647
659
|
eventStreamView.update();
|
|
648
660
|
}
|
|
649
661
|
if (eventStreamToggleBtn) {
|
|
650
|
-
eventStreamToggleBtn.classList.remove("
|
|
651
|
-
eventStreamToggleBtn.classList.add("
|
|
652
|
-
eventStreamToggleBtn.style.boxShadow = "inset 0 0 0 1.5px var(--
|
|
662
|
+
eventStreamToggleBtn.classList.remove("persona-text-persona-muted");
|
|
663
|
+
eventStreamToggleBtn.classList.add("persona-text-persona-accent");
|
|
664
|
+
eventStreamToggleBtn.style.boxShadow = "inset 0 0 0 1.5px var(--persona-accent, #3b82f6)";
|
|
653
665
|
const activeClasses = config.features?.eventStream?.classNames?.toggleButtonActive;
|
|
654
666
|
if (activeClasses) activeClasses.split(/\s+/).forEach(c => c && eventStreamToggleBtn!.classList.add(c));
|
|
655
667
|
}
|
|
@@ -676,8 +688,8 @@ export const createAgentExperience = (
|
|
|
676
688
|
}
|
|
677
689
|
body.style.display = "";
|
|
678
690
|
if (eventStreamToggleBtn) {
|
|
679
|
-
eventStreamToggleBtn.classList.remove("
|
|
680
|
-
eventStreamToggleBtn.classList.add("
|
|
691
|
+
eventStreamToggleBtn.classList.remove("persona-text-persona-accent");
|
|
692
|
+
eventStreamToggleBtn.classList.add("persona-text-persona-muted");
|
|
681
693
|
eventStreamToggleBtn.style.boxShadow = "";
|
|
682
694
|
const activeClasses = config.features?.eventStream?.classNames?.toggleButtonActive;
|
|
683
695
|
if (activeClasses) activeClasses.split(/\s+/).forEach(c => c && eventStreamToggleBtn!.classList.remove(c));
|
|
@@ -694,7 +706,7 @@ export const createAgentExperience = (
|
|
|
694
706
|
let eventStreamToggleBtn: HTMLButtonElement | null = null;
|
|
695
707
|
if (showEventStreamToggle) {
|
|
696
708
|
const esClassNames = config.features?.eventStream?.classNames;
|
|
697
|
-
const toggleBtnClasses = "
|
|
709
|
+
const toggleBtnClasses = "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-text-persona-muted hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none persona-bg-transparent persona-p-1" + (esClassNames?.toggleButton ? " " + esClassNames.toggleButton : "");
|
|
698
710
|
eventStreamToggleBtn = createElement("button", toggleBtnClasses) as HTMLButtonElement;
|
|
699
711
|
eventStreamToggleBtn.style.width = "28px";
|
|
700
712
|
eventStreamToggleBtn.style.height = "28px";
|
|
@@ -723,9 +735,38 @@ export const createAgentExperience = (
|
|
|
723
735
|
});
|
|
724
736
|
}
|
|
725
737
|
|
|
738
|
+
const ensureComposerAttachmentSurface = (rootFooter: HTMLElement) => {
|
|
739
|
+
const att = config.attachments;
|
|
740
|
+
if (!att?.enabled) return;
|
|
741
|
+
let previews = rootFooter.querySelector<HTMLElement>(".persona-attachment-previews");
|
|
742
|
+
if (!previews) {
|
|
743
|
+
previews = createElement(
|
|
744
|
+
"div",
|
|
745
|
+
"persona-attachment-previews persona-flex persona-flex-wrap persona-gap-2 persona-mb-2"
|
|
746
|
+
);
|
|
747
|
+
previews.style.display = "none";
|
|
748
|
+
const form = rootFooter.querySelector("[data-persona-composer-form]");
|
|
749
|
+
if (form?.parentNode) {
|
|
750
|
+
form.parentNode.insertBefore(previews, form);
|
|
751
|
+
} else {
|
|
752
|
+
rootFooter.insertBefore(previews, rootFooter.firstChild);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (!rootFooter.querySelector<HTMLInputElement>('input[type="file"]')) {
|
|
756
|
+
const fileIn = createElement("input") as HTMLInputElement;
|
|
757
|
+
fileIn.type = "file";
|
|
758
|
+
fileIn.accept = (att.allowedTypes ?? ALL_SUPPORTED_MIME_TYPES).join(",");
|
|
759
|
+
fileIn.multiple = (att.maxFiles ?? 4) > 1;
|
|
760
|
+
fileIn.style.display = "none";
|
|
761
|
+
fileIn.setAttribute("aria-label", att.buttonTooltipText ?? "Attach files");
|
|
762
|
+
rootFooter.appendChild(fileIn);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
726
766
|
// Plugin hook: renderComposer - allow plugins to provide custom composer
|
|
727
767
|
const composerPlugin = plugins.find(p => p.renderComposer);
|
|
728
768
|
if (composerPlugin?.renderComposer) {
|
|
769
|
+
const composerCfg = config.composer;
|
|
729
770
|
const customComposer = composerPlugin.renderComposer({
|
|
730
771
|
config,
|
|
731
772
|
defaultRenderer: () => {
|
|
@@ -737,17 +778,79 @@ export const createAgentExperience = (
|
|
|
737
778
|
session.sendMessage(text);
|
|
738
779
|
}
|
|
739
780
|
},
|
|
740
|
-
|
|
781
|
+
streaming: false,
|
|
782
|
+
disabled: false,
|
|
783
|
+
openAttachmentPicker: () => {
|
|
784
|
+
attachmentInput?.click();
|
|
785
|
+
},
|
|
786
|
+
models: composerCfg?.models,
|
|
787
|
+
selectedModelId: composerCfg?.selectedModelId,
|
|
788
|
+
onModelChange: (modelId: string) => {
|
|
789
|
+
config.composer = { ...config.composer, selectedModelId: modelId };
|
|
790
|
+
},
|
|
791
|
+
onVoiceToggle:
|
|
792
|
+
config.voiceRecognition?.enabled === true
|
|
793
|
+
? () => {
|
|
794
|
+
composerVoiceBridge?.();
|
|
795
|
+
}
|
|
796
|
+
: undefined
|
|
741
797
|
});
|
|
742
798
|
if (customComposer) {
|
|
743
799
|
// Replace the default footer with custom composer
|
|
744
800
|
footer.replaceWith(customComposer);
|
|
745
801
|
footer = customComposer;
|
|
746
|
-
// Note: When using custom composer, textarea/sendButton/etc may not exist
|
|
747
|
-
// The plugin is responsible for providing its own submit handling
|
|
748
802
|
}
|
|
749
803
|
}
|
|
750
804
|
|
|
805
|
+
const bindComposerRefsFromFooter = (rootFooter: HTMLElement) => {
|
|
806
|
+
const form = rootFooter.querySelector<HTMLFormElement>("[data-persona-composer-form]");
|
|
807
|
+
const ta = rootFooter.querySelector<HTMLTextAreaElement>("[data-persona-composer-input]");
|
|
808
|
+
const sb = rootFooter.querySelector<HTMLButtonElement>("[data-persona-composer-submit]");
|
|
809
|
+
const mic = rootFooter.querySelector<HTMLButtonElement>("[data-persona-composer-mic]");
|
|
810
|
+
const st = rootFooter.querySelector<HTMLElement>("[data-persona-composer-status]");
|
|
811
|
+
if (form) composerForm = form;
|
|
812
|
+
if (ta) textarea = ta;
|
|
813
|
+
if (sb) sendButton = sb;
|
|
814
|
+
if (mic) {
|
|
815
|
+
micButton = mic;
|
|
816
|
+
micButtonWrapper = mic.parentElement as HTMLElement | null;
|
|
817
|
+
}
|
|
818
|
+
if (st) statusText = st;
|
|
819
|
+
const sug = rootFooter.querySelector<HTMLElement>(
|
|
820
|
+
".persona-mb-3.persona-flex.persona-flex-wrap.persona-gap-2"
|
|
821
|
+
);
|
|
822
|
+
if (sug) suggestions = sug;
|
|
823
|
+
const attBtn = rootFooter.querySelector<HTMLButtonElement>(".persona-attachment-button");
|
|
824
|
+
if (attBtn) {
|
|
825
|
+
attachmentButton = attBtn;
|
|
826
|
+
attachmentButtonWrapper = attBtn.parentElement as HTMLElement | null;
|
|
827
|
+
}
|
|
828
|
+
attachmentInput = rootFooter.querySelector<HTMLInputElement>('input[type="file"]');
|
|
829
|
+
attachmentPreviewsContainer = rootFooter.querySelector<HTMLElement>(".persona-attachment-previews");
|
|
830
|
+
const ar = rootFooter.querySelector<HTMLElement>(".persona-widget-composer .persona-flex.persona-items-center.persona-justify-between");
|
|
831
|
+
if (ar) _actionsRow = ar;
|
|
832
|
+
};
|
|
833
|
+
ensureComposerAttachmentSurface(footer);
|
|
834
|
+
bindComposerRefsFromFooter(footer);
|
|
835
|
+
|
|
836
|
+
// Apply contentMaxWidth to composer form if configured
|
|
837
|
+
const contentMaxWidth = config.layout?.contentMaxWidth;
|
|
838
|
+
if (contentMaxWidth && composerForm) {
|
|
839
|
+
composerForm.style.maxWidth = contentMaxWidth;
|
|
840
|
+
composerForm.style.marginLeft = "auto";
|
|
841
|
+
composerForm.style.marginRight = "auto";
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (config.attachments?.enabled && attachmentInput && attachmentPreviewsContainer) {
|
|
845
|
+
attachmentManager = AttachmentManager.fromConfig(config.attachments);
|
|
846
|
+
attachmentManager.setPreviewsContainer(attachmentPreviewsContainer);
|
|
847
|
+
attachmentInput.addEventListener("change", (e) => {
|
|
848
|
+
const target = e.target as HTMLInputElement;
|
|
849
|
+
attachmentManager?.handleFileSelect(target.files);
|
|
850
|
+
target.value = "";
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
751
854
|
// Slot system: allow custom content injection into specific regions
|
|
752
855
|
const renderSlots = () => {
|
|
753
856
|
const slots = config.layout?.slots ?? {};
|
|
@@ -757,7 +860,7 @@ export const createAgentExperience = (
|
|
|
757
860
|
switch (slot) {
|
|
758
861
|
case "body-top":
|
|
759
862
|
// Default: the intro card
|
|
760
|
-
return container.querySelector(".
|
|
863
|
+
return container.querySelector(".persona-rounded-2xl.persona-bg-persona-surface.persona-p-6") as HTMLElement || null;
|
|
761
864
|
case "messages":
|
|
762
865
|
return messagesWrapper;
|
|
763
866
|
case "footer-top":
|
|
@@ -784,7 +887,7 @@ export const createAgentExperience = (
|
|
|
784
887
|
header.appendChild(element);
|
|
785
888
|
} else {
|
|
786
889
|
// header-center: insert after icon/title
|
|
787
|
-
const titleSection = header.querySelector(".
|
|
890
|
+
const titleSection = header.querySelector(".persona-flex-col");
|
|
788
891
|
if (titleSection) {
|
|
789
892
|
titleSection.parentNode?.insertBefore(element, titleSection.nextSibling);
|
|
790
893
|
} else {
|
|
@@ -794,7 +897,7 @@ export const createAgentExperience = (
|
|
|
794
897
|
break;
|
|
795
898
|
case "body-top": {
|
|
796
899
|
// Replace or prepend to body
|
|
797
|
-
const introCard = body.querySelector(".
|
|
900
|
+
const introCard = body.querySelector(".persona-rounded-2xl.persona-bg-persona-surface.persona-p-6");
|
|
798
901
|
if (introCard) {
|
|
799
902
|
introCard.replaceWith(element);
|
|
800
903
|
} else {
|
|
@@ -904,7 +1007,7 @@ export const createAgentExperience = (
|
|
|
904
1007
|
|
|
905
1008
|
messagesWrapper.addEventListener('click', (event) => {
|
|
906
1009
|
const target = event.target as HTMLElement;
|
|
907
|
-
const actionBtn = target.closest('.
|
|
1010
|
+
const actionBtn = target.closest('.persona-message-action-btn[data-action]') as HTMLElement;
|
|
908
1011
|
if (!actionBtn) return;
|
|
909
1012
|
|
|
910
1013
|
event.preventDefault();
|
|
@@ -926,14 +1029,14 @@ export const createAgentExperience = (
|
|
|
926
1029
|
const textToCopy = message.content || "";
|
|
927
1030
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
928
1031
|
// Show success feedback - swap icon temporarily
|
|
929
|
-
actionBtn.classList.add("
|
|
1032
|
+
actionBtn.classList.add("persona-message-action-success");
|
|
930
1033
|
const checkIcon = renderLucideIcon("check", 14, "currentColor", 2);
|
|
931
1034
|
if (checkIcon) {
|
|
932
1035
|
actionBtn.innerHTML = "";
|
|
933
1036
|
actionBtn.appendChild(checkIcon);
|
|
934
1037
|
}
|
|
935
1038
|
setTimeout(() => {
|
|
936
|
-
actionBtn.classList.remove("
|
|
1039
|
+
actionBtn.classList.remove("persona-message-action-success");
|
|
937
1040
|
const originalIcon = renderLucideIcon("copy", 14, "currentColor", 2);
|
|
938
1041
|
if (originalIcon) {
|
|
939
1042
|
actionBtn.innerHTML = "";
|
|
@@ -955,17 +1058,17 @@ export const createAgentExperience = (
|
|
|
955
1058
|
if (wasActive) {
|
|
956
1059
|
// Toggle off
|
|
957
1060
|
messageVoteState.delete(messageId);
|
|
958
|
-
actionBtn.classList.remove("
|
|
1061
|
+
actionBtn.classList.remove("persona-message-action-active");
|
|
959
1062
|
} else {
|
|
960
1063
|
// Clear opposite vote button
|
|
961
1064
|
const oppositeAction = action === 'upvote' ? 'downvote' : 'upvote';
|
|
962
1065
|
const oppositeBtn = actionsContainer.querySelector(`[data-action="${oppositeAction}"]`);
|
|
963
1066
|
if (oppositeBtn) {
|
|
964
|
-
oppositeBtn.classList.remove("
|
|
1067
|
+
oppositeBtn.classList.remove("persona-message-action-active");
|
|
965
1068
|
}
|
|
966
1069
|
|
|
967
1070
|
messageVoteState.set(messageId, action);
|
|
968
|
-
actionBtn.classList.add("
|
|
1071
|
+
actionBtn.classList.add("persona-message-action-active");
|
|
969
1072
|
|
|
970
1073
|
// Trigger feedback
|
|
971
1074
|
const messages = session.getMessages();
|
|
@@ -1021,32 +1124,334 @@ export const createAgentExperience = (
|
|
|
1021
1124
|
session.resolveApproval(approvalMessage.approval, decision);
|
|
1022
1125
|
});
|
|
1023
1126
|
|
|
1024
|
-
|
|
1127
|
+
let artifactPaneApi: ArtifactPaneApi | null = null;
|
|
1128
|
+
let artifactPanelResizeObs: ResizeObserver | null = null;
|
|
1129
|
+
let lastArtifactsState: {
|
|
1130
|
+
artifacts: PersonaArtifactRecord[];
|
|
1131
|
+
selectedId: string | null;
|
|
1132
|
+
} = { artifacts: [], selectedId: null };
|
|
1133
|
+
let artifactsPaneUserHidden = false;
|
|
1134
|
+
const sessionRef: { current: AgentWidgetSession | null } = { current: null };
|
|
1135
|
+
|
|
1136
|
+
// Click delegation for artifact download buttons
|
|
1137
|
+
messagesWrapper.addEventListener('click', (event) => {
|
|
1138
|
+
const target = event.target as HTMLElement;
|
|
1139
|
+
const dlBtn = target.closest('[data-download-artifact]') as HTMLElement;
|
|
1140
|
+
if (!dlBtn) return;
|
|
1141
|
+
event.preventDefault();
|
|
1142
|
+
event.stopPropagation();
|
|
1143
|
+
const artifactId = dlBtn.getAttribute('data-download-artifact');
|
|
1144
|
+
if (!artifactId) return;
|
|
1145
|
+
// Try session state first, fall back to content stored in the card's rawContent props
|
|
1146
|
+
const artifact = session.getArtifactById(artifactId);
|
|
1147
|
+
let markdown = artifact?.markdown;
|
|
1148
|
+
let title = artifact?.title || 'artifact';
|
|
1149
|
+
if (!markdown) {
|
|
1150
|
+
// After page refresh, session state is gone — read from the persisted card message
|
|
1151
|
+
const cardEl = dlBtn.closest('[data-open-artifact]');
|
|
1152
|
+
const msgEl = cardEl?.closest('[data-message-id]');
|
|
1153
|
+
const msgId = msgEl?.getAttribute('data-message-id');
|
|
1154
|
+
if (msgId) {
|
|
1155
|
+
const msgs = session.getMessages();
|
|
1156
|
+
const msg = msgs.find(m => m.id === msgId);
|
|
1157
|
+
if (msg?.rawContent) {
|
|
1158
|
+
try {
|
|
1159
|
+
const parsed = JSON.parse(msg.rawContent);
|
|
1160
|
+
markdown = parsed?.props?.markdown;
|
|
1161
|
+
title = parsed?.props?.title || title;
|
|
1162
|
+
} catch { /* ignore */ }
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (!markdown) return;
|
|
1167
|
+
const blob = new Blob([markdown], { type: 'text/markdown' });
|
|
1168
|
+
const url = URL.createObjectURL(blob);
|
|
1169
|
+
const a = document.createElement('a');
|
|
1170
|
+
a.href = url;
|
|
1171
|
+
a.download = `${title}.md`;
|
|
1172
|
+
a.click();
|
|
1173
|
+
URL.revokeObjectURL(url);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// Click delegation for artifact reference cards
|
|
1177
|
+
messagesWrapper.addEventListener('click', (event) => {
|
|
1178
|
+
const target = event.target as HTMLElement;
|
|
1179
|
+
const card = target.closest('[data-open-artifact]') as HTMLElement;
|
|
1180
|
+
if (!card) return;
|
|
1181
|
+
const artifactId = card.getAttribute('data-open-artifact');
|
|
1182
|
+
if (!artifactId) return;
|
|
1183
|
+
event.preventDefault();
|
|
1184
|
+
event.stopPropagation();
|
|
1185
|
+
session.selectArtifact(artifactId);
|
|
1186
|
+
syncArtifactPane();
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
// Keyboard support for artifact cards
|
|
1190
|
+
messagesWrapper.addEventListener('keydown', (event) => {
|
|
1191
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
1192
|
+
const target = event.target as HTMLElement;
|
|
1193
|
+
if (!target.hasAttribute('data-open-artifact')) return;
|
|
1194
|
+
event.preventDefault();
|
|
1195
|
+
target.click();
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
let artifactSplitRoot: HTMLElement | null = null;
|
|
1199
|
+
let artifactResizeHandle: HTMLElement | null = null;
|
|
1200
|
+
let artifactResizeUnbind: (() => void) | null = null;
|
|
1201
|
+
let artifactResizeDocEnd: (() => void) | null = null;
|
|
1202
|
+
let reconcileArtifactResize: () => void = () => {};
|
|
1203
|
+
|
|
1204
|
+
function stopArtifactResizePointer() {
|
|
1205
|
+
artifactResizeDocEnd?.();
|
|
1206
|
+
artifactResizeDocEnd = null;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/** Flush split: overlay handle on the seam so it does not consume flex gap (extension + resizable). */
|
|
1210
|
+
const positionExtensionArtifactResizeHandle = () => {
|
|
1211
|
+
if (!artifactSplitRoot || !artifactResizeHandle) return;
|
|
1212
|
+
const ext = mount.classList.contains("persona-artifact-appearance-seamless");
|
|
1213
|
+
const ownerWin = mount.ownerDocument.defaultView ?? window;
|
|
1214
|
+
const mobile = ownerWin.innerWidth <= 640;
|
|
1215
|
+
if (!ext || mount.classList.contains("persona-artifact-narrow-host") || mobile) {
|
|
1216
|
+
artifactResizeHandle.style.removeProperty("position");
|
|
1217
|
+
artifactResizeHandle.style.removeProperty("left");
|
|
1218
|
+
artifactResizeHandle.style.removeProperty("top");
|
|
1219
|
+
artifactResizeHandle.style.removeProperty("bottom");
|
|
1220
|
+
artifactResizeHandle.style.removeProperty("width");
|
|
1221
|
+
artifactResizeHandle.style.removeProperty("z-index");
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const chat = artifactSplitRoot.firstElementChild as HTMLElement | null;
|
|
1225
|
+
if (!chat || chat === artifactResizeHandle) return;
|
|
1226
|
+
const hitW = 10;
|
|
1227
|
+
artifactResizeHandle.style.position = "absolute";
|
|
1228
|
+
artifactResizeHandle.style.top = "0";
|
|
1229
|
+
artifactResizeHandle.style.bottom = "0";
|
|
1230
|
+
artifactResizeHandle.style.width = `${hitW}px`;
|
|
1231
|
+
artifactResizeHandle.style.zIndex = "5";
|
|
1232
|
+
const left = chat.offsetWidth - hitW / 2;
|
|
1233
|
+
artifactResizeHandle.style.left = `${Math.max(0, left)}px`;
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
/** No-op until artifact pane is created; replaced below when artifacts are enabled. */
|
|
1237
|
+
let applyLauncherArtifactPanelWidth: () => void = () => {};
|
|
1238
|
+
|
|
1239
|
+
const syncArtifactPane = () => {
|
|
1240
|
+
if (!artifactPaneApi || !artifactsSidebarEnabled(config)) return;
|
|
1241
|
+
applyArtifactLayoutCssVars(mount, config);
|
|
1242
|
+
applyArtifactPaneAppearance(mount, config);
|
|
1243
|
+
applyLauncherArtifactPanelWidth();
|
|
1244
|
+
const threshold = config.features?.artifacts?.layout?.narrowHostMaxWidth ?? 520;
|
|
1245
|
+
const w = panel.getBoundingClientRect().width || 0;
|
|
1246
|
+
mount.classList.toggle("persona-artifact-narrow-host", w > 0 && w <= threshold);
|
|
1247
|
+
artifactPaneApi.update(lastArtifactsState);
|
|
1248
|
+
if (artifactsPaneUserHidden) {
|
|
1249
|
+
artifactPaneApi.setMobileOpen(false);
|
|
1250
|
+
artifactPaneApi.element.classList.add("persona-hidden");
|
|
1251
|
+
artifactPaneApi.backdrop?.classList.add("persona-hidden");
|
|
1252
|
+
} else if (lastArtifactsState.artifacts.length > 0) {
|
|
1253
|
+
// User chose “show” again (e.g. programmatic showArtifacts): clear dismiss chrome
|
|
1254
|
+
// and force drawer open so narrow-host / mobile slide-out is not stuck off-screen.
|
|
1255
|
+
artifactPaneApi.element.classList.remove("persona-hidden");
|
|
1256
|
+
artifactPaneApi.setMobileOpen(true);
|
|
1257
|
+
}
|
|
1258
|
+
reconcileArtifactResize();
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
if (artifactsSidebarEnabled(config)) {
|
|
1262
|
+
panel.style.position = "relative";
|
|
1263
|
+
const chatColumn = createElement(
|
|
1264
|
+
"div",
|
|
1265
|
+
"persona-flex persona-flex-1 persona-flex-col persona-min-w-0 persona-min-h-0"
|
|
1266
|
+
);
|
|
1267
|
+
const splitRoot = createElement(
|
|
1268
|
+
"div",
|
|
1269
|
+
"persona-flex persona-h-full persona-w-full persona-min-h-0 persona-artifact-split-root"
|
|
1270
|
+
);
|
|
1271
|
+
chatColumn.appendChild(container);
|
|
1272
|
+
artifactPaneApi = createArtifactPane(config, {
|
|
1273
|
+
onSelect: (id) => sessionRef.current?.selectArtifact(id),
|
|
1274
|
+
onDismiss: () => {
|
|
1275
|
+
artifactsPaneUserHidden = true;
|
|
1276
|
+
syncArtifactPane();
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
artifactPaneApi.element.classList.add("persona-hidden");
|
|
1280
|
+
artifactSplitRoot = splitRoot;
|
|
1281
|
+
splitRoot.appendChild(chatColumn);
|
|
1282
|
+
splitRoot.appendChild(artifactPaneApi.element);
|
|
1283
|
+
if (artifactPaneApi.backdrop) {
|
|
1284
|
+
panel.appendChild(artifactPaneApi.backdrop);
|
|
1285
|
+
}
|
|
1286
|
+
panel.appendChild(splitRoot);
|
|
1287
|
+
|
|
1288
|
+
reconcileArtifactResize = () => {
|
|
1289
|
+
if (!artifactSplitRoot || !artifactPaneApi) return;
|
|
1290
|
+
const want = config.features?.artifacts?.layout?.resizable === true;
|
|
1291
|
+
if (!want) {
|
|
1292
|
+
artifactResizeUnbind?.();
|
|
1293
|
+
artifactResizeUnbind = null;
|
|
1294
|
+
stopArtifactResizePointer();
|
|
1295
|
+
if (artifactResizeHandle) {
|
|
1296
|
+
artifactResizeHandle.remove();
|
|
1297
|
+
artifactResizeHandle = null;
|
|
1298
|
+
}
|
|
1299
|
+
artifactPaneApi.element.style.removeProperty("width");
|
|
1300
|
+
artifactPaneApi.element.style.removeProperty("maxWidth");
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (!artifactResizeHandle) {
|
|
1304
|
+
const handle = createElement(
|
|
1305
|
+
"div",
|
|
1306
|
+
"persona-artifact-split-handle persona-shrink-0 persona-h-full"
|
|
1307
|
+
);
|
|
1308
|
+
handle.setAttribute("role", "separator");
|
|
1309
|
+
handle.setAttribute("aria-orientation", "vertical");
|
|
1310
|
+
handle.setAttribute("aria-label", "Resize artifacts panel");
|
|
1311
|
+
handle.tabIndex = 0;
|
|
1312
|
+
|
|
1313
|
+
const doc = mount.ownerDocument;
|
|
1314
|
+
const win = doc.defaultView ?? window;
|
|
1315
|
+
|
|
1316
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
1317
|
+
if (!artifactPaneApi || e.button !== 0) return;
|
|
1318
|
+
if (mount.classList.contains("persona-artifact-narrow-host")) return;
|
|
1319
|
+
if (win.innerWidth <= 640) return;
|
|
1320
|
+
e.preventDefault();
|
|
1321
|
+
stopArtifactResizePointer();
|
|
1322
|
+
const startX = e.clientX;
|
|
1323
|
+
const startW = artifactPaneApi.element.getBoundingClientRect().width;
|
|
1324
|
+
const layout = config.features?.artifacts?.layout;
|
|
1325
|
+
const onMove = (ev: PointerEvent) => {
|
|
1326
|
+
const splitW = artifactSplitRoot!.getBoundingClientRect().width;
|
|
1327
|
+
const extensionChrome = mount.classList.contains("persona-artifact-appearance-seamless");
|
|
1328
|
+
const gapPx = extensionChrome ? 0 : readFlexGapPx(artifactSplitRoot!, win);
|
|
1329
|
+
const handleW = extensionChrome ? 0 : handle.getBoundingClientRect().width || 6;
|
|
1330
|
+
// Handle is left of the artifact: drag left widens artifact, drag right narrows it.
|
|
1331
|
+
const next = startW - (ev.clientX - startX);
|
|
1332
|
+
const clamped = resolveArtifactPaneWidthPx(
|
|
1333
|
+
next,
|
|
1334
|
+
splitW,
|
|
1335
|
+
gapPx,
|
|
1336
|
+
handleW,
|
|
1337
|
+
layout?.resizableMinWidth,
|
|
1338
|
+
layout?.resizableMaxWidth
|
|
1339
|
+
);
|
|
1340
|
+
artifactPaneApi!.element.style.width = `${clamped}px`;
|
|
1341
|
+
artifactPaneApi!.element.style.maxWidth = "none";
|
|
1342
|
+
positionExtensionArtifactResizeHandle();
|
|
1343
|
+
};
|
|
1344
|
+
const onUp = () => {
|
|
1345
|
+
doc.removeEventListener("pointermove", onMove);
|
|
1346
|
+
doc.removeEventListener("pointerup", onUp);
|
|
1347
|
+
doc.removeEventListener("pointercancel", onUp);
|
|
1348
|
+
artifactResizeDocEnd = null;
|
|
1349
|
+
try {
|
|
1350
|
+
handle.releasePointerCapture(e.pointerId);
|
|
1351
|
+
} catch {
|
|
1352
|
+
/* ignore */
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
artifactResizeDocEnd = onUp;
|
|
1356
|
+
doc.addEventListener("pointermove", onMove);
|
|
1357
|
+
doc.addEventListener("pointerup", onUp);
|
|
1358
|
+
doc.addEventListener("pointercancel", onUp);
|
|
1359
|
+
try {
|
|
1360
|
+
handle.setPointerCapture(e.pointerId);
|
|
1361
|
+
} catch {
|
|
1362
|
+
/* ignore */
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
handle.addEventListener("pointerdown", onPointerDown);
|
|
1367
|
+
artifactResizeHandle = handle;
|
|
1368
|
+
artifactSplitRoot.insertBefore(handle, artifactPaneApi.element);
|
|
1369
|
+
artifactResizeUnbind = () => {
|
|
1370
|
+
handle.removeEventListener("pointerdown", onPointerDown);
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
if (artifactResizeHandle) {
|
|
1374
|
+
const has =
|
|
1375
|
+
lastArtifactsState.artifacts.length > 0 && !artifactsPaneUserHidden;
|
|
1376
|
+
artifactResizeHandle.classList.toggle("persona-hidden", !has);
|
|
1377
|
+
positionExtensionArtifactResizeHandle();
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
applyLauncherArtifactPanelWidth = () => {
|
|
1382
|
+
if (!launcherEnabled || !artifactPaneApi) return;
|
|
1383
|
+
const sidebarMode = config.launcher?.sidebarMode ?? false;
|
|
1384
|
+
if (sidebarMode) return;
|
|
1385
|
+
const ownerWindow = mount.ownerDocument.defaultView ?? window;
|
|
1386
|
+
const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
|
|
1387
|
+
const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
|
|
1388
|
+
if (mobileFullscreen && ownerWindow.innerWidth <= mobileBreakpoint) return;
|
|
1389
|
+
if (!shouldExpandLauncherForArtifacts(config, launcherEnabled)) return;
|
|
1390
|
+
|
|
1391
|
+
const base = config.launcher?.width ?? config.launcherWidth ?? "min(400px, calc(100vw - 24px))";
|
|
1392
|
+
const expanded =
|
|
1393
|
+
config.features?.artifacts?.layout?.expandedPanelWidth ??
|
|
1394
|
+
"min(720px, calc(100vw - 24px))";
|
|
1395
|
+
const hasVisible =
|
|
1396
|
+
lastArtifactsState.artifacts.length > 0 && !artifactsPaneUserHidden;
|
|
1397
|
+
if (hasVisible) {
|
|
1398
|
+
panel.style.width = expanded;
|
|
1399
|
+
panel.style.maxWidth = expanded;
|
|
1400
|
+
} else {
|
|
1401
|
+
panel.style.width = base;
|
|
1402
|
+
panel.style.maxWidth = base;
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1407
|
+
artifactPanelResizeObs = new ResizeObserver(() => {
|
|
1408
|
+
syncArtifactPane();
|
|
1409
|
+
});
|
|
1410
|
+
artifactPanelResizeObs.observe(panel);
|
|
1411
|
+
}
|
|
1412
|
+
} else {
|
|
1413
|
+
panel.appendChild(container);
|
|
1414
|
+
}
|
|
1025
1415
|
mount.appendChild(wrapper);
|
|
1026
1416
|
|
|
1027
1417
|
// Apply full-height and sidebar styles if enabled
|
|
1028
1418
|
// This ensures the widget fills its container height with proper flex layout
|
|
1029
1419
|
const applyFullHeightStyles = () => {
|
|
1420
|
+
const dockedMode = isDockedMountMode(config);
|
|
1030
1421
|
const sidebarMode = config.launcher?.sidebarMode ?? false;
|
|
1031
|
-
const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
1422
|
+
const fullHeight = dockedMode || sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
1423
|
+
/** Script-tag / div embed: launcher off, host supplies a sized mount. */
|
|
1424
|
+
const isInlineEmbed = config.launcher?.enabled === false;
|
|
1032
1425
|
const theme = config.theme ?? {};
|
|
1033
|
-
|
|
1426
|
+
|
|
1427
|
+
// Mobile fullscreen detection
|
|
1428
|
+
// Use mount's ownerDocument window to get correct viewport width when widget is inside an iframe
|
|
1429
|
+
const ownerWindow = mount.ownerDocument.defaultView ?? window;
|
|
1430
|
+
const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
|
|
1431
|
+
const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
|
|
1432
|
+
const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
|
|
1433
|
+
const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
|
|
1434
|
+
|
|
1034
1435
|
// Determine panel styling based on mode, with theme overrides
|
|
1035
1436
|
const position = config.launcher?.position ?? 'bottom-left';
|
|
1036
1437
|
const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
|
|
1037
|
-
|
|
1438
|
+
|
|
1038
1439
|
// Default values based on mode
|
|
1039
|
-
const defaultPanelBorder = sidebarMode ? 'none' : '1px solid var(--
|
|
1040
|
-
const defaultPanelShadow =
|
|
1041
|
-
?
|
|
1042
|
-
:
|
|
1043
|
-
|
|
1044
|
-
|
|
1440
|
+
const defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-persona-border)';
|
|
1441
|
+
const defaultPanelShadow = shouldGoFullscreen
|
|
1442
|
+
? 'none'
|
|
1443
|
+
: sidebarMode
|
|
1444
|
+
? (isLeftSidebar ? 'var(--persona-palette-shadows-sidebar-left, 2px 0 12px rgba(0, 0, 0, 0.08))' : 'var(--persona-palette-shadows-sidebar-right, -2px 0 12px rgba(0, 0, 0, 0.08))')
|
|
1445
|
+
: 'var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25))';
|
|
1446
|
+
const defaultPanelBorderRadius = (sidebarMode || shouldGoFullscreen)
|
|
1447
|
+
? '0'
|
|
1448
|
+
: 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))';
|
|
1449
|
+
|
|
1045
1450
|
// Apply theme overrides or defaults
|
|
1046
1451
|
const panelBorder = theme.panelBorder ?? defaultPanelBorder;
|
|
1047
1452
|
const panelShadow = theme.panelShadow ?? defaultPanelShadow;
|
|
1048
1453
|
const panelBorderRadius = theme.panelBorderRadius ?? defaultPanelBorderRadius;
|
|
1049
|
-
|
|
1454
|
+
|
|
1050
1455
|
// Reset all inline styles first to handle mode toggling
|
|
1051
1456
|
// This ensures styles don't persist when switching between modes
|
|
1052
1457
|
mount.style.cssText = '';
|
|
@@ -1056,14 +1461,87 @@ export const createAgentExperience = (
|
|
|
1056
1461
|
body.style.cssText = '';
|
|
1057
1462
|
footer.style.cssText = '';
|
|
1058
1463
|
|
|
1464
|
+
// Mobile fullscreen: fill entire viewport with no radius/shadow/margins
|
|
1465
|
+
if (shouldGoFullscreen) {
|
|
1466
|
+
// Remove position offset classes
|
|
1467
|
+
wrapper.classList.remove(
|
|
1468
|
+
'persona-bottom-6', 'persona-right-6', 'persona-left-6', 'persona-top-6',
|
|
1469
|
+
'persona-bottom-4', 'persona-right-4', 'persona-left-4', 'persona-top-4'
|
|
1470
|
+
);
|
|
1471
|
+
|
|
1472
|
+
// Wrapper — fill entire viewport
|
|
1473
|
+
wrapper.style.cssText = `
|
|
1474
|
+
position: fixed !important;
|
|
1475
|
+
inset: 0 !important;
|
|
1476
|
+
width: 100% !important;
|
|
1477
|
+
height: 100% !important;
|
|
1478
|
+
max-height: 100% !important;
|
|
1479
|
+
margin: 0 !important;
|
|
1480
|
+
padding: 0 !important;
|
|
1481
|
+
display: flex !important;
|
|
1482
|
+
flex-direction: column !important;
|
|
1483
|
+
z-index: inherit !important;
|
|
1484
|
+
`;
|
|
1485
|
+
|
|
1486
|
+
// Panel — fill wrapper, no radius/shadow
|
|
1487
|
+
panel.style.cssText = `
|
|
1488
|
+
position: relative !important;
|
|
1489
|
+
display: flex !important;
|
|
1490
|
+
flex-direction: column !important;
|
|
1491
|
+
flex: 1 1 0% !important;
|
|
1492
|
+
width: 100% !important;
|
|
1493
|
+
max-width: 100% !important;
|
|
1494
|
+
height: 100% !important;
|
|
1495
|
+
min-height: 0 !important;
|
|
1496
|
+
margin: 0 !important;
|
|
1497
|
+
padding: 0 !important;
|
|
1498
|
+
box-shadow: none !important;
|
|
1499
|
+
border-radius: 0 !important;
|
|
1500
|
+
`;
|
|
1501
|
+
|
|
1502
|
+
// Container — fill panel, no radius/border
|
|
1503
|
+
container.style.cssText = `
|
|
1504
|
+
display: flex !important;
|
|
1505
|
+
flex-direction: column !important;
|
|
1506
|
+
flex: 1 1 0% !important;
|
|
1507
|
+
width: 100% !important;
|
|
1508
|
+
height: 100% !important;
|
|
1509
|
+
min-height: 0 !important;
|
|
1510
|
+
max-height: 100% !important;
|
|
1511
|
+
overflow: hidden !important;
|
|
1512
|
+
border-radius: 0 !important;
|
|
1513
|
+
border: none !important;
|
|
1514
|
+
`;
|
|
1515
|
+
|
|
1516
|
+
// Body — scrollable messages
|
|
1517
|
+
body.style.flex = '1 1 0%';
|
|
1518
|
+
body.style.minHeight = '0';
|
|
1519
|
+
body.style.overflowY = 'auto';
|
|
1520
|
+
|
|
1521
|
+
// Footer — pinned at bottom
|
|
1522
|
+
footer.style.flexShrink = '0';
|
|
1523
|
+
|
|
1524
|
+
wasMobileFullscreen = true;
|
|
1525
|
+
return; // Skip remaining mode logic
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1059
1528
|
// Re-apply panel width/maxWidth from initial setup
|
|
1060
1529
|
const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
|
|
1061
1530
|
const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
|
|
1062
|
-
if (!sidebarMode) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1531
|
+
if (!sidebarMode && !dockedMode) {
|
|
1532
|
+
if (isInlineEmbed && fullHeight) {
|
|
1533
|
+
panel.style.width = "100%";
|
|
1534
|
+
panel.style.maxWidth = "100%";
|
|
1535
|
+
} else {
|
|
1536
|
+
panel.style.width = width;
|
|
1537
|
+
panel.style.maxWidth = width;
|
|
1538
|
+
}
|
|
1539
|
+
} else if (dockedMode) {
|
|
1540
|
+
panel.style.width = "100%";
|
|
1541
|
+
panel.style.maxWidth = "100%";
|
|
1065
1542
|
}
|
|
1066
|
-
|
|
1543
|
+
applyLauncherArtifactPanelWidth();
|
|
1544
|
+
|
|
1067
1545
|
// Apply panel styling
|
|
1068
1546
|
// Box-shadow is applied to panel (parent) instead of container to avoid
|
|
1069
1547
|
// rendering artifacts when container has overflow:hidden + border-radius
|
|
@@ -1072,16 +1550,16 @@ export const createAgentExperience = (
|
|
|
1072
1550
|
panel.style.borderRadius = panelBorderRadius;
|
|
1073
1551
|
container.style.border = panelBorder;
|
|
1074
1552
|
container.style.borderRadius = panelBorderRadius;
|
|
1075
|
-
|
|
1076
|
-
// Check if this is inline embed mode (launcher disabled) vs launcher mode
|
|
1077
|
-
const isInlineEmbed = config.launcher?.enabled === false;
|
|
1078
|
-
|
|
1553
|
+
|
|
1079
1554
|
if (fullHeight) {
|
|
1080
1555
|
// Mount container
|
|
1081
1556
|
mount.style.display = 'flex';
|
|
1082
1557
|
mount.style.flexDirection = 'column';
|
|
1083
1558
|
mount.style.height = '100%';
|
|
1084
1559
|
mount.style.minHeight = '0';
|
|
1560
|
+
if (isInlineEmbed) {
|
|
1561
|
+
mount.style.width = '100%';
|
|
1562
|
+
}
|
|
1085
1563
|
|
|
1086
1564
|
// Wrapper
|
|
1087
1565
|
// - Inline embed: needs overflow:hidden to contain the flex layout
|
|
@@ -1125,11 +1603,11 @@ export const createAgentExperience = (
|
|
|
1125
1603
|
// Handle positioning classes based on mode
|
|
1126
1604
|
// First remove all position classes to reset state
|
|
1127
1605
|
wrapper.classList.remove(
|
|
1128
|
-
'
|
|
1129
|
-
'
|
|
1606
|
+
'persona-bottom-6', 'persona-right-6', 'persona-left-6', 'persona-top-6',
|
|
1607
|
+
'persona-bottom-4', 'persona-right-4', 'persona-left-4', 'persona-top-4'
|
|
1130
1608
|
);
|
|
1131
1609
|
|
|
1132
|
-
if (!sidebarMode && !isInlineEmbed) {
|
|
1610
|
+
if (!sidebarMode && !isInlineEmbed && !dockedMode) {
|
|
1133
1611
|
// Restore positioning classes when not in sidebar mode (launcher mode only)
|
|
1134
1612
|
const positionClasses = positionMap[position as keyof typeof positionMap] ?? positionMap['bottom-right'];
|
|
1135
1613
|
positionClasses.split(' ').forEach(cls => wrapper.classList.add(cls));
|
|
@@ -1202,7 +1680,7 @@ export const createAgentExperience = (
|
|
|
1202
1680
|
// Use both -moz-available (Firefox) and stretch (standard) for cross-browser support
|
|
1203
1681
|
// Append to cssText to allow multiple fallback values for the same property
|
|
1204
1682
|
// Only apply to launcher mode (not sidebar or inline embed)
|
|
1205
|
-
if (!isInlineEmbed) {
|
|
1683
|
+
if (!isInlineEmbed && !dockedMode) {
|
|
1206
1684
|
const maxHeightStyles = 'max-height: -moz-available !important; max-height: stretch !important;';
|
|
1207
1685
|
const paddingStyles = sidebarMode ? '' : 'padding-top: 1.25em !important;';
|
|
1208
1686
|
wrapper.style.cssText += maxHeightStyles + paddingStyles;
|
|
@@ -1211,9 +1689,30 @@ export const createAgentExperience = (
|
|
|
1211
1689
|
applyFullHeightStyles();
|
|
1212
1690
|
// Apply theme variables after applyFullHeightStyles since it resets mount.style.cssText
|
|
1213
1691
|
applyThemeVariables(mount, config);
|
|
1692
|
+
applyArtifactLayoutCssVars(mount, config);
|
|
1693
|
+
applyArtifactPaneAppearance(mount, config);
|
|
1214
1694
|
|
|
1215
1695
|
const destroyCallbacks: Array<() => void> = [];
|
|
1216
1696
|
|
|
1697
|
+
if (artifactPanelResizeObs) {
|
|
1698
|
+
destroyCallbacks.push(() => {
|
|
1699
|
+
artifactPanelResizeObs?.disconnect();
|
|
1700
|
+
artifactPanelResizeObs = null;
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
destroyCallbacks.push(() => {
|
|
1705
|
+
artifactResizeUnbind?.();
|
|
1706
|
+
artifactResizeUnbind = null;
|
|
1707
|
+
stopArtifactResizePointer();
|
|
1708
|
+
if (artifactResizeHandle) {
|
|
1709
|
+
artifactResizeHandle.remove();
|
|
1710
|
+
artifactResizeHandle = null;
|
|
1711
|
+
}
|
|
1712
|
+
artifactPaneApi?.element.style.removeProperty("width");
|
|
1713
|
+
artifactPaneApi?.element.style.removeProperty("maxWidth");
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1217
1716
|
// Event stream cleanup
|
|
1218
1717
|
if (showEventStreamToggle) {
|
|
1219
1718
|
destroyCallbacks.push(() => {
|
|
@@ -1257,6 +1756,8 @@ export const createAgentExperience = (
|
|
|
1257
1756
|
let closeHandler: (() => void) | null = null;
|
|
1258
1757
|
let session: AgentWidgetSession;
|
|
1259
1758
|
let isStreaming = false;
|
|
1759
|
+
const messageCache = createMessageCache();
|
|
1760
|
+
let configVersion = 0;
|
|
1260
1761
|
let shouldAutoScroll = true;
|
|
1261
1762
|
let lastScrollTop = 0;
|
|
1262
1763
|
let lastAutoScrollTime = 0;
|
|
@@ -1530,7 +2031,20 @@ export const createAgentExperience = (
|
|
|
1530
2031
|
|
|
1531
2032
|
const inlineLoadingRenderer = getInlineLoadingIndicatorRenderer();
|
|
1532
2033
|
|
|
2034
|
+
// Track active message IDs for cache pruning
|
|
2035
|
+
const activeMessageIds = new Set<string>();
|
|
2036
|
+
|
|
1533
2037
|
messages.forEach((message) => {
|
|
2038
|
+
activeMessageIds.add(message.id);
|
|
2039
|
+
|
|
2040
|
+
// Fingerprint cache: skip re-rendering unchanged messages
|
|
2041
|
+
const fingerprint = computeMessageFingerprint(message, configVersion);
|
|
2042
|
+
const cachedWrapper = getCachedWrapper(messageCache, message.id, fingerprint);
|
|
2043
|
+
if (cachedWrapper) {
|
|
2044
|
+
tempContainer.appendChild(cachedWrapper.cloneNode(true));
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
1534
2048
|
let bubble: HTMLElement | null = null;
|
|
1535
2049
|
|
|
1536
2050
|
// Try plugins first
|
|
@@ -1612,36 +2126,59 @@ export const createAgentExperience = (
|
|
|
1612
2126
|
transform
|
|
1613
2127
|
});
|
|
1614
2128
|
if (componentBubble) {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
}
|
|
2129
|
+
const wrapChrome = config.wrapComponentDirectiveInBubble !== false;
|
|
2130
|
+
if (wrapChrome) {
|
|
2131
|
+
const componentWrapper = document.createElement("div");
|
|
2132
|
+
componentWrapper.className = [
|
|
2133
|
+
"vanilla-message-bubble",
|
|
2134
|
+
"persona-max-w-[85%]",
|
|
2135
|
+
"persona-rounded-2xl",
|
|
2136
|
+
"persona-bg-persona-surface",
|
|
2137
|
+
"persona-border",
|
|
2138
|
+
"persona-border-persona-message-border",
|
|
2139
|
+
"persona-p-4"
|
|
2140
|
+
].join(" ");
|
|
2141
|
+
componentWrapper.id = `bubble-${message.id}`;
|
|
2142
|
+
componentWrapper.setAttribute("data-message-id", message.id);
|
|
2143
|
+
|
|
2144
|
+
if (message.content && message.content.trim()) {
|
|
2145
|
+
const textDiv = document.createElement("div");
|
|
2146
|
+
textDiv.className = "persona-mb-3 persona-text-sm persona-leading-relaxed";
|
|
2147
|
+
textDiv.innerHTML = transform({
|
|
2148
|
+
text: message.content,
|
|
2149
|
+
message,
|
|
2150
|
+
streaming: Boolean(message.streaming),
|
|
2151
|
+
raw: message.rawContent
|
|
2152
|
+
});
|
|
2153
|
+
componentWrapper.appendChild(textDiv);
|
|
2154
|
+
}
|
|
1642
2155
|
|
|
1643
|
-
|
|
1644
|
-
|
|
2156
|
+
componentWrapper.appendChild(componentBubble);
|
|
2157
|
+
bubble = componentWrapper;
|
|
2158
|
+
} else {
|
|
2159
|
+
const stack = document.createElement("div");
|
|
2160
|
+
stack.className =
|
|
2161
|
+
"persona-flex persona-flex-col persona-w-full persona-max-w-full persona-gap-3 persona-items-stretch";
|
|
2162
|
+
stack.id = `bubble-${message.id}`;
|
|
2163
|
+
stack.setAttribute("data-message-id", message.id);
|
|
2164
|
+
stack.setAttribute("data-persona-component-directive", "true");
|
|
2165
|
+
|
|
2166
|
+
if (message.content && message.content.trim()) {
|
|
2167
|
+
const textDiv = document.createElement("div");
|
|
2168
|
+
textDiv.className =
|
|
2169
|
+
"persona-text-sm persona-leading-relaxed persona-text-persona-primary persona-w-full";
|
|
2170
|
+
textDiv.innerHTML = transform({
|
|
2171
|
+
text: message.content,
|
|
2172
|
+
message,
|
|
2173
|
+
streaming: Boolean(message.streaming),
|
|
2174
|
+
raw: message.rawContent
|
|
2175
|
+
});
|
|
2176
|
+
stack.appendChild(textDiv);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
stack.appendChild(componentBubble);
|
|
2180
|
+
bubble = stack;
|
|
2181
|
+
}
|
|
1645
2182
|
}
|
|
1646
2183
|
}
|
|
1647
2184
|
}
|
|
@@ -1693,17 +2230,24 @@ export const createAgentExperience = (
|
|
|
1693
2230
|
}
|
|
1694
2231
|
|
|
1695
2232
|
const wrapper = document.createElement("div");
|
|
1696
|
-
wrapper.className = "
|
|
2233
|
+
wrapper.className = "persona-flex";
|
|
1697
2234
|
// Set id for idiomorph matching
|
|
1698
2235
|
wrapper.id = `wrapper-${message.id}`;
|
|
1699
2236
|
wrapper.setAttribute("data-wrapper-id", message.id);
|
|
1700
2237
|
if (message.role === "user") {
|
|
1701
|
-
wrapper.classList.add("
|
|
2238
|
+
wrapper.classList.add("persona-justify-end");
|
|
2239
|
+
}
|
|
2240
|
+
if (bubble?.getAttribute("data-persona-component-directive") === "true") {
|
|
2241
|
+
wrapper.classList.add("persona-w-full");
|
|
1702
2242
|
}
|
|
1703
2243
|
wrapper.appendChild(bubble);
|
|
2244
|
+
setCachedWrapper(messageCache, message.id, fingerprint, wrapper);
|
|
1704
2245
|
tempContainer.appendChild(wrapper);
|
|
1705
2246
|
});
|
|
1706
2247
|
|
|
2248
|
+
// Remove cache entries for messages that no longer exist
|
|
2249
|
+
pruneCache(messageCache, activeMessageIds);
|
|
2250
|
+
|
|
1707
2251
|
// Add standalone typing indicator only if streaming but no assistant message is streaming yet
|
|
1708
2252
|
// (This shows while waiting for the stream to start)
|
|
1709
2253
|
// Check for ANY streaming assistant message, even if empty (to avoid duplicate bubbles)
|
|
@@ -1752,30 +2296,30 @@ export const createAgentExperience = (
|
|
|
1752
2296
|
const showBubble = config.loadingIndicator?.showBubble !== false; // default true
|
|
1753
2297
|
typingBubble.className = showBubble
|
|
1754
2298
|
? [
|
|
1755
|
-
"
|
|
1756
|
-
"
|
|
1757
|
-
"
|
|
1758
|
-
"
|
|
1759
|
-
"
|
|
1760
|
-
"
|
|
1761
|
-
"
|
|
1762
|
-
"
|
|
1763
|
-
"
|
|
1764
|
-
"
|
|
1765
|
-
"
|
|
2299
|
+
"persona-max-w-[85%]",
|
|
2300
|
+
"persona-rounded-2xl",
|
|
2301
|
+
"persona-text-sm",
|
|
2302
|
+
"persona-leading-relaxed",
|
|
2303
|
+
"persona-shadow-sm",
|
|
2304
|
+
"persona-bg-persona-surface",
|
|
2305
|
+
"persona-border",
|
|
2306
|
+
"persona-border-persona-message-border",
|
|
2307
|
+
"persona-text-persona-primary",
|
|
2308
|
+
"persona-px-5",
|
|
2309
|
+
"persona-py-3"
|
|
1766
2310
|
].join(" ")
|
|
1767
2311
|
: [
|
|
1768
|
-
"
|
|
1769
|
-
"
|
|
1770
|
-
"
|
|
1771
|
-
"
|
|
2312
|
+
"persona-max-w-[85%]",
|
|
2313
|
+
"persona-text-sm",
|
|
2314
|
+
"persona-leading-relaxed",
|
|
2315
|
+
"persona-text-persona-primary"
|
|
1772
2316
|
].join(" ");
|
|
1773
2317
|
typingBubble.setAttribute("data-typing-indicator", "true");
|
|
1774
2318
|
|
|
1775
2319
|
typingBubble.appendChild(typingIndicator);
|
|
1776
2320
|
|
|
1777
2321
|
const typingWrapper = document.createElement("div");
|
|
1778
|
-
typingWrapper.className = "
|
|
2322
|
+
typingWrapper.className = "persona-flex";
|
|
1779
2323
|
// Set id for idiomorph matching
|
|
1780
2324
|
typingWrapper.id = "wrapper-typing-indicator";
|
|
1781
2325
|
typingWrapper.setAttribute("data-wrapper-id", "typing-indicator");
|
|
@@ -1816,30 +2360,30 @@ export const createAgentExperience = (
|
|
|
1816
2360
|
const showBubble = config.loadingIndicator?.showBubble !== false; // default true
|
|
1817
2361
|
idleBubble.className = showBubble
|
|
1818
2362
|
? [
|
|
1819
|
-
"
|
|
1820
|
-
"
|
|
1821
|
-
"
|
|
1822
|
-
"
|
|
1823
|
-
"
|
|
1824
|
-
"
|
|
1825
|
-
"
|
|
1826
|
-
"
|
|
1827
|
-
"
|
|
1828
|
-
"
|
|
1829
|
-
"
|
|
2363
|
+
"persona-max-w-[85%]",
|
|
2364
|
+
"persona-rounded-2xl",
|
|
2365
|
+
"persona-text-sm",
|
|
2366
|
+
"persona-leading-relaxed",
|
|
2367
|
+
"persona-shadow-sm",
|
|
2368
|
+
"persona-bg-persona-surface",
|
|
2369
|
+
"persona-border",
|
|
2370
|
+
"persona-border-persona-message-border",
|
|
2371
|
+
"persona-text-persona-primary",
|
|
2372
|
+
"persona-px-5",
|
|
2373
|
+
"persona-py-3"
|
|
1830
2374
|
].join(" ")
|
|
1831
2375
|
: [
|
|
1832
|
-
"
|
|
1833
|
-
"
|
|
1834
|
-
"
|
|
1835
|
-
"
|
|
2376
|
+
"persona-max-w-[85%]",
|
|
2377
|
+
"persona-text-sm",
|
|
2378
|
+
"persona-leading-relaxed",
|
|
2379
|
+
"persona-text-persona-primary"
|
|
1836
2380
|
].join(" ");
|
|
1837
2381
|
idleBubble.setAttribute("data-idle-indicator", "true");
|
|
1838
2382
|
|
|
1839
2383
|
idleBubble.appendChild(idleIndicator);
|
|
1840
2384
|
|
|
1841
2385
|
const idleWrapper = document.createElement("div");
|
|
1842
|
-
idleWrapper.className = "
|
|
2386
|
+
idleWrapper.className = "persona-flex";
|
|
1843
2387
|
// Set id for idiomorph matching
|
|
1844
2388
|
idleWrapper.id = "wrapper-idle-indicator";
|
|
1845
2389
|
idleWrapper.setAttribute("data-wrapper-id", "idle-indicator");
|
|
@@ -1867,10 +2411,12 @@ export const createAgentExperience = (
|
|
|
1867
2411
|
|
|
1868
2412
|
const updateOpenState = () => {
|
|
1869
2413
|
if (!launcherEnabled) return;
|
|
2414
|
+
const dockedMode = isDockedMountMode(config);
|
|
1870
2415
|
if (open) {
|
|
1871
|
-
wrapper.
|
|
1872
|
-
|
|
1873
|
-
panel.classList.
|
|
2416
|
+
wrapper.style.display = dockedMode ? "flex" : "";
|
|
2417
|
+
wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
|
|
2418
|
+
panel.classList.remove("persona-scale-95", "persona-opacity-0");
|
|
2419
|
+
panel.classList.add("persona-scale-100", "persona-opacity-100");
|
|
1874
2420
|
// Hide launcher button when widget is open
|
|
1875
2421
|
if (launcherButtonInstance) {
|
|
1876
2422
|
launcherButtonInstance.element.style.display = "none";
|
|
@@ -1878,9 +2424,16 @@ export const createAgentExperience = (
|
|
|
1878
2424
|
customLauncherElement.style.display = "none";
|
|
1879
2425
|
}
|
|
1880
2426
|
} else {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2427
|
+
if (dockedMode) {
|
|
2428
|
+
wrapper.style.display = "none";
|
|
2429
|
+
wrapper.classList.remove("persona-pointer-events-none", "persona-opacity-0");
|
|
2430
|
+
panel.classList.remove("persona-scale-100", "persona-opacity-100", "persona-scale-95", "persona-opacity-0");
|
|
2431
|
+
} else {
|
|
2432
|
+
wrapper.style.display = "";
|
|
2433
|
+
wrapper.classList.add("persona-pointer-events-none", "persona-opacity-0");
|
|
2434
|
+
panel.classList.remove("persona-scale-100", "persona-opacity-100");
|
|
2435
|
+
panel.classList.add("persona-scale-95", "persona-opacity-0");
|
|
2436
|
+
}
|
|
1884
2437
|
// Show launcher button when widget is closed
|
|
1885
2438
|
if (launcherButtonInstance) {
|
|
1886
2439
|
launcherButtonInstance.element.style.display = "";
|
|
@@ -1935,6 +2488,17 @@ export const createAgentExperience = (
|
|
|
1935
2488
|
suggestionsManager.buttons.forEach((btn) => {
|
|
1936
2489
|
btn.disabled = disabled;
|
|
1937
2490
|
});
|
|
2491
|
+
footer.dataset.personaComposerStreaming = disabled ? "true" : "false";
|
|
2492
|
+
footer.querySelectorAll<HTMLElement>("[data-persona-composer-disable-when-streaming]").forEach((el) => {
|
|
2493
|
+
if (
|
|
2494
|
+
el instanceof HTMLButtonElement ||
|
|
2495
|
+
el instanceof HTMLInputElement ||
|
|
2496
|
+
el instanceof HTMLTextAreaElement ||
|
|
2497
|
+
el instanceof HTMLSelectElement
|
|
2498
|
+
) {
|
|
2499
|
+
el.disabled = disabled;
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
1938
2502
|
};
|
|
1939
2503
|
|
|
1940
2504
|
const maybeFocusInput = () => {
|
|
@@ -2053,16 +2617,44 @@ export const createAgentExperience = (
|
|
|
2053
2617
|
}
|
|
2054
2618
|
},
|
|
2055
2619
|
onVoiceStatusChanged(status: VoiceStatus) {
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2620
|
+
if (config.voiceRecognition?.provider?.type !== 'runtype') return;
|
|
2621
|
+
|
|
2622
|
+
switch (status) {
|
|
2623
|
+
case 'listening':
|
|
2624
|
+
// Recording styles are applied by toggleVoice() / startVoiceRecognition() flows
|
|
2625
|
+
break;
|
|
2626
|
+
case 'processing':
|
|
2627
|
+
removeRuntypeMicStateStyles();
|
|
2628
|
+
applyRuntypeMicProcessingStyles();
|
|
2629
|
+
break;
|
|
2630
|
+
case 'speaking':
|
|
2631
|
+
removeRuntypeMicStateStyles();
|
|
2632
|
+
applyRuntypeMicSpeakingStyles();
|
|
2633
|
+
break;
|
|
2634
|
+
default:
|
|
2635
|
+
// idle, connected, disconnected, error
|
|
2636
|
+
if (status === 'idle' && session.isBargeInActive()) {
|
|
2637
|
+
// Barge-in mic is still hot between turns — show it as active
|
|
2638
|
+
removeRuntypeMicStateStyles();
|
|
2639
|
+
applyRuntypeMicRecordingStyles();
|
|
2640
|
+
micButton?.setAttribute("aria-label", "End voice session");
|
|
2641
|
+
} else {
|
|
2642
|
+
voiceState.active = false;
|
|
2643
|
+
removeRuntypeMicStateStyles();
|
|
2644
|
+
emitVoiceState("system");
|
|
2645
|
+
persistVoiceMetadata();
|
|
2646
|
+
}
|
|
2647
|
+
break;
|
|
2062
2648
|
}
|
|
2649
|
+
},
|
|
2650
|
+
onArtifactsState(state) {
|
|
2651
|
+
lastArtifactsState = state;
|
|
2652
|
+
syncArtifactPane();
|
|
2063
2653
|
}
|
|
2064
2654
|
});
|
|
2065
2655
|
|
|
2656
|
+
sessionRef.current = session;
|
|
2657
|
+
|
|
2066
2658
|
// Setup Runtype voice provider when configured (connects WebSocket for server-side STT)
|
|
2067
2659
|
if (config.voiceRecognition?.provider?.type === 'runtype') {
|
|
2068
2660
|
try {
|
|
@@ -2176,6 +2768,8 @@ export const createAgentExperience = (
|
|
|
2176
2768
|
backgroundColor: string;
|
|
2177
2769
|
color: string;
|
|
2178
2770
|
borderColor: string;
|
|
2771
|
+
iconName: string;
|
|
2772
|
+
iconSize: number;
|
|
2179
2773
|
} | null = null;
|
|
2180
2774
|
|
|
2181
2775
|
const getSpeechRecognitionClass = (): any => {
|
|
@@ -2273,20 +2867,22 @@ export const createAgentExperience = (
|
|
|
2273
2867
|
emitVoiceState(source);
|
|
2274
2868
|
persistVoiceMetadata();
|
|
2275
2869
|
if (micButton) {
|
|
2276
|
-
// Store original styles
|
|
2870
|
+
// Store original styles (including icon info for restoration)
|
|
2871
|
+
const voiceConfig = config.voiceRecognition ?? {};
|
|
2277
2872
|
originalMicStyles = {
|
|
2278
2873
|
backgroundColor: micButton.style.backgroundColor,
|
|
2279
2874
|
color: micButton.style.color,
|
|
2280
|
-
borderColor: micButton.style.borderColor
|
|
2875
|
+
borderColor: micButton.style.borderColor,
|
|
2876
|
+
iconName: voiceConfig.iconName ?? "mic",
|
|
2877
|
+
iconSize: parseFloat(voiceConfig.iconSize ?? config.sendButton?.size ?? "40") || 24,
|
|
2281
2878
|
};
|
|
2282
|
-
|
|
2879
|
+
|
|
2283
2880
|
// Apply recording state styles from config
|
|
2284
|
-
const voiceConfig = config.voiceRecognition ?? {};
|
|
2285
2881
|
const recordingBackgroundColor = voiceConfig.recordingBackgroundColor ?? "#ef4444";
|
|
2286
2882
|
const recordingIconColor = voiceConfig.recordingIconColor;
|
|
2287
2883
|
const recordingBorderColor = voiceConfig.recordingBorderColor;
|
|
2288
2884
|
|
|
2289
|
-
micButton.classList.add("
|
|
2885
|
+
micButton.classList.add("persona-voice-recording");
|
|
2290
2886
|
micButton.style.backgroundColor = recordingBackgroundColor;
|
|
2291
2887
|
|
|
2292
2888
|
if (recordingIconColor) {
|
|
@@ -2334,7 +2930,7 @@ export const createAgentExperience = (
|
|
|
2334
2930
|
persistVoiceMetadata();
|
|
2335
2931
|
|
|
2336
2932
|
if (micButton) {
|
|
2337
|
-
micButton.classList.remove("
|
|
2933
|
+
micButton.classList.remove("persona-voice-recording");
|
|
2338
2934
|
|
|
2339
2935
|
// Restore original styles
|
|
2340
2936
|
if (originalMicStyles) {
|
|
@@ -2366,10 +2962,10 @@ export const createAgentExperience = (
|
|
|
2366
2962
|
|
|
2367
2963
|
if (!hasVoiceInput) return null;
|
|
2368
2964
|
|
|
2369
|
-
const micButtonWrapper = createElement("div", "
|
|
2965
|
+
const micButtonWrapper = createElement("div", "persona-send-button-wrapper");
|
|
2370
2966
|
const micButton = createElement(
|
|
2371
2967
|
"button",
|
|
2372
|
-
"
|
|
2968
|
+
"persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer"
|
|
2373
2969
|
) as HTMLButtonElement;
|
|
2374
2970
|
|
|
2375
2971
|
micButton.type = "button";
|
|
@@ -2407,14 +3003,14 @@ export const createAgentExperience = (
|
|
|
2407
3003
|
if (backgroundColor) {
|
|
2408
3004
|
micButton.style.backgroundColor = backgroundColor;
|
|
2409
3005
|
} else {
|
|
2410
|
-
micButton.classList.add("
|
|
3006
|
+
micButton.classList.add("persona-bg-persona-primary");
|
|
2411
3007
|
}
|
|
2412
3008
|
|
|
2413
3009
|
// Apply icon/text color
|
|
2414
3010
|
if (iconColor) {
|
|
2415
3011
|
micButton.style.color = iconColor;
|
|
2416
3012
|
} else if (!iconColor && !sendButtonConfig?.textColor) {
|
|
2417
|
-
micButton.classList.add("
|
|
3013
|
+
micButton.classList.add("persona-text-white");
|
|
2418
3014
|
}
|
|
2419
3015
|
|
|
2420
3016
|
// Apply border styling
|
|
@@ -2442,7 +3038,7 @@ export const createAgentExperience = (
|
|
|
2442
3038
|
const tooltipText = voiceConfig?.tooltipText ?? "Start voice recognition";
|
|
2443
3039
|
const showTooltip = voiceConfig?.showTooltip ?? false;
|
|
2444
3040
|
if (showTooltip && tooltipText) {
|
|
2445
|
-
const tooltip = createElement("div", "
|
|
3041
|
+
const tooltip = createElement("div", "persona-send-button-tooltip");
|
|
2446
3042
|
tooltip.textContent = tooltipText;
|
|
2447
3043
|
micButtonWrapper.appendChild(tooltip);
|
|
2448
3044
|
}
|
|
@@ -2450,19 +3046,47 @@ export const createAgentExperience = (
|
|
|
2450
3046
|
return { micButton, micButtonWrapper };
|
|
2451
3047
|
};
|
|
2452
3048
|
|
|
2453
|
-
// Helpers to
|
|
2454
|
-
|
|
2455
|
-
|
|
3049
|
+
// --- Helpers to store/restore original mic button state ---
|
|
3050
|
+
|
|
3051
|
+
const storeOriginalMicStyles = () => {
|
|
3052
|
+
if (!micButton || originalMicStyles) return; // Already stored
|
|
3053
|
+
const voiceConfig = config.voiceRecognition ?? {};
|
|
2456
3054
|
originalMicStyles = {
|
|
2457
3055
|
backgroundColor: micButton.style.backgroundColor,
|
|
2458
3056
|
color: micButton.style.color,
|
|
2459
|
-
borderColor: micButton.style.borderColor
|
|
3057
|
+
borderColor: micButton.style.borderColor,
|
|
3058
|
+
iconName: voiceConfig.iconName ?? "mic",
|
|
3059
|
+
iconSize: parseFloat(voiceConfig.iconSize ?? config.sendButton?.size ?? "40") || 24,
|
|
2460
3060
|
};
|
|
3061
|
+
};
|
|
3062
|
+
|
|
3063
|
+
/** Swap the mic button's SVG icon */
|
|
3064
|
+
const swapMicIcon = (iconName: string, color: string) => {
|
|
3065
|
+
if (!micButton) return;
|
|
3066
|
+
const existingSvg = micButton.querySelector("svg");
|
|
3067
|
+
if (existingSvg) existingSvg.remove();
|
|
3068
|
+
const size = originalMicStyles?.iconSize ?? (parseFloat(config.voiceRecognition?.iconSize ?? config.sendButton?.size ?? "40") || 24);
|
|
3069
|
+
const newSvg = renderLucideIcon(iconName, size, color, 1.5);
|
|
3070
|
+
if (newSvg) micButton.appendChild(newSvg);
|
|
3071
|
+
};
|
|
3072
|
+
|
|
3073
|
+
/** Remove all voice state CSS classes */
|
|
3074
|
+
const removeAllVoiceStateClasses = () => {
|
|
3075
|
+
if (!micButton) return;
|
|
3076
|
+
micButton.classList.remove("persona-voice-recording", "persona-voice-processing", "persona-voice-speaking");
|
|
3077
|
+
};
|
|
3078
|
+
|
|
3079
|
+
// --- Per-state style application ---
|
|
3080
|
+
|
|
3081
|
+
const applyRuntypeMicRecordingStyles = () => {
|
|
3082
|
+
if (!micButton) return;
|
|
3083
|
+
storeOriginalMicStyles();
|
|
2461
3084
|
const voiceConfig = config.voiceRecognition ?? {};
|
|
2462
3085
|
const recordingBackgroundColor = voiceConfig.recordingBackgroundColor ?? "#ef4444";
|
|
2463
3086
|
const recordingIconColor = voiceConfig.recordingIconColor;
|
|
2464
3087
|
const recordingBorderColor = voiceConfig.recordingBorderColor;
|
|
2465
|
-
|
|
3088
|
+
removeAllVoiceStateClasses();
|
|
3089
|
+
micButton.classList.add("persona-voice-recording");
|
|
2466
3090
|
micButton.style.backgroundColor = recordingBackgroundColor;
|
|
2467
3091
|
if (recordingIconColor) {
|
|
2468
3092
|
micButton.style.color = recordingIconColor;
|
|
@@ -2472,17 +3096,86 @@ export const createAgentExperience = (
|
|
|
2472
3096
|
if (recordingBorderColor) micButton.style.borderColor = recordingBorderColor;
|
|
2473
3097
|
micButton.setAttribute("aria-label", "Stop voice recognition");
|
|
2474
3098
|
};
|
|
2475
|
-
|
|
3099
|
+
|
|
3100
|
+
const applyRuntypeMicProcessingStyles = () => {
|
|
3101
|
+
if (!micButton) return;
|
|
3102
|
+
storeOriginalMicStyles();
|
|
3103
|
+
const voiceConfig = config.voiceRecognition ?? {};
|
|
3104
|
+
const interruptionMode = session.getVoiceInterruptionMode();
|
|
3105
|
+
const iconName = voiceConfig.processingIconName ?? "loader";
|
|
3106
|
+
const iconColor = voiceConfig.processingIconColor ?? originalMicStyles?.color ?? "";
|
|
3107
|
+
const bgColor = voiceConfig.processingBackgroundColor ?? originalMicStyles?.backgroundColor ?? "";
|
|
3108
|
+
const borderColor = voiceConfig.processingBorderColor ?? originalMicStyles?.borderColor ?? "";
|
|
3109
|
+
|
|
3110
|
+
removeAllVoiceStateClasses();
|
|
3111
|
+
micButton.classList.add("persona-voice-processing");
|
|
3112
|
+
micButton.style.backgroundColor = bgColor;
|
|
3113
|
+
micButton.style.borderColor = borderColor;
|
|
3114
|
+
const resolvedColor = iconColor || "currentColor";
|
|
3115
|
+
micButton.style.color = resolvedColor;
|
|
3116
|
+
swapMicIcon(iconName, resolvedColor);
|
|
3117
|
+
micButton.setAttribute("aria-label", "Processing voice input");
|
|
3118
|
+
// In "none" mode the button is not actionable during processing
|
|
3119
|
+
if (interruptionMode === "none") {
|
|
3120
|
+
micButton.style.cursor = "default";
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
|
|
3124
|
+
const applyRuntypeMicSpeakingStyles = () => {
|
|
3125
|
+
if (!micButton) return;
|
|
3126
|
+
storeOriginalMicStyles();
|
|
3127
|
+
const voiceConfig = config.voiceRecognition ?? {};
|
|
3128
|
+
const interruptionMode = session.getVoiceInterruptionMode();
|
|
3129
|
+
// Default icon depends on interruption mode:
|
|
3130
|
+
// "square" for cancel, "mic" for barge-in (hot mic), "volume-2" otherwise
|
|
3131
|
+
const defaultSpeakingIcon = interruptionMode === "cancel" ? "square"
|
|
3132
|
+
: interruptionMode === "barge-in" ? "mic"
|
|
3133
|
+
: "volume-2";
|
|
3134
|
+
const iconName = voiceConfig.speakingIconName ?? defaultSpeakingIcon;
|
|
3135
|
+
const iconColor = voiceConfig.speakingIconColor
|
|
3136
|
+
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingIconColor ?? originalMicStyles?.color ?? "") : (originalMicStyles?.color ?? ""));
|
|
3137
|
+
const bgColor = voiceConfig.speakingBackgroundColor
|
|
3138
|
+
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingBackgroundColor ?? "#ef4444") : (originalMicStyles?.backgroundColor ?? ""));
|
|
3139
|
+
const borderColor = voiceConfig.speakingBorderColor
|
|
3140
|
+
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingBorderColor ?? "") : (originalMicStyles?.borderColor ?? ""));
|
|
3141
|
+
|
|
3142
|
+
removeAllVoiceStateClasses();
|
|
3143
|
+
micButton.classList.add("persona-voice-speaking");
|
|
3144
|
+
micButton.style.backgroundColor = bgColor;
|
|
3145
|
+
micButton.style.borderColor = borderColor;
|
|
3146
|
+
const resolvedColor = iconColor || "currentColor";
|
|
3147
|
+
micButton.style.color = resolvedColor;
|
|
3148
|
+
swapMicIcon(iconName, resolvedColor);
|
|
3149
|
+
|
|
3150
|
+
// aria-label varies by interruption mode
|
|
3151
|
+
const ariaLabel = interruptionMode === "cancel"
|
|
3152
|
+
? "Stop playback and re-record"
|
|
3153
|
+
: interruptionMode === "barge-in"
|
|
3154
|
+
? "Speak to interrupt"
|
|
3155
|
+
: "Agent is speaking";
|
|
3156
|
+
micButton.setAttribute("aria-label", ariaLabel);
|
|
3157
|
+
// In "none" mode the button is not actionable during speaking
|
|
3158
|
+
if (interruptionMode === "none") {
|
|
3159
|
+
micButton.style.cursor = "default";
|
|
3160
|
+
}
|
|
3161
|
+
// In "barge-in" mode, add recording class to show mic is hot
|
|
3162
|
+
if (interruptionMode === "barge-in") {
|
|
3163
|
+
micButton.classList.add("persona-voice-recording");
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
|
|
3167
|
+
/** Restore mic button to idle state (icon, colors, aria-label, cursor) */
|
|
3168
|
+
const removeRuntypeMicStateStyles = () => {
|
|
2476
3169
|
if (!micButton) return;
|
|
2477
|
-
|
|
3170
|
+
removeAllVoiceStateClasses();
|
|
2478
3171
|
if (originalMicStyles) {
|
|
2479
3172
|
micButton.style.backgroundColor = originalMicStyles.backgroundColor ?? "";
|
|
2480
3173
|
micButton.style.color = originalMicStyles.color ?? "";
|
|
2481
3174
|
micButton.style.borderColor = originalMicStyles.borderColor ?? "";
|
|
2482
|
-
|
|
2483
|
-
if (svg) svg.setAttribute("stroke", originalMicStyles.color || "currentColor");
|
|
3175
|
+
swapMicIcon(originalMicStyles.iconName, originalMicStyles.color || "currentColor");
|
|
2484
3176
|
originalMicStyles = null;
|
|
2485
3177
|
}
|
|
3178
|
+
micButton.style.cursor = "";
|
|
2486
3179
|
micButton.setAttribute("aria-label", "Start voice recognition");
|
|
2487
3180
|
};
|
|
2488
3181
|
|
|
@@ -2490,6 +3183,36 @@ export const createAgentExperience = (
|
|
|
2490
3183
|
const handleMicButtonClick = () => {
|
|
2491
3184
|
// Runtype provider: use session.toggleVoice() (WebSocket-based STT)
|
|
2492
3185
|
if (config.voiceRecognition?.provider?.type === 'runtype') {
|
|
3186
|
+
const voiceStatus = session.getVoiceStatus();
|
|
3187
|
+
const interruptionMode = session.getVoiceInterruptionMode();
|
|
3188
|
+
|
|
3189
|
+
// In "none" mode, ignore clicks while processing or speaking
|
|
3190
|
+
if (interruptionMode === "none" &&
|
|
3191
|
+
(voiceStatus === "processing" || voiceStatus === "speaking")) {
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// In "cancel" mode during processing/speaking: stop playback only
|
|
3196
|
+
if (interruptionMode === "cancel" &&
|
|
3197
|
+
(voiceStatus === "processing" || voiceStatus === "speaking")) {
|
|
3198
|
+
session.stopVoicePlayback();
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// In barge-in mode, clicking mic = "hang up" (any state: speaking, idle, etc.)
|
|
3203
|
+
// Stops playback if active, tears down the always-on mic.
|
|
3204
|
+
if (session.isBargeInActive()) {
|
|
3205
|
+
session.stopVoicePlayback();
|
|
3206
|
+
session.deactivateBargeIn().then(() => {
|
|
3207
|
+
voiceState.active = false;
|
|
3208
|
+
voiceState.manuallyDeactivated = true;
|
|
3209
|
+
persistVoiceMetadata();
|
|
3210
|
+
emitVoiceState("user");
|
|
3211
|
+
removeRuntypeMicStateStyles();
|
|
3212
|
+
});
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
2493
3216
|
session.toggleVoice().then(() => {
|
|
2494
3217
|
voiceState.active = session.isVoiceActive();
|
|
2495
3218
|
voiceState.manuallyDeactivated = !session.isVoiceActive();
|
|
@@ -2498,7 +3221,7 @@ export const createAgentExperience = (
|
|
|
2498
3221
|
if (session.isVoiceActive()) {
|
|
2499
3222
|
applyRuntypeMicRecordingStyles();
|
|
2500
3223
|
} else {
|
|
2501
|
-
|
|
3224
|
+
removeRuntypeMicStateStyles();
|
|
2502
3225
|
}
|
|
2503
3226
|
});
|
|
2504
3227
|
return;
|
|
@@ -2524,13 +3247,15 @@ export const createAgentExperience = (
|
|
|
2524
3247
|
}
|
|
2525
3248
|
};
|
|
2526
3249
|
|
|
3250
|
+
composerVoiceBridge = handleMicButtonClick;
|
|
3251
|
+
|
|
2527
3252
|
if (micButton) {
|
|
2528
3253
|
micButton.addEventListener("click", handleMicButtonClick);
|
|
2529
3254
|
|
|
2530
3255
|
destroyCallbacks.push(() => {
|
|
2531
3256
|
if (config.voiceRecognition?.provider?.type === 'runtype') {
|
|
2532
3257
|
if (session.isVoiceActive()) session.toggleVoice();
|
|
2533
|
-
|
|
3258
|
+
removeRuntypeMicStateStyles();
|
|
2534
3259
|
} else {
|
|
2535
3260
|
stopVoiceRecognition("system");
|
|
2536
3261
|
}
|
|
@@ -2627,26 +3352,48 @@ export const createAgentExperience = (
|
|
|
2627
3352
|
}
|
|
2628
3353
|
|
|
2629
3354
|
const recalcPanelHeight = () => {
|
|
3355
|
+
const dockedMode = isDockedMountMode(config);
|
|
2630
3356
|
const sidebarMode = config.launcher?.sidebarMode ?? false;
|
|
2631
|
-
const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
2632
|
-
|
|
2633
|
-
|
|
3357
|
+
const fullHeight = dockedMode || sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
3358
|
+
|
|
3359
|
+
// Mobile fullscreen: re-apply fullscreen styles on resize (handles orientation changes)
|
|
3360
|
+
const ownerWindow = mount.ownerDocument.defaultView ?? window;
|
|
3361
|
+
const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
|
|
3362
|
+
const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
|
|
3363
|
+
const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
|
|
3364
|
+
const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
|
|
3365
|
+
|
|
3366
|
+
if (shouldGoFullscreen) {
|
|
3367
|
+
applyFullHeightStyles();
|
|
3368
|
+
applyThemeVariables(mount, config);
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
// Exiting mobile fullscreen (e.g., orientation change to landscape) — reset all styles
|
|
3373
|
+
if (wasMobileFullscreen) {
|
|
3374
|
+
wasMobileFullscreen = false;
|
|
3375
|
+
applyFullHeightStyles();
|
|
3376
|
+
applyThemeVariables(mount, config);
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
if (!launcherEnabled && !dockedMode) {
|
|
2634
3380
|
panel.style.height = "";
|
|
2635
3381
|
panel.style.width = "";
|
|
2636
3382
|
return;
|
|
2637
3383
|
}
|
|
2638
|
-
|
|
3384
|
+
|
|
2639
3385
|
// In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
|
|
2640
|
-
if (!sidebarMode) {
|
|
3386
|
+
if (!sidebarMode && !dockedMode) {
|
|
2641
3387
|
const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
|
|
2642
3388
|
const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
|
|
2643
3389
|
panel.style.width = width;
|
|
2644
3390
|
panel.style.maxWidth = width;
|
|
2645
3391
|
}
|
|
2646
|
-
|
|
3392
|
+
applyLauncherArtifactPanelWidth();
|
|
3393
|
+
|
|
2647
3394
|
// In fullHeight mode, don't set a fixed height
|
|
2648
3395
|
if (!fullHeight) {
|
|
2649
|
-
const viewportHeight =
|
|
3396
|
+
const viewportHeight = ownerWindow.innerHeight;
|
|
2650
3397
|
const verticalMargin = 64; // leave space for launcher's offset
|
|
2651
3398
|
const heightOffset = config.launcher?.heightOffset ?? 0;
|
|
2652
3399
|
const available = Math.max(200, viewportHeight - verticalMargin);
|
|
@@ -2657,8 +3404,9 @@ export const createAgentExperience = (
|
|
|
2657
3404
|
};
|
|
2658
3405
|
|
|
2659
3406
|
recalcPanelHeight();
|
|
2660
|
-
|
|
2661
|
-
|
|
3407
|
+
const ownerWindow = mount.ownerDocument.defaultView ?? window;
|
|
3408
|
+
ownerWindow.addEventListener("resize", recalcPanelHeight);
|
|
3409
|
+
destroyCallbacks.push(() => ownerWindow.removeEventListener("resize", recalcPanelHeight));
|
|
2662
3410
|
|
|
2663
3411
|
lastScrollTop = body.scrollTop;
|
|
2664
3412
|
|
|
@@ -2701,8 +3449,7 @@ export const createAgentExperience = (
|
|
|
2701
3449
|
if (launcherEnabled) {
|
|
2702
3450
|
closeButton.style.display = "";
|
|
2703
3451
|
closeHandler = () => {
|
|
2704
|
-
|
|
2705
|
-
updateOpenState();
|
|
3452
|
+
setOpenState(false, "user");
|
|
2706
3453
|
};
|
|
2707
3454
|
closeButton.addEventListener("click", closeHandler);
|
|
2708
3455
|
} else {
|
|
@@ -2720,6 +3467,7 @@ export const createAgentExperience = (
|
|
|
2720
3467
|
clearChatButton.addEventListener("click", () => {
|
|
2721
3468
|
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
2722
3469
|
session.clearMessages();
|
|
3470
|
+
messageCache.clear();
|
|
2723
3471
|
|
|
2724
3472
|
// Always clear the default localStorage key
|
|
2725
3473
|
try {
|
|
@@ -2778,14 +3526,18 @@ export const createAgentExperience = (
|
|
|
2778
3526
|
|
|
2779
3527
|
setupClearChatButton();
|
|
2780
3528
|
|
|
2781
|
-
composerForm
|
|
2782
|
-
|
|
2783
|
-
|
|
3529
|
+
if (composerForm) {
|
|
3530
|
+
composerForm.addEventListener("submit", handleSubmit);
|
|
3531
|
+
}
|
|
3532
|
+
textarea?.addEventListener("keydown", handleInputEnter);
|
|
3533
|
+
textarea?.addEventListener("paste", handleInputPaste);
|
|
2784
3534
|
|
|
2785
3535
|
destroyCallbacks.push(() => {
|
|
2786
|
-
composerForm
|
|
2787
|
-
|
|
2788
|
-
|
|
3536
|
+
if (composerForm) {
|
|
3537
|
+
composerForm.removeEventListener("submit", handleSubmit);
|
|
3538
|
+
}
|
|
3539
|
+
textarea?.removeEventListener("keydown", handleInputEnter);
|
|
3540
|
+
textarea?.removeEventListener("paste", handleInputPaste);
|
|
2789
3541
|
});
|
|
2790
3542
|
|
|
2791
3543
|
destroyCallbacks.push(() => {
|
|
@@ -2805,11 +3557,16 @@ export const createAgentExperience = (
|
|
|
2805
3557
|
const controller: Controller = {
|
|
2806
3558
|
update(nextConfig: AgentWidgetConfig) {
|
|
2807
3559
|
const previousToolCallConfig = config.toolCall;
|
|
3560
|
+
const previousMessageActions = config.messageActions;
|
|
3561
|
+
const previousLayoutMessages = config.layout?.messages;
|
|
2808
3562
|
const previousColorScheme = config.colorScheme;
|
|
2809
3563
|
config = { ...config, ...nextConfig };
|
|
2810
3564
|
// applyFullHeightStyles resets mount.style.cssText, so call it before applyThemeVariables
|
|
2811
3565
|
applyFullHeightStyles();
|
|
2812
3566
|
applyThemeVariables(mount, config);
|
|
3567
|
+
applyArtifactLayoutCssVars(mount, config);
|
|
3568
|
+
applyArtifactPaneAppearance(mount, config);
|
|
3569
|
+
syncArtifactPane();
|
|
2813
3570
|
|
|
2814
3571
|
// Re-setup theme observer if colorScheme changed
|
|
2815
3572
|
if (config.colorScheme !== previousColorScheme) {
|
|
@@ -2848,7 +3605,7 @@ export const createAgentExperience = (
|
|
|
2848
3605
|
// Add header toggle button if not present
|
|
2849
3606
|
if (!eventStreamToggleBtn && header) {
|
|
2850
3607
|
const dynEsClassNames = config.features?.eventStream?.classNames;
|
|
2851
|
-
const dynToggleBtnClasses = "
|
|
3608
|
+
const dynToggleBtnClasses = "persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-text-persona-muted hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none persona-bg-transparent persona-p-1" + (dynEsClassNames?.toggleButton ? " " + dynEsClassNames.toggleButton : "");
|
|
2852
3609
|
eventStreamToggleBtn = createElement("button", dynToggleBtnClasses) as HTMLButtonElement;
|
|
2853
3610
|
eventStreamToggleBtn.style.width = "28px";
|
|
2854
3611
|
eventStreamToggleBtn.style.height = "28px";
|
|
@@ -2980,11 +3737,11 @@ export const createAgentExperience = (
|
|
|
2980
3737
|
panelElements.clearChatButtonWrapper.style.display = showClearChat ? "" : "none";
|
|
2981
3738
|
// When clear chat is hidden, close button needs ml-auto to stay right-aligned
|
|
2982
3739
|
const { closeButtonWrapper } = panelElements;
|
|
2983
|
-
if (closeButtonWrapper && !closeButtonWrapper.classList.contains("
|
|
3740
|
+
if (closeButtonWrapper && !closeButtonWrapper.classList.contains("persona-absolute")) {
|
|
2984
3741
|
if (showClearChat) {
|
|
2985
|
-
closeButtonWrapper.classList.remove("
|
|
3742
|
+
closeButtonWrapper.classList.remove("persona-ml-auto");
|
|
2986
3743
|
} else {
|
|
2987
|
-
closeButtonWrapper.classList.add("
|
|
3744
|
+
closeButtonWrapper.classList.add("persona-ml-auto");
|
|
2988
3745
|
}
|
|
2989
3746
|
}
|
|
2990
3747
|
}
|
|
@@ -3029,9 +3786,13 @@ export const createAgentExperience = (
|
|
|
3029
3786
|
recalcPanelHeight();
|
|
3030
3787
|
refreshCloseButton();
|
|
3031
3788
|
|
|
3032
|
-
// Re-render messages if
|
|
3789
|
+
// Re-render messages if config affecting message rendering changed
|
|
3033
3790
|
const toolCallConfigChanged = JSON.stringify(nextConfig.toolCall) !== JSON.stringify(previousToolCallConfig);
|
|
3034
|
-
|
|
3791
|
+
const messageActionsChanged = JSON.stringify(config.messageActions) !== JSON.stringify(previousMessageActions);
|
|
3792
|
+
const layoutMessagesChanged = JSON.stringify(config.layout?.messages) !== JSON.stringify(previousLayoutMessages);
|
|
3793
|
+
const messagesConfigChanged = toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged;
|
|
3794
|
+
if (messagesConfigChanged && session) {
|
|
3795
|
+
configVersion++;
|
|
3035
3796
|
renderMessagesWithPlugins(messagesWrapper, session.getMessages(), postprocess);
|
|
3036
3797
|
}
|
|
3037
3798
|
|
|
@@ -3045,8 +3806,8 @@ export const createAgentExperience = (
|
|
|
3045
3806
|
const headerIconSize = launcher.headerIconSize ?? "48px";
|
|
3046
3807
|
|
|
3047
3808
|
if (iconHolder) {
|
|
3048
|
-
const headerEl = container.querySelector(".
|
|
3049
|
-
const headerCopy = headerEl?.querySelector(".
|
|
3809
|
+
const headerEl = container.querySelector(".persona-border-b-persona-divider");
|
|
3810
|
+
const headerCopy = headerEl?.querySelector(".persona-flex-col");
|
|
3050
3811
|
|
|
3051
3812
|
// Handle hide/show
|
|
3052
3813
|
if (shouldHideIcon) {
|
|
@@ -3096,7 +3857,7 @@ export const createAgentExperience = (
|
|
|
3096
3857
|
const newImg = document.createElement("img");
|
|
3097
3858
|
newImg.src = launcher.iconUrl;
|
|
3098
3859
|
newImg.alt = "";
|
|
3099
|
-
newImg.className = "
|
|
3860
|
+
newImg.className = "persona-rounded-xl persona-object-cover";
|
|
3100
3861
|
newImg.style.height = headerIconSize;
|
|
3101
3862
|
newImg.style.width = headerIconSize;
|
|
3102
3863
|
iconHolder.replaceChildren(newImg);
|
|
@@ -3147,7 +3908,7 @@ export const createAgentExperience = (
|
|
|
3147
3908
|
// Update placement if changed - move the wrapper (not just the button) to preserve tooltip
|
|
3148
3909
|
const { closeButtonWrapper } = panelElements;
|
|
3149
3910
|
const isTopRight = closeButtonPlacement === "top-right";
|
|
3150
|
-
const currentlyTopRight = closeButtonWrapper?.classList.contains("
|
|
3911
|
+
const currentlyTopRight = closeButtonWrapper?.classList.contains("persona-absolute");
|
|
3151
3912
|
|
|
3152
3913
|
if (closeButtonWrapper && isTopRight !== currentlyTopRight) {
|
|
3153
3914
|
// Placement changed - need to move wrapper and update classes
|
|
@@ -3155,16 +3916,16 @@ export const createAgentExperience = (
|
|
|
3155
3916
|
|
|
3156
3917
|
// Update wrapper classes
|
|
3157
3918
|
if (isTopRight) {
|
|
3158
|
-
closeButtonWrapper.className = "
|
|
3919
|
+
closeButtonWrapper.className = "persona-absolute persona-top-4 persona-right-4 persona-z-50";
|
|
3159
3920
|
container.style.position = "relative";
|
|
3160
3921
|
container.appendChild(closeButtonWrapper);
|
|
3161
3922
|
} else {
|
|
3162
3923
|
// Check if clear chat is inline to determine if we need ml-auto
|
|
3163
3924
|
const clearChatPlacement = launcher.clearChat?.placement ?? "inline";
|
|
3164
3925
|
const clearChatEnabled = launcher.clearChat?.enabled ?? true;
|
|
3165
|
-
closeButtonWrapper.className = (clearChatEnabled && clearChatPlacement === "inline") ? "" : "
|
|
3926
|
+
closeButtonWrapper.className = (clearChatEnabled && clearChatPlacement === "inline") ? "" : "persona-ml-auto";
|
|
3166
3927
|
// Find header element
|
|
3167
|
-
const header = container.querySelector(".
|
|
3928
|
+
const header = container.querySelector(".persona-border-b-persona-divider");
|
|
3168
3929
|
if (header) {
|
|
3169
3930
|
header.appendChild(closeButtonWrapper);
|
|
3170
3931
|
}
|
|
@@ -3174,18 +3935,18 @@ export const createAgentExperience = (
|
|
|
3174
3935
|
// Apply close button styling from config
|
|
3175
3936
|
if (launcher.closeButtonColor) {
|
|
3176
3937
|
closeButton.style.color = launcher.closeButtonColor;
|
|
3177
|
-
closeButton.classList.remove("
|
|
3938
|
+
closeButton.classList.remove("persona-text-persona-muted");
|
|
3178
3939
|
} else {
|
|
3179
3940
|
closeButton.style.color = "";
|
|
3180
|
-
closeButton.classList.add("
|
|
3941
|
+
closeButton.classList.add("persona-text-persona-muted");
|
|
3181
3942
|
}
|
|
3182
3943
|
|
|
3183
3944
|
if (launcher.closeButtonBackgroundColor) {
|
|
3184
3945
|
closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
|
|
3185
|
-
closeButton.classList.remove("hover:
|
|
3946
|
+
closeButton.classList.remove("hover:persona-bg-gray-100");
|
|
3186
3947
|
} else {
|
|
3187
3948
|
closeButton.style.backgroundColor = "";
|
|
3188
|
-
closeButton.classList.add("hover:
|
|
3949
|
+
closeButton.classList.add("hover:persona-bg-gray-100");
|
|
3189
3950
|
}
|
|
3190
3951
|
|
|
3191
3952
|
// Apply border if width and/or color are provided
|
|
@@ -3193,18 +3954,18 @@ export const createAgentExperience = (
|
|
|
3193
3954
|
const borderWidth = launcher.closeButtonBorderWidth || "0px";
|
|
3194
3955
|
const borderColor = launcher.closeButtonBorderColor || "transparent";
|
|
3195
3956
|
closeButton.style.border = `${borderWidth} solid ${borderColor}`;
|
|
3196
|
-
closeButton.classList.remove("
|
|
3957
|
+
closeButton.classList.remove("persona-border-none");
|
|
3197
3958
|
} else {
|
|
3198
3959
|
closeButton.style.border = "";
|
|
3199
|
-
closeButton.classList.add("
|
|
3960
|
+
closeButton.classList.add("persona-border-none");
|
|
3200
3961
|
}
|
|
3201
3962
|
|
|
3202
3963
|
if (launcher.closeButtonBorderRadius) {
|
|
3203
3964
|
closeButton.style.borderRadius = launcher.closeButtonBorderRadius;
|
|
3204
|
-
closeButton.classList.remove("
|
|
3965
|
+
closeButton.classList.remove("persona-rounded-full");
|
|
3205
3966
|
} else {
|
|
3206
3967
|
closeButton.style.borderRadius = "";
|
|
3207
|
-
closeButton.classList.add("
|
|
3968
|
+
closeButton.classList.add("persona-rounded-full");
|
|
3208
3969
|
}
|
|
3209
3970
|
|
|
3210
3971
|
// Update padding
|
|
@@ -3256,13 +4017,21 @@ export const createAgentExperience = (
|
|
|
3256
4017
|
const showTooltip = () => {
|
|
3257
4018
|
if (portaledTooltip || !closeButton) return; // Already showing or button doesn't exist
|
|
3258
4019
|
|
|
4020
|
+
const tooltipDocument = closeButton.ownerDocument;
|
|
4021
|
+
const tooltipContainer = tooltipDocument.body;
|
|
4022
|
+
if (!tooltipContainer) return;
|
|
4023
|
+
|
|
3259
4024
|
// Create tooltip element
|
|
3260
|
-
portaledTooltip =
|
|
4025
|
+
portaledTooltip = createElementInDocument(
|
|
4026
|
+
tooltipDocument,
|
|
4027
|
+
"div",
|
|
4028
|
+
"persona-clear-chat-tooltip"
|
|
4029
|
+
);
|
|
3261
4030
|
portaledTooltip.textContent = closeButtonTooltipText;
|
|
3262
4031
|
|
|
3263
4032
|
// Add arrow
|
|
3264
|
-
const arrow =
|
|
3265
|
-
arrow.className = "
|
|
4033
|
+
const arrow = createElementInDocument(tooltipDocument, "div");
|
|
4034
|
+
arrow.className = "persona-clear-chat-tooltip-arrow";
|
|
3266
4035
|
portaledTooltip.appendChild(arrow);
|
|
3267
4036
|
|
|
3268
4037
|
// Get button position
|
|
@@ -3275,7 +4044,7 @@ export const createAgentExperience = (
|
|
|
3275
4044
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
3276
4045
|
|
|
3277
4046
|
// Append to body
|
|
3278
|
-
|
|
4047
|
+
tooltipContainer.appendChild(portaledTooltip);
|
|
3279
4048
|
};
|
|
3280
4049
|
|
|
3281
4050
|
const hideTooltip = () => {
|
|
@@ -3326,36 +4095,36 @@ export const createAgentExperience = (
|
|
|
3326
4095
|
|
|
3327
4096
|
// When clear chat is hidden, close button needs ml-auto to stay right-aligned
|
|
3328
4097
|
const { closeButtonWrapper } = panelElements;
|
|
3329
|
-
if (closeButtonWrapper && !closeButtonWrapper.classList.contains("
|
|
4098
|
+
if (closeButtonWrapper && !closeButtonWrapper.classList.contains("persona-absolute")) {
|
|
3330
4099
|
if (shouldShowClearChat) {
|
|
3331
|
-
closeButtonWrapper.classList.remove("
|
|
4100
|
+
closeButtonWrapper.classList.remove("persona-ml-auto");
|
|
3332
4101
|
} else {
|
|
3333
|
-
closeButtonWrapper.classList.add("
|
|
4102
|
+
closeButtonWrapper.classList.add("persona-ml-auto");
|
|
3334
4103
|
}
|
|
3335
4104
|
}
|
|
3336
4105
|
|
|
3337
4106
|
// Update placement if changed
|
|
3338
4107
|
const isTopRight = clearChatPlacement === "top-right";
|
|
3339
|
-
const currentlyTopRight = clearChatButtonWrapper.classList.contains("
|
|
4108
|
+
const currentlyTopRight = clearChatButtonWrapper.classList.contains("persona-absolute");
|
|
3340
4109
|
|
|
3341
4110
|
if (isTopRight !== currentlyTopRight && shouldShowClearChat) {
|
|
3342
4111
|
clearChatButtonWrapper.remove();
|
|
3343
4112
|
|
|
3344
4113
|
if (isTopRight) {
|
|
3345
|
-
// Don't use
|
|
4114
|
+
// Don't use persona-clear-chat-button-wrapper class for top-right mode as its
|
|
3346
4115
|
// display: inline-flex causes alignment issues with the close button
|
|
3347
|
-
clearChatButtonWrapper.className = "
|
|
4116
|
+
clearChatButtonWrapper.className = "persona-absolute persona-top-4 persona-z-50";
|
|
3348
4117
|
// Position to the left of the close button (which is at right: 1rem/16px)
|
|
3349
4118
|
// Close button is ~32px wide, plus small gap = 48px from right
|
|
3350
4119
|
clearChatButtonWrapper.style.right = "48px";
|
|
3351
4120
|
container.style.position = "relative";
|
|
3352
4121
|
container.appendChild(clearChatButtonWrapper);
|
|
3353
4122
|
} else {
|
|
3354
|
-
clearChatButtonWrapper.className = "
|
|
4123
|
+
clearChatButtonWrapper.className = "persona-relative persona-ml-auto persona-clear-chat-button-wrapper";
|
|
3355
4124
|
// Clear the inline right style when switching back to inline mode
|
|
3356
4125
|
clearChatButtonWrapper.style.right = "";
|
|
3357
4126
|
// Find header and insert before close button
|
|
3358
|
-
const header = container.querySelector(".
|
|
4127
|
+
const header = container.querySelector(".persona-border-b-persona-divider");
|
|
3359
4128
|
const closeButtonWrapperEl = panelElements.closeButtonWrapper;
|
|
3360
4129
|
if (header && closeButtonWrapperEl && closeButtonWrapperEl.parentElement === header) {
|
|
3361
4130
|
header.insertBefore(clearChatButtonWrapper, closeButtonWrapperEl);
|
|
@@ -3366,13 +4135,13 @@ export const createAgentExperience = (
|
|
|
3366
4135
|
|
|
3367
4136
|
// Also update close button's ml-auto class based on clear chat position
|
|
3368
4137
|
const closeButtonWrapperEl = panelElements.closeButtonWrapper;
|
|
3369
|
-
if (closeButtonWrapperEl && !closeButtonWrapperEl.classList.contains("
|
|
4138
|
+
if (closeButtonWrapperEl && !closeButtonWrapperEl.classList.contains("persona-absolute")) {
|
|
3370
4139
|
if (isTopRight) {
|
|
3371
4140
|
// Clear chat moved to top-right, close needs ml-auto
|
|
3372
|
-
closeButtonWrapperEl.classList.add("
|
|
4141
|
+
closeButtonWrapperEl.classList.add("persona-ml-auto");
|
|
3373
4142
|
} else {
|
|
3374
4143
|
// Clear chat is inline, close doesn't need ml-auto
|
|
3375
|
-
closeButtonWrapperEl.classList.remove("
|
|
4144
|
+
closeButtonWrapperEl.classList.remove("persona-ml-auto");
|
|
3376
4145
|
}
|
|
3377
4146
|
}
|
|
3378
4147
|
}
|
|
@@ -3398,19 +4167,19 @@ export const createAgentExperience = (
|
|
|
3398
4167
|
// Update icon color
|
|
3399
4168
|
if (clearChatIconColor) {
|
|
3400
4169
|
clearChatButton.style.color = clearChatIconColor;
|
|
3401
|
-
clearChatButton.classList.remove("
|
|
4170
|
+
clearChatButton.classList.remove("persona-text-persona-muted");
|
|
3402
4171
|
} else {
|
|
3403
4172
|
clearChatButton.style.color = "";
|
|
3404
|
-
clearChatButton.classList.add("
|
|
4173
|
+
clearChatButton.classList.add("persona-text-persona-muted");
|
|
3405
4174
|
}
|
|
3406
4175
|
|
|
3407
4176
|
// Update background color
|
|
3408
4177
|
if (clearChatConfig.backgroundColor) {
|
|
3409
4178
|
clearChatButton.style.backgroundColor = clearChatConfig.backgroundColor;
|
|
3410
|
-
clearChatButton.classList.remove("hover:
|
|
4179
|
+
clearChatButton.classList.remove("hover:persona-bg-gray-100");
|
|
3411
4180
|
} else {
|
|
3412
4181
|
clearChatButton.style.backgroundColor = "";
|
|
3413
|
-
clearChatButton.classList.add("hover:
|
|
4182
|
+
clearChatButton.classList.add("hover:persona-bg-gray-100");
|
|
3414
4183
|
}
|
|
3415
4184
|
|
|
3416
4185
|
// Update border
|
|
@@ -3418,19 +4187,19 @@ export const createAgentExperience = (
|
|
|
3418
4187
|
const borderWidth = clearChatConfig.borderWidth || "0px";
|
|
3419
4188
|
const borderColor = clearChatConfig.borderColor || "transparent";
|
|
3420
4189
|
clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
|
|
3421
|
-
clearChatButton.classList.remove("
|
|
4190
|
+
clearChatButton.classList.remove("persona-border-none");
|
|
3422
4191
|
} else {
|
|
3423
4192
|
clearChatButton.style.border = "";
|
|
3424
|
-
clearChatButton.classList.add("
|
|
4193
|
+
clearChatButton.classList.add("persona-border-none");
|
|
3425
4194
|
}
|
|
3426
4195
|
|
|
3427
4196
|
// Update border radius
|
|
3428
4197
|
if (clearChatConfig.borderRadius) {
|
|
3429
4198
|
clearChatButton.style.borderRadius = clearChatConfig.borderRadius;
|
|
3430
|
-
clearChatButton.classList.remove("
|
|
4199
|
+
clearChatButton.classList.remove("persona-rounded-full");
|
|
3431
4200
|
} else {
|
|
3432
4201
|
clearChatButton.style.borderRadius = "";
|
|
3433
|
-
clearChatButton.classList.add("
|
|
4202
|
+
clearChatButton.classList.add("persona-rounded-full");
|
|
3434
4203
|
}
|
|
3435
4204
|
|
|
3436
4205
|
// Update padding
|
|
@@ -3468,13 +4237,21 @@ export const createAgentExperience = (
|
|
|
3468
4237
|
const showTooltip = () => {
|
|
3469
4238
|
if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
|
|
3470
4239
|
|
|
4240
|
+
const tooltipDocument = clearChatButton.ownerDocument;
|
|
4241
|
+
const tooltipContainer = tooltipDocument.body;
|
|
4242
|
+
if (!tooltipContainer) return;
|
|
4243
|
+
|
|
3471
4244
|
// Create tooltip element
|
|
3472
|
-
portaledTooltip =
|
|
4245
|
+
portaledTooltip = createElementInDocument(
|
|
4246
|
+
tooltipDocument,
|
|
4247
|
+
"div",
|
|
4248
|
+
"persona-clear-chat-tooltip"
|
|
4249
|
+
);
|
|
3473
4250
|
portaledTooltip.textContent = clearChatTooltipText;
|
|
3474
4251
|
|
|
3475
4252
|
// Add arrow
|
|
3476
|
-
const arrow =
|
|
3477
|
-
arrow.className = "
|
|
4253
|
+
const arrow = createElementInDocument(tooltipDocument, "div");
|
|
4254
|
+
arrow.className = "persona-clear-chat-tooltip-arrow";
|
|
3478
4255
|
portaledTooltip.appendChild(arrow);
|
|
3479
4256
|
|
|
3480
4257
|
// Get button position
|
|
@@ -3487,7 +4264,7 @@ export const createAgentExperience = (
|
|
|
3487
4264
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
3488
4265
|
|
|
3489
4266
|
// Append to body
|
|
3490
|
-
|
|
4267
|
+
tooltipContainer.appendChild(portaledTooltip);
|
|
3491
4268
|
};
|
|
3492
4269
|
|
|
3493
4270
|
const hideTooltip = () => {
|
|
@@ -3608,18 +4385,18 @@ export const createAgentExperience = (
|
|
|
3608
4385
|
const backgroundColor = voiceConfig.backgroundColor ?? sendButtonConfig.backgroundColor;
|
|
3609
4386
|
if (backgroundColor) {
|
|
3610
4387
|
micButton.style.backgroundColor = backgroundColor;
|
|
3611
|
-
micButton.classList.remove("
|
|
4388
|
+
micButton.classList.remove("persona-bg-persona-primary");
|
|
3612
4389
|
} else {
|
|
3613
4390
|
micButton.style.backgroundColor = "";
|
|
3614
|
-
micButton.classList.add("
|
|
4391
|
+
micButton.classList.add("persona-bg-persona-primary");
|
|
3615
4392
|
}
|
|
3616
4393
|
|
|
3617
4394
|
if (iconColor) {
|
|
3618
4395
|
micButton.style.color = iconColor;
|
|
3619
|
-
micButton.classList.remove("
|
|
4396
|
+
micButton.classList.remove("persona-text-white");
|
|
3620
4397
|
} else if (!iconColor && !sendButtonConfig.textColor) {
|
|
3621
4398
|
micButton.style.color = "";
|
|
3622
|
-
micButton.classList.add("
|
|
4399
|
+
micButton.classList.add("persona-text-white");
|
|
3623
4400
|
}
|
|
3624
4401
|
|
|
3625
4402
|
// Update border styling
|
|
@@ -3653,14 +4430,14 @@ export const createAgentExperience = (
|
|
|
3653
4430
|
}
|
|
3654
4431
|
|
|
3655
4432
|
// Update tooltip
|
|
3656
|
-
const tooltip = micButtonWrapper?.querySelector(".
|
|
4433
|
+
const tooltip = micButtonWrapper?.querySelector(".persona-send-button-tooltip") as HTMLElement | null;
|
|
3657
4434
|
const tooltipText = voiceConfig.tooltipText ?? "Start voice recognition";
|
|
3658
4435
|
const showTooltip = voiceConfig.showTooltip ?? false;
|
|
3659
4436
|
if (showTooltip && tooltipText) {
|
|
3660
4437
|
if (!tooltip) {
|
|
3661
4438
|
// Create tooltip if it doesn't exist
|
|
3662
4439
|
const newTooltip = document.createElement("div");
|
|
3663
|
-
newTooltip.className = "
|
|
4440
|
+
newTooltip.className = "persona-send-button-tooltip";
|
|
3664
4441
|
newTooltip.textContent = tooltipText;
|
|
3665
4442
|
micButtonWrapper?.insertBefore(newTooltip, micButton);
|
|
3666
4443
|
} else {
|
|
@@ -3701,7 +4478,7 @@ export const createAgentExperience = (
|
|
|
3701
4478
|
|
|
3702
4479
|
// Create previews container if not exists
|
|
3703
4480
|
if (!attachmentPreviewsContainer) {
|
|
3704
|
-
attachmentPreviewsContainer = createElement("div", "
|
|
4481
|
+
attachmentPreviewsContainer = createElement("div", "persona-attachment-previews persona-flex persona-flex-wrap persona-gap-2 persona-mb-2");
|
|
3705
4482
|
attachmentPreviewsContainer.style.display = "none";
|
|
3706
4483
|
composerForm.insertBefore(attachmentPreviewsContainer, textarea);
|
|
3707
4484
|
}
|
|
@@ -3718,12 +4495,12 @@ export const createAgentExperience = (
|
|
|
3718
4495
|
}
|
|
3719
4496
|
|
|
3720
4497
|
// Create attachment button wrapper
|
|
3721
|
-
attachmentButtonWrapper = createElement("div", "
|
|
4498
|
+
attachmentButtonWrapper = createElement("div", "persona-send-button-wrapper");
|
|
3722
4499
|
|
|
3723
4500
|
// Create attachment button
|
|
3724
4501
|
attachmentButton = createElement(
|
|
3725
4502
|
"button",
|
|
3726
|
-
"
|
|
4503
|
+
"persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer persona-attachment-button"
|
|
3727
4504
|
) as HTMLButtonElement;
|
|
3728
4505
|
attachmentButton.type = "button";
|
|
3729
4506
|
attachmentButton.setAttribute("aria-label", attachmentsConfig.buttonTooltipText ?? "Attach file");
|
|
@@ -3742,14 +4519,14 @@ export const createAgentExperience = (
|
|
|
3742
4519
|
attachmentButton.style.fontSize = "18px";
|
|
3743
4520
|
attachmentButton.style.lineHeight = "1";
|
|
3744
4521
|
attachmentButton.style.backgroundColor = "transparent";
|
|
3745
|
-
attachmentButton.style.color = "var(--
|
|
4522
|
+
attachmentButton.style.color = "var(--persona-primary, #111827)";
|
|
3746
4523
|
attachmentButton.style.border = "none";
|
|
3747
4524
|
attachmentButton.style.borderRadius = "6px";
|
|
3748
4525
|
attachmentButton.style.transition = "background-color 0.15s ease";
|
|
3749
4526
|
|
|
3750
4527
|
// Add hover effect via mouseenter/mouseleave
|
|
3751
4528
|
attachmentButton.addEventListener("mouseenter", () => {
|
|
3752
|
-
attachmentButton!.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
|
4529
|
+
attachmentButton!.style.backgroundColor = "var(--persona-palette-colors-black-alpha-50, rgba(0, 0, 0, 0.05))";
|
|
3753
4530
|
});
|
|
3754
4531
|
attachmentButton.addEventListener("mouseleave", () => {
|
|
3755
4532
|
attachmentButton!.style.backgroundColor = "transparent";
|
|
@@ -3771,7 +4548,7 @@ export const createAgentExperience = (
|
|
|
3771
4548
|
|
|
3772
4549
|
// Add tooltip
|
|
3773
4550
|
const attachTooltipText = attachmentsConfig.buttonTooltipText ?? "Attach file";
|
|
3774
|
-
const tooltip = createElement("div", "
|
|
4551
|
+
const tooltip = createElement("div", "persona-send-button-tooltip");
|
|
3775
4552
|
tooltip.textContent = attachTooltipText;
|
|
3776
4553
|
attachmentButtonWrapper.appendChild(tooltip);
|
|
3777
4554
|
|
|
@@ -3859,7 +4636,7 @@ export const createAgentExperience = (
|
|
|
3859
4636
|
if (textColor) {
|
|
3860
4637
|
sendButton.style.color = textColor;
|
|
3861
4638
|
} else {
|
|
3862
|
-
sendButton.classList.add("
|
|
4639
|
+
sendButton.classList.add("persona-text-white");
|
|
3863
4640
|
}
|
|
3864
4641
|
}
|
|
3865
4642
|
} else {
|
|
@@ -3867,18 +4644,18 @@ export const createAgentExperience = (
|
|
|
3867
4644
|
if (textColor) {
|
|
3868
4645
|
sendButton.style.color = textColor;
|
|
3869
4646
|
} else {
|
|
3870
|
-
sendButton.classList.add("
|
|
4647
|
+
sendButton.classList.add("persona-text-white");
|
|
3871
4648
|
}
|
|
3872
4649
|
}
|
|
3873
4650
|
|
|
3874
4651
|
// Update classes
|
|
3875
|
-
sendButton.className = "
|
|
4652
|
+
sendButton.className = "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer";
|
|
3876
4653
|
|
|
3877
4654
|
if (backgroundColor) {
|
|
3878
4655
|
sendButton.style.backgroundColor = backgroundColor;
|
|
3879
|
-
sendButton.classList.remove("
|
|
4656
|
+
sendButton.classList.remove("persona-bg-persona-primary");
|
|
3880
4657
|
} else {
|
|
3881
|
-
sendButton.classList.add("
|
|
4658
|
+
sendButton.classList.add("persona-bg-persona-primary");
|
|
3882
4659
|
}
|
|
3883
4660
|
} else {
|
|
3884
4661
|
// Text mode: existing behavior
|
|
@@ -3891,19 +4668,19 @@ export const createAgentExperience = (
|
|
|
3891
4668
|
sendButton.style.lineHeight = "";
|
|
3892
4669
|
|
|
3893
4670
|
// Update classes
|
|
3894
|
-
sendButton.className = "
|
|
4671
|
+
sendButton.className = "persona-rounded-button persona-bg-persona-accent persona-px-4 persona-py-2 persona-text-sm persona-font-semibold persona-text-white disabled:persona-opacity-50 persona-cursor-pointer";
|
|
3895
4672
|
|
|
3896
4673
|
if (backgroundColor) {
|
|
3897
4674
|
sendButton.style.backgroundColor = backgroundColor;
|
|
3898
|
-
sendButton.classList.remove("
|
|
4675
|
+
sendButton.classList.remove("persona-bg-persona-accent");
|
|
3899
4676
|
} else {
|
|
3900
|
-
sendButton.classList.add("
|
|
4677
|
+
sendButton.classList.add("persona-bg-persona-accent");
|
|
3901
4678
|
}
|
|
3902
4679
|
|
|
3903
4680
|
if (textColor) {
|
|
3904
4681
|
sendButton.style.color = textColor;
|
|
3905
4682
|
} else {
|
|
3906
|
-
sendButton.classList.add("
|
|
4683
|
+
sendButton.classList.add("persona-text-white");
|
|
3907
4684
|
}
|
|
3908
4685
|
}
|
|
3909
4686
|
|
|
@@ -3938,12 +4715,12 @@ export const createAgentExperience = (
|
|
|
3938
4715
|
}
|
|
3939
4716
|
|
|
3940
4717
|
// Update tooltip
|
|
3941
|
-
const tooltip = sendButtonWrapper?.querySelector(".
|
|
4718
|
+
const tooltip = sendButtonWrapper?.querySelector(".persona-send-button-tooltip") as HTMLElement | null;
|
|
3942
4719
|
if (showTooltip && tooltipText) {
|
|
3943
4720
|
if (!tooltip) {
|
|
3944
4721
|
// Create tooltip if it doesn't exist
|
|
3945
4722
|
const newTooltip = document.createElement("div");
|
|
3946
|
-
newTooltip.className = "
|
|
4723
|
+
newTooltip.className = "persona-send-button-tooltip";
|
|
3947
4724
|
newTooltip.textContent = tooltipText;
|
|
3948
4725
|
sendButtonWrapper?.insertBefore(newTooltip, sendButton);
|
|
3949
4726
|
} else {
|
|
@@ -3986,7 +4763,9 @@ export const createAgentExperience = (
|
|
|
3986
4763
|
},
|
|
3987
4764
|
clearChat() {
|
|
3988
4765
|
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
4766
|
+
artifactsPaneUserHidden = false;
|
|
3989
4767
|
session.clearMessages();
|
|
4768
|
+
messageCache.clear();
|
|
3990
4769
|
|
|
3991
4770
|
// Always clear the default localStorage key
|
|
3992
4771
|
try {
|
|
@@ -4102,7 +4881,7 @@ export const createAgentExperience = (
|
|
|
4102
4881
|
voiceState.manuallyDeactivated = true;
|
|
4103
4882
|
persistVoiceMetadata();
|
|
4104
4883
|
emitVoiceState("user");
|
|
4105
|
-
|
|
4884
|
+
removeRuntypeMicStateStyles();
|
|
4106
4885
|
});
|
|
4107
4886
|
return true;
|
|
4108
4887
|
}
|
|
@@ -4202,6 +4981,31 @@ export const createAgentExperience = (
|
|
|
4202
4981
|
isEventStreamVisible(): boolean {
|
|
4203
4982
|
return eventStreamVisible;
|
|
4204
4983
|
},
|
|
4984
|
+
showArtifacts(): void {
|
|
4985
|
+
if (!artifactsSidebarEnabled(config)) return;
|
|
4986
|
+
artifactsPaneUserHidden = false;
|
|
4987
|
+
syncArtifactPane();
|
|
4988
|
+
artifactPaneApi?.setMobileOpen(true);
|
|
4989
|
+
},
|
|
4990
|
+
hideArtifacts(): void {
|
|
4991
|
+
if (!artifactsSidebarEnabled(config)) return;
|
|
4992
|
+
artifactsPaneUserHidden = true;
|
|
4993
|
+
syncArtifactPane();
|
|
4994
|
+
},
|
|
4995
|
+
upsertArtifact(manual: PersonaArtifactManualUpsert): PersonaArtifactRecord | null {
|
|
4996
|
+
if (!artifactsSidebarEnabled(config)) return null;
|
|
4997
|
+
// Programmatic adds should surface the pane even if the user previously hit Close.
|
|
4998
|
+
artifactsPaneUserHidden = false;
|
|
4999
|
+
return session.upsertArtifact(manual);
|
|
5000
|
+
},
|
|
5001
|
+
selectArtifact(id: string): void {
|
|
5002
|
+
if (!artifactsSidebarEnabled(config)) return;
|
|
5003
|
+
session.selectArtifact(id);
|
|
5004
|
+
},
|
|
5005
|
+
clearArtifacts(): void {
|
|
5006
|
+
if (!artifactsSidebarEnabled(config)) return;
|
|
5007
|
+
session.clearArtifacts();
|
|
5008
|
+
},
|
|
4205
5009
|
focusInput(): boolean {
|
|
4206
5010
|
if (launcherEnabled && !open) return false;
|
|
4207
5011
|
if (!textarea) return false;
|
|
@@ -4261,7 +5065,7 @@ export const createAgentExperience = (
|
|
|
4261
5065
|
}
|
|
4262
5066
|
|
|
4263
5067
|
// Remove any existing feedback forms
|
|
4264
|
-
const existingFeedback = messagesWrapper.querySelector('.
|
|
5068
|
+
const existingFeedback = messagesWrapper.querySelector('.persona-feedback-container');
|
|
4265
5069
|
if (existingFeedback) {
|
|
4266
5070
|
existingFeedback.remove();
|
|
4267
5071
|
}
|
|
@@ -4288,7 +5092,7 @@ export const createAgentExperience = (
|
|
|
4288
5092
|
}
|
|
4289
5093
|
|
|
4290
5094
|
// Remove any existing feedback forms
|
|
4291
|
-
const existingFeedback = messagesWrapper.querySelector('.
|
|
5095
|
+
const existingFeedback = messagesWrapper.querySelector('.persona-feedback-container');
|
|
4292
5096
|
if (existingFeedback) {
|
|
4293
5097
|
existingFeedback.remove();
|
|
4294
5098
|
}
|
|
@@ -4387,6 +5191,51 @@ export const createAgentExperience = (
|
|
|
4387
5191
|
window.removeEventListener("persona:hideEventStream", handleHideEvent);
|
|
4388
5192
|
});
|
|
4389
5193
|
}
|
|
5194
|
+
|
|
5195
|
+
const handleShowArtifacts = (e: Event) => {
|
|
5196
|
+
const detail = (e as CustomEvent).detail;
|
|
5197
|
+
if (!detail?.instanceId || detail.instanceId === instanceId) {
|
|
5198
|
+
controller.showArtifacts();
|
|
5199
|
+
}
|
|
5200
|
+
};
|
|
5201
|
+
const handleHideArtifacts = (e: Event) => {
|
|
5202
|
+
const detail = (e as CustomEvent).detail;
|
|
5203
|
+
if (!detail?.instanceId || detail.instanceId === instanceId) {
|
|
5204
|
+
controller.hideArtifacts();
|
|
5205
|
+
}
|
|
5206
|
+
};
|
|
5207
|
+
const handleUpsertArtifact = (e: Event) => {
|
|
5208
|
+
const detail = (e as CustomEvent).detail;
|
|
5209
|
+
if (detail?.instanceId && detail.instanceId !== instanceId) return;
|
|
5210
|
+
if (detail?.artifact) {
|
|
5211
|
+
controller.upsertArtifact(detail.artifact as PersonaArtifactManualUpsert);
|
|
5212
|
+
}
|
|
5213
|
+
};
|
|
5214
|
+
const handleSelectArtifact = (e: Event) => {
|
|
5215
|
+
const detail = (e as CustomEvent).detail;
|
|
5216
|
+
if (detail?.instanceId && detail.instanceId !== instanceId) return;
|
|
5217
|
+
if (typeof detail?.id === "string") {
|
|
5218
|
+
controller.selectArtifact(detail.id);
|
|
5219
|
+
}
|
|
5220
|
+
};
|
|
5221
|
+
const handleClearArtifacts = (e: Event) => {
|
|
5222
|
+
const detail = (e as CustomEvent).detail;
|
|
5223
|
+
if (!detail?.instanceId || detail.instanceId === instanceId) {
|
|
5224
|
+
controller.clearArtifacts();
|
|
5225
|
+
}
|
|
5226
|
+
};
|
|
5227
|
+
window.addEventListener("persona:showArtifacts", handleShowArtifacts);
|
|
5228
|
+
window.addEventListener("persona:hideArtifacts", handleHideArtifacts);
|
|
5229
|
+
window.addEventListener("persona:upsertArtifact", handleUpsertArtifact);
|
|
5230
|
+
window.addEventListener("persona:selectArtifact", handleSelectArtifact);
|
|
5231
|
+
window.addEventListener("persona:clearArtifacts", handleClearArtifacts);
|
|
5232
|
+
destroyCallbacks.push(() => {
|
|
5233
|
+
window.removeEventListener("persona:showArtifacts", handleShowArtifacts);
|
|
5234
|
+
window.removeEventListener("persona:hideArtifacts", handleHideArtifacts);
|
|
5235
|
+
window.removeEventListener("persona:upsertArtifact", handleUpsertArtifact);
|
|
5236
|
+
window.removeEventListener("persona:selectArtifact", handleSelectArtifact);
|
|
5237
|
+
window.removeEventListener("persona:clearArtifacts", handleClearArtifacts);
|
|
5238
|
+
});
|
|
4390
5239
|
}
|
|
4391
5240
|
|
|
4392
5241
|
// ============================================================================
|