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 { useEffect, useRef, useState, useCallback, useMemo } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import useSWR from "swr";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
@ -1776,6 +1776,38 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
const shouldShowImageLoading = hasGenerationStart && !hasGenerationEnd && !!currentModel;
|
const shouldShowImageLoading = hasGenerationStart && !hasGenerationEnd && !!currentModel;
|
||||||
const loadingImageCount = imageParams.imageCount || 1;
|
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 createNewChatSession = useCallback(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const newChatSessionId = `canvas-${crypto.randomUUID()}`;
|
const newChatSessionId = `canvas-${crypto.randomUUID()}`;
|
||||||
@ -2259,8 +2291,6 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
{/* Bottom Toolbar */}
|
{/* Bottom Toolbar */}
|
||||||
<div className="absolute left-2 bottom-2 right-2 flex items-center gap-2">
|
<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 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
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@ -2269,113 +2299,118 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileSelect}
|
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 ? (
|
{isVideoModel ? (
|
||||||
<VideoParamsBar
|
<>
|
||||||
modelName={currentModel?.displayName}
|
<Button
|
||||||
initialParams={videoParams || undefined}
|
type="button"
|
||||||
onParamsChange={handleVideoParamsChange}
|
variant="ghost"
|
||||||
inline={true}
|
className="h-8 rounded-full border border-blue-500 bg-blue-50 px-3 text-blue-600 hover:bg-blue-100"
|
||||||
providerType={currentModel?.providerType}
|
>
|
||||||
/>
|
<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}
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
initialParams={imageParams}
|
<Button
|
||||||
onParamsChange={handleImageParamsChange}
|
variant="ghost"
|
||||||
inline={true}
|
size="icon"
|
||||||
providerType={currentModel?.providerType}
|
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>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧:发送按钮(固定不收缩) */}
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<Button
|
{isVideoModel && (
|
||||||
size="icon"
|
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||||
className={cn(
|
<SelectTrigger className="h-8 w-8 rounded-full border border-[#cfd4dc] bg-white p-0 justify-center [&>svg]:hidden">
|
||||||
"h-8 w-8 rounded-full transition-all duration-200 flex-shrink-0",
|
<span className="h-6 w-6 overflow-hidden rounded-full bg-white flex items-center justify-center">
|
||||||
input.trim() || attachments.length > 0
|
{allModels.find((m) => m.id === selectedModelId)?.avatarUrl ? (
|
||||||
? "bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
<img
|
||||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
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)}
|
<Button
|
||||||
>
|
size="icon"
|
||||||
{isLoading ? (
|
className={cn(
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
"h-8 w-8 rounded-full transition-all duration-200 flex-shrink-0",
|
||||||
) : (
|
input.trim() || attachments.length > 0
|
||||||
<ArrowUp className="h-4 w-4" />
|
? "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"
|
||||||
</Button>
|
)}
|
||||||
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user