Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/ComposerModelEffort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ export function ComposerEffortColumn(props: { traits: TraitsSelectionInput | nul
</div>
{/* pt-2.5 puts the first level on the same line as the first model. */}
<div
className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto overscroll-contain px-2 pt-2.5 pb-1.5"
className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-2 pt-2.5 pb-1.5"
// Capture, so Up and Down on an option row move rows instead of opening its menu.
onKeyDownCapture={moveBetweenRows}
>
Expand Down
22 changes: 18 additions & 4 deletions apps/web/src/components/chat/ModelPickerContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ const MODEL_LIST_ESTIMATED_ITEM_SIZE = 32;
// Rendered heights in `--spacing` units. Chrome is the search box and list padding, plus a 1px border.
const ROW_UNITS = 8;
const ROW_SEPARATOR_UNITS = 0.5;
const LIST_PADDING_UNITS = 5;
const LIST_CHROME_UNITS = 15.5;

function ModelListSeparator() {
Expand Down Expand Up @@ -255,11 +256,13 @@ function ModelPickerList({
itemKeys,
extraData,
renderItem,
fitRows,
}: {
listRef: RefObject<LegendListRef | null>;
itemKeys: ReadonlyArray<string>;
extraData: unknown;
renderItem: (props: { item: string; index: number }) => ReactNode;
fitRows: boolean | undefined;
}) {
const [showTopScrollFade, setShowTopScrollFade] = useState(false);
const [showBottomScrollFade, setShowBottomScrollFade] = useState(false);
Expand Down Expand Up @@ -301,7 +304,15 @@ function ModelPickerList({
className={cn(
"relative min-h-0 overflow-hidden pr-px",
itemKeys.length > 0 ? "flex-1" : "h-0",
fitRows && "[--row-units:9] sm:[--row-units:8]",
)}
style={
fitRows && itemKeys.length > 0
? {
flexBasis: `calc(var(--spacing) * (${itemKeys.length} * var(--row-units) + ${(itemKeys.length - 1) * ROW_SEPARATOR_UNITS} + ${LIST_PADDING_UNITS}))`,
}
: undefined
}
>
<ComboboxListVirtualized>
<LegendList<string>
Expand All @@ -318,7 +329,8 @@ function ModelPickerList({
onLayout={updateScrollFades}
onScroll={updateScrollFades}
className={cn(
"scrollbar-gutter-stable h-full overflow-x-hidden overscroll-y-contain py-1.5 [&::-webkit-scrollbar-track]:my-2",
"scrollbar-gutter-stable h-full overflow-x-hidden py-1.5 [&::-webkit-scrollbar-track]:my-2",
!fitRows && "overscroll-y-contain",
getVirtualizedScrollFadeClassName({
top: showTopScrollFade,
bottom: showBottomScrollFade,
Expand Down Expand Up @@ -366,7 +378,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
docked?: boolean;
/** Right at the end of the search text moves into the popup's effort column. */
onFocusEffort?: () => void;
/** Closes the list column, inside the popup's fixed height. */
/** Closes the list column. */
listFooter?: ReactNode;
}) {
const {
Expand Down Expand Up @@ -840,9 +852,9 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
ref={pickerContentRef}
className={cn(
"relative flex w-screen max-w-90 flex-row overflow-hidden",
props.docked ? "max-sm:h-56 max-sm:w-full max-sm:max-w-none" : "max-h-86.5",
props.docked ? "max-sm:max-h-56 max-sm:w-full max-sm:max-w-none" : "max-h-86.5",
)}
// Docked, the popup sets the height; a minimum here would overflow short windows.
// Docked, the list sets the height; a minimum here would overflow short windows.
style={
props.docked
? undefined
Expand All @@ -856,6 +868,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
>
{showSidebar && (
<ModelPickerSidebar
docked={props.docked ?? false}
selectedInstanceId={selectedInstanceId}
onSelectInstance={handleSelectInstance}
onFocusSearch={focusSearchInput}
Expand Down Expand Up @@ -1009,6 +1022,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
/>

<ModelPickerList
fitRows={props.docked}
listRef={modelListRef}
itemKeys={filteredItemKeys}
extraData={modelListExtraData}
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/components/chat/ModelPickerSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
onFocusSearch: () => void;
instanceEntries: ReadonlyArray<ProviderInstanceEntry>;
showFavorites?: boolean;
docked?: boolean;
disabledInstanceIds?: ReadonlySet<ProviderInstanceId>;
selectableUnavailableInstanceIds?: ReadonlySet<ProviderInstanceId>;
getDisabledInstanceTooltip?: (entry: ProviderInstanceEntry) => string;
Expand Down Expand Up @@ -82,7 +83,12 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
}
}}
>
<div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div
className={cn(
"h-full overflow-y-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
!props.docked && "overscroll-contain",
)}
>
<div ref={sidebarContentRef} className="relative flex min-h-full flex-col gap-1 p-1">
{selectedIndicatorTop !== null ? (
<div
Expand Down
43 changes: 25 additions & 18 deletions apps/web/src/components/chat/ProviderModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type ResolvedKeybindingsConfig,
} from "@t3tools/contracts";
import { createContext, memo, type ReactNode, useRef, useState } from "react";
import { AnimatedHeight } from "../AnimatedHeight";
import { Badge } from "../ui/badge";
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
Expand Down Expand Up @@ -278,7 +279,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
? {
docked,
onFocusEffort: () => void focusEffort("any"),
// Inside the fixed height, so switching to or from a ChatGPT plan never resizes it.
...(props.selectedModels === undefined
? { listFooter: <ChatGptSharingControl provider={activeEntry?.snapshot ?? null} /> }
: {}),
Expand Down Expand Up @@ -427,23 +427,30 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
{props.effortColumn ? (
<DockedPickerContext value={{ modelLabel: triggerTitle }}>
<div className="flex max-h-(--available-height) flex-col">
{/* One fixed height, so a model with more options never resizes the popup. Short
windows shrink it and keep the footer. */}
<div className="flex h-86.5 min-h-0 max-sm:h-auto max-sm:flex-col">
{picker}
<div
data-model-picker-effort
className="flex min-h-0 w-48 flex-col sm:shrink-0 border-l border-border/70 bg-muted/40 max-sm:h-64 max-sm:w-full max-sm:border-t max-sm:border-l-0"
onKeyDown={(event) => {
if (event.key !== "ArrowLeft" || event.altKey || event.ctrlKey || event.metaKey)
return;
event.preventDefault();
event.stopPropagation();
searchInputRef.current?.focus();
}}
>
{props.effortColumn}
</div>
<div className="min-h-0 overflow-y-auto overscroll-y-contain">
<AnimatedHeight>
<div className="flex max-h-86.5 max-sm:max-h-none max-sm:flex-col">
{picker}
<div
data-model-picker-effort
className="flex min-h-0 w-48 flex-col sm:shrink-0 border-l border-border/70 bg-muted/40 max-sm:max-h-64 max-sm:w-full max-sm:border-t max-sm:border-l-0"
onKeyDown={(event) => {
if (
event.key !== "ArrowLeft" ||
event.altKey ||
event.ctrlKey ||
event.metaKey
)
return;
event.preventDefault();
event.stopPropagation();
searchInputRef.current?.focus();
}}
>
{props.effortColumn}
</div>
</div>
</AnimatedHeight>
</div>
{props.footer}
</div>
Expand Down
Loading