style(chat): polish composer and sidebar interactions

This commit is contained in:
wasimysaid 2026-05-30 23:00:41 +02:00
commit 72eff32065
3 changed files with 90 additions and 19 deletions

View file

@ -10,7 +10,6 @@ import {
} from "@/components/ui/popover";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { usePlatformStore } from "@/config/env";
import { isCustomProviderType } from "@/features/chat/external-providers";
import { cn } from "@/lib/utils";
import {
ArrowDown01Icon,
@ -32,6 +31,22 @@ import type {
import { HubModelPicker, LoraModelPicker } from "./model-selector/pickers";
import { Input } from "../ui/input";
const CUSTOM_PROVIDER_TYPES_WITH_FALLBACK_LOGO = new Set([
"custom",
"llama_cpp",
"vllm",
"ollama",
]);
function isCustomProviderTypeWithFallbackLogo(
providerType: string | undefined,
): boolean {
return (
providerType !== undefined &&
CUSTOM_PROVIDER_TYPES_WITH_FALLBACK_LOGO.has(providerType)
);
}
const PROVIDER_LOGO_EXT: Record<string, "svg" | "png" | "jpg"> = {
openai: "svg",
mistral: "svg",
@ -64,7 +79,7 @@ function ExternalProviderLogo({
title?: string;
}) {
const src = providerLogoSrc(providerType);
if (!src && isCustomProviderType(providerType)) {
if (!src && isCustomProviderTypeWithFallbackLogo(providerType)) {
return (
<span title={title} aria-hidden={true} className="inline-flex shrink-0">
<HugeiconsIcon
@ -146,10 +161,11 @@ function ModelSelectorTrigger({
type="button"
data-tour={dataTour}
className={cn(
"flex min-w-0 items-center gap-2 transition-colors",
"flex min-w-0 cursor-pointer items-center gap-2 transition-colors",
variant === "outline" &&
"rounded-[10px] border border-border/60 hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
variant === "ghost" && "rounded-[10px] hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
"rounded-[10px] border border-border/60 hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
variant === "ghost" &&
"rounded-[10px] hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
variant === "muted" && "rounded-[10px] bg-muted hover:bg-muted/80",
size === "sm" && "h-8 px-3 text-xs",
size === "default" && "h-9 px-3.5 text-sm",
@ -161,7 +177,9 @@ function ModelSelectorTrigger({
<span className="size-2 shrink-0 rounded-full bg-emerald-500" />
)}
{currentModel?.icon ? (
<span className="flex shrink-0 items-center">{currentModel.icon}</span>
<span className="flex shrink-0 items-center">
{currentModel.icon}
</span>
) : null}
<span className="flex min-w-0 flex-1 items-baseline">
<span className="min-w-0 flex flex-1 items-baseline truncate font-heading text-[16px] font-medium leading-tight text-black dark:text-white">
@ -228,7 +246,10 @@ function ModelSelectorContent({
const chatOnly = usePlatformStore((s) => s.isChatOnly());
const hasExternal = externalModels.length > 0;
const chatOnlyTabsDefault = useMemo(
() => (value && externalModels.some((model) => model.id === value) ? "external" : "hub"),
() =>
value && externalModels.some((model) => model.id === value)
? "external"
: "hub",
[externalModels, value],
);
const studioTabsDefault = useMemo((): "hub" | "lora" | "external" => {
@ -246,7 +267,7 @@ function ModelSelectorContent({
align="start"
data-tour={dataTour}
className={cn(
"menu-soft-surface ring-0 w-[min(440px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] min-w-0 gap-0 p-2",
"model-selector-menu menu-soft-surface ring-0 w-[min(440px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] min-w-0 gap-0 p-2",
className,
)}
>
@ -258,7 +279,12 @@ function ModelSelectorContent({
<TabsTrigger value="external">Connected</TabsTrigger>
</TabsList>
<TabsContent value="hub" className="m-0">
<HubModelPicker models={models} value={value} onSelect={onSelect} onFoldersChange={onFoldersChange} />
<HubModelPicker
models={models}
value={value}
onSelect={onSelect}
onFoldersChange={onFoldersChange}
/>
</TabsContent>
<TabsContent value="external" className="m-0">
<ExternalModelPicker
@ -269,18 +295,30 @@ function ModelSelectorContent({
</TabsContent>
</Tabs>
) : (
<HubModelPicker models={models} value={value} onSelect={onSelect} onFoldersChange={onFoldersChange} />
<HubModelPicker
models={models}
value={value}
onSelect={onSelect}
onFoldersChange={onFoldersChange}
/>
)
) : (
<Tabs defaultValue={studioTabsDefault} className="w-full">
<TabsList className="mb-2 w-full">
<TabsTrigger value="hub">Hub models</TabsTrigger>
<TabsTrigger value="lora">Fine-tuned</TabsTrigger>
{hasExternal ? <TabsTrigger value="external">Connected</TabsTrigger> : null}
{hasExternal ? (
<TabsTrigger value="external">Connected</TabsTrigger>
) : null}
</TabsList>
<TabsContent value="hub" className="m-0">
<HubModelPicker models={models} value={value} onSelect={onSelect} onFoldersChange={onFoldersChange} />
<HubModelPicker
models={models}
value={value}
onSelect={onSelect}
onFoldersChange={onFoldersChange}
/>
</TabsContent>
<TabsContent value="lora" className="m-0">
@ -420,7 +458,9 @@ export function ModelSelector({
const found = optionById.get(selected);
if (activeGgufVariant) {
const desc = `GGUF · ${activeGgufVariant}`;
return found ? { ...found, description: desc } : { id: selected, name: selected, description: desc };
return found
? { ...found, description: desc }
: { id: selected, name: selected, description: desc };
}
return found ?? { id: selected, name: selected };
}, [selected, optionById, activeGgufVariant]);

View file

@ -1130,7 +1130,7 @@ export function SharedComposer({
dir="auto"
/>
<div className="composer-action-wrapper">
<div className="flex items-center gap-0.5">
<div className="flex items-center gap-1">
<input
ref={fileInputRef}
type="file"

View file

@ -592,6 +592,18 @@
);
}
/* Firefox can box/clip native scrollbars when the scroll container is
masked. Keep the sidebar chrome clean there; the header/footer fade
overlays still provide the edge cue. */
@supports (-moz-appearance: none) {
.app-sidebar-scroll-region,
.app-sidebar-scroll-region.is-scrolled,
.app-sidebar-scroll-region.can-scroll-down,
.app-sidebar-scroll-region.is-scrolled.can-scroll-down {
mask-image: none;
}
}
.app-sidebar-footer {
position: relative;
z-index: 25;
@ -785,6 +797,9 @@
.dark .app-user-menu [data-slot="dropdown-menu-item"] {
letter-spacing: 0.03em;
}
.app-user-menu [data-slot="dropdown-menu-item"]:not([data-disabled]) {
cursor: pointer;
}
.app-user-menu [data-slot="dropdown-menu-item"] svg {
width: 19px !important;
height: 19px !important;
@ -868,6 +883,10 @@
box-shadow: 0 3px 12px -6px rgba(0, 0, 0, 0.46);
}
.model-selector-menu button:not(:disabled) {
cursor: pointer;
}
.chat-composer-surface {
@apply relative flex w-full flex-col rounded-[32px] bg-white dark:bg-card px-3 py-3 outline-none transition-shadow;
font-family: var(--font-sans);
@ -1005,7 +1024,7 @@
}
.unsloth-composer-left {
@apply flex shrink-0 items-center gap-0.5;
@apply flex shrink-0 items-center gap-1;
order: 1;
/* Pull the plus button closer to the composer edge. */
margin-left: -0.25rem;
@ -1087,7 +1106,19 @@
/* Right-side Thinking pill (toggle or dropdown). */
.unsloth-thinking-pill {
@apply inline-flex h-6 shrink-0 cursor-pointer items-center gap-1 rounded-full px-2 py-0 text-[14px] font-medium leading-6 text-muted-foreground transition-colors hover:bg-black/[0.06] dark:hover:bg-white/[0.08] disabled:cursor-not-allowed disabled:opacity-40;
@apply inline-flex h-6 shrink-0 cursor-pointer items-center gap-1 rounded-full pl-1 pr-2 py-0 text-[14px] font-medium leading-6 text-muted-foreground/75 transition-colors hover:bg-black/[0.06] dark:hover:bg-white/[0.08] disabled:cursor-not-allowed disabled:opacity-40;
}
.unsloth-thinking-pill > span {
font-size: 13px;
font-weight: 400;
line-height: 17px;
}
.unsloth-thinking-pill svg {
width: 16px !important;
height: 16px !important;
flex-shrink: 0;
}
.unsloth-thinking-pill[data-active="true"],
@ -1555,15 +1586,15 @@
}
[data-sidebar="content"] {
scrollbar-color: oklch(0.5 0 0 / 0.22) var(--sidebar);
scrollbar-color: oklch(0.5 0 0 / 0.22) transparent;
}
.dark [data-sidebar="content"] {
scrollbar-color: oklch(0.72 0 0 / 0.25) var(--sidebar);
scrollbar-color: oklch(0.72 0 0 / 0.25) transparent;
}
[data-sidebar="content"]::-webkit-scrollbar-track {
background: var(--sidebar);
background: transparent;
}
[data-sidebar="content"]::-webkit-scrollbar-thumb {