Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 81 additions & 113 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import { folderDropTarget, resolveDroppedFolderPath } from "./folderDrop";
import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";
import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits";
import {
Fragment,
memo,
type ComponentProps,
type ReactNode,
Expand Down Expand Up @@ -185,17 +184,14 @@ import {
import { useComposerPathSearch } from "../../lib/composerPathSearchState";
import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";
import {
COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX,
COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,
getRestingComposerImagePreviewCounts,
resolveRestingComposerControlsLayout,
shouldAnimateComposerRestingTransition,
shouldUseCompactComposerPrimaryActions,
shouldUseCompactComposerFooter,
resolveRestingComposerControlsNaturalWidth,
shouldUseRestingComposerLayout,
} from "../composerFooterLayout";
import { measureRestingComposerControls } from "./restingComposerControlsMeasurement";
import { observeResponsiveBreakpointFade, usePanelAnimationSettings } from "../../panelAnimations";
import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor";
import {
ComposerContextActionsContext,
Expand Down Expand Up @@ -2096,8 +2092,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
key: 0,
active: false,
});
const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
usePanelAnimationSettings();
const isComposerCollapsedMobile =
isMobileViewport && !forceExpandedOnMobile && !isComposerFocused && !hasMultilinePrompt;

Expand Down Expand Up @@ -3133,71 +3127,48 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
// ------------------------------------------------------------------
// Footer compact layout observation
// ------------------------------------------------------------------
useLayoutEffect(() => {
const measureFooterCompactness = useCallback(() => {
const composerForm = composerFormRef.current;
if (!composerForm) return;
const measureComposerFormWidth = () => composerForm.clientWidth;
const measureFooterCompactness = () => {
const composerFormWidth = measureComposerFormWidth();
const footerCompact = shouldUseCompactComposerFooter(composerFormWidth, {
setIsComposerPrimaryActionsCompact(
shouldUseCompactComposerPrimaryActions(composerForm.clientWidth, {
hasWideActions: composerFooterHasWideActions,
});
const primaryActionsCompact =
footerCompact &&
shouldUseCompactComposerPrimaryActions(composerFormWidth, {
hasWideActions: composerFooterHasWideActions,
});
return {
primaryActionsCompact,
footerCompact,
};
};

const initialCompactness = measureFooterCompactness();
setIsComposerPrimaryActionsCompact(initialCompactness.primaryActionsCompact);
setIsComposerFooterCompact(initialCompactness.footerCompact);
if (typeof ResizeObserver === "undefined") return;
const footerControls = composerFooterControlsRef.current;
const stopFooterControlsFade = footerControls
? observeResponsiveBreakpointFade({
target: footerControls,
container: composerForm,
active: panelAnimationsActive,
durationMs: panelAnimationDurationMs,
breakpoint: {
value: composerFooterHasWideActions
? COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX
: COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX,
unit: "px",
},
})
: undefined;

const observer = new ResizeObserver(() => {
const nextCompactness = measureFooterCompactness();
setIsComposerPrimaryActionsCompact((previous) =>
previous === nextCompactness.primaryActionsCompact
? previous
: nextCompactness.primaryActionsCompact,
);
setIsComposerFooterCompact((previous) =>
previous === nextCompactness.footerCompact ? previous : nextCompactness.footerCompact,
);
});

}),
);
const controls = composerFooterControlsRef.current;
if (!controls || controls.clientWidth === 0) return;
const measurement = measureRestingComposerControls(controls);
if (!measurement) return;
const style = getComputedStyle(controls);
const availableWidth =
controls.clientWidth -
(Number.parseFloat(style.paddingInlineStart) || 0) -
(Number.parseFloat(style.paddingInlineEnd) || 0);
const naturalWidth = resolveRestingComposerControlsNaturalWidth(measurement);
// Hidden controls keep their natural width, so reopening the footer does
// not depend on the smaller overflow menu. Leave a pixel for rounding.
setIsComposerFooterCompact((previous) => naturalWidth > availableWidth - (previous ? 1 : 0));
}, [composerFooterHasWideActions]);

useLayoutEffect(measureFooterCompactness);
useEffect(() => {
const composerForm = composerFormRef.current;
if (!composerForm || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(measureFooterCompactness);
observer.observe(composerForm);
const controls = composerFooterControlsRef.current;
if (controls) observer.observe(controls);
document.fonts.addEventListener("loadingdone", measureFooterCompactness);
return () => {
observer.disconnect();
stopFooterControlsFade?.();
document.fonts.removeEventListener("loadingdone", measureFooterCompactness);
};
}, [
activeThreadId,
composerFooterActionLayoutKey,
composerFooterHasWideActions,
isComposerApprovalState,
isComposerCollapsedMobile,
panelAnimationDurationMs,
panelAnimationsActive,
measureFooterCompactness,
]);

// ------------------------------------------------------------------
Expand Down Expand Up @@ -4909,7 +4880,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
content: (
<>
<ComposerControlSeparator size={composerControlsInStrip ? "xs" : "sm"} />
{composerControlsInStrip ? restingProviderTraitsPicker : providerTraitsPicker}
{composerControlsInStrip
? restingProviderTraitsPicker
: renderProviderTraitsPicker({
...providerTraitsPickerInput,
hidden: composerControlsHidden || composerControlsCompact,
})}
</>
),
},
Expand All @@ -4923,7 +4899,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
interactionMode={interactionMode}
runtimeMode={runtimeMode}
size={composerControlsInStrip ? "xs" : "sm"}
hidden={composerControlsHidden || restingHiddenBlockCount > 0}
hidden={composerControlsHidden || composerControlsCompact || restingHiddenBlockCount > 0}
onToggleInteractionMode={toggleInteractionMode}
onRuntimeModeChange={handleRuntimeModeChange}
/>
Expand Down Expand Up @@ -5052,56 +5028,49 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
onToggleInteractionMode={toggleInteractionMode}
onRuntimeModeChange={handleRuntimeModeChange}
/>
) : (
<>
{restingBlockDefs.map((def, index) => {
if (!composerControlsInStrip) {
return <Fragment key={def.id}>{def.content}</Fragment>;
) : null}
{restingBlockDefs.map((def, index) => {
const hidden =
composerControlsCompact || index >= restingBlockDefs.length - restingHiddenBlockCount;
return (
<div
key={def.id}
data-resting-block={def.id}
aria-hidden={hidden || undefined}
inert={hidden || undefined}
className={cn(
"flex w-max min-w-max shrink-0 items-center gap-1",
hidden && "pointer-events-none invisible absolute",
)}
>
{def.content}
</div>
);
})}
{composerControlsInStrip ? (
<div
data-resting-controls-overflow
aria-hidden={hiddenRestingBlockIds.length === 0 || undefined}
inert={hiddenRestingBlockIds.length === 0 || undefined}
className={cn(
"min-w-0 shrink-0",
hiddenRestingBlockIds.length === 0 && "pointer-events-none invisible absolute",
)}
>
<CompactComposerControlsMenu
interactionMode={interactionMode}
runtimeMode={runtimeMode}
size="xs"
hidden={composerControlsHidden || hiddenRestingBlockIds.length === 0}
showInteractionModeToggle={planModeUiEnabled && hiddenRestingBlockIds.includes("mode")}
traitsMenuContent={
hiddenRestingBlockIds.includes("traits") ? providerTraitsMenuContent : undefined
}
const hidden = index >= restingBlockDefs.length - restingHiddenBlockCount;
return (
<div
key={def.id}
data-resting-block={def.id}
aria-hidden={hidden || undefined}
inert={hidden || undefined}
className={cn(
"flex w-max min-w-max shrink-0 items-center gap-1",
hidden && "pointer-events-none invisible absolute",
)}
>
{def.content}
</div>
);
})}
{composerControlsInStrip ? (
<div
data-resting-controls-overflow
aria-hidden={hiddenRestingBlockIds.length === 0 || undefined}
inert={hiddenRestingBlockIds.length === 0 || undefined}
className={cn(
"min-w-0 shrink-0",
hiddenRestingBlockIds.length === 0 && "pointer-events-none invisible absolute",
)}
>
<CompactComposerControlsMenu
interactionMode={interactionMode}
runtimeMode={runtimeMode}
size="xs"
hidden={composerControlsHidden || hiddenRestingBlockIds.length === 0}
showInteractionModeToggle={
planModeUiEnabled && hiddenRestingBlockIds.includes("mode")
}
traitsMenuContent={
hiddenRestingBlockIds.includes("traits") ? providerTraitsMenuContent : undefined
}
onToggleInteractionMode={toggleInteractionMode}
onRuntimeModeChange={handleRuntimeModeChange}
/>
</div>
) : null}
</>
)}
onToggleInteractionMode={toggleInteractionMode}
onRuntimeModeChange={handleRuntimeModeChange}
/>
</div>
) : null}
</>
);
const showTasksTab =
Expand Down Expand Up @@ -6901,9 +6870,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
data-chat-composer-footer="true"
data-chat-composer-footer-compact={isComposerFooterCompact ? "true" : "false"}
className={cn(
"flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-visible px-3 pb-3 sm:px-4 sm:pb-4",
"flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-visible px-3 pb-3 sm:gap-0 sm:px-4 sm:pb-4",
pendingUserInputs.length > 0 && "pt-2",
isComposerFooterCompact ? "gap-1.5" : "gap-2 sm:gap-0",
showMobilePendingAnswerActions && "hidden sm:flex",
isComposerResting &&
"absolute bottom-px right-px z-10 h-12 w-auto gap-0 py-0 sm:gap-0 sm:py-0",
Expand Down
Loading