feat(canvas): allow resizing model selector
This commit is contained in:
parent
d3a6497672
commit
31567af44e
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user