Compare commits
20 commits
main
...
studio-com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbc5b81b54 | ||
|
|
b24a450f26 | ||
|
|
5d2b189068 | ||
|
|
72eff32065 | ||
|
|
7bc44536ea | ||
|
|
def605d5b6 | ||
|
|
19436f589a | ||
|
|
14384cd4e5 | ||
|
|
65a9cf102a | ||
|
|
b76d90d1b3 | ||
|
|
f7b92e5f11 | ||
|
|
1b58287c07 | ||
|
|
7d53d49f22 | ||
|
|
a879ca1d82 |
||
|
|
fc23036470 | ||
|
|
cbb92749a8 | ||
|
|
e56d69f528 | ||
|
|
518c7e9916 |
||
|
|
83b2dfd08e | ||
|
|
cd67cb2223 |
13 changed files with 3800 additions and 2153 deletions
|
|
@ -42,7 +42,6 @@ import { useAnimatedThemeToggle } from "@/components/ui/animated-theme-toggler";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
ChefHatIcon,
|
ChefHatIcon,
|
||||||
ColumnInsertIcon,
|
|
||||||
CursorInfo02Icon,
|
CursorInfo02Icon,
|
||||||
Delete02Icon,
|
Delete02Icon,
|
||||||
DownloadSquare01Icon,
|
DownloadSquare01Icon,
|
||||||
|
|
@ -58,13 +57,16 @@ import {
|
||||||
TestTube01Icon,
|
TestTube01Icon,
|
||||||
ZapIcon,
|
ZapIcon,
|
||||||
} from "@hugeicons/core-free-icons";
|
} from "@hugeicons/core-free-icons";
|
||||||
import {
|
import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { ChevronDown, ChevronsUpDown, MoreHorizontalIcon, Moon, Sun } from "lucide-react";
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronsUpDown,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
Moon,
|
||||||
|
Sun,
|
||||||
|
} from "lucide-react";
|
||||||
import { Link, useNavigate, useRouterState } from "@tanstack/react-router";
|
import { Link, useNavigate, useRouterState } from "@tanstack/react-router";
|
||||||
import {
|
import {
|
||||||
ChatSearchDialog,
|
ChatSearchDialog,
|
||||||
|
|
@ -130,10 +132,7 @@ function getTourId(pathname: string): string | null {
|
||||||
// keeps the test-tube outline + horizontal cap + liquid line, dropping
|
// keeps the test-tube outline + horizontal cap + liquid line, dropping
|
||||||
// the bubbles. The original export stays untouched, and HugeiconsIcon
|
// the bubbles. The original export stays untouched, and HugeiconsIcon
|
||||||
// renders this trimmed array exactly the same way.
|
// renders this trimmed array exactly the same way.
|
||||||
const TestTubeOutlineIcon = TestTube01Icon.slice(
|
const TestTubeOutlineIcon = TestTube01Icon.slice(0, 3) as typeof TestTube01Icon;
|
||||||
0,
|
|
||||||
3,
|
|
||||||
) as typeof TestTube01Icon;
|
|
||||||
|
|
||||||
function runStatusDotClass(status: TrainingRunSummary["status"]): string {
|
function runStatusDotClass(status: TrainingRunSummary["status"]): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
@ -199,8 +198,14 @@ function NavItem({
|
||||||
data-tour={dataTour}
|
data-tour={dataTour}
|
||||||
className="sidebar-nav-btn h-[35px] rounded-[10px] gap-[8.5px] px-2.5 font-medium group-data-[collapsible=icon]:!w-[32px] group-data-[collapsible=icon]:!rounded-[10px] group-data-[collapsible=icon]:mx-auto"
|
className="sidebar-nav-btn h-[35px] rounded-[10px] gap-[8.5px] px-2.5 font-medium group-data-[collapsible=icon]:!w-[32px] group-data-[collapsible=icon]:!rounded-[10px] group-data-[collapsible=icon]:mx-auto"
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={icon} strokeWidth={1.75} className="size-icon! shrink-0 group-hover/menu-button:animate-icon-pop" />
|
<HugeiconsIcon
|
||||||
<span className="text-[14.5px] leading-[19px] tracking-nav">{label}</span>
|
icon={icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon! shrink-0 group-hover/menu-button:animate-icon-pop"
|
||||||
|
/>
|
||||||
|
<span className="text-[14.5px] leading-[19px] tracking-nav">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -230,17 +235,33 @@ export function AppSidebar() {
|
||||||
const [shutdownOpen, setShutdownOpen] = useState(false);
|
const [shutdownOpen, setShutdownOpen] = useState(false);
|
||||||
|
|
||||||
const isChatRoute = pathname.startsWith("/chat");
|
const isChatRoute = pathname.startsWith("/chat");
|
||||||
const isStudioRoute = pathname === "/studio" || pathname.startsWith("/studio/");
|
const isStudioRoute =
|
||||||
|
pathname === "/studio" || pathname.startsWith("/studio/");
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [canScrollDown, setCanScrollDown] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = scrollRef.current;
|
const el = scrollRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const handler = () => setScrolled(el.scrollTop > 0);
|
|
||||||
handler();
|
const updateScrollState = () => {
|
||||||
el.addEventListener("scroll", handler, { passive: true });
|
setScrolled(el.scrollTop > 0);
|
||||||
return () => el.removeEventListener("scroll", handler);
|
setCanScrollDown(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateScrollState();
|
||||||
|
el.addEventListener("scroll", updateScrollState, { passive: true });
|
||||||
|
const resizeObserver = new ResizeObserver(updateScrollState);
|
||||||
|
resizeObserver.observe(el);
|
||||||
|
const mutationObserver = new MutationObserver(updateScrollState);
|
||||||
|
mutationObserver.observe(el, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener("scroll", updateScrollState);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isRecipesRoute = pathname.startsWith("/data-recipes");
|
const isRecipesRoute = pathname.startsWith("/data-recipes");
|
||||||
|
|
@ -250,10 +271,10 @@ export function AppSidebar() {
|
||||||
const storeThreadId = useChatRuntimeStore((s) => s.activeThreadId);
|
const storeThreadId = useChatRuntimeStore((s) => s.activeThreadId);
|
||||||
const setActiveThreadId = useChatRuntimeStore((s) => s.setActiveThreadId);
|
const setActiveThreadId = useChatRuntimeStore((s) => s.setActiveThreadId);
|
||||||
const activeThreadId = isChatRoute
|
const activeThreadId = isChatRoute
|
||||||
? (search.thread as string | undefined) ??
|
? ((search.thread as string | undefined) ??
|
||||||
(search.compare as string | undefined) ??
|
(search.compare as string | undefined) ??
|
||||||
storeThreadId ??
|
storeThreadId ??
|
||||||
undefined
|
undefined)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Training runs
|
// Training runs
|
||||||
|
|
@ -261,12 +282,18 @@ export function AppSidebar() {
|
||||||
!chatOnly && isStudioRoute,
|
!chatOnly && isStudioRoute,
|
||||||
);
|
);
|
||||||
const activeJobId = useTrainingRuntimeStore((s) => s.jobId);
|
const activeJobId = useTrainingRuntimeStore((s) => s.jobId);
|
||||||
const selectedHistoryRunId = useTrainingRuntimeStore((s) => s.selectedHistoryRunId);
|
const selectedHistoryRunId = useTrainingRuntimeStore(
|
||||||
const setSelectedHistoryRunId = useTrainingRuntimeStore((s) => s.setSelectedHistoryRunId);
|
(s) => s.selectedHistoryRunId,
|
||||||
|
);
|
||||||
|
const setSelectedHistoryRunId = useTrainingRuntimeStore(
|
||||||
|
(s) => s.setSelectedHistoryRunId,
|
||||||
|
);
|
||||||
|
|
||||||
const chatDisabled = isTrainingRunning;
|
const chatDisabled = isTrainingRunning;
|
||||||
|
|
||||||
async function handleDeleteThread(item: Parameters<typeof deleteChatItem>[0]) {
|
async function handleDeleteThread(
|
||||||
|
item: Parameters<typeof deleteChatItem>[0],
|
||||||
|
) {
|
||||||
await deleteChatItem(item, activeThreadId, (view) => {
|
await deleteChatItem(item, activeThreadId, (view) => {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/chat",
|
to: "/chat",
|
||||||
|
|
@ -316,7 +343,10 @@ export function AppSidebar() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const updated = await renameTrainingRun(target.run.id, nextRunDisplayName);
|
const updated = await renameTrainingRun(
|
||||||
|
target.run.id,
|
||||||
|
nextRunDisplayName,
|
||||||
|
);
|
||||||
emitTrainingRunUpdated(updated);
|
emitTrainingRunUpdated(updated);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(translate("shell.toast.failedToRenameRun"), {
|
toast.error(translate("shell.toast.failedToRenameRun"), {
|
||||||
|
|
@ -328,8 +358,9 @@ export function AppSidebar() {
|
||||||
type DeleteTarget =
|
type DeleteTarget =
|
||||||
| { kind: "chat"; item: SidebarItem }
|
| { kind: "chat"; item: SidebarItem }
|
||||||
| { kind: "run"; run: TrainingRunSummary };
|
| { kind: "run"; run: TrainingRunSummary };
|
||||||
const [confirmingDelete, setConfirmingDelete] =
|
const [confirmingDelete, setConfirmingDelete] = useState<DeleteTarget | null>(
|
||||||
useState<DeleteTarget | null>(null);
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
async function commitDelete() {
|
async function commitDelete() {
|
||||||
const target = confirmingDelete;
|
const target = confirmingDelete;
|
||||||
|
|
@ -408,7 +439,11 @@ export function AppSidebar() {
|
||||||
className="inline-flex h-[35px] w-[32px] items-center justify-center rounded-[10px] text-nav-icon-idle dark:text-nav-fg-muted transition-colors hover:bg-nav-surface-hover hover:text-black dark:hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="inline-flex h-[35px] w-[32px] items-center justify-center rounded-[10px] text-nav-icon-idle dark:text-nav-fg-muted transition-colors hover:bg-nav-surface-hover hover:text-black dark:hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
aria-label={t("shell.aria.closeSidebar")}
|
aria-label={t("shell.aria.closeSidebar")}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={LayoutAlignLeftIcon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={LayoutAlignLeftIcon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</TooltipPrimitive.Trigger>
|
</TooltipPrimitive.Trigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
|
|
@ -433,7 +468,11 @@ export function AppSidebar() {
|
||||||
className="inline-flex h-[35px] w-[32px] items-center justify-center rounded-[10px] text-nav-fg transition-colors hover:bg-nav-surface-hover hover:text-black dark:hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="inline-flex h-[35px] w-[32px] items-center justify-center rounded-[10px] text-nav-fg transition-colors hover:bg-nav-surface-hover hover:text-black dark:hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
aria-label={t("shell.aria.openSidebar")}
|
aria-label={t("shell.aria.openSidebar")}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={LayoutAlignLeftIcon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={LayoutAlignLeftIcon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</TooltipPrimitive.Trigger>
|
</TooltipPrimitive.Trigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
|
|
@ -448,7 +487,12 @@ export function AppSidebar() {
|
||||||
)}
|
)}
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarGroup className="group-data-[collapsible=icon]:px-0 px-2 pt-[9px] pb-[8px] shrink-0">
|
<SidebarGroup
|
||||||
|
className={cn(
|
||||||
|
"app-sidebar-top-actions group-data-[collapsible=icon]:px-0 px-2 pt-[9px] pb-[8px] shrink-0",
|
||||||
|
scrolled && "is-scrolled",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<NavItem
|
<NavItem
|
||||||
|
|
@ -459,20 +503,10 @@ export function AppSidebar() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (chatDisabled) return;
|
if (chatDisabled) return;
|
||||||
setActiveThreadId(null);
|
setActiveThreadId(null);
|
||||||
navigate({ to: "/chat", search: { new: createNavigationNonce() } });
|
navigate({
|
||||||
closeMobileIfOpen();
|
to: "/chat",
|
||||||
}}
|
search: { new: createNavigationNonce() },
|
||||||
/>
|
});
|
||||||
<NavItem
|
|
||||||
icon={ColumnInsertIcon}
|
|
||||||
label={t("shell.navigation.compare")}
|
|
||||||
active={!!search.compare && !chatItems.some((i) => i.id === search.compare)}
|
|
||||||
disabled={chatDisabled}
|
|
||||||
dataTour="chat-compare"
|
|
||||||
onClick={() => {
|
|
||||||
if (chatDisabled) return;
|
|
||||||
setActiveThreadId(null);
|
|
||||||
navigate({ to: "/chat", search: { compare: createNavigationNonce() } });
|
|
||||||
closeMobileIfOpen();
|
closeMobileIfOpen();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -491,13 +525,26 @@ export function AppSidebar() {
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
||||||
<SidebarGroup data-tour="navbar" className="group-data-[collapsible=icon]:px-0 px-2 pt-[9px] pb-[20px] shrink-0">
|
<SidebarContent
|
||||||
|
ref={scrollRef}
|
||||||
|
className={cn(
|
||||||
|
"app-sidebar-scroll-region gap-0 overflow-y-auto overscroll-contain min-h-0",
|
||||||
|
scrolled && "is-scrolled",
|
||||||
|
canScrollDown && "can-scroll-down",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SidebarGroup
|
||||||
|
data-tour="navbar"
|
||||||
|
className="group-data-[collapsible=icon]:px-0 px-2 pt-[9px] pb-[20px]"
|
||||||
|
>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<NavItem
|
<NavItem
|
||||||
icon={TestTubeOutlineIcon}
|
icon={TestTubeOutlineIcon}
|
||||||
label={t("shell.navigation.train")}
|
label={t("shell.navigation.train")}
|
||||||
active={pathname === "/studio" || pathname.startsWith("/studio/")}
|
active={
|
||||||
|
pathname === "/studio" || pathname.startsWith("/studio/")
|
||||||
|
}
|
||||||
disabled={chatOnly}
|
disabled={chatOnly}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (chatOnly) return;
|
if (chatOnly) return;
|
||||||
|
|
@ -519,7 +566,9 @@ export function AppSidebar() {
|
||||||
<NavItem
|
<NavItem
|
||||||
icon={DownloadSquare01Icon}
|
icon={DownloadSquare01Icon}
|
||||||
label={t("shell.navigation.export")}
|
label={t("shell.navigation.export")}
|
||||||
active={pathname === "/export" || pathname.startsWith("/export/")}
|
active={
|
||||||
|
pathname === "/export" || pathname.startsWith("/export/")
|
||||||
|
}
|
||||||
disabled={chatOnly}
|
disabled={chatOnly}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (chatOnly) return;
|
if (chatOnly) return;
|
||||||
|
|
@ -531,7 +580,6 @@ export function AppSidebar() {
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
||||||
<SidebarContent ref={scrollRef} className="gap-0 overflow-y-auto overscroll-contain min-h-0">
|
|
||||||
{!isStudioRoute && chatItems.length > 0 && (
|
{!isStudioRoute && chatItems.length > 0 && (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
key={isChatRoute ? "chat-route" : "non-chat-route"}
|
key={isChatRoute ? "chat-route" : "non-chat-route"}
|
||||||
|
|
@ -539,7 +587,13 @@ export function AppSidebar() {
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden px-0 py-0">
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden px-0 py-0">
|
||||||
<SidebarGroupLabel className={cn("sidebar-sticky-label", scrolled && "is-scrolled")} asChild>
|
<SidebarGroupLabel
|
||||||
|
className={cn(
|
||||||
|
"sidebar-sticky-label",
|
||||||
|
scrolled && "is-scrolled",
|
||||||
|
)}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
<CollapsibleTrigger className="cursor-pointer flex w-full items-center justify-between">
|
<CollapsibleTrigger className="cursor-pointer flex w-full items-center justify-between">
|
||||||
{t("shell.navigation.recents")}
|
{t("shell.navigation.recents")}
|
||||||
<ChevronDown className="size-3.5 transition-transform duration-200 data-[state=open]:rotate-0 [[data-state=closed]_&]:rotate-[-90deg]" />
|
<ChevronDown className="size-3.5 transition-transform duration-200 data-[state=open]:rotate-0 [[data-state=closed]_&]:rotate-[-90deg]" />
|
||||||
|
|
@ -549,7 +603,10 @@ export function AppSidebar() {
|
||||||
<SidebarGroupContent className="px-2">
|
<SidebarGroupContent className="px-2">
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{chatItems.map((item) => (
|
{chatItems.map((item) => (
|
||||||
<SidebarMenuItem key={item.id} className="group/recent-item relative">
|
<SidebarMenuItem
|
||||||
|
key={item.id}
|
||||||
|
className="group/recent-item relative"
|
||||||
|
>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
data-testid="recent-thread"
|
data-testid="recent-thread"
|
||||||
data-thread-type={item.type}
|
data-thread-type={item.type}
|
||||||
|
|
@ -578,25 +635,40 @@ export function AppSidebar() {
|
||||||
className="sidebar-row-action group-hover/recent-item:opacity-100 group-hover/recent-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto"
|
className="sidebar-row-action group-hover/recent-item:opacity-100 group-hover/recent-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto"
|
||||||
>
|
>
|
||||||
<span className="sidebar-row-action-glyph">
|
<span className="sidebar-row-action-glyph">
|
||||||
<MoreHorizontalIcon strokeWidth={1.75} className="size-icon" />
|
<MoreHorizontalIcon
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="end"
|
align="start"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
className="app-user-menu menu-soft-surface menu-flat-destructive ring-0 w-44 py-2 font-heading rounded-[14px] border-0"
|
className="app-user-menu menu-soft-surface menu-flat-destructive ring-0 w-44 py-2 font-heading rounded-[14px] border-0"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem onSelect={() => openRenameChat(item)}>
|
<DropdownMenuItem
|
||||||
<HugeiconsIcon icon={Edit03Icon} strokeWidth={1.75} className="size-icon" />
|
onSelect={() => openRenameChat(item)}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={Edit03Icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("common.rename")}</span>
|
<span>{t("common.rename")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onSelect={() => setConfirmingDelete({ kind: "chat", item })}
|
onSelect={() =>
|
||||||
|
setConfirmingDelete({ kind: "chat", item })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={Delete02Icon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={Delete02Icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("common.delete")}</span>
|
<span>{t("common.delete")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
@ -613,7 +685,13 @@ export function AppSidebar() {
|
||||||
{isStudioRoute && runItems.length > 0 && !chatOnly && (
|
{isStudioRoute && runItems.length > 0 && !chatOnly && (
|
||||||
<Collapsible key="studio-runs-route" defaultOpen asChild>
|
<Collapsible key="studio-runs-route" defaultOpen asChild>
|
||||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden px-0 py-0">
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden px-0 py-0">
|
||||||
<SidebarGroupLabel className={cn("sidebar-sticky-label", scrolled && "is-scrolled")} asChild>
|
<SidebarGroupLabel
|
||||||
|
className={cn(
|
||||||
|
"sidebar-sticky-label",
|
||||||
|
scrolled && "is-scrolled",
|
||||||
|
)}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
<CollapsibleTrigger className="cursor-pointer flex w-full items-center justify-between">
|
<CollapsibleTrigger className="cursor-pointer flex w-full items-center justify-between">
|
||||||
{t("shell.navigation.recents")}
|
{t("shell.navigation.recents")}
|
||||||
<ChevronDown className="size-3.5 transition-transform duration-200 data-[state=open]:rotate-0 [[data-state=closed]_&]:rotate-[-90deg]" />
|
<ChevronDown className="size-3.5 transition-transform duration-200 data-[state=open]:rotate-0 [[data-state=closed]_&]:rotate-[-90deg]" />
|
||||||
|
|
@ -624,7 +702,8 @@ export function AppSidebar() {
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{runItems.map((run) => {
|
{runItems.map((run) => {
|
||||||
const isActiveRun =
|
const isActiveRun =
|
||||||
selectedHistoryRunId === run.id || activeJobId === run.id;
|
selectedHistoryRunId === run.id ||
|
||||||
|
activeJobId === run.id;
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem
|
<SidebarMenuItem
|
||||||
key={run.id}
|
key={run.id}
|
||||||
|
|
@ -666,18 +745,27 @@ export function AppSidebar() {
|
||||||
className="sidebar-row-action group-hover/run-item:opacity-100 group-hover/run-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto"
|
className="sidebar-row-action group-hover/run-item:opacity-100 group-hover/run-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto"
|
||||||
>
|
>
|
||||||
<span className="sidebar-row-action-glyph">
|
<span className="sidebar-row-action-glyph">
|
||||||
<MoreHorizontalIcon strokeWidth={1.75} className="size-icon" />
|
<MoreHorizontalIcon
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="end"
|
align="start"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
className="app-user-menu menu-soft-surface menu-flat-destructive ring-0 w-44 py-2 font-heading rounded-[14px] border-0"
|
className="app-user-menu menu-soft-surface menu-flat-destructive ring-0 w-44 py-2 font-heading rounded-[14px] border-0"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem onSelect={() => openRenameRun(run)}>
|
<DropdownMenuItem
|
||||||
<HugeiconsIcon icon={Edit03Icon} strokeWidth={1.75} className="size-icon" />
|
onSelect={() => openRenameRun(run)}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={Edit03Icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("common.rename")}</span>
|
<span>{t("common.rename")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|
@ -687,7 +775,11 @@ export function AppSidebar() {
|
||||||
setConfirmingDelete({ kind: "run", run })
|
setConfirmingDelete({ kind: "run", run })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={Delete02Icon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={Delete02Icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("common.delete")}</span>
|
<span>{t("common.delete")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
@ -703,7 +795,12 @@ export function AppSidebar() {
|
||||||
)}
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter className="border-t border-sidebar-border group-data-[collapsible=icon]:border-transparent group-data-[collapsible=icon]:px-0">
|
<SidebarFooter
|
||||||
|
className={cn(
|
||||||
|
"app-sidebar-footer group-data-[collapsible=icon]:px-0",
|
||||||
|
canScrollDown && "can-scroll-down",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
@ -722,29 +819,48 @@ export function AppSidebar() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 leading-tight group-data-[collapsible=icon]:hidden">
|
<div className="flex flex-col gap-0.5 leading-tight group-data-[collapsible=icon]:hidden">
|
||||||
<span className="truncate font-heading text-[13.5px] tracking-[0.025em] dark:tracking-[0.04em] font-semibold text-nav-fg">{displayTitle}</span>
|
<span className="truncate font-heading text-[13.5px] tracking-[0.025em] dark:tracking-[0.04em] font-semibold text-nav-fg">
|
||||||
<span className="truncate text-[11.5px] tracking-nav text-muted-foreground">Unsloth</span>
|
{displayTitle}
|
||||||
|
</span>
|
||||||
|
<span className="truncate text-[11.5px] tracking-nav text-muted-foreground">
|
||||||
|
Unsloth
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown strokeWidth={1.25} className="ml-auto size-4 text-muted-foreground group-data-[collapsible=icon]:hidden" />
|
<ChevronsUpDown
|
||||||
|
strokeWidth={1.25}
|
||||||
|
className="ml-auto size-4 text-muted-foreground group-data-[collapsible=icon]:hidden"
|
||||||
|
/>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side="top"
|
side="top"
|
||||||
align="start"
|
align="start"
|
||||||
className="app-user-menu menu-soft-surface-up ring-0 w-[15rem] py-2.5 font-heading rounded-[14px] border-0"
|
className="app-user-menu menu-soft-surface-up ring-0 py-2.5 font-heading rounded-[14px] border-0"
|
||||||
>
|
>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => useSettingsDialogStore.getState().openDialog()}
|
onSelect={() =>
|
||||||
|
useSettingsDialogStore.getState().openDialog()
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={Settings02Icon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={Settings02Icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("shell.navigation.settings")}</span>
|
<span>{t("shell.navigation.settings")}</span>
|
||||||
<DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
|
<DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => useSettingsDialogStore.getState().openDialog("api-keys")}
|
onSelect={() =>
|
||||||
|
useSettingsDialogStore.getState().openDialog("api-keys")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={Globe02Icon} strokeWidth={1.75} className="size-[18px]" />
|
<HugeiconsIcon
|
||||||
|
icon={Globe02Icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-[18px]"
|
||||||
|
/>
|
||||||
<span>{t("shell.navigation.api")}</span>
|
<span>{t("shell.navigation.api")}</span>
|
||||||
<span className="ml-auto rounded-[6px] border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] leading-none font-semibold text-emerald-700 dark:text-emerald-300">
|
<span className="ml-auto rounded-[6px] border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] leading-none font-semibold text-emerald-700 dark:text-emerald-300">
|
||||||
{t("common.new")}
|
{t("common.new")}
|
||||||
|
|
@ -752,9 +868,16 @@ export function AppSidebar() {
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
ref={anchorRef as React.Ref<HTMLDivElement>}
|
ref={anchorRef as React.Ref<HTMLDivElement>}
|
||||||
onSelect={(e) => { e.preventDefault(); toggleTheme(); }}
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleTheme();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isDark ? <Sun strokeWidth={1.75} className="size-icon" /> : <Moon strokeWidth={1.75} className="size-icon" />}
|
{isDark ? (
|
||||||
|
<Sun strokeWidth={1.75} className="size-icon" />
|
||||||
|
) : (
|
||||||
|
<Moon strokeWidth={1.75} className="size-icon" />
|
||||||
|
)}
|
||||||
<span>
|
<span>
|
||||||
{isDark
|
{isDark
|
||||||
? t("shell.navigation.lightMode")
|
? t("shell.navigation.lightMode")
|
||||||
|
|
@ -773,15 +896,25 @@ export function AppSidebar() {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={CursorInfo02Icon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={CursorInfo02Icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("shell.navigation.guidedTour")}</span>
|
<span>{t("shell.navigation.guidedTour")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator className="mx-2.5! my-2.5! h-0! border-t border-border/70 bg-transparent!" />
|
<DropdownMenuSeparator className="mx-2.5! my-2.5! h-0! border-t border-border/70 bg-transparent!" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => useSettingsDialogStore.getState().openDialog("about")}
|
onSelect={() =>
|
||||||
|
useSettingsDialogStore.getState().openDialog("about")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={HelpCircleIcon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={HelpCircleIcon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("common.help")}</span>
|
<span>{t("common.help")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|
@ -796,11 +929,19 @@ export function AppSidebar() {
|
||||||
void navigate({ to: "/login" });
|
void navigate({ to: "/login" });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={Logout01Icon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={Logout01Icon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("shell.navigation.logOut")}</span>
|
<span>{t("shell.navigation.logOut")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onSelect={() => setShutdownOpen(true)}>
|
<DropdownMenuItem onSelect={() => setShutdownOpen(true)}>
|
||||||
<HugeiconsIcon icon={PowerIcon} strokeWidth={1.75} className="size-icon" />
|
<HugeiconsIcon
|
||||||
|
icon={PowerIcon}
|
||||||
|
strokeWidth={1.75}
|
||||||
|
className="size-icon"
|
||||||
|
/>
|
||||||
<span>{t("common.shutdown")}</span>
|
<span>{t("common.shutdown")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
@ -829,20 +970,20 @@ export function AppSidebar() {
|
||||||
: t("shell.dialog.deleteChat.title")}
|
: t("shell.dialog.deleteChat.title")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{confirmingDelete?.kind === "run" ? (
|
{confirmingDelete?.kind === "run"
|
||||||
renderEmphasizedTranslation(
|
? renderEmphasizedTranslation(
|
||||||
t,
|
t,
|
||||||
"shell.dialog.deleteRun.description",
|
"shell.dialog.deleteRun.description",
|
||||||
confirmingDelete.run.display_name ??
|
confirmingDelete.run.display_name ??
|
||||||
confirmingDelete.run.model_name,
|
confirmingDelete.run.model_name,
|
||||||
)
|
)
|
||||||
) : confirmingDelete?.kind === "chat" ? (
|
: confirmingDelete?.kind === "chat"
|
||||||
renderEmphasizedTranslation(
|
? renderEmphasizedTranslation(
|
||||||
t,
|
t,
|
||||||
"shell.dialog.deleteChat.description",
|
"shell.dialog.deleteChat.description",
|
||||||
confirmingDelete.item.title,
|
confirmingDelete.item.title,
|
||||||
)
|
)
|
||||||
) : null}
|
: null}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter className="flex-wrap gap-2 sm:justify-end">
|
<DialogFooter className="flex-wrap gap-2 sm:justify-end">
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { usePlatformStore } from "@/config/env";
|
import { usePlatformStore } from "@/config/env";
|
||||||
import { isCustomProviderType } from "@/features/chat/external-providers";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
ArrowDown01Icon,
|
ArrowDown01Icon,
|
||||||
|
|
@ -32,6 +31,22 @@ import type {
|
||||||
import { HubModelPicker, LoraModelPicker } from "./model-selector/pickers";
|
import { HubModelPicker, LoraModelPicker } from "./model-selector/pickers";
|
||||||
import { Input } from "../ui/input";
|
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"> = {
|
const PROVIDER_LOGO_EXT: Record<string, "svg" | "png" | "jpg"> = {
|
||||||
openai: "svg",
|
openai: "svg",
|
||||||
mistral: "svg",
|
mistral: "svg",
|
||||||
|
|
@ -64,7 +79,7 @@ function ExternalProviderLogo({
|
||||||
title?: string;
|
title?: string;
|
||||||
}) {
|
}) {
|
||||||
const src = providerLogoSrc(providerType);
|
const src = providerLogoSrc(providerType);
|
||||||
if (!src && isCustomProviderType(providerType)) {
|
if (!src && isCustomProviderTypeWithFallbackLogo(providerType)) {
|
||||||
return (
|
return (
|
||||||
<span title={title} aria-hidden={true} className="inline-flex shrink-0">
|
<span title={title} aria-hidden={true} className="inline-flex shrink-0">
|
||||||
<HugeiconsIcon
|
<HugeiconsIcon
|
||||||
|
|
@ -146,10 +161,11 @@ function ModelSelectorTrigger({
|
||||||
type="button"
|
type="button"
|
||||||
data-tour={dataTour}
|
data-tour={dataTour}
|
||||||
className={cn(
|
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" &&
|
variant === "outline" &&
|
||||||
"rounded-[10px] border border-border/60 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 === "ghost" &&
|
||||||
|
"rounded-[10px] hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
|
||||||
variant === "muted" && "rounded-[10px] bg-muted hover:bg-muted/80",
|
variant === "muted" && "rounded-[10px] bg-muted hover:bg-muted/80",
|
||||||
size === "sm" && "h-8 px-3 text-xs",
|
size === "sm" && "h-8 px-3 text-xs",
|
||||||
size === "default" && "h-9 px-3.5 text-sm",
|
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" />
|
<span className="size-2 shrink-0 rounded-full bg-emerald-500" />
|
||||||
)}
|
)}
|
||||||
{currentModel?.icon ? (
|
{currentModel?.icon ? (
|
||||||
<span className="flex shrink-0 items-center">{currentModel.icon}</span>
|
<span className="flex shrink-0 items-center">
|
||||||
|
{currentModel.icon}
|
||||||
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="flex min-w-0 flex-1 items-baseline">
|
<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">
|
<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 chatOnly = usePlatformStore((s) => s.isChatOnly());
|
||||||
const hasExternal = externalModels.length > 0;
|
const hasExternal = externalModels.length > 0;
|
||||||
const chatOnlyTabsDefault = useMemo(
|
const chatOnlyTabsDefault = useMemo(
|
||||||
() => (value && externalModels.some((model) => model.id === value) ? "external" : "hub"),
|
() =>
|
||||||
|
value && externalModels.some((model) => model.id === value)
|
||||||
|
? "external"
|
||||||
|
: "hub",
|
||||||
[externalModels, value],
|
[externalModels, value],
|
||||||
);
|
);
|
||||||
const studioTabsDefault = useMemo((): "hub" | "lora" | "external" => {
|
const studioTabsDefault = useMemo((): "hub" | "lora" | "external" => {
|
||||||
|
|
@ -246,7 +267,7 @@ function ModelSelectorContent({
|
||||||
align="start"
|
align="start"
|
||||||
data-tour={dataTour}
|
data-tour={dataTour}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -258,7 +279,12 @@ function ModelSelectorContent({
|
||||||
<TabsTrigger value="external">Connected</TabsTrigger>
|
<TabsTrigger value="external">Connected</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="hub" className="m-0">
|
<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>
|
||||||
<TabsContent value="external" className="m-0">
|
<TabsContent value="external" className="m-0">
|
||||||
<ExternalModelPicker
|
<ExternalModelPicker
|
||||||
|
|
@ -269,18 +295,30 @@ function ModelSelectorContent({
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</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">
|
<Tabs defaultValue={studioTabsDefault} className="w-full">
|
||||||
<TabsList className="mb-2 w-full">
|
<TabsList className="mb-2 w-full">
|
||||||
<TabsTrigger value="hub">Hub models</TabsTrigger>
|
<TabsTrigger value="hub">Hub models</TabsTrigger>
|
||||||
<TabsTrigger value="lora">Fine-tuned</TabsTrigger>
|
<TabsTrigger value="lora">Fine-tuned</TabsTrigger>
|
||||||
{hasExternal ? <TabsTrigger value="external">Connected</TabsTrigger> : null}
|
{hasExternal ? (
|
||||||
|
<TabsTrigger value="external">Connected</TabsTrigger>
|
||||||
|
) : null}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="hub" className="m-0">
|
<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>
|
||||||
|
|
||||||
<TabsContent value="lora" className="m-0">
|
<TabsContent value="lora" className="m-0">
|
||||||
|
|
@ -420,7 +458,9 @@ export function ModelSelector({
|
||||||
const found = optionById.get(selected);
|
const found = optionById.get(selected);
|
||||||
if (activeGgufVariant) {
|
if (activeGgufVariant) {
|
||||||
const desc = `GGUF · ${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 };
|
return found ?? { id: selected, name: selected };
|
||||||
}, [selected, optionById, activeGgufVariant]);
|
}, [selected, optionById, activeGgufVariant]);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -30,7 +30,7 @@ import { LayoutAlignLeftIcon } from "@hugeicons/core-free-icons"
|
||||||
|
|
||||||
const noop = () => {}
|
const noop = () => {}
|
||||||
|
|
||||||
const SIDEBAR_WIDTH = "16rem"
|
const SIDEBAR_WIDTH = "18rem"
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||||
|
|
||||||
|
|
@ -305,9 +305,8 @@ function Sidebar({
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-slot="sidebar-inner"
|
data-slot="sidebar-inner"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-sidebar flex size-full flex-col overflow-hidden border-r border-sidebar-border",
|
"bg-sidebar flex size-full flex-col overflow-hidden",
|
||||||
"group-data-[variant=floating]:ring-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1",
|
"group-data-[variant=floating]:ring-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1",
|
||||||
hasPinMode && "ring-1 ring-sidebar-border/60",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
{
|
{
|
||||||
"--normal-bg": "var(--popover)",
|
"--normal-bg": "var(--popover)",
|
||||||
"--normal-text": "var(--popover-foreground)",
|
"--normal-text": "var(--popover-foreground)",
|
||||||
"--normal-border": "var(--border)",
|
// No border line; elevation comes from the composer's drop shadow.
|
||||||
|
"--normal-border": "transparent",
|
||||||
"--border-radius": "var(--radius)",
|
"--border-radius": "var(--radius)",
|
||||||
// Pin close button to the top-right corner inside the toast.
|
// Pin close button to the top-right corner inside the toast.
|
||||||
// Overrides sonner's default left placement and outside-corner
|
// Overrides sonner's default left placement and outside-corner
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,15 @@ import {
|
||||||
import { Thread } from "@/components/assistant-ui/thread";
|
import { Thread } from "@/components/assistant-ui/thread";
|
||||||
import { useSidebar } from "@/components/ui/sidebar";
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
||||||
import { NativeModelChip } from "@/features/native-intents/components/native-model-chip";
|
import {
|
||||||
import { NativeModelDropOverlay } from "@/features/native-intents/components/native-model-drop-overlay";
|
NativeModelChip,
|
||||||
import { useNativeIntentStore } from "@/features/native-intents/store";
|
NativeModelDropOverlay,
|
||||||
import type { NativeIntent } from "@/features/native-intents/types";
|
type NativeIntent,
|
||||||
import { useChooseNativeModel } from "@/features/native-intents/use-native-dialogs";
|
useChooseNativeModel,
|
||||||
import { useNativeModelDrop } from "@/features/native-intents/use-native-drop";
|
useNativeIntentStore,
|
||||||
import { useNativePathLeasesSupported } from "@/features/native-intents/use-native-readiness";
|
useNativeModelDrop,
|
||||||
|
useNativePathLeasesSupported,
|
||||||
|
} from "@/features/native-intents";
|
||||||
import { GuidedTour, useGuidedTourController } from "@/features/tour";
|
import { GuidedTour, useGuidedTourController } from "@/features/tour";
|
||||||
import { isTauri } from "@/lib/api-base";
|
import { isTauri } from "@/lib/api-base";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -85,6 +87,7 @@ import {
|
||||||
listStoredChatMessages,
|
listStoredChatMessages,
|
||||||
listStoredChatThreads,
|
listStoredChatThreads,
|
||||||
} from "./utils/chat-history-storage";
|
} from "./utils/chat-history-storage";
|
||||||
|
import { createCompareId } from "./utils/compare-id";
|
||||||
|
|
||||||
type LoraCandidate = {
|
type LoraCandidate = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -156,7 +159,14 @@ function messageHasImage(message: MessageRecord): boolean {
|
||||||
const SingleContent = memo(function SingleContent({
|
const SingleContent = memo(function SingleContent({
|
||||||
threadId,
|
threadId,
|
||||||
newThreadNonce,
|
newThreadNonce,
|
||||||
}: { threadId?: string; newThreadNonce?: string }): ReactElement {
|
canCompare,
|
||||||
|
onEnterCompare,
|
||||||
|
}: {
|
||||||
|
threadId?: string;
|
||||||
|
newThreadNonce?: string;
|
||||||
|
canCompare?: boolean;
|
||||||
|
onEnterCompare?: () => void;
|
||||||
|
}): ReactElement {
|
||||||
return (
|
return (
|
||||||
<ChatRuntimeProvider
|
<ChatRuntimeProvider
|
||||||
modelType="base"
|
modelType="base"
|
||||||
|
|
@ -164,7 +174,12 @@ const SingleContent = memo(function SingleContent({
|
||||||
newThreadNonce={newThreadNonce}
|
newThreadNonce={newThreadNonce}
|
||||||
>
|
>
|
||||||
<div className="flex min-h-0 min-w-0 flex-1 basis-0 flex-col overflow-hidden">
|
<div className="flex min-h-0 min-w-0 flex-1 basis-0 flex-col overflow-hidden">
|
||||||
<Thread hideWelcome={Boolean(threadId)} targetThreadId={threadId} />
|
<Thread
|
||||||
|
hideWelcome={Boolean(threadId)}
|
||||||
|
targetThreadId={threadId}
|
||||||
|
canCompare={canCompare}
|
||||||
|
onEnterCompare={onEnterCompare}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ChatRuntimeProvider>
|
</ChatRuntimeProvider>
|
||||||
);
|
);
|
||||||
|
|
@ -207,6 +222,7 @@ const CompareContent = memo(function CompareContent({
|
||||||
onFoldersChange,
|
onFoldersChange,
|
||||||
onModelsChange,
|
onModelsChange,
|
||||||
deleteDisabled,
|
deleteDisabled,
|
||||||
|
onExitCompare,
|
||||||
}: {
|
}: {
|
||||||
pairId: string;
|
pairId: string;
|
||||||
models: ModelOption[];
|
models: ModelOption[];
|
||||||
|
|
@ -214,11 +230,12 @@ const CompareContent = memo(function CompareContent({
|
||||||
onFoldersChange?: () => void;
|
onFoldersChange?: () => void;
|
||||||
onModelsChange?: (deletedModel?: DeletedModelRef) => void;
|
onModelsChange?: (deletedModel?: DeletedModelRef) => void;
|
||||||
deleteDisabled?: boolean;
|
deleteDisabled?: boolean;
|
||||||
|
onExitCompare?: () => void;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const isLoraCompare = useIsLoraCompare();
|
const isLoraCompare = useIsLoraCompare();
|
||||||
|
|
||||||
return isLoraCompare ? (
|
return isLoraCompare ? (
|
||||||
<LoraCompareContent pairId={pairId} />
|
<LoraCompareContent pairId={pairId} onExitCompare={onExitCompare} />
|
||||||
) : (
|
) : (
|
||||||
<GeneralCompareContent
|
<GeneralCompareContent
|
||||||
pairId={pairId}
|
pairId={pairId}
|
||||||
|
|
@ -227,6 +244,7 @@ const CompareContent = memo(function CompareContent({
|
||||||
onFoldersChange={onFoldersChange}
|
onFoldersChange={onFoldersChange}
|
||||||
onModelsChange={onModelsChange}
|
onModelsChange={onModelsChange}
|
||||||
deleteDisabled={deleteDisabled}
|
deleteDisabled={deleteDisabled}
|
||||||
|
onExitCompare={onExitCompare}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -311,7 +329,7 @@ function CompareShell({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 bg-background pl-5 pr-5 md:pr-[30px] pb-2 pt-1">
|
<div className="shrink-0 bg-background pl-5 pr-5 md:pr-[30px] pb-2 pt-1">
|
||||||
<div className="mx-auto w-full max-w-[48rem]">{composer}</div>
|
<div className="mx-auto w-full max-w-[660px]">{composer}</div>
|
||||||
<p className="composer-footer-note">
|
<p className="composer-footer-note">
|
||||||
LLMs can make mistakes. Double-check responses.
|
LLMs can make mistakes. Double-check responses.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -324,18 +342,21 @@ function CompareShell({
|
||||||
/** Fast path: same model, adapter on/off, simultaneous generation. */
|
/** Fast path: same model, adapter on/off, simultaneous generation. */
|
||||||
const LoraCompareContent = memo(function LoraCompareContent({
|
const LoraCompareContent = memo(function LoraCompareContent({
|
||||||
pairId,
|
pairId,
|
||||||
}: { pairId: string }): ReactElement {
|
onExitCompare,
|
||||||
|
}: { pairId: string; onExitCompare?: () => void }): ReactElement {
|
||||||
const handlesRef = useRef<Record<string, CompareHandle>>({});
|
const handlesRef = useRef<Record<string, CompareHandle>>({});
|
||||||
const [baseThreadId, setBaseThreadId] = useState<string>();
|
const [baseThreadId, setBaseThreadId] = useState<string>();
|
||||||
const [loraThreadId, setLoraThreadId] = useState<string>();
|
const [loraThreadId, setLoraThreadId] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isActive = true;
|
let isActive = true;
|
||||||
listStoredChatThreads({ pairId }).then((threads) => {
|
listStoredChatThreads({ pairId })
|
||||||
|
.then((threads) => {
|
||||||
if (!isActive) return;
|
if (!isActive) return;
|
||||||
setBaseThreadId(threads.find((t) => t.modelType === "base")?.id);
|
setBaseThreadId(threads.find((t) => t.modelType === "base")?.id);
|
||||||
setLoraThreadId(threads.find((t) => t.modelType === "lora")?.id);
|
setLoraThreadId(threads.find((t) => t.modelType === "lora")?.id);
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
if (!isExpectedBackgroundChatStorageError(error)) {
|
if (!isExpectedBackgroundChatStorageError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +369,9 @@ const LoraCompareContent = memo(function LoraCompareContent({
|
||||||
return (
|
return (
|
||||||
<CompareShell
|
<CompareShell
|
||||||
handlesRef={handlesRef}
|
handlesRef={handlesRef}
|
||||||
composer={<SharedComposer handlesRef={handlesRef} />}
|
composer={
|
||||||
|
<SharedComposer handlesRef={handlesRef} onExitCompare={onExitCompare} />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<ComparePane
|
<ComparePane
|
||||||
|
|
@ -441,6 +464,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
||||||
onFoldersChange,
|
onFoldersChange,
|
||||||
onModelsChange,
|
onModelsChange,
|
||||||
deleteDisabled,
|
deleteDisabled,
|
||||||
|
onExitCompare,
|
||||||
}: {
|
}: {
|
||||||
pairId: string;
|
pairId: string;
|
||||||
models: ModelOption[];
|
models: ModelOption[];
|
||||||
|
|
@ -448,6 +472,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
||||||
onFoldersChange?: () => void;
|
onFoldersChange?: () => void;
|
||||||
onModelsChange?: (deletedModel?: DeletedModelRef) => void;
|
onModelsChange?: (deletedModel?: DeletedModelRef) => void;
|
||||||
deleteDisabled?: boolean;
|
deleteDisabled?: boolean;
|
||||||
|
onExitCompare?: () => void;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const handlesRef = useRef<Record<string, CompareHandle>>({});
|
const handlesRef = useRef<Record<string, CompareHandle>>({});
|
||||||
const [model1ThreadId, setModel1ThreadId] = useState<string>();
|
const [model1ThreadId, setModel1ThreadId] = useState<string>();
|
||||||
|
|
@ -480,17 +505,21 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isActive = true;
|
let isActive = true;
|
||||||
listStoredChatThreads({ pairId }).then((threads) => {
|
listStoredChatThreads({ pairId })
|
||||||
|
.then((threads) => {
|
||||||
if (!isActive) return;
|
if (!isActive) return;
|
||||||
setModel1ThreadId(
|
setModel1ThreadId(
|
||||||
threads.find((t) => t.modelType === "model1" || t.modelType === "base")
|
threads.find(
|
||||||
?.id,
|
(t) => t.modelType === "model1" || t.modelType === "base",
|
||||||
|
)?.id,
|
||||||
);
|
);
|
||||||
setModel2ThreadId(
|
setModel2ThreadId(
|
||||||
threads.find((t) => t.modelType === "model2" || t.modelType === "lora")
|
threads.find(
|
||||||
?.id,
|
(t) => t.modelType === "model2" || t.modelType === "lora",
|
||||||
|
)?.id,
|
||||||
);
|
);
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
if (!isExpectedBackgroundChatStorageError(error)) {
|
if (!isExpectedBackgroundChatStorageError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -508,6 +537,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
||||||
handlesRef={handlesRef}
|
handlesRef={handlesRef}
|
||||||
model1={model1}
|
model1={model1}
|
||||||
model2={model2}
|
model2={model2}
|
||||||
|
onExitCompare={onExitCompare}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -617,7 +647,9 @@ export function ChatPage(): ReactElement {
|
||||||
|
|
||||||
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
|
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
|
||||||
const [modelSelectorLocked, setModelSelectorLocked] = useState(false);
|
const [modelSelectorLocked, setModelSelectorLocked] = useState(false);
|
||||||
const viewBeforeCompareRef = useRef<ChatSearch | null>(null);
|
const [viewBeforeCompare, setViewBeforeCompare] = useState<ChatSearch | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const inferenceParams = useChatRuntimeStore((state) => state.params);
|
const inferenceParams = useChatRuntimeStore((state) => state.params);
|
||||||
const setInferenceParams = useChatRuntimeStore((state) => state.setParams);
|
const setInferenceParams = useChatRuntimeStore((state) => state.setParams);
|
||||||
const activeGgufVariant = useChatRuntimeStore(
|
const activeGgufVariant = useChatRuntimeStore(
|
||||||
|
|
@ -647,8 +679,7 @@ export function ChatPage(): ReactElement {
|
||||||
} = useChatModelRuntime();
|
} = useChatModelRuntime();
|
||||||
const prevConnectionsEnabledRef = useRef(connectionsEnabled);
|
const prevConnectionsEnabledRef = useRef(connectionsEnabled);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const turnedOff =
|
const turnedOff = prevConnectionsEnabledRef.current && !connectionsEnabled;
|
||||||
prevConnectionsEnabledRef.current && !connectionsEnabled;
|
|
||||||
if (!connectionsEnabled && isExternalModelId(inferenceParams.checkpoint)) {
|
if (!connectionsEnabled && isExternalModelId(inferenceParams.checkpoint)) {
|
||||||
clearCheckpoint();
|
clearCheckpoint();
|
||||||
if (turnedOff) {
|
if (turnedOff) {
|
||||||
|
|
@ -658,11 +689,7 @@ export function ChatPage(): ReactElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prevConnectionsEnabledRef.current = connectionsEnabled;
|
prevConnectionsEnabledRef.current = connectionsEnabled;
|
||||||
}, [
|
}, [clearCheckpoint, connectionsEnabled, inferenceParams.checkpoint]);
|
||||||
clearCheckpoint,
|
|
||||||
connectionsEnabled,
|
|
||||||
inferenceParams.checkpoint,
|
|
||||||
]);
|
|
||||||
const pendingNativeModelIntent = useNativeIntentStore(
|
const pendingNativeModelIntent = useNativeIntentStore(
|
||||||
(state) => state.pendingModelIntent,
|
(state) => state.pendingModelIntent,
|
||||||
);
|
);
|
||||||
|
|
@ -681,17 +708,19 @@ export function ChatPage(): ReactElement {
|
||||||
const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled);
|
const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled);
|
||||||
const reasoningStyle = useChatRuntimeStore((s) => s.reasoningStyle);
|
const reasoningStyle = useChatRuntimeStore((s) => s.reasoningStyle);
|
||||||
const reasoningEffort = useChatRuntimeStore((s) => s.reasoningEffort);
|
const reasoningEffort = useChatRuntimeStore((s) => s.reasoningEffort);
|
||||||
const supportsReasoningOff = useChatRuntimeStore((s) => s.supportsReasoningOff);
|
const supportsReasoningOff = useChatRuntimeStore(
|
||||||
|
(s) => s.supportsReasoningOff,
|
||||||
|
);
|
||||||
const activeExternalProvider = useMemo(() => {
|
const activeExternalProvider = useMemo(() => {
|
||||||
const selection = parseExternalModelId(inferenceParams.checkpoint);
|
const selection = parseExternalModelId(inferenceParams.checkpoint);
|
||||||
if (!selection) return null;
|
if (!selection) return null;
|
||||||
return (
|
return (
|
||||||
externalProvidersForChat.find(
|
externalProvidersForChat.find((p) => p.id === selection.providerId) ??
|
||||||
(p) => p.id === selection.providerId,
|
null
|
||||||
) ?? null
|
|
||||||
);
|
);
|
||||||
}, [externalProvidersForChat, inferenceParams.checkpoint]);
|
}, [externalProvidersForChat, inferenceParams.checkpoint]);
|
||||||
const activeExternalProviderType = activeExternalProvider?.providerType ?? null;
|
const activeExternalProviderType =
|
||||||
|
activeExternalProvider?.providerType ?? null;
|
||||||
const activeProviderCapabilities = useMemo(() => {
|
const activeProviderCapabilities = useMemo(() => {
|
||||||
const selection = parseExternalModelId(inferenceParams.checkpoint);
|
const selection = parseExternalModelId(inferenceParams.checkpoint);
|
||||||
if (!selection) return null;
|
if (!selection) return null;
|
||||||
|
|
@ -807,7 +836,9 @@ export function ChatPage(): ReactElement {
|
||||||
(provider?.providerType === "anthropic" ||
|
(provider?.providerType === "anthropic" ||
|
||||||
provider?.providerType === "openai");
|
provider?.providerType === "openai");
|
||||||
const storedToolsEnabled = loadOptionalBool(CHAT_TOOLS_ENABLED_KEY);
|
const storedToolsEnabled = loadOptionalBool(CHAT_TOOLS_ENABLED_KEY);
|
||||||
const storedCodeToolsEnabled = loadOptionalBool(CHAT_CODE_TOOLS_ENABLED_KEY);
|
const storedCodeToolsEnabled = loadOptionalBool(
|
||||||
|
CHAT_CODE_TOOLS_ENABLED_KEY,
|
||||||
|
);
|
||||||
const storedImageToolsEnabled = loadOptionalBool(
|
const storedImageToolsEnabled = loadOptionalBool(
|
||||||
CHAT_IMAGE_TOOLS_ENABLED_KEY,
|
CHAT_IMAGE_TOOLS_ENABLED_KEY,
|
||||||
);
|
);
|
||||||
|
|
@ -971,8 +1002,7 @@ export function ChatPage(): ReactElement {
|
||||||
selectedProvider?.providerType,
|
selectedProvider?.providerType,
|
||||||
selectedExternal?.modelId,
|
selectedExternal?.modelId,
|
||||||
{
|
{
|
||||||
isReasoningProvider:
|
isReasoningProvider: selectedProvider?.isReasoningModel === true,
|
||||||
selectedProvider?.isReasoningModel === true,
|
|
||||||
baseUrl: selectedProvider?.baseUrl ?? null,
|
baseUrl: selectedProvider?.baseUrl ?? null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -1018,7 +1048,8 @@ export function ChatPage(): ReactElement {
|
||||||
selectedExternal?.modelId,
|
selectedExternal?.modelId,
|
||||||
selectedProvider?.baseUrl,
|
selectedProvider?.baseUrl,
|
||||||
);
|
);
|
||||||
const supportsBuiltinCodeExecution = providerSupportsBuiltinCodeExecution(
|
const supportsBuiltinCodeExecution =
|
||||||
|
providerSupportsBuiltinCodeExecution(
|
||||||
selectedProvider?.providerType,
|
selectedProvider?.providerType,
|
||||||
selectedExternal?.modelId,
|
selectedExternal?.modelId,
|
||||||
selectedProvider?.baseUrl,
|
selectedProvider?.baseUrl,
|
||||||
|
|
@ -1185,16 +1216,27 @@ export function ChatPage(): ReactElement {
|
||||||
const openSidebar = useCallback(() => setPinned(true), [setPinned]);
|
const openSidebar = useCallback(() => setPinned(true), [setPinned]);
|
||||||
|
|
||||||
const enterCompare = useCallback(() => {
|
const enterCompare = useCallback(() => {
|
||||||
viewBeforeCompareRef.current = { ...search };
|
const saved: ChatSearch = { ...search };
|
||||||
|
const active = useChatRuntimeStore.getState().activeThreadId;
|
||||||
|
if (!(saved.thread || saved.new) && active) {
|
||||||
|
if (!active.startsWith("__LOCALID_")) {
|
||||||
|
saved.thread = active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setViewBeforeCompare(saved);
|
||||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||||
useChatRuntimeStore.getState().setContextUsage(null);
|
useChatRuntimeStore.getState().setContextUsage(null);
|
||||||
navigate({ to: "/chat", search: { compare: crypto.randomUUID() } });
|
navigate({ to: "/chat", search: { compare: createCompareId() } });
|
||||||
}, [navigate, search]);
|
}, [navigate, search]);
|
||||||
|
|
||||||
const exitCompare = useCallback(() => {
|
const exitCompare = useCallback(() => {
|
||||||
const saved = viewBeforeCompareRef.current;
|
const saved = viewBeforeCompare;
|
||||||
if (!saved) return;
|
// No saved view (compare opened by direct URL); fall back to a fresh chat.
|
||||||
viewBeforeCompareRef.current = null;
|
if (!saved) {
|
||||||
|
navigate({ to: "/chat" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setViewBeforeCompare(null);
|
||||||
navigate({ to: "/chat", search: saved });
|
navigate({ to: "/chat", search: saved });
|
||||||
// Restore usage from the last assistant message, but only if it
|
// Restore usage from the last assistant message, but only if it
|
||||||
// matches the currently active checkpoint. Without this guard the
|
// matches the currently active checkpoint. Without this guard the
|
||||||
|
|
@ -1215,8 +1257,7 @@ export function ChatPage(): ReactElement {
|
||||||
if (!usage) return;
|
if (!usage) return;
|
||||||
const store = useChatRuntimeStore.getState();
|
const store = useChatRuntimeStore.getState();
|
||||||
const activeCheckpoint = store.params.checkpoint;
|
const activeCheckpoint = store.params.checkpoint;
|
||||||
const usageModelId =
|
const usageModelId = (usage as { modelId?: unknown }).modelId;
|
||||||
(usage as { modelId?: unknown }).modelId;
|
|
||||||
// Scope by modelId when present; reject if no active checkpoint
|
// Scope by modelId when present; reject if no active checkpoint
|
||||||
// (model-scoped usage cannot be attributed to "nothing").
|
// (model-scoped usage cannot be attributed to "nothing").
|
||||||
if (typeof usageModelId === "string" && usageModelId) {
|
if (typeof usageModelId === "string" && usageModelId) {
|
||||||
|
|
@ -1242,7 +1283,7 @@ export function ChatPage(): ReactElement {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate, viewBeforeCompare]);
|
||||||
|
|
||||||
const models = useMemo<ModelOption[]>(
|
const models = useMemo<ModelOption[]>(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -1398,7 +1439,7 @@ export function ChatPage(): ReactElement {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||||
useChatRuntimeStore.getState().setContextUsage(null);
|
useChatRuntimeStore.getState().setContextUsage(null);
|
||||||
navigate({ to: "/chat", search: { compare: crypto.randomUUID() } });
|
navigate({ to: "/chat", search: { compare: createCompareId() } });
|
||||||
clearHandoff();
|
clearHandoff();
|
||||||
console.info("[chat-handoff] loaded lora + opened compare");
|
console.info("[chat-handoff] loaded lora + opened compare");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1596,6 +1637,8 @@ export function ChatPage(): ReactElement {
|
||||||
key={view.threadId ?? "single"}
|
key={view.threadId ?? "single"}
|
||||||
threadId={view.threadId}
|
threadId={view.threadId}
|
||||||
newThreadNonce={view.newThreadNonce}
|
newThreadNonce={view.newThreadNonce}
|
||||||
|
canCompare={canCompare}
|
||||||
|
onEnterCompare={enterCompare}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CompareContent
|
<CompareContent
|
||||||
|
|
@ -1606,6 +1649,7 @@ export function ChatPage(): ReactElement {
|
||||||
onFoldersChange={refreshLocalModels}
|
onFoldersChange={refreshLocalModels}
|
||||||
onModelsChange={refreshModelLists}
|
onModelsChange={refreshModelLists}
|
||||||
deleteDisabled={modelOperationInProgress}
|
deleteDisabled={modelOperationInProgress}
|
||||||
|
onExitCompare={exitCompare}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ import {
|
||||||
LayoutAlignRightIcon,
|
LayoutAlignRightIcon,
|
||||||
} from "@hugeicons/core-free-icons";
|
} from "@hugeicons/core-free-icons";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown, ExternalLink } from "lucide-react";
|
||||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||||
import { Fragment, type ReactNode } from "react";
|
import { Fragment, type ReactNode } from "react";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
@ -327,11 +327,14 @@ function saveCollapsibleOpen(label: string, open: boolean) {
|
||||||
|
|
||||||
function CollapsibleSection({
|
function CollapsibleSection({
|
||||||
label,
|
label,
|
||||||
|
labelHref,
|
||||||
children,
|
children,
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
first = false,
|
first = false,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
|
/** Optional link for the label (e.g. its PR); chevron still toggles. */
|
||||||
|
labelHref?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
first?: boolean;
|
first?: boolean;
|
||||||
|
|
@ -341,6 +344,17 @@ function CollapsibleSection({
|
||||||
return Object.hasOwn(saved, label) ? saved[label] : defaultOpen;
|
return Object.hasOwn(saved, label) ? saved[label] : defaultOpen;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
const next = !open;
|
||||||
|
setOpen(next);
|
||||||
|
saveCollapsibleOpen(label, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerClasses = cn(
|
||||||
|
"flex w-full items-center justify-between text-[12px] font-medium normal-case tracking-[0.04em] text-nav-fg-muted transition-colors focus-visible:outline-none focus-visible:ring-0",
|
||||||
|
first ? "pt-4 pb-5" : "py-5",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -348,17 +362,33 @@ function CollapsibleSection({
|
||||||
"border-t border-black/[0.13] dark:border-white/[0.09]",
|
"border-t border-black/[0.13] dark:border-white/[0.09]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{labelHref ? (
|
||||||
|
<div className={headerClasses}>
|
||||||
|
<a
|
||||||
|
href={labelHref}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex cursor-pointer items-center gap-1 leading-none transition-colors hover:text-nav-fg"
|
||||||
|
>
|
||||||
|
<span>{label}</span>
|
||||||
|
<ExternalLink className="size-3" />
|
||||||
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={toggle}
|
||||||
const next = !open;
|
aria-label={open ? `Collapse ${label}` : `Expand ${label}`}
|
||||||
setOpen(next);
|
className="flex shrink-0 cursor-pointer items-center leading-none transition-colors hover:text-nav-fg"
|
||||||
saveCollapsibleOpen(label, next);
|
>
|
||||||
}}
|
<ChevronDown
|
||||||
className={cn(
|
className={cn("size-3.5", open ? "rotate-0" : "-rotate-90")}
|
||||||
"flex w-full cursor-pointer items-center justify-between text-[12px] font-medium normal-case tracking-[0.04em] text-nav-fg-muted transition-colors hover:text-nav-fg focus-visible:outline-none focus-visible:ring-0",
|
/>
|
||||||
first ? "pt-4 pb-5" : "py-5",
|
</button>
|
||||||
)}
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggle}
|
||||||
|
className={cn("cursor-pointer hover:text-nav-fg", headerClasses)}
|
||||||
>
|
>
|
||||||
<span className="leading-none">{label}</span>
|
<span className="leading-none">{label}</span>
|
||||||
<span className="flex shrink-0 items-center leading-none">
|
<span className="flex shrink-0 items-center leading-none">
|
||||||
|
|
@ -367,6 +397,7 @@ function CollapsibleSection({
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
{open && <div className="pb-7">{children}</div>}
|
{open && <div className="pb-7">{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -1345,7 +1376,10 @@ export function ChatSettingsPanel({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!isExternalModel ? (
|
{!isExternalModel ? (
|
||||||
<CollapsibleSection label="MCP Servers">
|
<CollapsibleSection
|
||||||
|
label="MCP Servers"
|
||||||
|
labelHref="https://github.com/unslothai/unsloth/pull/5852"
|
||||||
|
>
|
||||||
<McpServersSection />
|
<McpServersSection />
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ export function ModelLoadDescription({
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-label="Stop model loading"
|
aria-label="Stop model loading"
|
||||||
className="h-auto self-stretch shrink-0 !rounded-none !border-0 bg-transparent px-1 text-[10px] text-muted-foreground hover:bg-transparent hover:text-destructive focus-visible:text-destructive"
|
className="h-auto self-stretch shrink-0 !rounded-none !border-0 bg-transparent px-1 text-[10px] text-muted-foreground hover:!bg-transparent dark:hover:!bg-transparent hover:text-destructive focus-visible:text-destructive"
|
||||||
onClick={onStop}
|
onClick={onStop}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||||
|
|
||||||
export { ChatPage } from "./chat-page";
|
export { ChatPage } from "./chat-page";
|
||||||
|
export { sentAudioNames } from "./api/chat-adapter";
|
||||||
|
export { parseExternalModelId } from "./external-providers";
|
||||||
|
export { getExternalReasoningCapabilities } from "./provider-capabilities";
|
||||||
|
export { useExternalProvidersStore } from "./stores/external-providers-store";
|
||||||
|
export { deleteThreadMessage } from "./utils/delete-thread-message";
|
||||||
|
export { createCompareId } from "./utils/compare-id";
|
||||||
|
export { applyQwenThinkingParams } from "./utils/qwen-params";
|
||||||
export {
|
export {
|
||||||
getInferenceStatus,
|
getInferenceStatus,
|
||||||
listGgufVariants,
|
listGgufVariants,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
13
studio/frontend/src/features/chat/utils/compare-id.ts
Normal file
13
studio/frontend/src/features/chat/utils/compare-id.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||||
|
|
||||||
|
export function createSafeId(): string {
|
||||||
|
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||||
|
return globalThis.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCompareId(): string {
|
||||||
|
return createSafeId();
|
||||||
|
}
|
||||||
8
studio/frontend/src/features/native-intents/index.ts
Normal file
8
studio/frontend/src/features/native-intents/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export { NativeModelChip } from "./components/native-model-chip";
|
||||||
|
export { NativeModelDropOverlay } from "./components/native-model-drop-overlay";
|
||||||
|
export { useNativeIntentStore } from "./store";
|
||||||
|
export type { NativeIntent } from "./types";
|
||||||
|
export { useChooseNativeModel } from "./use-native-dialogs";
|
||||||
|
export { useNativeModelDrop } from "./use-native-drop";
|
||||||
|
export type { NativeModelDropState } from "./use-native-drop";
|
||||||
|
export { useNativePathLeasesSupported } from "./use-native-readiness";
|
||||||
|
|
@ -41,7 +41,8 @@
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Fira Code";
|
font-family: "Fira Code";
|
||||||
src: url("/fonts/FiraCode-VariableFont_wght.ttf") format("truetype-variations");
|
src: url("/fonts/FiraCode-VariableFont_wght.ttf")
|
||||||
|
format("truetype-variations");
|
||||||
font-weight: 300 700;
|
font-weight: 300 700;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
|
@ -224,11 +225,15 @@
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
--shadow-2xs: 0px 0px 0px 0px hsl(0 0% 0% / 0);
|
--shadow-2xs: 0px 0px 0px 0px hsl(0 0% 0% / 0);
|
||||||
--shadow-xs: 0px 0px 0px 0px hsl(0 0% 0% / 0);
|
--shadow-xs: 0px 0px 0px 0px hsl(0 0% 0% / 0);
|
||||||
--shadow-sm: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px 0px hsl(0 0% 0% / 0);
|
--shadow-sm: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px 0px
|
||||||
|
hsl(0 0% 0% / 0);
|
||||||
--shadow: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px 0px hsl(0 0% 0% / 0);
|
--shadow: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px 0px hsl(0 0% 0% / 0);
|
||||||
--shadow-md: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px 0px hsl(0 0% 0% / 0);
|
--shadow-md: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px 0px
|
||||||
--shadow-lg: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px 0px hsl(0 0% 0% / 0);
|
hsl(0 0% 0% / 0);
|
||||||
--shadow-xl: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px 0px hsl(0 0% 0% / 0);
|
--shadow-lg: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px 0px
|
||||||
|
hsl(0 0% 0% / 0);
|
||||||
|
--shadow-xl: 0px 0px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px 0px
|
||||||
|
hsl(0 0% 0% / 0);
|
||||||
--shadow-2xl: 0px 0px 0px 0px hsl(0 0% 0% / 0);
|
--shadow-2xl: 0px 0px 0px 0px hsl(0 0% 0% / 0);
|
||||||
|
|
||||||
--nav-fg: #c7c7c4;
|
--nav-fg: #c7c7c4;
|
||||||
|
|
@ -345,7 +350,6 @@
|
||||||
--animate-pulse: pulse var(--duration) ease-out infinite;
|
--animate-pulse: pulse var(--duration) ease-out infinite;
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 0 0 var(--pulse-color);
|
box-shadow: 0 0 0 0 var(--pulse-color);
|
||||||
|
|
@ -359,7 +363,6 @@
|
||||||
--animate-shiny-text: shiny-text 8s infinite;
|
--animate-shiny-text: shiny-text 8s infinite;
|
||||||
|
|
||||||
@keyframes shiny-text {
|
@keyframes shiny-text {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
90%,
|
90%,
|
||||||
100% {
|
100% {
|
||||||
|
|
@ -443,7 +446,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|
||||||
/* Heading font utility — the logo applies its own dark-mode tracking
|
/* Heading font utility — the logo applies its own dark-mode tracking
|
||||||
to offset optical bloom at large sizes. For small UI text (menus,
|
to offset optical bloom at large sizes. For small UI text (menus,
|
||||||
nav items) we keep a single tight tracking in both themes, relying
|
nav items) we keep a single tight tracking in both themes, relying
|
||||||
|
|
@ -486,9 +488,11 @@
|
||||||
.sidebar-nav-btn[data-active="true"],
|
.sidebar-nav-btn[data-active="true"],
|
||||||
.sidebar-nav-btn[data-state="open"],
|
.sidebar-nav-btn[data-state="open"],
|
||||||
.group\/recent-item:hover .sidebar-nav-btn,
|
.group\/recent-item:hover .sidebar-nav-btn,
|
||||||
.group\/recent-item:has(.sidebar-row-action[data-state="open"]) .sidebar-nav-btn,
|
.group\/recent-item:has(.sidebar-row-action[data-state="open"])
|
||||||
|
.sidebar-nav-btn,
|
||||||
.group\/run-item:hover .sidebar-nav-btn,
|
.group\/run-item:hover .sidebar-nav-btn,
|
||||||
.group\/run-item:has(.sidebar-row-action[data-state="open"]) .sidebar-nav-btn {
|
.group\/run-item:has(.sidebar-row-action[data-state="open"])
|
||||||
|
.sidebar-nav-btn {
|
||||||
background-color: var(--nav-surface-hover) !important;
|
background-color: var(--nav-surface-hover) !important;
|
||||||
color: #000 !important;
|
color: #000 !important;
|
||||||
}
|
}
|
||||||
|
|
@ -496,9 +500,13 @@
|
||||||
.dark .sidebar-nav-btn[data-active="true"],
|
.dark .sidebar-nav-btn[data-active="true"],
|
||||||
.dark .sidebar-nav-btn[data-state="open"],
|
.dark .sidebar-nav-btn[data-state="open"],
|
||||||
.dark .group\/recent-item:hover .sidebar-nav-btn,
|
.dark .group\/recent-item:hover .sidebar-nav-btn,
|
||||||
.dark .group\/recent-item:has(.sidebar-row-action[data-state="open"]) .sidebar-nav-btn,
|
.dark
|
||||||
|
.group\/recent-item:has(.sidebar-row-action[data-state="open"])
|
||||||
|
.sidebar-nav-btn,
|
||||||
.dark .group\/run-item:hover .sidebar-nav-btn,
|
.dark .group\/run-item:hover .sidebar-nav-btn,
|
||||||
.dark .group\/run-item:has(.sidebar-row-action[data-state="open"]) .sidebar-nav-btn {
|
.dark
|
||||||
|
.group\/run-item:has(.sidebar-row-action[data-state="open"])
|
||||||
|
.sidebar-nav-btn {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,10 +537,88 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sticky-label {
|
.sidebar-sticky-label {
|
||||||
@apply sticky top-0 z-20 rounded-none bg-sidebar pt-0 pb-1.5 pl-[18px] pr-4 text-[13px]! font-medium normal-case tracking-[0.04em] text-nav-fg-muted focus-visible:ring-0! focus-visible:outline-none shadow-[0_-8px_0_0_var(--sidebar)] transition-shadow duration-150;
|
@apply rounded-none bg-sidebar pt-0 pb-1.5 pl-[18px] pr-4 text-[13px]! font-medium normal-case tracking-[0.04em] text-nav-fg-muted focus-visible:ring-0! focus-visible:outline-none;
|
||||||
}
|
}
|
||||||
.sidebar-sticky-label.is-scrolled {
|
|
||||||
@apply shadow-[0_-8px_0_0_var(--sidebar),0_0.5px_0_0_var(--sidebar-border)];
|
.app-sidebar-top-actions {
|
||||||
|
position: relative;
|
||||||
|
z-index: 25;
|
||||||
|
background: var(--sidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-top-actions::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -16px;
|
||||||
|
height: 16px;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
background: linear-gradient(to bottom, var(--sidebar) 0, transparent 100%);
|
||||||
|
transition: opacity var(--duration-fast) ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-top-actions.is-scrolled::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-scroll-region {
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 100% 100%;
|
||||||
|
transition: mask-image var(--duration-fast) ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-scroll-region.is-scrolled {
|
||||||
|
mask-image: linear-gradient(to bottom, transparent 0, #000 18px, #000 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-scroll-region.can-scroll-down {
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
#000 0,
|
||||||
|
#000 calc(100% - 18px),
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-scroll-region.is-scrolled.can-scroll-down {
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0,
|
||||||
|
#000 18px,
|
||||||
|
#000 calc(100% - 18px),
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
background: var(--sidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-footer.can-scroll-down::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: -24px;
|
||||||
|
height: 24px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(to bottom, transparent 0, var(--sidebar) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Neutral panel input surface — sidesteps the green cast on
|
/* Neutral panel input surface — sidesteps the green cast on
|
||||||
|
|
@ -711,6 +797,9 @@
|
||||||
.dark .app-user-menu [data-slot="dropdown-menu-item"] {
|
.dark .app-user-menu [data-slot="dropdown-menu-item"] {
|
||||||
letter-spacing: 0.03em;
|
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 {
|
.app-user-menu [data-slot="dropdown-menu-item"] svg {
|
||||||
width: 19px !important;
|
width: 19px !important;
|
||||||
height: 19px !important;
|
height: 19px !important;
|
||||||
|
|
@ -737,13 +826,17 @@
|
||||||
--destructive: #ed7878;
|
--destructive: #ed7878;
|
||||||
}
|
}
|
||||||
.app-user-menu [data-slot="dropdown-menu-item"][data-variant="destructive"],
|
.app-user-menu [data-slot="dropdown-menu-item"][data-variant="destructive"],
|
||||||
.app-user-menu [data-slot="dropdown-menu-item"][data-variant="destructive"]:focus {
|
.app-user-menu
|
||||||
|
[data-slot="dropdown-menu-item"][data-variant="destructive"]:focus {
|
||||||
color: var(--destructive);
|
color: var(--destructive);
|
||||||
}
|
}
|
||||||
.app-user-menu [data-slot="dropdown-menu-item"][data-variant="destructive"]:focus {
|
.app-user-menu
|
||||||
|
[data-slot="dropdown-menu-item"][data-variant="destructive"]:focus {
|
||||||
background-color: color-mix(in oklab, var(--destructive) 10%, transparent);
|
background-color: color-mix(in oklab, var(--destructive) 10%, transparent);
|
||||||
}
|
}
|
||||||
.app-user-menu [data-slot="dropdown-menu-item"][data-variant="destructive"]:focus * {
|
.app-user-menu
|
||||||
|
[data-slot="dropdown-menu-item"][data-variant="destructive"]:focus
|
||||||
|
* {
|
||||||
color: var(--destructive) !important;
|
color: var(--destructive) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -767,10 +860,9 @@
|
||||||
--menu-soft-blur: 28px;
|
--menu-soft-blur: 28px;
|
||||||
--menu-soft-spread: -6px;
|
--menu-soft-spread: -6px;
|
||||||
@apply bg-popover text-popover-foreground;
|
@apply bg-popover text-popover-foreground;
|
||||||
box-shadow:
|
box-shadow: inset 0 0 0 1px var(--menu-soft-edge), 0
|
||||||
inset 0 0 0 1px var(--menu-soft-edge),
|
var(--menu-soft-offset-y) var(--menu-soft-blur) var(--menu-soft-spread)
|
||||||
0 var(--menu-soft-offset-y) var(--menu-soft-blur)
|
var(--menu-soft-shadow);
|
||||||
var(--menu-soft-spread) var(--menu-soft-shadow);
|
|
||||||
}
|
}
|
||||||
.menu-soft-surface-up {
|
.menu-soft-surface-up {
|
||||||
--menu-soft-offset-y: -6px;
|
--menu-soft-offset-y: -6px;
|
||||||
|
|
@ -782,35 +874,127 @@
|
||||||
--menu-soft-shadow: rgba(0, 0, 0, 0.28);
|
--menu-soft-shadow: rgba(0, 0, 0, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-user-menu.menu-soft-surface,
|
||||||
|
.app-user-menu.menu-soft-surface-up {
|
||||||
|
box-shadow: 0 2px 8px -2px rgba(27, 27, 31, 0.16);
|
||||||
|
}
|
||||||
|
.dark .app-user-menu.menu-soft-surface,
|
||||||
|
.dark .app-user-menu.menu-soft-surface-up {
|
||||||
|
box-shadow: 0 3px 12px -6px rgba(0, 0, 0, 0.46);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-selector-menu button:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-composer-surface {
|
.chat-composer-surface {
|
||||||
@apply relative flex w-full flex-col rounded-[24px] bg-background dark:bg-card px-1 pt-2 outline-none transition-shadow;
|
@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);
|
font-family: var(--font-sans);
|
||||||
border: 1px solid oklch(0.93 0 0 / 1);
|
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
box-shadow:
|
box-shadow: 0px 2px 8px -2px rgba(0, 0, 0, 0.16);
|
||||||
0 1px 2px oklch(0 0 0 / 0.04),
|
|
||||||
0 6px 14px oklch(0 0 0 / 0.05),
|
|
||||||
0 18px 40px oklch(0 0 0 / 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .chat-composer-surface {
|
.dark .chat-composer-surface {
|
||||||
border-color: #34363a;
|
background-color: #2a2a2c;
|
||||||
box-shadow: 0 -6px 36px -14px rgba(0, 0, 0, 0.15);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-pill-btn {
|
.composer-pill-btn {
|
||||||
@apply flex items-center gap-1.5 rounded-full px-1.5 py-1.5 text-[13px] font-medium text-muted-foreground/70 transition-colors hover:bg-primary/10 dark:hover:bg-white/[0.08] disabled:cursor-not-allowed disabled:opacity-40;
|
@apply flex h-6 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-primary/10 dark:hover:bg-primary/15 disabled:cursor-not-allowed disabled:opacity-40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-pill-btn:is(:hover, :focus-visible) {
|
||||||
|
background-color: color-mix(in oklch, var(--primary) 10%, transparent);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .composer-pill-btn:is(:hover, :focus-visible) {
|
||||||
|
background-color: color-mix(in oklch, var(--primary) 15%, transparent);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
.composer-pill-btn[data-active="true"] {
|
.composer-pill-btn[data-active="true"] {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-pill-btn > span {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-pill-btn svg {
|
||||||
|
width: 16px !important;
|
||||||
|
height: 16px !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-pill-close {
|
||||||
|
display: none;
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-pill-btn[data-active="true"]:not(:disabled):is(
|
||||||
|
:hover,
|
||||||
|
:focus-visible
|
||||||
|
) {
|
||||||
|
background-color: color-mix(in oklch, var(--primary) 10%, transparent);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark
|
||||||
|
.composer-pill-btn[data-active="true"]:not(:disabled):is(
|
||||||
|
:hover,
|
||||||
|
:focus-visible
|
||||||
|
) {
|
||||||
|
background-color: color-mix(in oklch, var(--primary) 15%, transparent);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-pill-btn[data-active="true"]:not(:disabled):is(
|
||||||
|
:hover,
|
||||||
|
:focus-visible
|
||||||
|
)
|
||||||
|
> svg:not(.composer-pill-close) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-pill-btn[data-active="true"]:not(:disabled):is(
|
||||||
|
:hover,
|
||||||
|
:focus-visible
|
||||||
|
)
|
||||||
|
.composer-pill-close {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-send-enter {
|
||||||
|
animation: composer-send-enter 120ms var(--ease-out-quart) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes composer-send-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.composer-send-enter {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.composer-input {
|
.composer-input {
|
||||||
@apply mt-2 mb-1 mx-3 min-h-12 w-[calc(100%-1.5rem)] resize-none overflow-y-auto bg-transparent pl-5 pr-4 pt-2 pb-3 text-sm font-[450] outline-none placeholder:text-muted-foreground focus-visible:ring-0;
|
@apply mb-1 min-h-10 w-full resize-none overflow-y-auto bg-transparent px-1.5 py-2 text-[15px] font-[450] leading-6 outline-none placeholder:text-muted-foreground focus-visible:ring-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-action-wrapper {
|
.composer-action-wrapper {
|
||||||
@apply relative mx-2 mb-2 flex items-center justify-between;
|
@apply relative mt-1 flex items-center justify-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-footer-note {
|
.composer-footer-note {
|
||||||
|
|
@ -818,6 +1002,252 @@
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pill composer; own classes so compare-mode keeps its stacked layout. */
|
||||||
|
.unsloth-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);
|
||||||
|
background-clip: padding-box;
|
||||||
|
box-shadow: 0px 2px 8px -2px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-surface:focus-within {
|
||||||
|
box-shadow: 0px 2px 10px -2px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .unsloth-composer-surface,
|
||||||
|
.dark .unsloth-composer-surface:focus-within {
|
||||||
|
background-color: #2a2a2c;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the expand/collapse width swap instant. A transition on width (e.g.
|
||||||
|
the reduced-motion blanket rule) makes getComputedStyle().width lag a
|
||||||
|
frame, so autosize measures the stale width and leaves a stray blank row. */
|
||||||
|
.unsloth-composer-line,
|
||||||
|
.unsloth-composer-line .unsloth-composer-input,
|
||||||
|
.unsloth-composer-left {
|
||||||
|
transition-property: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Composer row: one centered line when empty; two rows (input over controls)
|
||||||
|
when filled, so the textarea never remounts. */
|
||||||
|
.unsloth-composer-line {
|
||||||
|
@apply flex w-full flex-wrap items-center gap-0.5 px-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-left {
|
||||||
|
@apply flex shrink-0 items-center gap-1;
|
||||||
|
order: 1;
|
||||||
|
/* Pull the plus button closer to the composer edge. */
|
||||||
|
margin-left: -0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-line .unsloth-composer-input {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-line .aui-composer-action-wrapper {
|
||||||
|
order: 3;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-line[data-expanded="true"] .unsloth-composer-input {
|
||||||
|
order: 1;
|
||||||
|
flex-basis: 100%;
|
||||||
|
width: 100%;
|
||||||
|
/* Sits close to the left edge, near the plus. */
|
||||||
|
padding-left: 0.375rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-line[data-expanded="true"] .unsloth-composer-left {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty (placeholder shown): clamp back to one row. max-height beats the
|
||||||
|
autosize textarea's inline !important height, so a cleared message leaves
|
||||||
|
no stale tall box or stray scrollbar. */
|
||||||
|
.unsloth-composer-input:placeholder-shown {
|
||||||
|
max-height: 40px !important;
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-input {
|
||||||
|
@apply min-h-[40px] min-w-0 flex-1 resize-none overflow-y-auto bg-transparent pl-0.5 pr-2 py-2 text-[15px] font-[450] leading-6 outline-none placeholder:text-muted-foreground focus-visible:ring-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-plus {
|
||||||
|
@apply flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-foreground transition-colors hover:bg-muted-foreground/15 disabled:cursor-not-allowed disabled:opacity-40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-plus svg {
|
||||||
|
/* 250ms reads better than --duration-normal for the plus/x morph. */
|
||||||
|
transition: transform 250ms ease-in-out;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-plus[data-state="open"] {
|
||||||
|
/* Radix's modal menu sets body pointer-events:none; re-enable on the open
|
||||||
|
trigger so the cursor and click-to-close work. */
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .unsloth-composer-plus[data-state="open"] {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-plus[data-state="open"]:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .unsloth-composer-plus[data-state="open"]:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-composer-plus[data-state="open"] svg {
|
||||||
|
transform: rotate(35deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set Hellix explicitly; .aui-thread-root resets --font-heading to sans. */
|
||||||
|
.unsloth-welcome-title {
|
||||||
|
font-family: "Hellix", "Space Grotesk Variable", var(--font-sans);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 pl-1 pr-2 py-0 text-[14px] font-medium leading-6 text-muted-foreground/75 transition-colors hover:bg-primary/10 dark:hover:bg-primary/15 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"] {
|
||||||
|
color: var(--primary);
|
||||||
|
/* See .unsloth-composer-plus: re-enable pointer-events on the open trigger. */
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-thinking-pill[data-state="open"] {
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
color: var(--foreground);
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-thinking-pill[data-active="true"][data-state="open"] {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .unsloth-thinking-pill[data-state="open"] {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .unsloth-thinking-pill[data-active="true"][data-state="open"] {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-thinking-pill:is(:hover, :focus-visible) {
|
||||||
|
background-color: color-mix(in oklch, var(--primary) 10%, transparent);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .unsloth-thinking-pill:is(:hover, :focus-visible) {
|
||||||
|
background-color: color-mix(in oklch, var(--primary) 15%, transparent);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-thinking-pill[data-active="true"]:is(:hover, :focus-visible) {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smaller tick for selected Thinking options. */
|
||||||
|
.unsloth-tick {
|
||||||
|
width: 0.8rem !important;
|
||||||
|
height: 0.8rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Soft elevation; [data-slot] outranks the component ring-1, dropping the border. */
|
||||||
|
.unsloth-plus-menu[data-slot] {
|
||||||
|
/* Pin radius so dark matches light (rounded-lg resolves smaller in dark). */
|
||||||
|
border-radius: 18px;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
box-shadow: 0 1px 2px oklch(0 0 0 / 0.04), 0 6px 18px oklch(0 0 0 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .unsloth-plus-menu[data-slot] {
|
||||||
|
background-color: #2a2a2c;
|
||||||
|
/* Shadow tinted to the page bg so it blends, not a dark halo. */
|
||||||
|
box-shadow: 0 4px 14px var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact items; also applied to portaled sub-content. */
|
||||||
|
.unsloth-plus-menu
|
||||||
|
:is(
|
||||||
|
[data-slot="dropdown-menu-item"],
|
||||||
|
[data-slot="dropdown-menu-sub-trigger"]
|
||||||
|
) {
|
||||||
|
@apply gap-3 pl-4 pr-3 py-2 text-[14px];
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-plus-menu [data-slot="dropdown-menu-label"] {
|
||||||
|
@apply pl-4 pr-3 py-1.5 text-[12px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark hover: the accent nearly matches the surface, so use a clear overlay. */
|
||||||
|
.dark
|
||||||
|
.unsloth-plus-menu
|
||||||
|
:is(
|
||||||
|
[data-slot="dropdown-menu-item"],
|
||||||
|
[data-slot="dropdown-menu-sub-trigger"]
|
||||||
|
):is(
|
||||||
|
:hover,
|
||||||
|
:focus,
|
||||||
|
:focus-visible,
|
||||||
|
[data-highlighted],
|
||||||
|
[data-state="open"]
|
||||||
|
) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsloth-plus-menu
|
||||||
|
:is(
|
||||||
|
[data-slot="dropdown-menu-item"],
|
||||||
|
[data-slot="dropdown-menu-sub-trigger"]
|
||||||
|
)
|
||||||
|
svg {
|
||||||
|
width: 1.05rem;
|
||||||
|
height: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thinking menu: tighter gap; nowrap keeps "Preserve thinking" on one line
|
||||||
|
so the menu sizes to its content. */
|
||||||
|
.unsloth-thinking-menu
|
||||||
|
:is(
|
||||||
|
[data-slot="dropdown-menu-item"],
|
||||||
|
[data-slot="dropdown-menu-sub-trigger"]
|
||||||
|
) {
|
||||||
|
@apply gap-1.5 pl-2.5 pr-2.5;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* Fine-tuning Studio: equal default height, expandable when needed (md+) */
|
/* Fine-tuning Studio: equal default height, expandable when needed (md+) */
|
||||||
.min-h-studio-config-column {
|
.min-h-studio-config-column {
|
||||||
@apply md:min-h-[470px];
|
@apply md:min-h-[470px];
|
||||||
|
|
@ -915,9 +1345,24 @@
|
||||||
.aui-assistant-message-content > *:last-child,
|
.aui-assistant-message-content > *:last-child,
|
||||||
.aui-assistant-message-content > *:last-child > *:last-child,
|
.aui-assistant-message-content > *:last-child > *:last-child,
|
||||||
.aui-assistant-message-content > *:last-child > *:last-child > *:last-child,
|
.aui-assistant-message-content > *:last-child > *:last-child > *:last-child,
|
||||||
.aui-assistant-message-content > *:last-child > *:last-child > *:last-child > *:last-child,
|
.aui-assistant-message-content
|
||||||
.aui-assistant-message-content > *:last-child > *:last-child > *:last-child > *:last-child > *:last-child,
|
> *:last-child
|
||||||
.aui-assistant-message-content > *:last-child > *:last-child > *:last-child > *:last-child > *:last-child > *:last-child {
|
> *:last-child
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child,
|
||||||
|
.aui-assistant-message-content
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child,
|
||||||
|
.aui-assistant-message-content
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child
|
||||||
|
> *:last-child {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -945,13 +1390,19 @@
|
||||||
font-family: "Fira Code", ui-monospace, monospace;
|
font-family: "Fira Code", ui-monospace, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aui-thread-root :where(p, li, td, th, blockquote, h1, h2, h3, h4, h5, h6) code {
|
.aui-thread-root
|
||||||
|
:where(p, li, td, th, blockquote, h1, h2, h3, h4, h5, h6)
|
||||||
|
code {
|
||||||
font-family: "Fira Code", ui-monospace, monospace;
|
font-family: "Fira Code", ui-monospace, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Align fenced code blocks with the main chat column even when nested in lists. */
|
/* Align fenced code blocks with the main chat column even when nested in lists. */
|
||||||
.aui-thread-root [data-streamdown="list-item"] > [data-streamdown="code-block"],
|
.aui-thread-root
|
||||||
.aui-thread-root [data-streamdown="list-item"] [data-streamdown="code-block"] {
|
[data-streamdown="list-item"]
|
||||||
|
> [data-streamdown="code-block"],
|
||||||
|
.aui-thread-root
|
||||||
|
[data-streamdown="list-item"]
|
||||||
|
[data-streamdown="code-block"] {
|
||||||
margin-left: -1.25rem;
|
margin-left: -1.25rem;
|
||||||
width: calc(100% + 1.25rem);
|
width: calc(100% + 1.25rem);
|
||||||
max-width: calc(100% + 1.25rem);
|
max-width: calc(100% + 1.25rem);
|
||||||
|
|
@ -996,15 +1447,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lighter shadow + tighter vertical padding than Sonner's defaults; !important because Sonner injects its base rules at runtime. */
|
/* No border line; same drop shadow as the composer (.unsloth-composer-surface).
|
||||||
[data-sonner-toast][data-styled='true'] {
|
!important because Sonner injects its base rules at runtime. */
|
||||||
|
[data-sonner-toast][data-styled="true"] {
|
||||||
padding: 10px 16px !important;
|
padding: 10px 16px !important;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08) !important;
|
box-shadow: 0 2px 8px -2px rgba(27, 27, 31, 0.16) !important;
|
||||||
|
/* Pin to the light --radius so dark mode (smaller --radius) matches. */
|
||||||
|
border-radius: 1.1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Boost shadow on dark surfaces; mirrors .shadow-border / .menu-soft-surface pattern. */
|
/* Match the composer (.unsloth-composer-surface) in dark mode: same surface color, no shadow. */
|
||||||
.dark [data-sonner-toast][data-styled='true'] {
|
.dark [data-sonner-toast][data-styled="true"] {
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important;
|
background-color: #2a2a2c !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selectable toast text; non-selectable toast buttons. */
|
/* Selectable toast text; non-selectable toast buttons. */
|
||||||
|
|
@ -1135,15 +1590,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-sidebar="content"] {
|
[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"] {
|
.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 {
|
[data-sidebar="content"]::-webkit-scrollbar-track {
|
||||||
background: var(--sidebar);
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-sidebar="content"]::-webkit-scrollbar-thumb {
|
[data-sidebar="content"]::-webkit-scrollbar-thumb {
|
||||||
|
|
@ -1177,15 +1632,21 @@
|
||||||
top: 8px !important;
|
top: 8px !important;
|
||||||
background: var(--popover) !important;
|
background: var(--popover) !important;
|
||||||
color: var(--popover-foreground) !important;
|
color: var(--popover-foreground) !important;
|
||||||
border-color: var(--border) !important;
|
border-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep the (borderless) close button blended with the dark toast surface. */
|
||||||
|
.dark [data-sonner-toast][data-styled="true"] [data-close-button] {
|
||||||
|
background: #2a2a2c !important;
|
||||||
|
}
|
||||||
|
|
||||||
[data-sonner-toast][data-styled="true"] [data-close-button] svg {
|
[data-sonner-toast][data-styled="true"] [data-close-button] svg {
|
||||||
stroke-width: 2.25;
|
stroke-width: 2.25;
|
||||||
}
|
}
|
||||||
[data-sonner-toast][data-styled="true"]:hover [data-close-button]:hover {
|
[data-sonner-toast][data-styled="true"]:hover [data-close-button]:hover {
|
||||||
background: var(--muted) !important;
|
background: var(--muted) !important;
|
||||||
color: var(--popover-foreground) !important;
|
color: var(--popover-foreground) !important;
|
||||||
border-color: var(--border) !important;
|
border-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.generated-image-loading-card {
|
.generated-image-loading-card {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue