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";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { usePlatformStore } from "@/config/env";
|
||||
import { isCustomProviderType } from "@/features/chat/external-providers";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
ArrowDown01Icon,
|
||||
|
|
@ -32,6 +31,22 @@ import type {
|
|||
import { HubModelPicker, LoraModelPicker } from "./model-selector/pickers";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
const CUSTOM_PROVIDER_TYPES_WITH_FALLBACK_LOGO = new Set([
|
||||
"custom",
|
||||
"llama_cpp",
|
||||
"vllm",
|
||||
"ollama",
|
||||
]);
|
||||
|
||||
function isCustomProviderTypeWithFallbackLogo(
|
||||
providerType: string | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
providerType !== undefined &&
|
||||
CUSTOM_PROVIDER_TYPES_WITH_FALLBACK_LOGO.has(providerType)
|
||||
);
|
||||
}
|
||||
|
||||
const PROVIDER_LOGO_EXT: Record<string, "svg" | "png" | "jpg"> = {
|
||||
openai: "svg",
|
||||
mistral: "svg",
|
||||
|
|
@ -64,7 +79,7 @@ function ExternalProviderLogo({
|
|||
title?: string;
|
||||
}) {
|
||||
const src = providerLogoSrc(providerType);
|
||||
if (!src && isCustomProviderType(providerType)) {
|
||||
if (!src && isCustomProviderTypeWithFallbackLogo(providerType)) {
|
||||
return (
|
||||
<span title={title} aria-hidden={true} className="inline-flex shrink-0">
|
||||
<HugeiconsIcon
|
||||
|
|
@ -146,10 +161,11 @@ function ModelSelectorTrigger({
|
|||
type="button"
|
||||
data-tour={dataTour}
|
||||
className={cn(
|
||||
"flex min-w-0 items-center gap-2 transition-colors",
|
||||
"flex min-w-0 cursor-pointer items-center gap-2 transition-colors",
|
||||
variant === "outline" &&
|
||||
"rounded-[10px] border border-border/60 hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
|
||||
variant === "ghost" && "rounded-[10px] hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
|
||||
"rounded-[10px] border border-border/60 hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
|
||||
variant === "ghost" &&
|
||||
"rounded-[10px] hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
|
||||
variant === "muted" && "rounded-[10px] bg-muted hover:bg-muted/80",
|
||||
size === "sm" && "h-8 px-3 text-xs",
|
||||
size === "default" && "h-9 px-3.5 text-sm",
|
||||
|
|
@ -161,7 +177,9 @@ function ModelSelectorTrigger({
|
|||
<span className="size-2 shrink-0 rounded-full bg-emerald-500" />
|
||||
)}
|
||||
{currentModel?.icon ? (
|
||||
<span className="flex shrink-0 items-center">{currentModel.icon}</span>
|
||||
<span className="flex shrink-0 items-center">
|
||||
{currentModel.icon}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="flex min-w-0 flex-1 items-baseline">
|
||||
<span className="min-w-0 flex flex-1 items-baseline truncate font-heading text-[16px] font-medium leading-tight text-black dark:text-white">
|
||||
|
|
@ -228,7 +246,10 @@ function ModelSelectorContent({
|
|||
const chatOnly = usePlatformStore((s) => s.isChatOnly());
|
||||
const hasExternal = externalModels.length > 0;
|
||||
const chatOnlyTabsDefault = useMemo(
|
||||
() => (value && externalModels.some((model) => model.id === value) ? "external" : "hub"),
|
||||
() =>
|
||||
value && externalModels.some((model) => model.id === value)
|
||||
? "external"
|
||||
: "hub",
|
||||
[externalModels, value],
|
||||
);
|
||||
const studioTabsDefault = useMemo((): "hub" | "lora" | "external" => {
|
||||
|
|
@ -246,7 +267,7 @@ function ModelSelectorContent({
|
|||
align="start"
|
||||
data-tour={dataTour}
|
||||
className={cn(
|
||||
"menu-soft-surface ring-0 w-[min(440px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] min-w-0 gap-0 p-2",
|
||||
"model-selector-menu menu-soft-surface ring-0 w-[min(440px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] min-w-0 gap-0 p-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
@ -258,7 +279,12 @@ function ModelSelectorContent({
|
|||
<TabsTrigger value="external">Connected</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="hub" className="m-0">
|
||||
<HubModelPicker models={models} value={value} onSelect={onSelect} onFoldersChange={onFoldersChange} />
|
||||
<HubModelPicker
|
||||
models={models}
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
onFoldersChange={onFoldersChange}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="external" className="m-0">
|
||||
<ExternalModelPicker
|
||||
|
|
@ -269,18 +295,30 @@ function ModelSelectorContent({
|
|||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<HubModelPicker models={models} value={value} onSelect={onSelect} onFoldersChange={onFoldersChange} />
|
||||
<HubModelPicker
|
||||
models={models}
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
onFoldersChange={onFoldersChange}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Tabs defaultValue={studioTabsDefault} className="w-full">
|
||||
<TabsList className="mb-2 w-full">
|
||||
<TabsTrigger value="hub">Hub models</TabsTrigger>
|
||||
<TabsTrigger value="lora">Fine-tuned</TabsTrigger>
|
||||
{hasExternal ? <TabsTrigger value="external">Connected</TabsTrigger> : null}
|
||||
{hasExternal ? (
|
||||
<TabsTrigger value="external">Connected</TabsTrigger>
|
||||
) : null}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="hub" className="m-0">
|
||||
<HubModelPicker models={models} value={value} onSelect={onSelect} onFoldersChange={onFoldersChange} />
|
||||
<HubModelPicker
|
||||
models={models}
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
onFoldersChange={onFoldersChange}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="lora" className="m-0">
|
||||
|
|
@ -420,7 +458,9 @@ export function ModelSelector({
|
|||
const found = optionById.get(selected);
|
||||
if (activeGgufVariant) {
|
||||
const desc = `GGUF · ${activeGgufVariant}`;
|
||||
return found ? { ...found, description: desc } : { id: selected, name: selected, description: desc };
|
||||
return found
|
||||
? { ...found, description: desc }
|
||||
: { id: selected, name: selected, description: desc };
|
||||
}
|
||||
return found ?? { id: selected, name: selected };
|
||||
}, [selected, optionById, activeGgufVariant]);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -30,7 +30,7 @@ import { LayoutAlignLeftIcon } from "@hugeicons/core-free-icons"
|
|||
|
||||
const noop = () => {}
|
||||
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
|
|
@ -305,9 +305,8 @@ function Sidebar({
|
|||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
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",
|
||||
hasPinMode && "ring-1 ring-sidebar-border/60",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--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)",
|
||||
// Pin close button to the top-right corner inside the toast.
|
||||
// Overrides sonner's default left placement and outside-corner
|
||||
|
|
|
|||
|
|
@ -12,13 +12,15 @@ import {
|
|||
import { Thread } from "@/components/assistant-ui/thread";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { NativeModelChip } from "@/features/native-intents/components/native-model-chip";
|
||||
import { NativeModelDropOverlay } from "@/features/native-intents/components/native-model-drop-overlay";
|
||||
import { useNativeIntentStore } from "@/features/native-intents/store";
|
||||
import type { NativeIntent } from "@/features/native-intents/types";
|
||||
import { useChooseNativeModel } from "@/features/native-intents/use-native-dialogs";
|
||||
import { useNativeModelDrop } from "@/features/native-intents/use-native-drop";
|
||||
import { useNativePathLeasesSupported } from "@/features/native-intents/use-native-readiness";
|
||||
import {
|
||||
NativeModelChip,
|
||||
NativeModelDropOverlay,
|
||||
type NativeIntent,
|
||||
useChooseNativeModel,
|
||||
useNativeIntentStore,
|
||||
useNativeModelDrop,
|
||||
useNativePathLeasesSupported,
|
||||
} from "@/features/native-intents";
|
||||
import { GuidedTour, useGuidedTourController } from "@/features/tour";
|
||||
import { isTauri } from "@/lib/api-base";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -85,6 +87,7 @@ import {
|
|||
listStoredChatMessages,
|
||||
listStoredChatThreads,
|
||||
} from "./utils/chat-history-storage";
|
||||
import { createCompareId } from "./utils/compare-id";
|
||||
|
||||
type LoraCandidate = {
|
||||
id: string;
|
||||
|
|
@ -156,7 +159,14 @@ function messageHasImage(message: MessageRecord): boolean {
|
|||
const SingleContent = memo(function SingleContent({
|
||||
threadId,
|
||||
newThreadNonce,
|
||||
}: { threadId?: string; newThreadNonce?: string }): ReactElement {
|
||||
canCompare,
|
||||
onEnterCompare,
|
||||
}: {
|
||||
threadId?: string;
|
||||
newThreadNonce?: string;
|
||||
canCompare?: boolean;
|
||||
onEnterCompare?: () => void;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<ChatRuntimeProvider
|
||||
modelType="base"
|
||||
|
|
@ -164,7 +174,12 @@ const SingleContent = memo(function SingleContent({
|
|||
newThreadNonce={newThreadNonce}
|
||||
>
|
||||
<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>
|
||||
</ChatRuntimeProvider>
|
||||
);
|
||||
|
|
@ -207,6 +222,7 @@ const CompareContent = memo(function CompareContent({
|
|||
onFoldersChange,
|
||||
onModelsChange,
|
||||
deleteDisabled,
|
||||
onExitCompare,
|
||||
}: {
|
||||
pairId: string;
|
||||
models: ModelOption[];
|
||||
|
|
@ -214,11 +230,12 @@ const CompareContent = memo(function CompareContent({
|
|||
onFoldersChange?: () => void;
|
||||
onModelsChange?: (deletedModel?: DeletedModelRef) => void;
|
||||
deleteDisabled?: boolean;
|
||||
onExitCompare?: () => void;
|
||||
}): ReactElement {
|
||||
const isLoraCompare = useIsLoraCompare();
|
||||
|
||||
return isLoraCompare ? (
|
||||
<LoraCompareContent pairId={pairId} />
|
||||
<LoraCompareContent pairId={pairId} onExitCompare={onExitCompare} />
|
||||
) : (
|
||||
<GeneralCompareContent
|
||||
pairId={pairId}
|
||||
|
|
@ -227,6 +244,7 @@ const CompareContent = memo(function CompareContent({
|
|||
onFoldersChange={onFoldersChange}
|
||||
onModelsChange={onModelsChange}
|
||||
deleteDisabled={deleteDisabled}
|
||||
onExitCompare={onExitCompare}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -311,7 +329,7 @@ function CompareShell({
|
|||
{children}
|
||||
</div>
|
||||
<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">
|
||||
LLMs can make mistakes. Double-check responses.
|
||||
</p>
|
||||
|
|
@ -324,22 +342,25 @@ function CompareShell({
|
|||
/** Fast path: same model, adapter on/off, simultaneous generation. */
|
||||
const LoraCompareContent = memo(function LoraCompareContent({
|
||||
pairId,
|
||||
}: { pairId: string }): ReactElement {
|
||||
onExitCompare,
|
||||
}: { pairId: string; onExitCompare?: () => void }): ReactElement {
|
||||
const handlesRef = useRef<Record<string, CompareHandle>>({});
|
||||
const [baseThreadId, setBaseThreadId] = useState<string>();
|
||||
const [loraThreadId, setLoraThreadId] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
listStoredChatThreads({ pairId }).then((threads) => {
|
||||
if (!isActive) return;
|
||||
setBaseThreadId(threads.find((t) => t.modelType === "base")?.id);
|
||||
setLoraThreadId(threads.find((t) => t.modelType === "lora")?.id);
|
||||
}).catch((error) => {
|
||||
if (!isExpectedBackgroundChatStorageError(error)) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
listStoredChatThreads({ pairId })
|
||||
.then((threads) => {
|
||||
if (!isActive) return;
|
||||
setBaseThreadId(threads.find((t) => t.modelType === "base")?.id);
|
||||
setLoraThreadId(threads.find((t) => t.modelType === "lora")?.id);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!isExpectedBackgroundChatStorageError(error)) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
|
|
@ -348,7 +369,9 @@ const LoraCompareContent = memo(function LoraCompareContent({
|
|||
return (
|
||||
<CompareShell
|
||||
handlesRef={handlesRef}
|
||||
composer={<SharedComposer handlesRef={handlesRef} />}
|
||||
composer={
|
||||
<SharedComposer handlesRef={handlesRef} onExitCompare={onExitCompare} />
|
||||
}
|
||||
>
|
||||
<>
|
||||
<ComparePane
|
||||
|
|
@ -441,6 +464,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
onFoldersChange,
|
||||
onModelsChange,
|
||||
deleteDisabled,
|
||||
onExitCompare,
|
||||
}: {
|
||||
pairId: string;
|
||||
models: ModelOption[];
|
||||
|
|
@ -448,6 +472,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
onFoldersChange?: () => void;
|
||||
onModelsChange?: (deletedModel?: DeletedModelRef) => void;
|
||||
deleteDisabled?: boolean;
|
||||
onExitCompare?: () => void;
|
||||
}): ReactElement {
|
||||
const handlesRef = useRef<Record<string, CompareHandle>>({});
|
||||
const [model1ThreadId, setModel1ThreadId] = useState<string>();
|
||||
|
|
@ -480,21 +505,25 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
listStoredChatThreads({ pairId }).then((threads) => {
|
||||
if (!isActive) return;
|
||||
setModel1ThreadId(
|
||||
threads.find((t) => t.modelType === "model1" || t.modelType === "base")
|
||||
?.id,
|
||||
);
|
||||
setModel2ThreadId(
|
||||
threads.find((t) => t.modelType === "model2" || t.modelType === "lora")
|
||||
?.id,
|
||||
);
|
||||
}).catch((error) => {
|
||||
if (!isExpectedBackgroundChatStorageError(error)) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
listStoredChatThreads({ pairId })
|
||||
.then((threads) => {
|
||||
if (!isActive) return;
|
||||
setModel1ThreadId(
|
||||
threads.find(
|
||||
(t) => t.modelType === "model1" || t.modelType === "base",
|
||||
)?.id,
|
||||
);
|
||||
setModel2ThreadId(
|
||||
threads.find(
|
||||
(t) => t.modelType === "model2" || t.modelType === "lora",
|
||||
)?.id,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!isExpectedBackgroundChatStorageError(error)) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
|
|
@ -508,6 +537,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
|
|||
handlesRef={handlesRef}
|
||||
model1={model1}
|
||||
model2={model2}
|
||||
onExitCompare={onExitCompare}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
|
@ -617,7 +647,9 @@ export function ChatPage(): ReactElement {
|
|||
|
||||
const [modelSelectorOpen, setModelSelectorOpen] = 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 setInferenceParams = useChatRuntimeStore((state) => state.setParams);
|
||||
const activeGgufVariant = useChatRuntimeStore(
|
||||
|
|
@ -647,8 +679,7 @@ export function ChatPage(): ReactElement {
|
|||
} = useChatModelRuntime();
|
||||
const prevConnectionsEnabledRef = useRef(connectionsEnabled);
|
||||
useEffect(() => {
|
||||
const turnedOff =
|
||||
prevConnectionsEnabledRef.current && !connectionsEnabled;
|
||||
const turnedOff = prevConnectionsEnabledRef.current && !connectionsEnabled;
|
||||
if (!connectionsEnabled && isExternalModelId(inferenceParams.checkpoint)) {
|
||||
clearCheckpoint();
|
||||
if (turnedOff) {
|
||||
|
|
@ -658,11 +689,7 @@ export function ChatPage(): ReactElement {
|
|||
}
|
||||
}
|
||||
prevConnectionsEnabledRef.current = connectionsEnabled;
|
||||
}, [
|
||||
clearCheckpoint,
|
||||
connectionsEnabled,
|
||||
inferenceParams.checkpoint,
|
||||
]);
|
||||
}, [clearCheckpoint, connectionsEnabled, inferenceParams.checkpoint]);
|
||||
const pendingNativeModelIntent = useNativeIntentStore(
|
||||
(state) => state.pendingModelIntent,
|
||||
);
|
||||
|
|
@ -681,17 +708,19 @@ export function ChatPage(): ReactElement {
|
|||
const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled);
|
||||
const reasoningStyle = useChatRuntimeStore((s) => s.reasoningStyle);
|
||||
const reasoningEffort = useChatRuntimeStore((s) => s.reasoningEffort);
|
||||
const supportsReasoningOff = useChatRuntimeStore((s) => s.supportsReasoningOff);
|
||||
const supportsReasoningOff = useChatRuntimeStore(
|
||||
(s) => s.supportsReasoningOff,
|
||||
);
|
||||
const activeExternalProvider = useMemo(() => {
|
||||
const selection = parseExternalModelId(inferenceParams.checkpoint);
|
||||
if (!selection) return null;
|
||||
return (
|
||||
externalProvidersForChat.find(
|
||||
(p) => p.id === selection.providerId,
|
||||
) ?? null
|
||||
externalProvidersForChat.find((p) => p.id === selection.providerId) ??
|
||||
null
|
||||
);
|
||||
}, [externalProvidersForChat, inferenceParams.checkpoint]);
|
||||
const activeExternalProviderType = activeExternalProvider?.providerType ?? null;
|
||||
const activeExternalProviderType =
|
||||
activeExternalProvider?.providerType ?? null;
|
||||
const activeProviderCapabilities = useMemo(() => {
|
||||
const selection = parseExternalModelId(inferenceParams.checkpoint);
|
||||
if (!selection) return null;
|
||||
|
|
@ -807,7 +836,9 @@ export function ChatPage(): ReactElement {
|
|||
(provider?.providerType === "anthropic" ||
|
||||
provider?.providerType === "openai");
|
||||
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(
|
||||
CHAT_IMAGE_TOOLS_ENABLED_KEY,
|
||||
);
|
||||
|
|
@ -971,8 +1002,7 @@ export function ChatPage(): ReactElement {
|
|||
selectedProvider?.providerType,
|
||||
selectedExternal?.modelId,
|
||||
{
|
||||
isReasoningProvider:
|
||||
selectedProvider?.isReasoningModel === true,
|
||||
isReasoningProvider: selectedProvider?.isReasoningModel === true,
|
||||
baseUrl: selectedProvider?.baseUrl ?? null,
|
||||
},
|
||||
);
|
||||
|
|
@ -1018,11 +1048,12 @@ export function ChatPage(): ReactElement {
|
|||
selectedExternal?.modelId,
|
||||
selectedProvider?.baseUrl,
|
||||
);
|
||||
const supportsBuiltinCodeExecution = providerSupportsBuiltinCodeExecution(
|
||||
selectedProvider?.providerType,
|
||||
selectedExternal?.modelId,
|
||||
selectedProvider?.baseUrl,
|
||||
);
|
||||
const supportsBuiltinCodeExecution =
|
||||
providerSupportsBuiltinCodeExecution(
|
||||
selectedProvider?.providerType,
|
||||
selectedExternal?.modelId,
|
||||
selectedProvider?.baseUrl,
|
||||
);
|
||||
const supportsBuiltinImageGeneration =
|
||||
providerSupportsBuiltinImageGeneration(
|
||||
selectedProvider?.providerType,
|
||||
|
|
@ -1185,16 +1216,27 @@ export function ChatPage(): ReactElement {
|
|||
const openSidebar = useCallback(() => setPinned(true), [setPinned]);
|
||||
|
||||
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().setContextUsage(null);
|
||||
navigate({ to: "/chat", search: { compare: crypto.randomUUID() } });
|
||||
navigate({ to: "/chat", search: { compare: createCompareId() } });
|
||||
}, [navigate, search]);
|
||||
|
||||
const exitCompare = useCallback(() => {
|
||||
const saved = viewBeforeCompareRef.current;
|
||||
if (!saved) return;
|
||||
viewBeforeCompareRef.current = null;
|
||||
const saved = viewBeforeCompare;
|
||||
// No saved view (compare opened by direct URL); fall back to a fresh chat.
|
||||
if (!saved) {
|
||||
navigate({ to: "/chat" });
|
||||
return;
|
||||
}
|
||||
setViewBeforeCompare(null);
|
||||
navigate({ to: "/chat", search: saved });
|
||||
// Restore usage from the last assistant message, but only if it
|
||||
// matches the currently active checkpoint. Without this guard the
|
||||
|
|
@ -1215,8 +1257,7 @@ export function ChatPage(): ReactElement {
|
|||
if (!usage) return;
|
||||
const store = useChatRuntimeStore.getState();
|
||||
const activeCheckpoint = store.params.checkpoint;
|
||||
const usageModelId =
|
||||
(usage as { modelId?: unknown }).modelId;
|
||||
const usageModelId = (usage as { modelId?: unknown }).modelId;
|
||||
// Scope by modelId when present; reject if no active checkpoint
|
||||
// (model-scoped usage cannot be attributed to "nothing").
|
||||
if (typeof usageModelId === "string" && usageModelId) {
|
||||
|
|
@ -1242,7 +1283,7 @@ export function ChatPage(): ReactElement {
|
|||
}
|
||||
});
|
||||
}
|
||||
}, [navigate]);
|
||||
}, [navigate, viewBeforeCompare]);
|
||||
|
||||
const models = useMemo<ModelOption[]>(
|
||||
() =>
|
||||
|
|
@ -1398,7 +1439,7 @@ export function ChatPage(): ReactElement {
|
|||
if (canceled) return;
|
||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||
useChatRuntimeStore.getState().setContextUsage(null);
|
||||
navigate({ to: "/chat", search: { compare: crypto.randomUUID() } });
|
||||
navigate({ to: "/chat", search: { compare: createCompareId() } });
|
||||
clearHandoff();
|
||||
console.info("[chat-handoff] loaded lora + opened compare");
|
||||
return;
|
||||
|
|
@ -1596,6 +1637,8 @@ export function ChatPage(): ReactElement {
|
|||
key={view.threadId ?? "single"}
|
||||
threadId={view.threadId}
|
||||
newThreadNonce={view.newThreadNonce}
|
||||
canCompare={canCompare}
|
||||
onEnterCompare={enterCompare}
|
||||
/>
|
||||
) : (
|
||||
<CompareContent
|
||||
|
|
@ -1606,6 +1649,7 @@ export function ChatPage(): ReactElement {
|
|||
onFoldersChange={refreshLocalModels}
|
||||
onModelsChange={refreshModelLists}
|
||||
deleteDisabled={modelOperationInProgress}
|
||||
onExitCompare={exitCompare}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ import {
|
|||
LayoutAlignRightIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { ChevronDown, ExternalLink } from "lucide-react";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { Fragment, type ReactNode } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
|
@ -327,11 +327,14 @@ function saveCollapsibleOpen(label: string, open: boolean) {
|
|||
|
||||
function CollapsibleSection({
|
||||
label,
|
||||
labelHref,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
first = false,
|
||||
}: {
|
||||
label: string;
|
||||
/** Optional link for the label (e.g. its PR); chevron still toggles. */
|
||||
labelHref?: string;
|
||||
children?: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
first?: boolean;
|
||||
|
|
@ -341,6 +344,17 @@ function CollapsibleSection({
|
|||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -348,25 +362,42 @@ function CollapsibleSection({
|
|||
"border-t border-black/[0.13] dark:border-white/[0.09]",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = !open;
|
||||
setOpen(next);
|
||||
saveCollapsibleOpen(label, next);
|
||||
}}
|
||||
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",
|
||||
first ? "pt-4 pb-5" : "py-5",
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
{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
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-label={open ? `Collapse ${label}` : `Expand ${label}`}
|
||||
className="flex shrink-0 cursor-pointer items-center leading-none transition-colors hover:text-nav-fg"
|
||||
>
|
||||
<ChevronDown
|
||||
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>}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1345,7 +1376,10 @@ export function ChatSettingsPanel({
|
|||
) : null}
|
||||
|
||||
{!isExternalModel ? (
|
||||
<CollapsibleSection label="MCP Servers">
|
||||
<CollapsibleSection
|
||||
label="MCP Servers"
|
||||
labelHref="https://github.com/unslothai/unsloth/pull/5852"
|
||||
>
|
||||
<McpServersSection />
|
||||
</CollapsibleSection>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export function ModelLoadDescription({
|
|||
size="xs"
|
||||
variant="ghost"
|
||||
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}
|
||||
>
|
||||
Cancel
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
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 {
|
||||
getInferenceStatus,
|
||||
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