style(chat): polish composer and sidebar interactions
This commit is contained in:
parent
7bc44536ea
commit
72eff32065
3 changed files with 90 additions and 19 deletions
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue