feat(canvas): allow resizing model selector

This commit is contained in:
134 2026-08-04 21:25:04 +08:00
parent d3a6497672
commit 31567af44e

View File

@ -2,7 +2,7 @@
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useEffect, useRef, useState, useCallback, useMemo, lazy, Suspense, type ClipboardEvent as ReactClipboardEvent, type ComponentProps, type DragEvent as ReactDragEvent, type MouseEvent as ReactMouseEvent } from "react";
import { useEffect, useRef, useState, useCallback, useMemo, lazy, Suspense, type ClipboardEvent as ReactClipboardEvent, type ComponentProps, type DragEvent as ReactDragEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from "react";
import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { createPortal } from "react-dom";
@ -25,7 +25,7 @@ import {
type UiSurfaceSnapshot,
} from "@/lib/ui-surface-protocol";
import useSWR, { useSWRConfig } from "swr";
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot, FileSpreadsheet, Paperclip, Copy, Square } from "lucide-react";
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot, FileSpreadsheet, Paperclip, Copy, Square, GripVertical } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Collapsible,
@ -374,6 +374,22 @@ type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd";
const CLAW_MODE_LABEL = "Claw";
const CLAW_UPGRADE_RETRY_MESSAGE = "claw正在升级更新稍等5分钟再试";
const CANVAS_MODEL_SELECTOR_WIDTH_STORAGE_KEY =
"ai-chatbot:canvas-model-selector-width";
const DEFAULT_CANVAS_MODEL_SELECTOR_WIDTH = 360;
const MIN_CANVAS_MODEL_SELECTOR_WIDTH = 320;
const MAX_CANVAS_MODEL_SELECTOR_WIDTH = 840;
function clampCanvasModelSelectorWidth(value: number) {
const viewportLimit =
typeof window === "undefined"
? MAX_CANVAS_MODEL_SELECTOR_WIDTH
: Math.max(MIN_CANVAS_MODEL_SELECTOR_WIDTH, window.innerWidth - 32);
return Math.min(
Math.max(Math.round(value), MIN_CANVAS_MODEL_SELECTOR_WIDTH),
Math.min(MAX_CANVAS_MODEL_SELECTOR_WIDTH, viewportLimit),
);
}
function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) {
if (mode === "clawd") return CLAW_MODE_LABEL;
@ -2563,6 +2579,9 @@ export function CanvasChatPanel({
const [selectedModelId, setSelectedModelId] = useState<string>(
effectiveAgentEnabled ? "auto" : ""
);
const [modelSelectorWidth, setModelSelectorWidth] = useState(
DEFAULT_CANVAS_MODEL_SELECTOR_WIDTH,
);
const [isDetailGenerationMode, setIsDetailGenerationMode] = useState(false);
const [detailGenerationMode, setDetailGenerationMode] = useState<DetailGenerationMode>("classic");
const [isDetailGenerating, setIsDetailGenerating] = useState(false);
@ -3153,6 +3172,69 @@ export function CanvasChatPanel({
}
}, []);
useEffect(() => {
try {
const savedWidth = Number(
localStorage.getItem(CANVAS_MODEL_SELECTOR_WIDTH_STORAGE_KEY),
);
if (Number.isFinite(savedWidth) && savedWidth > 0) {
setModelSelectorWidth(clampCanvasModelSelectorWidth(savedWidth));
}
} catch {
// Browser storage can be unavailable in private or restricted contexts.
}
const handleViewportResize = () => {
setModelSelectorWidth((currentWidth) =>
clampCanvasModelSelectorWidth(currentWidth),
);
};
window.addEventListener("resize", handleViewportResize);
return () => window.removeEventListener("resize", handleViewportResize);
}, []);
const handleModelSelectorResizeStart = useCallback(
(
event: ReactPointerEvent<HTMLButtonElement>,
edge: "left" | "right",
) => {
event.preventDefault();
event.stopPropagation();
const startX = event.clientX;
const startWidth = modelSelectorWidth;
let latestWidth = startWidth;
const handlePointerMove = (moveEvent: PointerEvent) => {
const delta =
edge === "left"
? startX - moveEvent.clientX
: moveEvent.clientX - startX;
latestWidth = clampCanvasModelSelectorWidth(startWidth + delta);
setModelSelectorWidth(latestWidth);
};
const finishResize = () => {
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", finishResize);
window.removeEventListener("pointercancel", finishResize);
try {
localStorage.setItem(
CANVAS_MODEL_SELECTOR_WIDTH_STORAGE_KEY,
String(latestWidth),
);
} catch {
// Keep the resized width for this page even if storage is unavailable.
}
};
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", finishResize);
window.addEventListener("pointercancel", finishResize);
},
[modelSelectorWidth],
);
useEffect(() => {
if (!fullscreenPreview) return;
const handleEsc = (event: KeyboardEvent) => {
@ -13582,12 +13664,37 @@ export function CanvasChatPanel({
const renderModelSelectContent = () => (
<SelectContent
className={cn(
"max-h-[360px] w-[292px] max-w-[calc(100vw-24px)] overflow-hidden rounded-[var(--ui-radius-panel)] border border-[#cfd4dc] bg-white p-0 text-[#252b33] shadow-[0_14px_36px_rgba(15,23,42,0.14)]",
"group/model-selector max-h-[360px] max-w-[calc(100vw-24px)] overflow-hidden rounded-[var(--ui-radius-panel)] border border-[#cfd4dc] bg-white p-0 text-[#252b33] shadow-[0_14px_36px_rgba(15,23,42,0.14)]",
isVideoWorkspace
? "border border-border/60 bg-background/95 shadow-[0_14px_30px_-20px_rgba(15,23,42,0.35)] backdrop-blur-xl"
: "dark:border-white/10 dark:bg-zinc-900/95 dark:text-zinc-100"
)}
style={{ width: `${modelSelectorWidth}px` }}
>
<button
type="button"
tabIndex={-1}
aria-label="向左拖动调整模型列表宽度"
title="拖动调整宽度"
className="absolute inset-y-3 left-0 z-20 flex w-4 touch-none select-none items-center justify-center border-0 bg-transparent p-0 text-[#98a0ad] opacity-0 outline-none transition-opacity hover:text-[#4b5563] hover:opacity-100 group-hover/model-selector:opacity-60 dark:text-zinc-500 dark:hover:text-zinc-200"
onPointerDown={(event) =>
handleModelSelectorResizeStart(event, "left")
}
>
<GripVertical className="h-5 w-3 cursor-col-resize" />
</button>
<button
type="button"
tabIndex={-1}
aria-label="向右拖动调整模型列表宽度"
title="拖动调整宽度"
className="absolute inset-y-3 right-0 z-20 flex w-4 touch-none select-none items-center justify-center border-0 bg-transparent p-0 text-[#98a0ad] opacity-0 outline-none transition-opacity hover:text-[#4b5563] hover:opacity-100 group-hover/model-selector:opacity-60 dark:text-zinc-500 dark:hover:text-zinc-200"
onPointerDown={(event) =>
handleModelSelectorResizeStart(event, "right")
}
>
<GripVertical className="h-5 w-3 cursor-col-resize" />
</button>
<div className="max-h-[344px] space-y-0.5 overflow-y-auto p-1.5">
{selectableModels.map((model) => {
const discountLabel = getImageModelCreditDiscountLabel(