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
File diff suppressed because it is too large
Load diff
|
|
@ -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,22 +342,25 @@ 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 })
|
||||||
if (!isActive) return;
|
.then((threads) => {
|
||||||
setBaseThreadId(threads.find((t) => t.modelType === "base")?.id);
|
if (!isActive) return;
|
||||||
setLoraThreadId(threads.find((t) => t.modelType === "lora")?.id);
|
setBaseThreadId(threads.find((t) => t.modelType === "base")?.id);
|
||||||
}).catch((error) => {
|
setLoraThreadId(threads.find((t) => t.modelType === "lora")?.id);
|
||||||
if (!isExpectedBackgroundChatStorageError(error)) {
|
})
|
||||||
throw error;
|
.catch((error) => {
|
||||||
}
|
if (!isExpectedBackgroundChatStorageError(error)) {
|
||||||
});
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
return () => {
|
return () => {
|
||||||
isActive = false;
|
isActive = false;
|
||||||
};
|
};
|
||||||
|
|
@ -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,21 +505,25 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isActive = true;
|
let isActive = true;
|
||||||
listStoredChatThreads({ pairId }).then((threads) => {
|
listStoredChatThreads({ pairId })
|
||||||
if (!isActive) return;
|
.then((threads) => {
|
||||||
setModel1ThreadId(
|
if (!isActive) return;
|
||||||
threads.find((t) => t.modelType === "model1" || t.modelType === "base")
|
setModel1ThreadId(
|
||||||
?.id,
|
threads.find(
|
||||||
);
|
(t) => t.modelType === "model1" || t.modelType === "base",
|
||||||
setModel2ThreadId(
|
)?.id,
|
||||||
threads.find((t) => t.modelType === "model2" || t.modelType === "lora")
|
);
|
||||||
?.id,
|
setModel2ThreadId(
|
||||||
);
|
threads.find(
|
||||||
}).catch((error) => {
|
(t) => t.modelType === "model2" || t.modelType === "lora",
|
||||||
if (!isExpectedBackgroundChatStorageError(error)) {
|
)?.id,
|
||||||
throw error;
|
);
|
||||||
}
|
})
|
||||||
});
|
.catch((error) => {
|
||||||
|
if (!isExpectedBackgroundChatStorageError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
return () => {
|
return () => {
|
||||||
isActive = false;
|
isActive = false;
|
||||||
};
|
};
|
||||||
|
|
@ -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,11 +1048,12 @@ export function ChatPage(): ReactElement {
|
||||||
selectedExternal?.modelId,
|
selectedExternal?.modelId,
|
||||||
selectedProvider?.baseUrl,
|
selectedProvider?.baseUrl,
|
||||||
);
|
);
|
||||||
const supportsBuiltinCodeExecution = providerSupportsBuiltinCodeExecution(
|
const supportsBuiltinCodeExecution =
|
||||||
selectedProvider?.providerType,
|
providerSupportsBuiltinCodeExecution(
|
||||||
selectedExternal?.modelId,
|
selectedProvider?.providerType,
|
||||||
selectedProvider?.baseUrl,
|
selectedExternal?.modelId,
|
||||||
);
|
selectedProvider?.baseUrl,
|
||||||
|
);
|
||||||
const supportsBuiltinImageGeneration =
|
const supportsBuiltinImageGeneration =
|
||||||
providerSupportsBuiltinImageGeneration(
|
providerSupportsBuiltinImageGeneration(
|
||||||
selectedProvider?.providerType,
|
selectedProvider?.providerType,
|
||||||
|
|
@ -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,25 +362,42 @@ function CollapsibleSection({
|
||||||
"border-t border-black/[0.13] dark:border-white/[0.09]",
|
"border-t border-black/[0.13] dark:border-white/[0.09]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
{labelHref ? (
|
||||||
type="button"
|
<div className={headerClasses}>
|
||||||
onClick={() => {
|
<a
|
||||||
const next = !open;
|
href={labelHref}
|
||||||
setOpen(next);
|
target="_blank"
|
||||||
saveCollapsibleOpen(label, next);
|
rel="noopener noreferrer"
|
||||||
}}
|
className="inline-flex cursor-pointer items-center gap-1 leading-none transition-colors hover:text-nav-fg"
|
||||||
className={cn(
|
>
|
||||||
"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",
|
<span>{label}</span>
|
||||||
first ? "pt-4 pb-5" : "py-5",
|
<ExternalLink className="size-3" />
|
||||||
)}
|
</a>
|
||||||
>
|
<button
|
||||||
<span className="leading-none">{label}</span>
|
type="button"
|
||||||
<span className="flex shrink-0 items-center leading-none">
|
onClick={toggle}
|
||||||
<ChevronDown
|
aria-label={open ? `Collapse ${label}` : `Expand ${label}`}
|
||||||
className={cn("size-3.5", open ? "rotate-0" : "-rotate-90")}
|
className="flex shrink-0 cursor-pointer items-center leading-none transition-colors hover:text-nav-fg"
|
||||||
/>
|
>
|
||||||
</span>
|
<ChevronDown
|
||||||
</button>
|
className={cn("size-3.5", open ? "rotate-0" : "-rotate-90")}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggle}
|
||||||
|
className={cn("cursor-pointer hover:text-nav-fg", headerClasses)}
|
||||||
|
>
|
||||||
|
<span className="leading-none">{label}</span>
|
||||||
|
<span className="flex shrink-0 items-center leading-none">
|
||||||
|
<ChevronDown
|
||||||
|
className={cn("size-3.5", open ? "rotate-0" : "-rotate-90")}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</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";
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue