From 8773b108e4b515c7491aa2afd5f4f2da04ca127b Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 4 Aug 2026 21:25:04 +0800 Subject: [PATCH] feat(canvas): allow resizing model selector --- components/canvas/CanvasChatPanel.tsx | 113 +++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 3 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 0fcac8abe..6aba7c4d9 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -2,7 +2,7 @@ import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; -import { useEffect, useRef, useState, useCallback, useMemo, type ClipboardEvent as ReactClipboardEvent, type DragEvent as ReactDragEvent, type MouseEvent as ReactMouseEvent } from "react"; +import { useEffect, useRef, useState, useCallback, useMemo, type ClipboardEvent as ReactClipboardEvent, 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, Search } 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, Search, GripVertical } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Collapsible, @@ -354,6 +354,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; @@ -2628,6 +2644,9 @@ export function CanvasChatPanel({ const [selectedModelId, setSelectedModelId] = useState( effectiveAgentEnabled ? "auto" : "" ); + const [modelSelectorWidth, setModelSelectorWidth] = useState( + DEFAULT_CANVAS_MODEL_SELECTOR_WIDTH, + ); const [isDetailGenerationMode, setIsDetailGenerationMode] = useState(false); const [detailGenerationMode, setDetailGenerationMode] = useState("classic"); const [isDetailGenerating, setIsDetailGenerating] = useState(false); @@ -3268,6 +3287,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, + 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) => { @@ -14054,12 +14136,37 @@ export function CanvasChatPanel({ const renderModelSelectContent = () => ( + +
{selectableModels.map((model) => { const discountLabel = getImageModelCreditDiscountLabel(