@elizaos/ui 2.0.3-beta.3 → 2.0.3-beta.5
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/App.d.ts.map +1 -1
- package/App.js +3 -4
- package/app-navigate-view.d.ts.map +1 -1
- package/app-navigate-view.js +1 -1
- package/cloud/api-keys/ApiKeysView.d.ts +6 -3
- package/cloud/api-keys/ApiKeysView.d.ts.map +1 -1
- package/cloud/api-keys/ApiKeysView.js +21 -29
- package/cloud/api-keys/copy-api-key.d.ts +1 -8
- package/cloud/api-keys/copy-api-key.d.ts.map +1 -1
- package/cloud/api-keys/copy-api-key.js +1 -10
- package/cloud/api-keys/index.d.ts +1 -1
- package/cloud/api-keys/index.d.ts.map +1 -1
- package/cloud/api-keys/index.js +1 -1
- package/cloud/shell/CloudRouterShell.d.ts +12 -0
- package/cloud/shell/CloudRouterShell.d.ts.map +1 -1
- package/cloud/shell/CloudRouterShell.js +38 -2
- package/cloud-ui/components/data-list/api-keys-table.d.ts +1 -2
- package/cloud-ui/components/data-list/api-keys-table.d.ts.map +1 -1
- package/cloud-ui/components/data-list/api-keys-table.js +3 -9
- package/cloud-ui/components/docs/api-route-explorer-client.d.ts.map +1 -1
- package/cloud-ui/components/docs/api-route-explorer-client.js +2 -1
- package/cloud-ui/components/layout/dashboard-route-page.d.ts.map +1 -1
- package/cloud-ui/components/layout/dashboard-route-page.js +3 -1
- package/components/chat/widgets/home-widget-card.d.ts.map +1 -1
- package/components/chat/widgets/home-widget-card.js +1 -3
- package/components/config-ui/config-field.js +1 -1
- package/components/pages/Springboard.d.ts.map +1 -1
- package/components/pages/Springboard.js +37 -48
- package/components/pages/SpringboardSurface.d.ts.map +1 -1
- package/components/pages/SpringboardSurface.js +116 -6
- package/components/shell/ContinuousChatOverlay.d.ts.map +1 -1
- package/components/shell/ContinuousChatOverlay.js +207 -146
- package/components/shell/HomeSpringboardSurface.d.ts.map +1 -1
- package/components/shell/HomeSpringboardSurface.js +31 -76
- package/components/shell/use-pull-gesture.d.ts +6 -0
- package/components/shell/use-pull-gesture.d.ts.map +1 -1
- package/components/shell/use-pull-gesture.js +66 -27
- package/components/shell/useShellController.d.ts.map +1 -1
- package/components/shell/useShellController.js +13 -3
- package/components/views/ViewTileImage.d.ts +8 -8
- package/components/views/ViewTileImage.d.ts.map +1 -1
- package/components/views/ViewTileImage.js +38 -9
- package/hooks/useHorizontalPager.d.ts +33 -0
- package/hooks/useHorizontalPager.d.ts.map +1 -0
- package/hooks/useHorizontalPager.js +226 -0
- package/hooks/useLayoutShiftMonitor.d.ts +2 -0
- package/hooks/useLayoutShiftMonitor.d.ts.map +1 -1
- package/hooks/useLayoutShiftMonitor.js +23 -0
- package/hooks/view-catalog.d.ts +3 -4
- package/hooks/view-catalog.d.ts.map +1 -1
- package/hooks/view-catalog.js +4 -6
- package/package.json +10 -10
- package/state/startup-telemetry.d.ts +6 -0
- package/state/startup-telemetry.d.ts.map +1 -1
- package/state/startup-telemetry.js +23 -2
- package/styles/base.css +6 -6
- package/styles/brand-gold.css +6 -5
- package/testing/layout-stability.d.ts +11 -3
- package/testing/layout-stability.d.ts.map +1 -1
- package/testing/layout-stability.js +29 -4
- package/testing/run-animation-frames-immediately.d.ts +2 -0
- package/testing/run-animation-frames-immediately.d.ts.map +1 -0
- package/testing/run-animation-frames-immediately.js +8 -0
- package/themes/presets.js +14 -14
- package/utils/cloud-agent-base.d.ts.map +1 -1
- package/utils/cloud-agent-base.js +6 -0
- package/widgets/registry.d.ts.map +1 -1
- package/widgets/registry.js +7 -0
- package/components/pages/automation-conversations.d.ts +0 -16
- package/components/pages/automation-conversations.d.ts.map +0 -1
- package/components/pages/automation-conversations.js +0 -204
- package/components/pages/automations-missing-creds.d.ts +0 -25
- package/components/pages/automations-missing-creds.d.ts.map +0 -1
- package/components/pages/automations-missing-creds.js +0 -39
- package/components/settings/desktop-workspace-audit-config.d.ts +0 -3
- package/components/settings/desktop-workspace-audit-config.d.ts.map +0 -1
- package/components/settings/desktop-workspace-audit-config.js +0 -171
- package/components/shell/first-run-theme.d.ts +0 -6
- package/components/shell/first-run-theme.d.ts.map +0 -1
- package/components/shell/first-run-theme.js +0 -31
- package/genui/use-json-render-message.d.ts +0 -20
- package/genui/use-json-render-message.d.ts.map +0 -1
- package/genui/use-json-render-message.js +0 -38
- package/state/first-run-restart.d.ts +0 -8
- package/state/first-run-restart.d.ts.map +0 -1
- package/state/first-run-restart.js +0 -8
|
@@ -6,8 +6,10 @@ import * as React from "react";
|
|
|
6
6
|
import { client } from "../../api/client.js";
|
|
7
7
|
import { parseSlashDraft, resolveClientShortcutExecution, runSlashExecution, splitLeadingSlashCommand, } from "../../chat/slash-menu.js";
|
|
8
8
|
import { CHAT_PREFILL_EVENT, TUTORIAL_CHAT_CONTROL_EVENT, } from "../../events/index.js";
|
|
9
|
+
import { LAYOUT_SHIFT_INTENT_ATTR, LAYOUT_SHIFT_INTENT_TRANSIENT, } from "../../hooks/useLayoutShiftMonitor.js";
|
|
9
10
|
import { Z_SHELL_OVERLAY } from "../../lib/floating-layers.js";
|
|
10
11
|
import { cn } from "../../lib/utils.js";
|
|
12
|
+
import { goHome, goSpringboard } from "../../state/shell-surface-store.js";
|
|
11
13
|
import { useViewChatBinding } from "../../state/view-chat-binding.js";
|
|
12
14
|
import { copyTextToClipboard } from "../../utils/clipboard.js";
|
|
13
15
|
import { CHAT_UPLOAD_ACCEPT, chatUploadKind, classifyComposerPaste, intakeAttachmentFiles, MAX_CHAT_IMAGES, summarizeDroppedAttachments, } from "../../utils/image-attachment.js";
|
|
@@ -44,9 +46,9 @@ const EMPTY_SLASH_CONTROLLER = {
|
|
|
44
46
|
* useShellController).
|
|
45
47
|
*
|
|
46
48
|
* Layout is a fixed composer at the bottom with a pull-up history SHEET above
|
|
47
|
-
* it. At rest the sheet is
|
|
48
|
-
*
|
|
49
|
-
*
|
|
49
|
+
* it. At rest the sheet is only the composer + grabber; pull the grabber UP, or
|
|
50
|
+
* just start typing, to spring it open into the full transcript. Pull the
|
|
51
|
+
* grabber back DOWN, or press Escape, to close.
|
|
50
52
|
* Nothing else dismisses it — clicking or scrolling the view behind does
|
|
51
53
|
* nothing. The composer never moves; the history slides up over it.
|
|
52
54
|
*
|
|
@@ -224,17 +226,14 @@ function SheetGrabber({ open, onOpen, onClose, binding, glow, opacity, pilled, }
|
|
|
224
226
|
}, ...binding, className: cn("appearance-none border-0 bg-transparent text-left",
|
|
225
227
|
// ABSOLUTELY positioned over the panel top (zero layout height — it
|
|
226
228
|
// floats slightly on top of the input row, so collapsed height == the
|
|
227
|
-
// input bar).
|
|
228
|
-
//
|
|
229
|
-
// than the bar) so it's easy to grab without covering the edge buttons.
|
|
229
|
+
// input bar). Keep the invisible hit target local to the visible handle:
|
|
230
|
+
// it should be forgiving, not register drags far above the bar.
|
|
230
231
|
// z-20 keeps it above the input row (z-10) so it always wins the drag.
|
|
231
|
-
"absolute left-1/2 top-0.5 z-20 -translate-x-1/2 flex cursor-grab touch-none select-none items-center justify-center px-16 py-
|
|
232
|
-
// The hit zone reaches
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
// handle wouldn't). Pull gestures start from the bar / the upward zone.
|
|
237
|
-
"before:absolute before:-inset-x-6 before:-top-16 before:bottom-0 before:content-['']", " "), children: _jsx("span", { "aria-hidden": "true", className: cn(
|
|
232
|
+
"absolute left-1/2 top-0.5 z-20 -translate-x-1/2 flex cursor-grab touch-none select-none items-center justify-center px-16 py-2 active:cursor-grabbing",
|
|
233
|
+
// The hit zone reaches only a small distance above the panel and stops
|
|
234
|
+
// at the handle's own bottom, so the handle does not steal taps intended
|
|
235
|
+
// for the composer or feel like it starts in empty space.
|
|
236
|
+
"before:absolute before:-inset-x-4 before:-top-4 before:bottom-0 before:content-['']", " "), children: _jsx("span", { "aria-hidden": "true", className: cn(
|
|
238
237
|
// The visible grabber line. Its show/hide is driven by the WRAPPER's
|
|
239
238
|
// `grabberOpacity` crossfade (fades in over [0.55, 0.95] of the open),
|
|
240
239
|
// strictly anti-phase with the pill bar so the two are never on screen
|
|
@@ -364,7 +363,7 @@ function useDebouncedTurnStatus(status) {
|
|
|
364
363
|
function TurnStatusInner({ status, }) {
|
|
365
364
|
const shown = useDebouncedTurnStatus(status);
|
|
366
365
|
const speaking = shown?.kind === "speaking";
|
|
367
|
-
const label = shown ? turnStatusLabel(shown) : null;
|
|
366
|
+
const label = shown && shown.kind !== "thinking" ? turnStatusLabel(shown) : null;
|
|
368
367
|
return (_jsxs("span", { className: "inline-flex items-center gap-2", "data-testid": "turn-status-indicator", "data-status-kind": shown?.kind ?? "none", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "flex gap-1.5", "data-testid": "typing-dots", "aria-hidden": "true", children: [0, 1, 2].map((i) => (_jsx("span", { className: cn("h-1.5 w-1.5 animate-pulse rounded-full motion-reduce:animate-none", speaking ? "bg-[rgba(255,190,140,0.9)]" : "bg-white/70"), style: { animationDelay: `${i * 180}ms` } }, i))) }), label ? (_jsx("span", { className: cn("text-[13px] font-medium", speaking ? "text-[rgba(255,200,150,0.95)]" : "text-white/90"), "data-testid": "turn-status-label", children: label })) : null] }));
|
|
369
368
|
}
|
|
370
369
|
/**
|
|
@@ -403,7 +402,7 @@ function ThreadLineText({ content }) {
|
|
|
403
402
|
return content;
|
|
404
403
|
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-bold", "data-testid": "slash-command-token", children: slash.command }), slash.rest] }));
|
|
405
404
|
}
|
|
406
|
-
const ThreadLine = React.memo(function ThreadLine({ message, floating, reduce, onCopy, onOpenSettings, turnStatus, }) {
|
|
405
|
+
const ThreadLine = React.memo(function ThreadLine({ message, floating, reduce, onCopy, onOpenSettings, turnStatus, suppressReasoning, }) {
|
|
407
406
|
const isUser = message.role === "user";
|
|
408
407
|
const isAssistant = message.role === "assistant";
|
|
409
408
|
// Press-and-hold to copy an assistant answer — the only extraction affordance
|
|
@@ -481,7 +480,7 @@ const ThreadLine = React.memo(function ThreadLine({ message, floating, reduce, o
|
|
|
481
480
|
? "bg-white/20 text-white"
|
|
482
481
|
: "bg-white/10 text-white/90"), children: [_jsx("div", { "data-chat-selectable": "true", children: isAssistant &&
|
|
483
482
|
!message.content.trim() &&
|
|
484
|
-
!message.attachments?.length ? (_jsxs(_Fragment, { children: [_jsx(TurnStatusInner, { status: turnStatus ?? null }), message.attachments?.length ? (_jsx(MessageAttachments, { attachments: message.attachments })) : null] })) : isUser ? (_jsxs(_Fragment, { children: [_jsx(ThreadLineText, { content: message.content }), message.attachments?.length ? (_jsx(MessageAttachments, { attachments: message.attachments })) : null] })) : (_jsxs(_Fragment, { children: [_jsx(InlineWidgetText, { content: message.content }), message.attachments?.length ? (_jsx(MessageAttachments, { attachments: message.attachments })) : null, message.secretRequest ? (_jsx("div", { className: "pointer-events-auto", children: _jsx(SensitiveRequestBlock, { request: message.secretRequest }) })) : null, message.reasoning?.trim() ? (_jsx(ThinkingBlock, { reasoning: message.reasoning })) : null] })) }), _jsx(AnimatePresence, { children: copied ? (_jsx(motion.span, { "data-testid": "thread-line-copied", initial: reduce ? { opacity: 0 } : { opacity: 0, y: 4 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0 }, transition: { duration: reduce ? 0 : 0.18 }, className: "pointer-events-none absolute -top-2 right-2 rounded-full bg-white/90 px-2 py-0.5 text-[11px] font-medium text-black", children: "Copied" }, "copied")) : null })] }) }));
|
|
483
|
+
!message.attachments?.length ? (_jsxs(_Fragment, { children: [_jsx(TurnStatusInner, { status: turnStatus ?? null }), message.attachments?.length ? (_jsx(MessageAttachments, { attachments: message.attachments })) : null] })) : isUser ? (_jsxs(_Fragment, { children: [_jsx(ThreadLineText, { content: message.content }), message.attachments?.length ? (_jsx(MessageAttachments, { attachments: message.attachments })) : null] })) : (_jsxs(_Fragment, { children: [_jsx(InlineWidgetText, { content: message.content }), message.attachments?.length ? (_jsx(MessageAttachments, { attachments: message.attachments })) : null, message.secretRequest ? (_jsx("div", { className: "pointer-events-auto", children: _jsx(SensitiveRequestBlock, { request: message.secretRequest }) })) : null, !suppressReasoning && message.reasoning?.trim() ? (_jsx(ThinkingBlock, { reasoning: message.reasoning })) : null] })) }), _jsx(AnimatePresence, { children: copied ? (_jsx(motion.span, { "data-testid": "thread-line-copied", initial: reduce ? { opacity: 0 } : { opacity: 0, y: 4 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0 }, transition: { duration: reduce ? 0 : 0.18 }, className: "pointer-events-none absolute -top-2 right-2 rounded-full bg-white/90 px-2 py-0.5 text-[11px] font-medium text-black", children: "Copied" }, "copied")) : null })] }) }));
|
|
485
484
|
});
|
|
486
485
|
export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash: slashProp, }) {
|
|
487
486
|
const { messages, phase, responding, turnStatus, send, canSend, recording, startRecording, stopRecording, handsFree, toggleHandsFree, transcriptionMode, toggleTranscriptionMode, stopTranscriptionAndMic, setDictationSink, setTranscriptSessionSink, setComposerHasDraft, transcript, needsAudioUnlock, unlockAudio, openSettings, navigateHome, currentTab, clearConversation, stop, modelStatus, } = controller;
|
|
@@ -523,12 +522,12 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
523
522
|
const viewChatBinding = useViewChatBinding();
|
|
524
523
|
// Escape dismisses the slash menu without clearing the draft; typing reopens.
|
|
525
524
|
const [slashDismissed, setSlashDismissed] = React.useState(false);
|
|
526
|
-
// The chat-history sheet: closed (
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
525
|
+
// The chat-history sheet: closed (composer + grabber) ↔ open (full scrollable
|
|
526
|
+
// history). The ONLY open/close driver — opened by a pull-up drag, by focusing
|
|
527
|
+
// the composer, or by sending; closed by a pull-down drag or Escape. Never by
|
|
528
|
+
// click-out, scroll, or blur.
|
|
530
529
|
// The sheet's vertical position is ONE ordinal — the single source of truth for
|
|
531
|
-
// how far the chat is open: `input` (composer-only
|
|
530
|
+
// how far the chat is open: `input` (composer-only) → `half` (reading
|
|
532
531
|
// height) → `full` (near-fullscreen). `sheetOpen`/`expanded` are derived
|
|
533
532
|
// read-only views so the two can never disagree (no impossible "open but not
|
|
534
533
|
// open" combos). `pilled` sits BELOW input; `maximized` drops the inset at full.
|
|
@@ -563,10 +562,6 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
563
562
|
// tell a FIRST tap (keyboard up → just dismiss + restore) from a SECOND tap
|
|
564
563
|
// (keyboard already down → close the chat).
|
|
565
564
|
const composerFocusedAtPressRef = React.useRef(false);
|
|
566
|
-
// Composer focus ⟺ the soft keyboard is up on mobile. This is the reliable
|
|
567
|
-
// keyboard signal: Capacitor's resize:"body" shrinks innerHeight too, so a
|
|
568
|
-
// visualViewport-derived keyboardInset reads 0 and can't gate the layout.
|
|
569
|
-
const [composerFocused, setComposerFocused] = React.useState(false);
|
|
570
565
|
// The live thread (history) height in px, as a MOTION VALUE — driven directly
|
|
571
566
|
// by the pointer during a drag and spring-animated to a detent on release.
|
|
572
567
|
// Keeping it off React state means a drag updates the DOM height every frame
|
|
@@ -622,6 +617,11 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
622
617
|
window.clearTimeout(delayedNavigationTimerRef.current);
|
|
623
618
|
delayedNavigationTimerRef.current = null;
|
|
624
619
|
}
|
|
620
|
+
if (layoutShiftIntentTimerRef.current !== null) {
|
|
621
|
+
window.clearTimeout(layoutShiftIntentTimerRef.current);
|
|
622
|
+
layoutShiftIntentTimerRef.current = null;
|
|
623
|
+
}
|
|
624
|
+
overlayRef.current?.removeAttribute(LAYOUT_SHIFT_INTENT_ATTR);
|
|
625
625
|
clearPrefillFocusSchedule();
|
|
626
626
|
}, [stopThreadAnimation, stopOpenProgressAnimation, clearPrefillFocusSchedule]);
|
|
627
627
|
// Latest `settleDrag` (defined below) exposed to the viewport-resize effect
|
|
@@ -629,6 +629,19 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
629
629
|
// the morph keeps the pill↔input crossfade from stranding both bars visible.
|
|
630
630
|
const settleDragRef = React.useRef(null);
|
|
631
631
|
const draggingRef = React.useRef(false);
|
|
632
|
+
// At rest the collapsed composer should not carry hidden transcript/header
|
|
633
|
+
// DOM. During an upward pull, though, the sheet needs a mounted body so the
|
|
634
|
+
// MotionValue-driven height can follow the finger before the release commits
|
|
635
|
+
// to an open detent. This boolean changes only at gesture boundaries; the
|
|
636
|
+
// per-frame drag still stays outside React.
|
|
637
|
+
const [dragPreviewVisible, setDragPreviewVisible] = React.useState(false);
|
|
638
|
+
const dragPreviewVisibleRef = React.useRef(false);
|
|
639
|
+
const setDragPreviewMounted = React.useCallback((visible) => {
|
|
640
|
+
if (dragPreviewVisibleRef.current === visible)
|
|
641
|
+
return;
|
|
642
|
+
dragPreviewVisibleRef.current = visible;
|
|
643
|
+
setDragPreviewVisible(visible);
|
|
644
|
+
}, []);
|
|
632
645
|
// Push-to-talk phase (single source of truth) + a label-only mirror.
|
|
633
646
|
const pttRef = React.useRef({ kind: "idle" });
|
|
634
647
|
const [pttHolding, setPttHolding] = React.useState(false);
|
|
@@ -642,6 +655,20 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
642
655
|
const overlayRef = React.useRef(null);
|
|
643
656
|
const panelRef = React.useRef(null);
|
|
644
657
|
const threadRef = React.useRef(null);
|
|
658
|
+
const layoutShiftIntentTimerRef = React.useRef(null);
|
|
659
|
+
const markLayoutShiftIntent = React.useCallback(() => {
|
|
660
|
+
const overlay = overlayRef.current;
|
|
661
|
+
if (!overlay || typeof window === "undefined")
|
|
662
|
+
return;
|
|
663
|
+
overlay.setAttribute(LAYOUT_SHIFT_INTENT_ATTR, LAYOUT_SHIFT_INTENT_TRANSIENT);
|
|
664
|
+
if (layoutShiftIntentTimerRef.current !== null) {
|
|
665
|
+
window.clearTimeout(layoutShiftIntentTimerRef.current);
|
|
666
|
+
}
|
|
667
|
+
layoutShiftIntentTimerRef.current = window.setTimeout(() => {
|
|
668
|
+
layoutShiftIntentTimerRef.current = null;
|
|
669
|
+
overlayRef.current?.removeAttribute(LAYOUT_SHIFT_INTENT_ATTR);
|
|
670
|
+
}, 180);
|
|
671
|
+
}, []);
|
|
645
672
|
// The composer content (textarea + thread). Held so we can imperatively clear
|
|
646
673
|
// its `inert` (set while pilled) the instant the pill is tapped open, before
|
|
647
674
|
// React re-renders — iOS only raises the keyboard for a focus() that lands on
|
|
@@ -659,6 +686,20 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
659
686
|
const visibleMessages = React.useMemo(() => selectVisibleShellMessages(messages, phase), [messages, phase]);
|
|
660
687
|
const lastId = visibleMessages.at(-1)?.id ?? null;
|
|
661
688
|
const lastContent = visibleMessages.at(-1)?.content ?? "";
|
|
689
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: these values are the event keys for transient layout-motion intent.
|
|
690
|
+
React.useEffect(() => {
|
|
691
|
+
markLayoutShiftIntent();
|
|
692
|
+
}, [
|
|
693
|
+
visibleMessages.length,
|
|
694
|
+
lastId,
|
|
695
|
+
lastContent,
|
|
696
|
+
responding,
|
|
697
|
+
turnStatus?.kind,
|
|
698
|
+
turnStatus?.label,
|
|
699
|
+
turnStatus?.actionName,
|
|
700
|
+
turnStatus?.toolName,
|
|
701
|
+
markLayoutShiftIntent,
|
|
702
|
+
]);
|
|
662
703
|
// Copy the whole thread as a plain-text transcript from the full-state header
|
|
663
704
|
// — parity with the desktop ChatView header. Flashes a check on success.
|
|
664
705
|
const [conversationCopied, setConversationCopied] = React.useState(false);
|
|
@@ -724,15 +765,15 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
724
765
|
// Render one transcript line; shared by the flat and topic-grouped paths so
|
|
725
766
|
// the in-flight-turn detection stays identical.
|
|
726
767
|
const renderThreadLine = React.useCallback((m, index) => {
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
return (_jsx(ThreadLine, { message: m, floating: true, reduce: reduce, onCopy: handleCopyMessage, onOpenSettings: openSettings, turnStatus: isInFlight ? turnStatus : undefined }, m.id));
|
|
768
|
+
const isLastAssistant = index === visibleMessages.length - 1 && m.role === "assistant";
|
|
769
|
+
const isInFlight = isLastAssistant && !m.content.trim();
|
|
770
|
+
return (_jsx(ThreadLine, { message: m, floating: true, reduce: reduce, onCopy: handleCopyMessage, onOpenSettings: openSettings, turnStatus: isInFlight ? turnStatus : undefined, suppressReasoning: responding && isLastAssistant }, m.id));
|
|
731
771
|
}, [
|
|
732
772
|
visibleMessages.length,
|
|
733
773
|
reduce,
|
|
734
774
|
handleCopyMessage,
|
|
735
775
|
openSettings,
|
|
776
|
+
responding,
|
|
736
777
|
turnStatus,
|
|
737
778
|
]);
|
|
738
779
|
const booting = phase === "booting";
|
|
@@ -783,19 +824,18 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
783
824
|
if (followRafRef.current != null)
|
|
784
825
|
cancelAnimationFrame(followRafRef.current);
|
|
785
826
|
}, []);
|
|
786
|
-
|
|
827
|
+
const threadPresented = sheetOpen || dragPreviewVisible;
|
|
828
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: lastId/lastContent/sheetOpen/dragPreviewVisible are the triggers; the body reads refs
|
|
787
829
|
React.useLayoutEffect(() => {
|
|
788
830
|
const el = threadRef.current;
|
|
789
831
|
if (!el)
|
|
790
832
|
return;
|
|
791
833
|
const isNewLine = lastId !== scrollPinnedIdRef.current;
|
|
792
834
|
scrollPinnedIdRef.current = lastId;
|
|
793
|
-
// CLOSED peek: always pin to the bottom so it whispers the LATEST line (the
|
|
794
|
-
// one nearest the composer) — even though it can't be user-scrolled, the
|
|
795
|
-
// clipped content must show the end of the thread, not the top.
|
|
796
835
|
if (!sheetOpen) {
|
|
797
836
|
wasOpenRef.current = false;
|
|
798
|
-
|
|
837
|
+
if (dragPreviewVisible)
|
|
838
|
+
el.scrollTop = el.scrollHeight;
|
|
799
839
|
return;
|
|
800
840
|
}
|
|
801
841
|
// OPEN: jump to the bottom on first open; a NEW line re-pins (smooth); while
|
|
@@ -841,25 +881,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
841
881
|
if (atBottom)
|
|
842
882
|
node.scrollTop = node.scrollHeight;
|
|
843
883
|
});
|
|
844
|
-
}, [lastId, lastContent, sheetOpen]);
|
|
845
|
-
// The closed peek must always whisper the NEWEST line, but closing is an
|
|
846
|
-
// animated height collapse: a one-shot scroll set runs before the height
|
|
847
|
-
// finishes shrinking, leaving the peek parked mid-thread as clientHeight
|
|
848
|
-
// drops. Observe the peek while closed and re-pin to the bottom on every size
|
|
849
|
-
// change (animation frames, web-font reflow, viewport resize) until it
|
|
850
|
-
// settles. Disconnects the moment the sheet opens.
|
|
851
|
-
React.useEffect(() => {
|
|
852
|
-
const el = threadRef.current;
|
|
853
|
-
if (!el || sheetOpen || typeof ResizeObserver === "undefined")
|
|
854
|
-
return;
|
|
855
|
-
const pin = () => {
|
|
856
|
-
el.scrollTop = el.scrollHeight;
|
|
857
|
-
};
|
|
858
|
-
pin();
|
|
859
|
-
const ro = new ResizeObserver(pin);
|
|
860
|
-
ro.observe(el);
|
|
861
|
-
return () => ro.disconnect();
|
|
862
|
-
}, [sheetOpen]);
|
|
884
|
+
}, [lastId, lastContent, sheetOpen, dragPreviewVisible]);
|
|
863
885
|
// Send `text` (and optional images) through the normal chat pipeline, clearing
|
|
864
886
|
// the composer. Shared by the send button, the slash menu (agent commands),
|
|
865
887
|
// and suggestion taps.
|
|
@@ -887,7 +909,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
887
909
|
// Sending COMMITS to the open chat: a deliberate message means this is now
|
|
888
910
|
// an active conversation, so dismissing the keyboard afterwards keeps the
|
|
889
911
|
// thread open (preFocusCollapsedRef gates that) instead of collapsing the
|
|
890
|
-
// whole conversation back to the bare input
|
|
912
|
+
// whole conversation back to the bare input bar — even when the chat was
|
|
891
913
|
// opened by tapping the collapsed input.
|
|
892
914
|
preFocusCollapsedRef.current = false;
|
|
893
915
|
detentHaptic();
|
|
@@ -1023,6 +1045,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1023
1045
|
stopTranscriptionAndMic,
|
|
1024
1046
|
]);
|
|
1025
1047
|
const hasThread = visibleMessages.length > 0;
|
|
1048
|
+
const hasRevealableThread = hasThread || conversationLoading;
|
|
1026
1049
|
// Track the VISUAL viewport so the chat sizes to — and sits above — whatever
|
|
1027
1050
|
// the mobile keyboard leaves visible. `height` shrinks when the keyboard opens
|
|
1028
1051
|
// (on iOS innerHeight does not, so read visualViewport); `keyboardInset` is how
|
|
@@ -1077,28 +1100,27 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1077
1100
|
commit();
|
|
1078
1101
|
});
|
|
1079
1102
|
};
|
|
1080
|
-
// A
|
|
1081
|
-
// mid-crossfade — rotation often cancels the in-flight
|
|
1082
|
-
// pointerup, leaving the drag orphaned
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
|
|
1088
|
-
const syncAndSettle = () => {
|
|
1103
|
+
// A real WINDOW resize (rotation/desktop resize) must never strand the
|
|
1104
|
+
// pill↔input morph mid-crossfade — rotation often cancels the in-flight
|
|
1105
|
+
// pointer with no pointerup, leaving the drag orphaned. Re-settle to a clean
|
|
1106
|
+
// 0/1 end there. `visualViewport.resize`, however, fires continuously during
|
|
1107
|
+
// soft-keyboard animation; settling on those events fights typing, detent
|
|
1108
|
+
// drags, and keyboard open/close. For vv resize/scroll, update measurements
|
|
1109
|
+
// only and let the current sheet state remain authoritative.
|
|
1110
|
+
const syncAndSettleWindow = () => {
|
|
1089
1111
|
sync();
|
|
1090
1112
|
settleDragRef.current?.();
|
|
1091
1113
|
};
|
|
1092
|
-
|
|
1114
|
+
syncAndSettleWindow();
|
|
1093
1115
|
const vv = window.visualViewport;
|
|
1094
|
-
window.addEventListener("resize",
|
|
1095
|
-
vv?.addEventListener("resize",
|
|
1116
|
+
window.addEventListener("resize", syncAndSettleWindow);
|
|
1117
|
+
vv?.addEventListener("resize", sync);
|
|
1096
1118
|
vv?.addEventListener("scroll", sync, { passive: true });
|
|
1097
1119
|
return () => {
|
|
1098
1120
|
if (rafId !== 0)
|
|
1099
1121
|
cancelAnimationFrame(rafId);
|
|
1100
|
-
window.removeEventListener("resize",
|
|
1101
|
-
vv?.removeEventListener("resize",
|
|
1122
|
+
window.removeEventListener("resize", syncAndSettleWindow);
|
|
1123
|
+
vv?.removeEventListener("resize", sync);
|
|
1102
1124
|
vv?.removeEventListener("scroll", sync);
|
|
1103
1125
|
};
|
|
1104
1126
|
}, [readViewport]);
|
|
@@ -1171,6 +1193,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1171
1193
|
const layoutShrink = Math.max(0, baseInnerHeightRef.current - viewport.innerHeight);
|
|
1172
1194
|
const nativeLift = Math.max(0, nativeKeyboardHeight - layoutShrink);
|
|
1173
1195
|
const effectiveKeyboardInset = Math.max(keyboardInset, nativeLift);
|
|
1196
|
+
const keyboardLiftActive = effectiveKeyboardInset > 0;
|
|
1174
1197
|
// FULL-SCREEN derived gate: maximized only takes effect AT the full detent, so
|
|
1175
1198
|
// a stale flag can never leak into half/collapsed/pill. Drives the edge-to-edge
|
|
1176
1199
|
// panel styles + a zero top margin.
|
|
@@ -1228,9 +1251,10 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1228
1251
|
// bug). They show only when the panel actually renders at/over half (or is
|
|
1229
1252
|
// full-bleed), tracking the finger frame-by-frame; the prev===next guard keeps
|
|
1230
1253
|
// re-renders to the two threshold crossings.
|
|
1231
|
-
const evalHeaderVisible = React.useCallback((h) => !pilled && (fullBleed || h >= halfH - 1), [pilled, fullBleed, halfH]);
|
|
1254
|
+
const evalHeaderVisible = React.useCallback((h) => threadPresented && !pilled && (fullBleed || h >= halfH - 1), [threadPresented, pilled, fullBleed, halfH]);
|
|
1232
1255
|
const [headerVisible, setHeaderVisible] = React.useState(false);
|
|
1233
1256
|
useMotionValueEvent(threadHeight, "change", (h) => {
|
|
1257
|
+
markLayoutShiftIntent();
|
|
1234
1258
|
const next = evalHeaderVisible(h);
|
|
1235
1259
|
setHeaderVisible((prev) => (prev === next ? prev : next));
|
|
1236
1260
|
});
|
|
@@ -1252,10 +1276,12 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1252
1276
|
const scrimVisibility = useTransform(threadHeight, (h) => h > 0 ? "visible" : "hidden");
|
|
1253
1277
|
const suggestionsOpacity = useTransform(threadHeight, (h) => Math.max(0, 1 - h / Math.max(1, openH * 0.5)));
|
|
1254
1278
|
const threadFlexBasis = useTransform(threadHeight, (h) => `${h}px`);
|
|
1255
|
-
// Corner radius tracks the live height
|
|
1256
|
-
//
|
|
1257
|
-
//
|
|
1258
|
-
|
|
1279
|
+
// Corner radius tracks the live height with real pixel radii. `9999px` works
|
|
1280
|
+
// for a static pill, but while the panel grows the browser keeps reclamping it
|
|
1281
|
+
// against the changing box, so the corners visibly swim before snapping to the
|
|
1282
|
+
// sheet radius. A 32px radius still renders as a capsule for the collapsed
|
|
1283
|
+
// composer, then relaxes gradually into the open sheet.
|
|
1284
|
+
const panelRadius = useTransform(threadHeight, [0, 160], [32, 24], {
|
|
1259
1285
|
clamp: true,
|
|
1260
1286
|
});
|
|
1261
1287
|
// --- Liquid-glass pill → input morph (driven by openProgress) ---------------
|
|
@@ -1303,16 +1329,23 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1303
1329
|
// is hidden, so the thread viewport would start at the panel's very top —
|
|
1304
1330
|
// tucking the topmost line under the floating drag handle (a partial bubble
|
|
1305
1331
|
// pinned beneath the grabber at a small free-rest height). Inset the thread
|
|
1306
|
-
// down by the grabber's height in that window only: 0
|
|
1332
|
+
// down by the grabber's height in that window only: 0 in the collapsed state
|
|
1307
1333
|
// (threadHeight ~0, so the closed input bar stays exactly its own height),
|
|
1308
1334
|
// ramping to the inset once a thread is actually open, then back to 0 as the
|
|
1309
1335
|
// header reveals at half+ (it provides the clearance itself).
|
|
1310
1336
|
const threadGrabberClearance = useTransform(threadHeight, [0, 40, halfH - 64, halfH], [0, 20, 20, 0], { clamp: true });
|
|
1337
|
+
// The glass should lead the gesture; transcript content fades in only after
|
|
1338
|
+
// there is enough vertical space to avoid clipped bubble slivers during the
|
|
1339
|
+
// first few pixels of a pull.
|
|
1340
|
+
const threadContentOpacity = useTransform(threadHeight, [72, 128], [0, 1], {
|
|
1341
|
+
clamp: true,
|
|
1342
|
+
});
|
|
1311
1343
|
// Sub-threshold release: spring back to the current detent (no state change).
|
|
1312
1344
|
// Also settles the pill→input morph to its resting end (0 while pilled, 1 once
|
|
1313
1345
|
// open) so a half-finished pill drag springs cleanly back to the capsule.
|
|
1314
1346
|
const settleDrag = React.useCallback(() => {
|
|
1315
1347
|
draggingRef.current = false;
|
|
1348
|
+
setDragPreviewMounted(false);
|
|
1316
1349
|
const open = pilled ? 0 : 1;
|
|
1317
1350
|
if (reduce) {
|
|
1318
1351
|
stopThreadAnimation();
|
|
@@ -1334,6 +1367,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1334
1367
|
stopOpenProgressAnimation,
|
|
1335
1368
|
animateThreadHeight,
|
|
1336
1369
|
animateOpenProgress,
|
|
1370
|
+
setDragPreviewMounted,
|
|
1337
1371
|
]);
|
|
1338
1372
|
// Keep the ref the (earlier-declared) viewport-resize effect calls pointing at
|
|
1339
1373
|
// the latest settleDrag, so a rotation re-settles with current pilled/baseH.
|
|
@@ -1433,13 +1467,22 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1433
1467
|
setFreeH(null);
|
|
1434
1468
|
if (to !== "full")
|
|
1435
1469
|
setMaximized(false);
|
|
1436
|
-
// "collapsed" is the input
|
|
1470
|
+
// "collapsed" is the input bar (sheet closed); half/full open the thread.
|
|
1437
1471
|
setMode(to === "collapsed" ? "input" : to);
|
|
1438
1472
|
// Stepping all the way down closes the keyboard (the chat is dismissed).
|
|
1439
1473
|
if (to === "collapsed")
|
|
1440
1474
|
inputRef.current?.blur();
|
|
1441
1475
|
detentHaptic();
|
|
1442
1476
|
}, [stopThreadAnimation]);
|
|
1477
|
+
const openFromGrabber = React.useCallback(() => {
|
|
1478
|
+
if (hasRevealableThread) {
|
|
1479
|
+
preFocusCollapsedRef.current = false;
|
|
1480
|
+
focusThreadRef.current = true;
|
|
1481
|
+
goToDetent("half");
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
inputRef.current?.focus();
|
|
1485
|
+
}, [goToDetent, hasRevealableThread]);
|
|
1443
1486
|
// Collapsing always drops input focus, so the mobile keyboard goes away the
|
|
1444
1487
|
// moment the chat is dismissed (pull-down, Escape, or click-out) — the chat is
|
|
1445
1488
|
// no longer "focused". Blurring (rather than the old refocus dance) also means
|
|
@@ -1501,13 +1544,13 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1501
1544
|
// handle) can return to that prior resting state. Clears any free-rest so the
|
|
1502
1545
|
// height matches the detent (no stale freeH pinning it below half).
|
|
1503
1546
|
const expand = React.useCallback(() => {
|
|
1504
|
-
if (!
|
|
1547
|
+
if (!hasRevealableThread)
|
|
1505
1548
|
return;
|
|
1506
1549
|
preFocusCollapsedRef.current = !sheetOpen;
|
|
1507
1550
|
setFreeH(null);
|
|
1508
1551
|
// Open to at least HALF; if already at half/full, keep the taller mode.
|
|
1509
1552
|
setMode((m) => (m === "half" || m === "full" ? m : "half"));
|
|
1510
|
-
}, [
|
|
1553
|
+
}, [hasRevealableThread, sheetOpen]);
|
|
1511
1554
|
// Interactive tour control: the tutorial drives the chat into a clean, known
|
|
1512
1555
|
// state at the start of each frame (so the spotlight always lands on the right
|
|
1513
1556
|
// control) and pre-fills the composer for the guided "ask to navigate" demo.
|
|
@@ -1742,15 +1785,16 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1742
1785
|
// this (focus may be gone by the time the click fires) to tell a first
|
|
1743
1786
|
// "dismiss the keyboard" tap from a second "close the chat" tap.
|
|
1744
1787
|
composerFocusedAtPressRef.current = focused;
|
|
1745
|
-
// Keyboard already down
|
|
1746
|
-
//
|
|
1788
|
+
// Keyboard already down -> outside taps do nothing here; the grabber,
|
|
1789
|
+
// scrim, Escape key, and pull-down gesture own disclosure/collapse.
|
|
1747
1790
|
if (!focused)
|
|
1748
1791
|
return;
|
|
1749
1792
|
const target = event.target;
|
|
1750
1793
|
if (target && panelRef.current?.contains(target))
|
|
1751
1794
|
return;
|
|
1752
|
-
// Leave a tap on the GRABBER to
|
|
1753
|
-
//
|
|
1795
|
+
// Leave a tap on the GRABBER to the gesture onTap; blurring here would
|
|
1796
|
+
// preempt the disclosure toggle and make press-time focus impossible to
|
|
1797
|
+
// distinguish from click-time focus.
|
|
1754
1798
|
if (target instanceof Element &&
|
|
1755
1799
|
target.closest('[data-testid="chat-sheet-grabber"]')) {
|
|
1756
1800
|
return;
|
|
@@ -1797,13 +1841,13 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1797
1841
|
const openFromPill = React.useCallback(() => {
|
|
1798
1842
|
draggingRef.current = false;
|
|
1799
1843
|
// A pill tap OPENS the chat. With a conversation to show, go straight to the
|
|
1800
|
-
// HALF detent — a tap reveals the thread exactly like a flick-up, so a
|
|
1801
|
-
// tap always opens the chat (never the old "tap lands on a bare input
|
|
1802
|
-
// tap again to actually open" two-step). Mark it deliberately open so
|
|
1844
|
+
// HALF detent — a tap reveals the thread/loader exactly like a flick-up, so a
|
|
1845
|
+
// SINGLE tap always opens the chat (never the old "tap lands on a bare input
|
|
1846
|
+
// bar, tap again to actually open" two-step). Mark it deliberately open so
|
|
1803
1847
|
// dismissing the keyboard then KEEPS it at half (preFocusCollapsedRef gates
|
|
1804
1848
|
// that). With no thread yet, there's nothing to open into — just form the
|
|
1805
1849
|
// bare input bar, and treat a later keyboard dismiss as a re-collapse.
|
|
1806
|
-
if (
|
|
1850
|
+
if (hasRevealableThread) {
|
|
1807
1851
|
goToDetent("half");
|
|
1808
1852
|
preFocusCollapsedRef.current = false;
|
|
1809
1853
|
}
|
|
@@ -1834,7 +1878,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1834
1878
|
}, [
|
|
1835
1879
|
openProgress,
|
|
1836
1880
|
reduce,
|
|
1837
|
-
|
|
1881
|
+
hasRevealableThread,
|
|
1838
1882
|
goToDetent,
|
|
1839
1883
|
stopOpenProgressAnimation,
|
|
1840
1884
|
animateOpenProgress,
|
|
@@ -1859,6 +1903,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1859
1903
|
const up = Math.max(0, offset);
|
|
1860
1904
|
openProgress.set(Math.min(1, up / PILL_OPEN_DISTANCE));
|
|
1861
1905
|
const excess = up - PILL_OPEN_DISTANCE;
|
|
1906
|
+
setDragPreviewMounted(excess > 0 && hasRevealableThread);
|
|
1862
1907
|
threadHeight.set(excess > 0 ? clampHeight(excess) : 0);
|
|
1863
1908
|
return;
|
|
1864
1909
|
}
|
|
@@ -1869,11 +1914,15 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1869
1914
|
// on release (the dead, unresponsive collapse gesture). The thread stays at
|
|
1870
1915
|
// 0 (nothing to size below the input).
|
|
1871
1916
|
if (!sheetOpen && offset < 0) {
|
|
1917
|
+
setDragPreviewMounted(false);
|
|
1872
1918
|
const down = -offset;
|
|
1873
1919
|
openProgress.set(Math.max(0, 1 - down / PILL_OPEN_DISTANCE));
|
|
1874
1920
|
threadHeight.set(0);
|
|
1875
1921
|
return;
|
|
1876
1922
|
}
|
|
1923
|
+
if (!sheetOpen) {
|
|
1924
|
+
setDragPreviewMounted(offset > 0 && hasRevealableThread);
|
|
1925
|
+
}
|
|
1877
1926
|
// Pin the dead direction at each end so the panel feels held: collapsed →
|
|
1878
1927
|
// only upward (positive); full → only downward (negative); half → both.
|
|
1879
1928
|
const off = !sheetOpen
|
|
@@ -1884,6 +1933,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1884
1933
|
threadHeight.set(clampHeight(baseH + off));
|
|
1885
1934
|
}, [
|
|
1886
1935
|
pilled,
|
|
1936
|
+
hasRevealableThread,
|
|
1887
1937
|
sheetOpen,
|
|
1888
1938
|
expanded,
|
|
1889
1939
|
baseH,
|
|
@@ -1892,20 +1942,35 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1892
1942
|
openProgress,
|
|
1893
1943
|
stopThreadAnimation,
|
|
1894
1944
|
stopOpenProgressAnimation,
|
|
1945
|
+
setDragPreviewMounted,
|
|
1895
1946
|
]);
|
|
1896
1947
|
const pullBinding = usePullGesture({
|
|
1897
1948
|
onDrag: onDragOffset,
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1949
|
+
onDragReset: settleDrag,
|
|
1950
|
+
swipeEnabled: !sheetOpen,
|
|
1951
|
+
onSwipeLeft: () => {
|
|
1952
|
+
settleDrag();
|
|
1953
|
+
if (!sheetOpen)
|
|
1954
|
+
goSpringboard();
|
|
1955
|
+
},
|
|
1956
|
+
onSwipeRight: () => {
|
|
1957
|
+
settleDrag();
|
|
1958
|
+
if (!sheetOpen)
|
|
1959
|
+
goHome();
|
|
1960
|
+
},
|
|
1961
|
+
// Flicks step one detent; released drags from the collapsed input honor the
|
|
1962
|
+
// live height so a long pull can land full instead of snapping back to half.
|
|
1963
|
+
// The inline closures are rebuilt every render, so they always read the
|
|
1964
|
+
// current detent.
|
|
1901
1965
|
onPullUp: () => {
|
|
1966
|
+
setDragPreviewMounted(false);
|
|
1902
1967
|
if (pilled) {
|
|
1903
1968
|
// PILL → INPUT, or straight into the chat when there's history: a flick
|
|
1904
1969
|
// up opens. Mirror the slow-drag path so a flick and a slow drag BOTH
|
|
1905
1970
|
// reach the chat (no hard stop at the bare input). Releasing draggingRef
|
|
1906
1971
|
// first lets the pilled→openProgress effect spring the morph 0→1.
|
|
1907
1972
|
draggingRef.current = false;
|
|
1908
|
-
if (
|
|
1973
|
+
if (hasRevealableThread) {
|
|
1909
1974
|
focusThreadRef.current = true;
|
|
1910
1975
|
goToDetent("half");
|
|
1911
1976
|
}
|
|
@@ -1923,9 +1988,15 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1923
1988
|
return;
|
|
1924
1989
|
}
|
|
1925
1990
|
if (!sheetOpen) {
|
|
1926
|
-
if (!
|
|
1991
|
+
if (!hasRevealableThread)
|
|
1927
1992
|
return settleDrag();
|
|
1928
|
-
|
|
1993
|
+
const releasedH = Math.max(0, Math.min(threadHeight.get(), panelMaxH));
|
|
1994
|
+
if (releasedH >= halfH + SHEET_DETENT_MAGNET) {
|
|
1995
|
+
goToDetent("full");
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
goToDetent("half");
|
|
1999
|
+
}
|
|
1929
2000
|
focusThreadRef.current = true;
|
|
1930
2001
|
}
|
|
1931
2002
|
else if (!expanded) {
|
|
@@ -1937,6 +2008,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1937
2008
|
}
|
|
1938
2009
|
},
|
|
1939
2010
|
onPullDown: () => {
|
|
2011
|
+
setDragPreviewMounted(false);
|
|
1940
2012
|
if (pilled)
|
|
1941
2013
|
return settleDrag(); // already the lowest detent
|
|
1942
2014
|
// Step down ONE detent based on the EFFECTIVE height (so a free-rest above
|
|
@@ -1955,39 +2027,31 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
1955
2027
|
setMode("pill");
|
|
1956
2028
|
setMaximized(false);
|
|
1957
2029
|
draggingRef.current = false;
|
|
2030
|
+
setDragPreviewMounted(false);
|
|
1958
2031
|
inputRef.current?.blur();
|
|
1959
2032
|
detentHaptic();
|
|
1960
2033
|
}
|
|
1961
2034
|
},
|
|
1962
2035
|
// A tap (no drag) on the handle. A tap on the PILL brings the input back.
|
|
1963
|
-
// When OPEN, the
|
|
1964
|
-
//
|
|
1965
|
-
//
|
|
1966
|
-
// is just "focus to type" — it must only focus the input, never dismiss or
|
|
1967
|
-
// collapse (the native focus already raised the keyboard, and the tap pierces
|
|
1968
|
-
// through to the input). Tapping OUTSIDE the panel is what drops the keyboard.
|
|
2036
|
+
// When OPEN, the grabber acts as a disclosure toggle: tap once to close.
|
|
2037
|
+
// When COLLAPSED, tap opens the thread or its loader; thread-less chats focus
|
|
2038
|
+
// the composer because there is nothing above the input to reveal.
|
|
1969
2039
|
onTap: () => {
|
|
1970
2040
|
if (pilled) {
|
|
1971
2041
|
openFromPill();
|
|
1972
2042
|
return;
|
|
1973
2043
|
}
|
|
1974
2044
|
if (sheetOpen) {
|
|
1975
|
-
|
|
1976
|
-
document.activeElement === inputRef.current;
|
|
1977
|
-
// Keyboard up → drop it and return to the pre-focus resting state (an
|
|
1978
|
-
// already-open sheet stays open; an auto-opened one re-collapses).
|
|
1979
|
-
// Keyboard down → the grabber is just the open chat's top bar; a tap
|
|
1980
|
-
// there does nothing (collapse is a pull-down / Escape / scrim tap).
|
|
1981
|
-
if (composerFocused)
|
|
1982
|
-
dismissKeyboardToPriorState();
|
|
2045
|
+
collapse();
|
|
1983
2046
|
return;
|
|
1984
2047
|
}
|
|
1985
|
-
|
|
2048
|
+
openFromGrabber();
|
|
1986
2049
|
},
|
|
1987
2050
|
// A deliberate (slow) drag: REST exactly where released instead of snapping
|
|
1988
2051
|
// to a detent — drag the sheet to any size and it stays.
|
|
1989
2052
|
onSettleFree: (direction) => {
|
|
1990
2053
|
draggingRef.current = false;
|
|
2054
|
+
setDragPreviewMounted(false);
|
|
1991
2055
|
if (pilled) {
|
|
1992
2056
|
// From the pill: a slow drag under the halfway-open mark (openProgress
|
|
1993
2057
|
// < 0.5) springs back to the capsule; past it we commit to LEAVING the
|
|
@@ -2007,15 +2071,21 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2007
2071
|
// Leaving the pill: fall through to the magnetism below, which sets the
|
|
2008
2072
|
// mode (input / half / full) from where the drag was released — so pill →
|
|
2009
2073
|
// input → chat reads as one continuum.
|
|
2010
|
-
if (
|
|
2074
|
+
if (hasRevealableThread)
|
|
2011
2075
|
focusThreadRef.current = true;
|
|
2012
2076
|
}
|
|
2013
2077
|
// From the collapsed input, a downward drag has nothing to "size" below
|
|
2014
|
-
// it
|
|
2078
|
+
// it. Require the input→pill morph to cross halfway before committing;
|
|
2079
|
+
// small thumb drift should spring back to the input, not collapse the chat.
|
|
2015
2080
|
if (!sheetOpen && direction === "down") {
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2081
|
+
if (openProgress.get() <= 0.5) {
|
|
2082
|
+
setMode("pill");
|
|
2083
|
+
inputRef.current?.blur();
|
|
2084
|
+
detentHaptic();
|
|
2085
|
+
}
|
|
2086
|
+
else {
|
|
2087
|
+
settleDrag();
|
|
2088
|
+
}
|
|
2019
2089
|
return;
|
|
2020
2090
|
}
|
|
2021
2091
|
const h = Math.max(0, Math.min(threadHeight.get(), panelMaxH));
|
|
@@ -2025,7 +2095,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2025
2095
|
// between them keep the free-drag rest height. goToDetent commits the
|
|
2026
2096
|
// honest flags so data-detent + the maximize header match the height.
|
|
2027
2097
|
if (h <= SHEET_DETENT_MAGNET) {
|
|
2028
|
-
// Near the bottom → collapse to the input
|
|
2098
|
+
// Near the bottom → collapse to the input bar.
|
|
2029
2099
|
closeSheet();
|
|
2030
2100
|
return;
|
|
2031
2101
|
}
|
|
@@ -2050,12 +2120,11 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2050
2120
|
// Full-bleed (maximized) removes the side inset so the chat is edge-to-edge.
|
|
2051
2121
|
fullBleed ? "px-0" : "px-3 sm:px-4"),
|
|
2052
2122
|
// Lift the whole overlay above the on-screen keyboard (`bottom`); padding
|
|
2053
|
-
// below the composer is conditional
|
|
2054
|
-
// up
|
|
2055
|
-
// composer and
|
|
2056
|
-
//
|
|
2057
|
-
//
|
|
2058
|
-
// touching that zone.
|
|
2123
|
+
// below the composer is conditional on an actual keyboard lift, not focus
|
|
2124
|
+
// alone. With the keyboard up, only a small gap (0.75rem, matching the side
|
|
2125
|
+
// margin) sits between composer and keyboard. At rest, clear the
|
|
2126
|
+
// home-gesture zone (max safe-area / android inset) plus a hair, keeping the
|
|
2127
|
+
// chat low without touching that zone.
|
|
2059
2128
|
style: {
|
|
2060
2129
|
zIndex: Z_SHELL_OVERLAY,
|
|
2061
2130
|
bottom: effectiveKeyboardInset,
|
|
@@ -2066,7 +2135,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2066
2135
|
// chat lifted off the gesture zone as before.
|
|
2067
2136
|
paddingBottom: fullBleed
|
|
2068
2137
|
? 0
|
|
2069
|
-
:
|
|
2138
|
+
: keyboardLiftActive
|
|
2070
2139
|
? "0.75rem"
|
|
2071
2140
|
: "calc(var(--eliza-mobile-nav-offset, 0px) + max(var(--safe-area-bottom, 0px), var(--android-gesture-inset-bottom, 0px)) + 0.25rem)",
|
|
2072
2141
|
}, "data-testid": "continuous-chat-overlay", "data-open": sheetOpen ? "true" : undefined, children: [_jsx(motion.div, { "aria-hidden": "true", "data-testid": "chat-sheet-backdrop", "data-active": sheetOpen ? "true" : "false", onClick: sheetOpen
|
|
@@ -2090,12 +2159,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2090
2159
|
pointerEvents: sheetOpen ? "auto" : "none",
|
|
2091
2160
|
} }), recording && transcript ? (_jsxs("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: cn("pointer-events-none relative mb-2 w-full max-w-3xl text-center text-sm italic text-white/85", FLOAT_SHADOW), children: [transcript, _jsx("span", { "aria-hidden": "true", children: "\u2026" })] })) : null, needsAudioUnlock ? (_jsx("div", { role: "status", "aria-live": "polite", className: "pointer-events-none relative mb-2 flex w-full justify-center", children: _jsxs("button", { type: "button", onClick: unlockAudio, "data-testid": "overlay-voice-audio-unlock", className: cn("pointer-events-auto inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors", "border-warn/40 bg-warn/15 text-warn hover:bg-warn/25", " ", FLOAT_SHADOW), children: [_jsx(Glyph, { d: SPEAKER_MUTED_GLYPH }), _jsx("span", { children: "Tap to enable sound" })] }) })) : null, modelStatus.kind === "downloading" || modelStatus.kind === "loading" ? (_jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", "data-testid": "overlay-model-download-status", className: "pointer-events-none relative mb-2 flex w-full justify-center", children: _jsxs("span", { className: cn("inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/10 px-3 py-1.5 text-sm font-medium text-white/85", FLOAT_SHADOW), children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin text-[#FF5800]" }), modelStatus.kind === "downloading" ? (_jsxs("span", { children: ["Downloading ", modelStatus.modelName ?? "local model", typeof modelStatus.percent === "number"
|
|
2092
2161
|
? ` — ${Math.round(modelStatus.percent)}%`
|
|
2093
|
-
: "…"] })) : (_jsxs("span", { children: ["Loading ", modelStatus.modelName ?? "local model", "\u2026"] }))] }) })) : null, suggestionsVisible ? (_jsx(motion.fieldset, { "aria-label": "Suggested prompts", className: cn("pointer-events-auto relative m-0 mb-2 flex w-full max-w-3xl flex-wrap items-center justify-center gap-2 border-0 p-0"), style: { order: -1, opacity: suggestionsOpacity }, "data-testid": "chat-suggestions", children: suggestions.map((s, i) => (_jsx("button", { type: "button", "data-testid": `chat-suggestion-${i}`, "aria-label": s, onClick: () => pickSuggestion(s), className: cn("max-w-full truncate rounded-full border border-white/15 bg-black/40 px-3 py-1.5", "text-[12px] text-white/80 transition-colors", "hover:border-white/30 hover:bg-white/15 hover:text-white", " "), children: s }, s))) })) : null, _jsxs("div", { className: cn("pointer-events-none relative flex w-full flex-col items-center", fullBleed ? "max-w-none" : "max-w-3xl"), children: [!fullBleed ? (_jsx(SheetGrabber, { open: sheetOpen, onOpen:
|
|
2094
|
-
if (!hasThread)
|
|
2095
|
-
return;
|
|
2096
|
-
goToDetent("half");
|
|
2097
|
-
focusThreadRef.current = true;
|
|
2098
|
-
}, onClose: collapse, binding: pullBinding, glow: listening || responding, opacity: grabberOpacity, pilled: pilled })) : null, _jsxs(motion.fieldset, { ref: panelRef, "aria-label": "Chat composer", "data-testid": "chat-sheet", "data-variant": sheetOpen ? "open" : "closed", "data-detent": pilled
|
|
2162
|
+
: "…"] })) : (_jsxs("span", { children: ["Loading ", modelStatus.modelName ?? "local model", "\u2026"] }))] }) })) : null, suggestionsVisible ? (_jsx(motion.fieldset, { "aria-label": "Suggested prompts", className: cn("pointer-events-auto relative m-0 mb-2 flex w-full max-w-3xl flex-wrap items-center justify-center gap-2 border-0 p-0"), style: { order: -1, opacity: suggestionsOpacity }, "data-testid": "chat-suggestions", children: suggestions.map((s, i) => (_jsx("button", { type: "button", "data-testid": `chat-suggestion-${i}`, "aria-label": s, onClick: () => pickSuggestion(s), className: cn("max-w-full truncate rounded-full border border-white/15 bg-black/40 px-3 py-1.5", "text-[12px] text-white/80 transition-colors", "hover:border-white/30 hover:bg-white/15 hover:text-white", " "), children: s }, s))) })) : null, _jsxs("div", { className: cn("pointer-events-none relative flex w-full flex-col items-center", fullBleed ? "max-w-none" : "max-w-3xl"), children: [!fullBleed ? (_jsx(SheetGrabber, { open: sheetOpen, onOpen: openFromGrabber, onClose: collapse, binding: pullBinding, glow: listening || responding, opacity: grabberOpacity, pilled: pilled })) : null, _jsxs(motion.fieldset, { ref: panelRef, "aria-label": "Chat composer", "data-testid": "chat-sheet", "data-variant": sheetOpen ? "open" : "closed", "data-detent": pilled
|
|
2099
2163
|
? "pill"
|
|
2100
2164
|
: !sheetOpen
|
|
2101
2165
|
? "collapsed"
|
|
@@ -2105,7 +2169,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2105
2169
|
: "half"
|
|
2106
2170
|
: expanded
|
|
2107
2171
|
? "full"
|
|
2108
|
-
: "half", "data-maximized": fullBleed ? "true" : undefined, "data-revealed":
|
|
2172
|
+
: "half", "data-maximized": fullBleed ? "true" : undefined, "data-revealed": threadPresented ? "true" : "false", "data-chat-state": chatState, "data-header-shown": headerVisible ? "true" : "false",
|
|
2109
2173
|
// ONE persistent element across pill ↔ input ↔ chat (never remounts —
|
|
2110
2174
|
// that pop was the core jank). It's a transparent scale/position
|
|
2111
2175
|
// container; the liquid glass lives in an inner layer faded by
|
|
@@ -2152,19 +2216,17 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2152
2216
|
opacity: glassOpacity,
|
|
2153
2217
|
pointerEvents: pilled ? "none" : "auto",
|
|
2154
2218
|
borderRadius: fullBleed ? 0 : panelRadius,
|
|
2155
|
-
}, children: [sheetOpen ? (_jsxs(_Fragment, { children: [_jsx(SwipeEdgeHint, { side: "left", active: swipeDx < 0 && conversationNav.hasPrev, amount: -swipeDx }), _jsx(SwipeEdgeHint, { side: "right", active: swipeDx > 0 && conversationNav.hasNext, amount: swipeDx })] })) : null, _jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0 top-0 z-0 h-20 bg-gradient-to-b from-white/[0.07] to-transparent" }),
|
|
2156
|
-
//
|
|
2157
|
-
//
|
|
2158
|
-
//
|
|
2159
|
-
// interactivity + the a11y tree
|
|
2160
|
-
// can't be clicked or read.
|
|
2219
|
+
}, children: [sheetOpen ? (_jsxs(_Fragment, { children: [_jsx(SwipeEdgeHint, { side: "left", active: swipeDx < 0 && conversationNav.hasPrev, amount: -swipeDx }), _jsx(SwipeEdgeHint, { side: "right", active: swipeDx > 0 && conversationNav.hasNext, amount: swipeDx })] })) : null, _jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0 top-0 z-0 h-20 bg-gradient-to-b from-white/[0.07] to-transparent" }), threadPresented ? (_jsxs(motion.div
|
|
2220
|
+
// Mounted while the sheet is open, or while an upward drag is
|
|
2221
|
+
// previewing the sheet before release. It can FADE + LERP its
|
|
2222
|
+
// space as the live height crosses the header threshold.
|
|
2223
|
+
// `headerVisible` gates interactivity + the a11y tree.
|
|
2161
2224
|
, {
|
|
2162
|
-
//
|
|
2163
|
-
//
|
|
2164
|
-
//
|
|
2165
|
-
// interactivity + the a11y tree
|
|
2166
|
-
|
|
2167
|
-
inert: !headerVisible || undefined, style: {
|
|
2225
|
+
// Mounted while the sheet is open, or while an upward drag is
|
|
2226
|
+
// previewing the sheet before release. It can FADE + LERP its
|
|
2227
|
+
// space as the live height crosses the header threshold.
|
|
2228
|
+
// `headerVisible` gates interactivity + the a11y tree.
|
|
2229
|
+
inert: !sheetOpen || !headerVisible || undefined, style: {
|
|
2168
2230
|
// Full-bleed is always fully open: show the header at full
|
|
2169
2231
|
// opacity and UNCAP its height. The reveal lerp tops out at
|
|
2170
2232
|
// 100px, but the safe-area top padding (status-bar height +
|
|
@@ -2185,11 +2247,11 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2185
2247
|
: headerPadTop,
|
|
2186
2248
|
}, className: cn("relative z-10 flex shrink-0 items-center justify-between gap-1.5 overflow-hidden px-3"), children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(HeaderButton, { icon: maximized ? Minimize2 : Maximize2, label: maximized ? "exit full screen" : "full screen", active: maximized, onClick: toggleMaximize, testId: "chat-full-maximize" }), hasThread ? (_jsx(HeaderButton, { icon: conversationCopied ? Check : Copy, label: conversationCopied
|
|
2187
2249
|
? "conversation copied"
|
|
2188
|
-
: "copy conversation", onClick: handleCopyConversation, testId: "chat-full-copy-conversation" })) : null, _jsx(HeaderButton, { icon: RotateCcw, label: "clear conversation", onClick: () => clearConversation(), testId: "chat-full-clear" })] }), transcriptionMode ? (_jsx("div", { "data-testid": "chat-transcribing-badge", className: "pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 whitespace-nowrap rounded-full bg-[var(--brand-orange,#ff6a00)]/15 px-2.5 py-0.5 text-[11px] font-medium text-[var(--brand-orange,#ff6a00)]", children: "Transcribing \u2014 say \u201Cexit transcription mode\u201D to stop" })) : null, _jsx("div", { className: "flex items-center gap-1.5", children: _jsx(HeaderButton, { icon: LayoutGrid, label: "springboard", onClick: () => navigateAndClose(() => navigateHome?.()), testId: "chat-full-springboard" }) })] })) : null,
|
|
2250
|
+
: "copy conversation", onClick: handleCopyConversation, testId: "chat-full-copy-conversation" })) : null, _jsx(HeaderButton, { icon: RotateCcw, label: "clear conversation", onClick: () => clearConversation(), testId: "chat-full-clear" })] }), transcriptionMode ? (_jsx("div", { "data-testid": "chat-transcribing-badge", className: "pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 whitespace-nowrap rounded-full bg-[var(--brand-orange,#ff6a00)]/15 px-2.5 py-0.5 text-[11px] font-medium text-[var(--brand-orange,#ff6a00)]", children: "Transcribing \u2014 say \u201Cexit transcription mode\u201D to stop" })) : null, _jsx("div", { className: "flex items-center gap-1.5", children: _jsx(HeaderButton, { icon: LayoutGrid, label: "springboard", onClick: () => navigateAndClose(() => navigateHome?.()), testId: "chat-full-springboard" }) })] })) : null, threadPresented ? (_jsx(motion.div, { "data-testid": "chat-thread", className: cn("relative z-10 min-h-0 w-full shrink grow-0 overflow-hidden",
|
|
2189
2251
|
// When open, fade the top edge into the glass so the topmost
|
|
2190
2252
|
// message dissolves under the drag handle instead of butting
|
|
2191
2253
|
// against it.
|
|
2192
|
-
|
|
2254
|
+
threadPresented &&
|
|
2193
2255
|
"[mask-image:linear-gradient(to_bottom,transparent_0,#000_34px)] [-webkit-mask-image:linear-gradient(to_bottom,transparent_0,#000_34px)]"),
|
|
2194
2256
|
// Flex-basis IS the motion value (px string) — set 1:1 during a drag,
|
|
2195
2257
|
// spring-animated to a detent on release; no `animate`/`transition`,
|
|
@@ -2201,12 +2263,12 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2201
2263
|
style: {
|
|
2202
2264
|
flexBasis: threadFlexBasis,
|
|
2203
2265
|
paddingTop: threadGrabberClearance,
|
|
2204
|
-
}, children: _jsxs(
|
|
2266
|
+
}, children: _jsxs(motion.div, { id: "continuous-thread", ref: threadRef, role: "log", "aria-label": "conversation history", "aria-live": "polite", "aria-hidden": !sheetOpen ? true : undefined, tabIndex: sheetOpen ? 0 : -1, onKeyDown: (e) => {
|
|
2205
2267
|
if (e.key === "Escape") {
|
|
2206
2268
|
e.preventDefault();
|
|
2207
2269
|
collapse();
|
|
2208
2270
|
}
|
|
2209
|
-
}, ...(sheetOpen ? conversationSwipe : {}), className: "relative flex h-full w-full touch-pan-y flex-col overflow-y-auto px-5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: [visibleMessages.length === 0 && conversationLoading ? (_jsx("div", { "data-testid": "chat-thread-loading", className: "pointer-events-none absolute inset-0 grid place-items-center", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-[#FF5800]" }) })) : null, hasTopics ? (_jsx(TopicChipsBar, { topics: channelTopics, onSelectTopic: scrollToTopic, className: "sticky top-0 z-[2] -mx-5 mb-1 bg-gradient-to-b from-black/40 to-transparent px-5" })) : null, _jsxs("div", { className: "mt-auto flex flex-col pb-3 pt-1", children: [hasTopics
|
|
2271
|
+
}, ...(sheetOpen ? conversationSwipe : {}), className: "relative flex h-full w-full touch-pan-y flex-col overflow-y-auto px-5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", style: { opacity: threadContentOpacity }, children: [visibleMessages.length === 0 && conversationLoading ? (_jsx("div", { "data-testid": "chat-thread-loading", className: "pointer-events-none absolute inset-0 grid place-items-center", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-[#FF5800]" }) })) : null, hasTopics ? (_jsx(TopicChipsBar, { topics: channelTopics, onSelectTopic: scrollToTopic, className: "sticky top-0 z-[2] -mx-5 mb-1 bg-gradient-to-b from-black/40 to-transparent px-5" })) : null, _jsxs("div", { className: "mt-auto flex flex-col pb-3 pt-1", children: [hasTopics
|
|
2210
2272
|
? // Topic-grouped transcript: each cluster collapses via a
|
|
2211
2273
|
// gesture on its header (no visible buttons).
|
|
2212
2274
|
(() => {
|
|
@@ -2257,7 +2319,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2257
2319
|
// edge-to-edge), so the composer carries the home-gesture
|
|
2258
2320
|
// clearance itself — except while the keyboard is up, which
|
|
2259
2321
|
// already covers that zone.
|
|
2260
|
-
style: fullBleed && !
|
|
2322
|
+
style: fullBleed && !keyboardLiftActive
|
|
2261
2323
|
? {
|
|
2262
2324
|
paddingBottom: "calc(0.5rem + max(var(--safe-area-bottom, 0px), var(--android-gesture-inset-bottom, 0px)))",
|
|
2263
2325
|
}
|
|
@@ -2268,7 +2330,6 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2268
2330
|
if (e.target.value.trim().length > 0)
|
|
2269
2331
|
expand();
|
|
2270
2332
|
}, onFocus: () => {
|
|
2271
|
-
setComposerFocused(true);
|
|
2272
2333
|
// A pill-open focus only raises the keyboard; it must not
|
|
2273
2334
|
// expand a history thread (see suppressExpandOnFocusRef).
|
|
2274
2335
|
if (suppressExpandOnFocusRef.current) {
|
|
@@ -2277,7 +2338,7 @@ export function ContinuousChatOverlay({ controller, agentName = "Eliza", slash:
|
|
|
2277
2338
|
else {
|
|
2278
2339
|
expand();
|
|
2279
2340
|
}
|
|
2280
|
-
},
|
|
2341
|
+
}, onPaste: (e) => {
|
|
2281
2342
|
// Shared with the desktop composer: a pasted image/file
|
|
2282
2343
|
// attaches, a large plain-text paste becomes a collapsed
|
|
2283
2344
|
// text-attachment chip, and small text falls through to the
|