feat(canvas): allow resizing model selector
This commit is contained in:
parent
05de6d005c
commit
8773b108e4
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useChat } from "@ai-sdk/react";
|
import { useChat } from "@ai-sdk/react";
|
||||||
import { DefaultChatTransport } from "ai";
|
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 { useSession } from "next-auth/react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
@ -25,7 +25,7 @@ import {
|
|||||||
type UiSurfaceSnapshot,
|
type UiSurfaceSnapshot,
|
||||||
} from "@/lib/ui-surface-protocol";
|
} from "@/lib/ui-surface-protocol";
|
||||||
import useSWR, { useSWRConfig } from "swr";
|
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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@ -354,6 +354,22 @@ type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd";
|
|||||||
|
|
||||||
const CLAW_MODE_LABEL = "Claw";
|
const CLAW_MODE_LABEL = "Claw";
|
||||||
const CLAW_UPGRADE_RETRY_MESSAGE = "claw正在升级更新稍等5分钟再试";
|
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) {
|
function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) {
|
||||||
if (mode === "clawd") return CLAW_MODE_LABEL;
|
if (mode === "clawd") return CLAW_MODE_LABEL;
|
||||||
@ -2628,6 +2644,9 @@ export function CanvasChatPanel({
|
|||||||
const [selectedModelId, setSelectedModelId] = useState<string>(
|
const [selectedModelId, setSelectedModelId] = useState<string>(
|
||||||
effectiveAgentEnabled ? "auto" : ""
|
effectiveAgentEnabled ? "auto" : ""
|
||||||
);
|
);
|
||||||
|
const [modelSelectorWidth, setModelSelectorWidth] = useState(
|
||||||
|
DEFAULT_CANVAS_MODEL_SELECTOR_WIDTH,
|
||||||
|
);
|
||||||
const [isDetailGenerationMode, setIsDetailGenerationMode] = useState(false);
|
const [isDetailGenerationMode, setIsDetailGenerationMode] = useState(false);
|
||||||
const [detailGenerationMode, setDetailGenerationMode] = useState<DetailGenerationMode>("classic");
|
const [detailGenerationMode, setDetailGenerationMode] = useState<DetailGenerationMode>("classic");
|
||||||
const [isDetailGenerating, setIsDetailGenerating] = useState(false);
|
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<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(() => {
|
useEffect(() => {
|
||||||
if (!fullscreenPreview) return;
|
if (!fullscreenPreview) return;
|
||||||
const handleEsc = (event: KeyboardEvent) => {
|
const handleEsc = (event: KeyboardEvent) => {
|
||||||
@ -14054,12 +14136,37 @@ export function CanvasChatPanel({
|
|||||||
const renderModelSelectContent = () => (
|
const renderModelSelectContent = () => (
|
||||||
<SelectContent
|
<SelectContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-[360px] max-w-[calc(100vw-32px)] max-h-[420px] overflow-hidden rounded-[22px] border border-[#cfd4dc] bg-white p-0 text-[#252b33] shadow-[0_18px_44px_rgba(15,23,42,0.16)]",
|
"group/model-selector max-h-[420px] max-w-[calc(100vw-32px)] overflow-hidden rounded-[22px] border border-[#cfd4dc] bg-white p-0 text-[#252b33] shadow-[0_18px_44px_rgba(15,23,42,0.16)]",
|
||||||
isVideoWorkspace
|
isVideoWorkspace
|
||||||
? "border border-border/60 bg-background/95 shadow-[0_14px_30px_-20px_rgba(15,23,42,0.35)] backdrop-blur-xl"
|
? "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"
|
: "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-[392px] space-y-1 overflow-y-auto p-2">
|
<div className="max-h-[392px] space-y-1 overflow-y-auto p-2">
|
||||||
{selectableModels.map((model) => {
|
{selectableModels.map((model) => {
|
||||||
const discountLabel = getImageModelCreditDiscountLabel(
|
const discountLabel = getImageModelCreditDiscountLabel(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user