refactor: align canvas video toolbar layout with mode-tools-avatar flow
This commit is contained in:
parent
66bf492d81
commit
2aa2aaf0e3
@ -5,7 +5,7 @@ import { DefaultChatTransport } from "ai";
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import useSWR from "swr";
|
||||
import { Send, Loader2, ImagePlus, Paperclip, X, User, Bot, Plus, History, LayoutGrid, Maximize2, RefreshCw, Globe, Box, ArrowUp } from "lucide-react";
|
||||
import { Send, Loader2, ImagePlus, Paperclip, X, User, Bot, Plus, History, LayoutGrid, Maximize2, RefreshCw, Globe, Box, ArrowUp, Video } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
@ -1776,6 +1776,38 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const shouldShowImageLoading = hasGenerationStart && !hasGenerationEnd && !!currentModel;
|
||||
const loadingImageCount = imageParams.imageCount || 1;
|
||||
|
||||
const renderModelSelectContent = () => (
|
||||
<SelectContent className="w-[238px] max-h-[340px] overflow-hidden rounded-[18px] border border-[#cfd4dc] bg-[#f3f4f6] p-1.5 shadow-[0_8px_20px_rgba(15,23,42,0.13)]">
|
||||
<div className="max-h-[312px] space-y-0.5 overflow-y-auto pr-1">
|
||||
{allModels.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
className="h-[34px] rounded-xl px-2.5 text-[#2f3640] [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full focus:bg-[#e7ebf0] focus:text-[#1f2937] data-[state=checked]:bg-[#e7ebf0] data-[state=checked]:text-[#1f2937]"
|
||||
>
|
||||
<span className="flex w-full items-center justify-between gap-2">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="h-5 w-5 overflow-hidden rounded-full border border-[#d4d9df] bg-white">
|
||||
{model.avatarUrl ? (
|
||||
<img src={model.avatarUrl} alt={model.displayName} className="h-full w-full object-cover" />
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-[9px] font-medium text-[#6b7280]">
|
||||
{model.id === "auto" ? "A" : model.displayName.slice(0, 1)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate text-[12px] font-medium leading-none">{model.displayName}</span>
|
||||
</span>
|
||||
{selectedModelId === model.id ? (
|
||||
<span className="text-sm font-semibold text-[#1f2937]">✓</span>
|
||||
) : null}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
);
|
||||
|
||||
const createNewChatSession = useCallback(() => {
|
||||
const now = Date.now();
|
||||
const newChatSessionId = `canvas-${crypto.randomUUID()}`;
|
||||
@ -2259,8 +2291,6 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
{/* Bottom Toolbar */}
|
||||
<div className="absolute left-2 bottom-2 right-2 flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-x-auto overscroll-x-contain touch-pan-x pr-1 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
|
||||
{/* 左侧:附件和模型选择 */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@ -2269,113 +2299,118 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full hover:bg-muted text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Agent Selector (Model) - 包含Agent模式选项 */}
|
||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||
<SelectTrigger className={cn(
|
||||
"h-8 text-xs transition-colors rounded-full px-2 gap-1.5 w-auto flex-shrink-0",
|
||||
selectedModelId === "auto"
|
||||
? "bg-gradient-to-r from-purple-50 to-blue-50 text-purple-600 border-purple-100 hover:from-purple-100 hover:to-blue-100"
|
||||
: "bg-blue-50 text-blue-600 border-blue-100 hover:bg-blue-100 hover:border-blue-200"
|
||||
)}>
|
||||
<span className="h-5 w-5 overflow-hidden rounded-full border border-blue-200 bg-white">
|
||||
{allModels.find((m) => m.id === selectedModelId)?.avatarUrl ? (
|
||||
<img
|
||||
src={allModels.find((m) => m.id === selectedModelId)?.avatarUrl || ""}
|
||||
alt={allModels.find((m) => m.id === selectedModelId)?.displayName || "模型头像"}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-blue-500">
|
||||
{(allModels.find((m) => m.id === selectedModelId)?.displayName || "A").slice(0, 1)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate max-w-[66px]">
|
||||
{allModels.find(m => m.id === selectedModelId)?.displayName || "Agent模式"}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[238px] max-h-[340px] overflow-hidden rounded-[18px] border border-[#cfd4dc] bg-[#f3f4f6] p-1.5 shadow-[0_8px_20px_rgba(15,23,42,0.13)]">
|
||||
<div className="max-h-[312px] space-y-0.5 overflow-y-auto pr-1">
|
||||
{allModels.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
className="h-[34px] rounded-xl px-2.5 text-[#2f3640] [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full focus:bg-[#e7ebf0] focus:text-[#1f2937] data-[state=checked]:bg-[#e7ebf0] data-[state=checked]:text-[#1f2937]"
|
||||
>
|
||||
<span className="flex w-full items-center justify-between gap-2">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="h-5 w-5 overflow-hidden rounded-full border border-[#d4d9df] bg-white">
|
||||
{model.avatarUrl ? (
|
||||
<img src={model.avatarUrl} alt={model.displayName} className="h-full w-full object-cover" />
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-[9px] font-medium text-[#6b7280]">
|
||||
{model.id === "auto" ? "A" : model.displayName.slice(0, 1)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate text-[12px] font-medium leading-none">{model.displayName}</span>
|
||||
</span>
|
||||
{selectedModelId === model.id ? (
|
||||
<span className="text-sm font-semibold text-[#1f2937]">✓</span>
|
||||
) : null}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 中间:图片参数(可收缩) */}
|
||||
<div className="shrink-0">
|
||||
{isVideoModel ? (
|
||||
<VideoParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={videoParams || undefined}
|
||||
onParamsChange={handleVideoParamsChange}
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
/>
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-8 rounded-full border border-blue-500 bg-blue-50 px-3 text-blue-600 hover:bg-blue-100"
|
||||
>
|
||||
<Video className="mr-1 h-4 w-4" />
|
||||
<span className="text-sm font-medium">视频</span>
|
||||
</Button>
|
||||
<div className="shrink-0">
|
||||
<VideoParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={videoParams || undefined}
|
||||
onParamsChange={handleVideoParamsChange}
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<ImageParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
onParamsChange={handleImageParamsChange}
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
/>
|
||||
<>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full hover:bg-muted text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||
<SelectTrigger className={cn(
|
||||
"h-8 text-xs transition-colors rounded-full px-2 gap-1.5 w-auto flex-shrink-0",
|
||||
selectedModelId === "auto"
|
||||
? "bg-gradient-to-r from-purple-50 to-blue-50 text-purple-600 border-purple-100 hover:from-purple-100 hover:to-blue-100"
|
||||
: "bg-blue-50 text-blue-600 border-blue-100 hover:bg-blue-100 hover:border-blue-200"
|
||||
)}>
|
||||
<span className="h-5 w-5 overflow-hidden rounded-full border border-blue-200 bg-white">
|
||||
{allModels.find((m) => m.id === selectedModelId)?.avatarUrl ? (
|
||||
<img
|
||||
src={allModels.find((m) => m.id === selectedModelId)?.avatarUrl || ""}
|
||||
alt={allModels.find((m) => m.id === selectedModelId)?.displayName || "模型头像"}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-blue-500">
|
||||
{(allModels.find((m) => m.id === selectedModelId)?.displayName || "A").slice(0, 1)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate max-w-[66px]">
|
||||
{allModels.find((m) => m.id === selectedModelId)?.displayName || "Agent模式"}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
{renderModelSelectContent()}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<ImageParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
onParamsChange={handleImageParamsChange}
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:发送按钮(固定不收缩) */}
|
||||
<Button
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full transition-all duration-200 flex-shrink-0",
|
||||
input.trim() || attachments.length > 0
|
||||
? "bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{isVideoModel && (
|
||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||
<SelectTrigger className="h-8 w-8 rounded-full border border-[#cfd4dc] bg-white p-0 justify-center [&>svg]:hidden">
|
||||
<span className="h-6 w-6 overflow-hidden rounded-full bg-white flex items-center justify-center">
|
||||
{allModels.find((m) => m.id === selectedModelId)?.avatarUrl ? (
|
||||
<img
|
||||
src={allModels.find((m) => m.id === selectedModelId)?.avatarUrl || ""}
|
||||
alt={allModels.find((m) => m.id === selectedModelId)?.displayName || "模型头像"}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-[#4b5563]">
|
||||
{(allModels.find((m) => m.id === selectedModelId)?.displayName || "A").slice(0, 1)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
{renderModelSelectContent()}
|
||||
</Select>
|
||||
)}
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || (!input.trim() && attachments.length === 0)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full transition-all duration-200 flex-shrink-0",
|
||||
input.trim() || attachments.length > 0
|
||||
? "bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
)}
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || (!input.trim() && attachments.length === 0)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user