Merge pull request #257 from unslothai/feature/chat-model-switch-warning
feat: chat model switching toast and add image detection logic
This commit is contained in:
commit
c67da8f349
5 changed files with 86 additions and 41 deletions
|
|
@ -10,7 +10,7 @@ export function AppProvider({ children }: AppProviderProps) {
|
|||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="light">
|
||||
{children}
|
||||
<Toaster position="top-right" />
|
||||
<Toaster position="top-right" visibleToasts={2} expand={true} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { GuidedTour, useGuidedTourController } from "@/features/tour";
|
||||
import { ChatSettingsPanel } from "./chat-settings-sheet";
|
||||
import { db } from "./db";
|
||||
|
|
@ -50,7 +51,7 @@ import {
|
|||
SharedComposer,
|
||||
} from "./shared-composer";
|
||||
import { ThreadSidebar } from "./thread-sidebar";
|
||||
import type { ChatView } from "./types";
|
||||
import type { ChatView, MessageRecord } from "./types";
|
||||
import { buildChatTourSteps } from "./tour";
|
||||
|
||||
type LoraCandidate = {
|
||||
|
|
@ -90,6 +91,40 @@ function pickBestLoraForBase(
|
|||
return partial ?? sorted[0];
|
||||
}
|
||||
|
||||
function messageHasImage(message: MessageRecord): boolean {
|
||||
const contentParts = Array.isArray(message.content) ? message.content : [];
|
||||
if (contentParts.some((part) => part.type === "image")) {
|
||||
return true;
|
||||
}
|
||||
const attachments = Array.isArray(message.attachments) ? message.attachments : [];
|
||||
for (const attachment of attachments) {
|
||||
const parts = Array.isArray(attachment.content) ? attachment.content : [];
|
||||
for (const part of parts as Array<{ type?: string }>) {
|
||||
if (part?.type === "image") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function resolveActiveSingleThreadId(view: ChatView): Promise<string | undefined> {
|
||||
if (view.mode !== "single") {
|
||||
return undefined;
|
||||
}
|
||||
if (view.threadId) {
|
||||
return view.threadId;
|
||||
}
|
||||
|
||||
// New-thread flow keeps threadId undefined in local view state.
|
||||
// Fall back to most recent regular base thread.
|
||||
const candidates = await db.threads.where("modelType").equals("base").toArray();
|
||||
const latest = candidates
|
||||
.filter((thread) => !thread.archived && !thread.pairId)
|
||||
.sort((a, b) => b.createdAt - a.createdAt)[0];
|
||||
return latest?.id;
|
||||
}
|
||||
|
||||
const SingleContent = memo(function SingleContent({
|
||||
threadId,
|
||||
newThreadNonce,
|
||||
|
|
@ -304,15 +339,42 @@ export function ChatPage(): ReactElement {
|
|||
const currentCheckpoint =
|
||||
useChatRuntimeStore.getState().params.checkpoint;
|
||||
if (!value || value === currentCheckpoint) return;
|
||||
setView({ mode: "single", newThreadNonce: crypto.randomUUID() });
|
||||
void (async () => {
|
||||
if (currentCheckpoint) {
|
||||
await ejectModel();
|
||||
let switchNote: string | undefined;
|
||||
const activeThreadId = await resolveActiveSingleThreadId(view);
|
||||
if (activeThreadId) {
|
||||
const thread = await db.threads.get(activeThreadId);
|
||||
if (thread?.modelId && thread.modelId !== value) {
|
||||
const messages = await db.messages
|
||||
.where("threadId")
|
||||
.equals(activeThreadId)
|
||||
.toArray();
|
||||
const hasImage = messages.some(messageHasImage);
|
||||
const targetModel = modelsFromStore.find((model) => model.id === value);
|
||||
const nonVisionWithImages = hasImage && targetModel?.isVision === false;
|
||||
|
||||
switchNote = nonVisionWithImages
|
||||
? "Full chat history will be sent to the new model. This chat has images; text-only models may fail."
|
||||
: hasImage
|
||||
? "Full chat history will be sent to the new model. This chat includes images."
|
||||
: "Full chat history will be sent to the new model.";
|
||||
}
|
||||
}
|
||||
await selectModel({ id: value, isLora: meta?.isLora });
|
||||
|
||||
if (switchNote) {
|
||||
toast.warning("Model changed for this chat", {
|
||||
description: switchNote,
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
|
||||
await selectModel({
|
||||
id: value,
|
||||
isLora: meta?.isLora,
|
||||
});
|
||||
})();
|
||||
},
|
||||
[selectModel, ejectModel],
|
||||
[modelsFromStore, selectModel, view],
|
||||
);
|
||||
const handleEject = useCallback(() => {
|
||||
void ejectModel();
|
||||
|
|
@ -361,36 +423,8 @@ export function ChatPage(): ReactElement {
|
|||
const handleThreadSelect = useCallback(
|
||||
(nextView: ChatView) => {
|
||||
setView(nextView);
|
||||
|
||||
const threadId =
|
||||
nextView.mode === "single" ? nextView.threadId : undefined;
|
||||
const pairId =
|
||||
nextView.mode === "compare" ? nextView.pairId : undefined;
|
||||
|
||||
void (async () => {
|
||||
let thread: import("./types").ThreadRecord | undefined;
|
||||
if (threadId) {
|
||||
thread = await db.threads.get(threadId);
|
||||
} else if (pairId) {
|
||||
thread = await db.threads
|
||||
.where("pairId")
|
||||
.equals(pairId)
|
||||
.first();
|
||||
}
|
||||
const threadModelId = thread?.modelId;
|
||||
if (!threadModelId) return;
|
||||
|
||||
const currentCheckpoint =
|
||||
useChatRuntimeStore.getState().params.checkpoint;
|
||||
if (threadModelId === currentCheckpoint) return;
|
||||
|
||||
if (currentCheckpoint) {
|
||||
await ejectModel();
|
||||
}
|
||||
await selectModel({ id: threadModelId });
|
||||
})();
|
||||
},
|
||||
[ejectModel, selectModel],
|
||||
[],
|
||||
);
|
||||
|
||||
const models = useMemo<ModelOption[]>(
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ export function ChatSettingsPanel({
|
|||
label="Max Tokens"
|
||||
value={params.maxTokens}
|
||||
min={64}
|
||||
max={4096}
|
||||
max={4092}
|
||||
step={64}
|
||||
onChange={set("maxTokens")}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const DEFAULT_MODEL_MAX_SEQ_LENGTH = 2048;
|
|||
type SelectedModelInput = {
|
||||
id: string;
|
||||
isLora?: boolean;
|
||||
loadingDescription?: string;
|
||||
};
|
||||
|
||||
const LORA_SUFFIX_RE = /_(\d{9,})$/;
|
||||
|
|
@ -159,11 +160,22 @@ export function useChatModelRuntime() {
|
|||
|
||||
const explicitIsLora =
|
||||
typeof selection === "string" ? undefined : selection.isLora;
|
||||
const extraLoadingDescription =
|
||||
typeof selection === "string" ? undefined : selection.loadingDescription;
|
||||
const model = models.find((entry) => entry.id === modelId);
|
||||
const lora = loras.find((entry) => entry.id === modelId);
|
||||
const isLora =
|
||||
explicitIsLora ?? model?.isLora ?? (lora ? true : false);
|
||||
const displayName = model?.name || lora?.name || modelId;
|
||||
const currentCheckpoint =
|
||||
useChatRuntimeStore.getState().params.checkpoint;
|
||||
const loadingDescription = [
|
||||
currentCheckpoint ? "Unloading previous model first." : null,
|
||||
extraLoadingDescription ?? null,
|
||||
"This may include downloading. Large models can take a while.",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
setModelsError(null);
|
||||
setLoadingModel({ id: modelId, displayName });
|
||||
|
|
@ -197,8 +209,7 @@ export function useChatModelRuntime() {
|
|||
success: `${displayName} loaded`,
|
||||
error: (err) =>
|
||||
err instanceof Error ? err.message : "Failed to load model",
|
||||
description:
|
||||
"This may include downloading. Large models can take a while.",
|
||||
description: loadingDescription,
|
||||
});
|
||||
} catch (error) {
|
||||
setLoadingModel(null);
|
||||
|
|
@ -224,7 +235,7 @@ export function useChatModelRuntime() {
|
|||
|
||||
await toast.promise(performUnload(), {
|
||||
loading: "Unloading model",
|
||||
success: "Model unloaded",
|
||||
success: { message: "Model unloaded", duration: 1200 },
|
||||
error: (err) =>
|
||||
err instanceof Error ? err.message : "Failed to unload model",
|
||||
description: "Releases VRAM and resets inference state.",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const DEFAULT_INFERENCE_PARAMS: InferenceParams = {
|
|||
topK: 50,
|
||||
minP: 0.01,
|
||||
repetitionPenalty: 1.1,
|
||||
maxTokens: 512,
|
||||
maxTokens: 4092,
|
||||
systemPrompt: "",
|
||||
checkpoint: "",
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue