feat: redesign chat panel to match LOVART style
This commit is contained in:
parent
79e5a870e1
commit
f5c52bfdc6
@ -22,8 +22,8 @@ import {
|
||||
Maximize2,
|
||||
Move3D,
|
||||
Layers,
|
||||
PanelLeftClose,
|
||||
PanelLeft,
|
||||
PanelRightClose,
|
||||
PanelRight,
|
||||
Download,
|
||||
Upload,
|
||||
Wand2,
|
||||
@ -369,7 +369,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
</div>
|
||||
|
||||
{/* 2. Floating Header (Top Left) */}
|
||||
<div className="absolute top-4 left-4 z-20 flex items-center gap-2 bg-background/80 backdrop-blur-md border rounded-xl p-1.5 shadow-sm">
|
||||
<div className="absolute top-20 left-4 z-20 flex items-center gap-2 bg-background/80 backdrop-blur-md border rounded-xl p-1.5 shadow-sm">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" onClick={() => router.push('/')} className="rounded-lg h-8 w-8">
|
||||
@ -407,7 +407,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
</div>
|
||||
|
||||
{/* 3. Floating Actions (Top Right) */}
|
||||
<div className="absolute top-4 right-4 z-20 flex items-center gap-2">
|
||||
<div className="absolute top-20 right-4 z-20 flex items-center gap-2">
|
||||
<div className="bg-background/80 backdrop-blur-md border rounded-xl p-1.5 shadow-sm flex items-center gap-1">
|
||||
{isGenerating && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground px-2 border-r mr-1">
|
||||
@ -444,7 +444,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
onClick={() => setIsPanelOpen(!isPanelOpen)}
|
||||
className={cn("rounded-lg h-8 w-8", isPanelOpen && "bg-muted")}
|
||||
>
|
||||
{isPanelOpen ? <PanelLeftClose className="h-4 w-4" /> : <PanelLeft className="h-4 w-4" />}
|
||||
{isPanelOpen ? <PanelRightClose className="h-4 w-4" /> : <PanelRight className="h-4 w-4" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{isPanelOpen ? "隐藏面板" : "显示面板"}</TooltipContent>
|
||||
@ -452,10 +452,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4. Floating Sidebar (Left) */}
|
||||
{/* 4. Floating Sidebar (Right) */}
|
||||
<div className={cn(
|
||||
"absolute top-20 left-4 bottom-24 z-10 w-[400px] transition-all duration-300 ease-in-out",
|
||||
isPanelOpen ? "translate-x-0 opacity-100" : "-translate-x-[120%] opacity-0"
|
||||
"absolute top-36 right-4 bottom-24 z-10 w-[400px] transition-all duration-300 ease-in-out",
|
||||
isPanelOpen ? "translate-x-0 opacity-100" : "translate-x-[120%] opacity-0"
|
||||
)}>
|
||||
<div className="h-full w-full rounded-2xl border bg-background/95 backdrop-blur-md shadow-2xl overflow-hidden flex flex-col">
|
||||
<Tabs defaultValue="chat" className="flex flex-col h-full">
|
||||
|
||||
@ -4,10 +4,11 @@ import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import useSWR from "swr";
|
||||
import { Send, Loader2, ImagePlus, Paperclip, X, Sparkles } from "lucide-react";
|
||||
import { Send, Loader2, ImagePlus, Paperclip, X, Sparkles, User, Bot, Plus, History, Share2, LayoutGrid, Maximize2, RefreshCw, Zap, Globe, Box, ArrowUp } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -26,6 +27,25 @@ interface CanvasChatPanelProps {
|
||||
initialMessages?: ChatMessage[];
|
||||
}
|
||||
|
||||
// 建议卡片数据
|
||||
const SUGGESTIONS = [
|
||||
{
|
||||
title: "Wine List",
|
||||
desc: "Mimic this effect to genera...",
|
||||
image: "https://images.unsplash.com/photo-1510812431401-41d2bd2722f3?w=300&h=200&fit=crop"
|
||||
},
|
||||
{
|
||||
title: "Coffee Shop Branding",
|
||||
desc: "you are a brand design...",
|
||||
image: "https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=300&h=200&fit=crop"
|
||||
},
|
||||
{
|
||||
title: "Story Board",
|
||||
desc: "I NEED A STORY BOARD...",
|
||||
image: "https://images.unsplash.com/photo-1626785774573-4b799314346d?w=300&h=200&fit=crop"
|
||||
}
|
||||
];
|
||||
|
||||
// 从消息中提取图片 URL
|
||||
function extractImageUrls(text: string): string[] {
|
||||
const urls: string[] = [];
|
||||
@ -270,34 +290,75 @@ export function CanvasChatPanel({ chatId, initialMessages = [] }: CanvasChatPane
|
||||
const isLoading = status === 'streaming' || status === 'submitted';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 模型选择 */}
|
||||
<div className="p-3 border-b">
|
||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="选择模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{imageModels.map(model => (
|
||||
<SelectItem key={model.id} value={model.id} className="text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
{model.displayName}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-col h-full bg-background/50 backdrop-blur-sm">
|
||||
{/* Header - Minimal with Icons */}
|
||||
<div className="flex items-center justify-end gap-1 p-3 border-b bg-background/80 backdrop-blur-md sticky top-0 z-10">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
||||
<History className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<ScrollArea className="flex-1 p-3">
|
||||
<div className="space-y-3">
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-6">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center text-muted-foreground text-sm py-8">
|
||||
<Sparkles className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p>开始对话生成图片</p>
|
||||
<p className="text-xs mt-1">生成的图片将自动添加到画布</p>
|
||||
<div className="flex flex-col h-full pt-4">
|
||||
{/* Greeting */}
|
||||
<div className="mb-8">
|
||||
<div className="w-12 h-12 bg-black rounded-full flex items-center justify-center mb-4">
|
||||
<span className="text-white font-bold text-xl">-L°</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold mb-2">Hi, 我是你的AI设计师</h1>
|
||||
<p className="text-muted-foreground">让我们开始今天的创作吧!</p>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
<div className="space-y-3 mb-8">
|
||||
{SUGGESTIONS.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between p-3 bg-card rounded-xl border hover:shadow-md transition-all cursor-pointer group"
|
||||
onClick={() => setInput(prev => prev || item.desc)}
|
||||
>
|
||||
<div className="flex-1 min-w-0 mr-3">
|
||||
<h3 className="font-medium text-sm mb-1 group-hover:text-primary transition-colors">{item.title}</h3>
|
||||
<p className="text-xs text-muted-foreground truncate">{item.desc}</p>
|
||||
</div>
|
||||
<div className="w-16 h-12 rounded-lg overflow-hidden shrink-0">
|
||||
<img src={item.image} alt={item.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Refresh */}
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-4 cursor-pointer hover:text-foreground w-fit">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
<span>切换</span>
|
||||
</div>
|
||||
|
||||
{/* Banner */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 p-3 rounded-xl text-sm flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1 bg-blue-100 dark:bg-blue-800 rounded">
|
||||
<Box className="h-4 w-4" />
|
||||
</div>
|
||||
<span>升级会员, Nano Banana Pro 免费365天!</span>
|
||||
</div>
|
||||
<X className="h-4 w-4 cursor-pointer opacity-50 hover:opacity-100" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -305,67 +366,105 @@ export function CanvasChatPanel({ chatId, initialMessages = [] }: CanvasChatPane
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn(
|
||||
"text-sm rounded-lg p-2",
|
||||
message.role === 'user'
|
||||
? "bg-primary text-primary-foreground ml-8"
|
||||
: "bg-muted mr-8"
|
||||
"flex gap-3",
|
||||
message.role === 'user' ? "flex-row-reverse" : "flex-row"
|
||||
)}
|
||||
>
|
||||
{/* 附件预览 */}
|
||||
{message.attachments && message.attachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-1">
|
||||
{message.attachments.map((att: any, i: number) => (
|
||||
<img
|
||||
key={i}
|
||||
src={att.url}
|
||||
alt=""
|
||||
className="h-12 w-12 object-cover rounded"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 消息内容 */}
|
||||
<div className="whitespace-pre-wrap break-words">
|
||||
{message.parts?.map((part: any, i: number) => {
|
||||
if (part.type === 'text') {
|
||||
// 提取并显示图片
|
||||
const imageUrls = extractImageUrls(part.text || '');
|
||||
const textWithoutImages = (part.text || '')
|
||||
.replace(/!\[(?:thumb)?\|\|\|[^|]+\|\|\|[^\]]*\]\([^)]+\)/g, '')
|
||||
.trim();
|
||||
|
||||
return (
|
||||
<div key={i}>
|
||||
{textWithoutImages && <span>{textWithoutImages}</span>}
|
||||
{imageUrls.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{imageUrls.map((url, idx) => (
|
||||
<img
|
||||
key={idx}
|
||||
src={url}
|
||||
alt=""
|
||||
className="max-w-full h-auto rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
style={{ maxHeight: '200px' }}
|
||||
onClick={() => window.open(url, '_blank')}
|
||||
/>
|
||||
))}
|
||||
{/* 头像 */}
|
||||
<Avatar className="h-8 w-8 shrink-0 border shadow-sm">
|
||||
{message.role === 'user' ? (
|
||||
<>
|
||||
<AvatarImage src="/placeholder-user.jpg" />
|
||||
<AvatarFallback className="bg-primary text-primary-foreground">
|
||||
<User className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AvatarImage src="/placeholder-bot.jpg" />
|
||||
<AvatarFallback className="bg-muted text-foreground">
|
||||
<Bot className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</>
|
||||
)}
|
||||
</Avatar>
|
||||
|
||||
{/* 消息气泡 */}
|
||||
<div className={cn(
|
||||
"flex flex-col gap-2 max-w-[85%]",
|
||||
message.role === 'user' ? "items-end" : "items-start"
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm px-4 py-2.5 shadow-sm",
|
||||
message.role === 'user'
|
||||
? "bg-primary text-primary-foreground rounded-2xl rounded-tr-sm"
|
||||
: "bg-card border text-card-foreground rounded-2xl rounded-tl-sm"
|
||||
)}
|
||||
>
|
||||
{/* 附件预览 */}
|
||||
{message.attachments && message.attachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{message.attachments.map((att: any, i: number) => (
|
||||
<img
|
||||
key={i}
|
||||
src={att.url}
|
||||
alt=""
|
||||
className="h-16 w-16 object-cover rounded-lg border bg-background"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 消息内容 */}
|
||||
<div className="whitespace-pre-wrap break-words leading-relaxed">
|
||||
{message.parts?.map((part: any, i: number) => {
|
||||
if (part.type === 'text') {
|
||||
// 提取并显示图片
|
||||
const imageUrls = extractImageUrls(part.text || '');
|
||||
const textWithoutImages = (part.text || '')
|
||||
.replace(/!\[(?:thumb)?\|\|\|[^|]+\|\|\|[^\]]*\]\([^)]+\)/g, '')
|
||||
.trim();
|
||||
|
||||
return (
|
||||
<div key={i}>
|
||||
{textWithoutImages && <span>{textWithoutImages}</span>}
|
||||
{imageUrls.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{imageUrls.map((url, idx) => (
|
||||
<div key={idx} className="relative group rounded-xl overflow-hidden border bg-background shadow-sm transition-all hover:shadow-md">
|
||||
<img
|
||||
src={url}
|
||||
alt=""
|
||||
className="max-w-full h-auto cursor-pointer hover:scale-105 transition-transform duration-300"
|
||||
style={{ maxHeight: '200px' }}
|
||||
onClick={() => window.open(url, '_blank')}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
<div className="bg-muted rounded-lg p-2 mr-8">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
生成中...
|
||||
<div className="flex gap-3">
|
||||
<Avatar className="h-8 w-8 shrink-0 border shadow-sm">
|
||||
<AvatarFallback className="bg-muted text-foreground">
|
||||
<Bot className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="bg-card border text-card-foreground rounded-2xl rounded-tl-sm px-4 py-2.5 shadow-sm flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
||||
<span className="text-sm text-muted-foreground">正在思考中...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -374,75 +473,120 @@ export function CanvasChatPanel({ chatId, initialMessages = [] }: CanvasChatPane
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* 附件预览 */}
|
||||
{attachments.length > 0 && (
|
||||
<div className="p-2 border-t flex flex-wrap gap-2">
|
||||
{attachments.map((att, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<img
|
||||
src={att.url}
|
||||
alt=""
|
||||
className="h-12 w-12 object-cover rounded"
|
||||
/>
|
||||
<button
|
||||
className="absolute -top-1 -right-1 bg-destructive text-destructive-foreground rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => removeAttachment(index)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="p-3 border-t">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
placeholder="描述你想生成的图片..."
|
||||
className="min-h-[60px] max-h-[120px] text-sm resize-none pr-10"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 bottom-1 h-7 w-7"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* 底部输入区域 - Redesigned */}
|
||||
<div className="p-4 bg-background/80 backdrop-blur-md border-t">
|
||||
{/* 附件预览 */}
|
||||
{attachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{attachments.map((att, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<img
|
||||
src={att.url}
|
||||
alt=""
|
||||
className="h-14 w-14 object-cover rounded-lg border shadow-sm"
|
||||
/>
|
||||
<button
|
||||
className="absolute -top-1.5 -right-1.5 bg-destructive text-destructive-foreground rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-all shadow-sm hover:scale-110"
|
||||
onClick={() => removeAttachment(index)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative bg-card rounded-3xl border shadow-sm focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary/50 transition-all">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
placeholder="请输入你的设计需求"
|
||||
className="min-h-[80px] max-h-[150px] w-full text-sm resize-none bg-transparent border-none focus-visible:ring-0 p-4 pb-12 placeholder:text-muted-foreground/50"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
{/* Bottom Toolbar */}
|
||||
<div className="absolute left-2 bottom-2 right-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
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"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Agent Selector (Model) */}
|
||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||
<SelectTrigger className="h-8 text-xs bg-blue-50 text-blue-600 border-blue-100 hover:bg-blue-100 hover:border-blue-200 transition-colors rounded-full px-3 gap-1 w-auto">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
<span className="truncate max-w-[80px]">
|
||||
{imageModels.find(m => m.id === selectedModelId)?.displayName || "Agent"}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{imageModels.map(model => (
|
||||
<SelectItem key={model.id} value={model.id} className="text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-3 w-3 text-primary" />
|
||||
{model.displayName}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Style/Settings Icons (Visual only for now) */}
|
||||
<div className="flex items-center gap-1 mr-2 border-r pr-2">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full text-muted-foreground">
|
||||
<Zap className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full text-muted-foreground">
|
||||
<Globe className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full text-muted-foreground">
|
||||
<Box className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full transition-all duration-200",
|
||||
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>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-[60px] w-10"
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || (!input.trim() && attachments.length === 0)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -228,26 +228,20 @@ export function CanvasWorkspace() {
|
||||
onMouseLeave={handleMouseUp}
|
||||
onWheel={handleWheel}
|
||||
>
|
||||
{/* 网格背景 - 浅色模式 */}
|
||||
{/* 点阵背景 - 浅色模式 */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none dark:hidden"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundImage: `radial-gradient(rgba(0,0,0,0.1) 1px, transparent 1px)`,
|
||||
backgroundSize: `${20 * viewport.zoom}px ${20 * viewport.zoom}px`,
|
||||
backgroundPosition: `${viewport.x}px ${viewport.y}px`,
|
||||
}}
|
||||
/>
|
||||
{/* 深色模式网格 */}
|
||||
{/* 深色模式点阵 */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none hidden dark:block"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundImage: `radial-gradient(rgba(255,255,255,0.05) 1px, transparent 1px)`,
|
||||
backgroundSize: `${20 * viewport.zoom}px ${20 * viewport.zoom}px`,
|
||||
backgroundPosition: `${viewport.x}px ${viewport.y}px`,
|
||||
}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user