Skip to content
Merged
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
82 changes: 81 additions & 1 deletion apps/playground/src/components/model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ interface ModelSelectorProps {
mode?: "chat" | "video" | "image" | "audio";
isOptionDisabled?: (value: string) => boolean;
getOptionDisabledReason?: (value: string) => string | undefined;
/**
* When true, regional provider mappings are listed as separate selectable
* entries (each labeled with its region) instead of being collapsed into a
* single base entry with a region dropdown. Used by group chat.
*/
showRegionalVariants?: boolean;
}

interface FilterState {
Expand Down Expand Up @@ -800,6 +806,7 @@ export function ModelSelector({
mode = "chat",
isOptionDisabled,
getOptionDisabledReason,
showRegionalVariants = false,
}: ModelSelectorProps) {
const { isFavorite, toggleFavorite } = useFavoriteModels();
const isMobile = useIsMobile();
Expand Down Expand Up @@ -948,6 +955,16 @@ export function ModelSelector({
continue;
}

// Hide fully deactivated models: if every provider mapping is
// deactivated there's nothing left to route to, so skip the model
// (including its root "auto-select" entry) entirely.
const hasActiveMapping = m.mappings.some(
(mp) => !(mp.deactivatedAt && new Date(mp.deactivatedAt) <= now),
);
if (!hasActiveMapping) {
continue;
}

// Add root model entry (auto-routing)
const aliasText = m.aliases?.join(" ") ?? "";
const rootSearchText = normalize(
Expand All @@ -960,6 +977,9 @@ export function ModelSelector({
});

for (const mp of m.mappings) {
if (mp.region && !showRegionalVariants) {
continue;
}
const isDeactivated =
mp.deactivatedAt && new Date(mp.deactivatedAt) <= now;
if (!isDeactivated) {
Expand All @@ -984,7 +1004,7 @@ export function ModelSelector({
}
}
return out;
}, [models, providers]);
}, [models, providers, showRegionalVariants]);

// Defer search input value to keep typing responsive with large lists
const deferredSearch = React.useDeferredValue(searchQuery);
Expand Down Expand Up @@ -2268,6 +2288,36 @@ export function ModelSelector({
</div>
) : null;
})()}
{(() => {
if (!previewEntry.mapping) {
return null;
}
const regions = previewEntry.model.mappings
.filter(
(m) =>
m.providerId ===
previewEntry.mapping!.providerId && m.region,
)
.map((m) => m.region as string);
return regions.length > 0 ? (
<div className="space-y-1">
<h5 className="font-medium text-xs">
Available Regions
</h5>
<div className="flex flex-wrap gap-1">
{regions.map((r) => (
<Badge
key={r}
variant="secondary"
className="text-[10px] px-1.5 py-0.5"
>
{r}
</Badge>
))}
</div>
</div>
) : null;
})()}
</>
)}
</>
Expand Down Expand Up @@ -2789,6 +2839,36 @@ export function ModelSelector({
</div>
) : null;
})()}
{(() => {
if (!selectedDetails.mapping) {
return null;
}
const regions = selectedDetails.model.mappings
.filter(
(m) =>
m.providerId ===
selectedDetails.mapping!.providerId && m.region,
)
.map((m) => m.region as string);
return regions.length > 0 ? (
<div className="space-y-2">
<h5 className="font-medium text-sm">
Available Regions
</h5>
<div className="flex flex-wrap gap-1.5">
{regions.map((r) => (
<Badge
key={r}
variant="secondary"
className="text-xs px-2 py-1"
>
{r}
</Badge>
))}
</div>
</div>
) : null;
})()}
</>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/playground/src/components/playground/chat-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const ChatHeader = ({
<div className="flex min-w-0 flex-1 items-center gap-3">
{isTemporaryChat ? null : <SidebarTrigger />}
{showGlobalModelSelector ? (
<div className="flex w-full min-w-0 max-w-[360px] items-center gap-2 sm:max-w-[420px]">
<div className="w-full min-w-0 max-w-[360px] sm:max-w-[420px]">
<ModelSelector
models={models}
providers={providers}
Expand Down
59 changes: 55 additions & 4 deletions apps/playground/src/components/playground/chat-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,28 @@ export default function ChatPageClient({
[pathname, router, comparisonEnabled, extraPanelModels, models],
);

const {
providerId: selectedProviderId,
modelId: selectedModelId,
region: selectedRegion,
} = parseModelSelectorValue(selectedModel);

const availableRegions = selectedProviderId
? (models
.find((m) => m.id === selectedModelId)
?.mappings.filter(
(m) => m.providerId === selectedProviderId && m.region,
)
.map((m) => m.region as string) ?? [])
: [];

const handleRegionChange = (region: string) => {
const newValue = region
? `${selectedProviderId}/${selectedModelId}:${region}`
: `${selectedProviderId}/${selectedModelId}`;
handleSelectModel(newValue);
};

const handleExtraPanelModelChange = useCallback(
(index: number, model: string) => {
setExtraPanelModels((prev) => {
Expand Down Expand Up @@ -1914,6 +1936,9 @@ export default function ChatPageClient({
setImageQuality={setImageQuality}
imageCount={imageCount}
setImageCount={setImageCount}
availableRegions={availableRegions}
selectedRegion={selectedRegion}
onRegionChange={handleRegionChange}
onUserMessage={handleUserMessage}
isLoading={isLoading || isChatLoading}
error={error}
Expand Down Expand Up @@ -1971,6 +1996,9 @@ export default function ChatPageClient({
supportsWebSearch={supportsWebSearch}
webSearchEnabled={webSearchEnabled}
setWebSearchEnabled={setWebSearchEnabled}
availableRegions={availableRegions}
selectedRegion={selectedRegion}
onRegionChange={handleRegionChange}
onUserMessage={handleUserMessage}
onEditUserMessage={handleEditUserMessage}
isLoading={isLoading || isChatLoading}
Expand Down Expand Up @@ -2178,9 +2206,9 @@ function ExtraChatPanel({
const [selectedModel, setSelectedModel] = useState(initialModel);
const handleModelChange = useCallback(
(model: string) => {
const modelId = model.includes("/")
? (model.split("/")[1] ?? model)
: model;
const modelId = (
model.includes("/") ? (model.split("/")[1] ?? model) : model
).split(":")[0];
const def = models.find((m) => m.id === modelId);
if (def?.output?.includes("video")) {
onVideoModelSelected?.(modelId);
Expand Down Expand Up @@ -2431,6 +2459,26 @@ function ExtraChatPanel({
return !!mapping?.webSearch;
}, [models, selectedModel]);

const {
providerId: panelProviderId,
modelId: panelModelId,
region: panelSelectedRegion,
} = parseModelSelectorValue(selectedModel);

const panelAvailableRegions = panelProviderId
? (models
.find((m) => m.id === panelModelId)
?.mappings.filter((m) => m.providerId === panelProviderId && m.region)
.map((m) => m.region as string) ?? [])
: [];

const handlePanelRegionChange = (region: string) => {
const newValue = region
? `${panelProviderId}/${panelModelId}:${region}`
: `${panelProviderId}/${panelModelId}`;
handleModelChange(newValue);
};

useEffect(() => {
const config = getModelImageConfig(selectedModel);
if (config.usesPixelDimensions) {
Expand Down Expand Up @@ -2737,7 +2785,7 @@ function ExtraChatPanel({
<span className="text-xs font-medium text-muted-foreground">
Model {panelIndex}
</span>
<div className="w-full max-w-xs">
<div className="w-full min-w-0 max-w-xs">
<ModelSelector
models={models}
providers={providers}
Expand Down Expand Up @@ -2777,6 +2825,9 @@ function ExtraChatPanel({
supportsWebSearch={supportsWebSearch}
webSearchEnabled={webSearchEnabled}
setWebSearchEnabled={setWebSearchEnabled}
availableRegions={panelAvailableRegions}
selectedRegion={panelSelectedRegion}
onRegionChange={handlePanelRegionChange}
onUserMessage={handlePanelUserMessage}
forkChat={
comparisonChatId && status === "ready"
Expand Down
46 changes: 43 additions & 3 deletions apps/playground/src/components/playground/chat-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
GitFork,
Loader2,
ExternalLinkIcon,
MapPin,
PlusIcon,
ScrollTextIcon,
TrendingDown,
Expand Down Expand Up @@ -219,6 +220,9 @@ interface ChatUIProps {
supportsWebSearch: boolean;
webSearchEnabled: boolean;
setWebSearchEnabled: (value: boolean) => void;
availableRegions?: string[];
selectedRegion?: string;
onRegionChange?: (region: string) => void;
onUserMessage?: (
content: string,
images?: Array<{
Expand Down Expand Up @@ -1005,6 +1009,9 @@ export const ChatUI = ({
supportsWebSearch,
webSearchEnabled,
setWebSearchEnabled,
availableRegions = [],
selectedRegion,
onRegionChange,
onUserMessage,
onEditUserMessage,
isLoading = false,
Expand Down Expand Up @@ -1706,6 +1713,33 @@ export const ChatUI = ({
/>
</PromptInputTools>
<div className="flex items-center gap-2">
{availableRegions.length > 0 && (
<Select
value={selectedRegion ?? "__default__"}
onValueChange={(val) =>
onRegionChange?.(val === "__default__" ? "" : val)
}
>
<SelectTrigger
size="sm"
className="min-w-0 sm:min-w-[120px]"
aria-label="Region"
>
<MapPin size={16} className="shrink-0" />
<span className="hidden sm:contents">
<SelectValue placeholder="Default" />
</span>
</SelectTrigger>
<SelectContent>
<SelectItem value="__default__">Default</SelectItem>
{availableRegions.map((r) => (
<SelectItem key={r} value={r}>
{r}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{supportsReasoning && (
<Select
value={reasoningEffort ? reasoningEffort : "off"}
Expand All @@ -1718,9 +1752,15 @@ export const ChatUI = ({
)
}
>
<SelectTrigger size="sm" className="min-w-[120px]">
<Brain size={16} />
<SelectValue placeholder="Reasoning" />
<SelectTrigger
size="sm"
className="min-w-0 sm:min-w-[120px]"
aria-label="Reasoning effort"
>
<Brain size={16} className="shrink-0" />
<span className="hidden sm:contents">
<SelectValue placeholder="Reasoning" />
</span>
</SelectTrigger>
<SelectContent>
<SelectItem value="off">Auto</SelectItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ export default function GroupChatClient({
}
}}
placeholder="Add a model..."
showRegionalVariants
/>
)}
</div>
Expand Down
Loading
Loading