feat(ui): 替换原生select为shadcn/ui组件并支持图片消息发送
- 将 AccountBasicTab 和 ConfigForm 中的原生 select 替换为 shadcn/ui Select 组件 - ChatInput 支持拖放上传图片附件 - 添加 attachment button 并整合图片附件到消息发送 - 优化 Select 组件样式,增加过渡动画和悬停效果 - 调整 ConfigForm tab 切换样式为按钮风格 - AssetGallery 使用 useAccounts hook 获取账户列表 - 重新构建前端静态资源
This commit is contained in:
1
web/client/dist/assets/index-B_JK8LRt.css
vendored
Normal file
1
web/client/dist/assets/index-B_JK8LRt.css
vendored
Normal file
File diff suppressed because one or more lines are too long
370
web/client/dist/assets/index-Cgtxrl8D.js
vendored
Normal file
370
web/client/dist/assets/index-Cgtxrl8D.js
vendored
Normal file
File diff suppressed because one or more lines are too long
320
web/client/dist/assets/index-DSKEPThI.js
vendored
320
web/client/dist/assets/index-DSKEPThI.js
vendored
File diff suppressed because one or more lines are too long
1
web/client/dist/assets/index-DXUyFc07.css
vendored
1
web/client/dist/assets/index-DXUyFc07.css
vendored
File diff suppressed because one or more lines are too long
4
web/client/dist/index.html
vendored
4
web/client/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>美图 Agent</title>
|
<title>美图 Agent</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DSKEPThI.js"></script>
|
<script type="module" crossorigin src="/assets/index-Cgtxrl8D.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DXUyFc07.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-B_JK8LRt.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-zinc-900 antialiased">
|
<body class="bg-white text-zinc-900 antialiased">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -50,44 +50,60 @@ export function AccountBasicTab({ account, onSave }: Props) {
|
|||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-zinc-500">画幅</label>
|
<label className="text-xs font-medium text-zinc-500">画幅</label>
|
||||||
<select value={form.defaultFormat} onChange={(e) => handleChange('defaultFormat', e.target.value)}
|
<Select value={form.defaultFormat} onValueChange={(v) => handleChange('defaultFormat', v)}>
|
||||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm">
|
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||||
<option value="9:16">9:16 竖屏</option>
|
<SelectValue />
|
||||||
<option value="16:9">16:9 横屏</option>
|
</SelectTrigger>
|
||||||
<option value="1:1">1:1 方形</option>
|
<SelectContent>
|
||||||
<option value="4:3">4:3</option>
|
<SelectItem value="9:16">9:16 竖屏</SelectItem>
|
||||||
</select>
|
<SelectItem value="16:9">16:9 横屏</SelectItem>
|
||||||
|
<SelectItem value="1:1">1:1 方形</SelectItem>
|
||||||
|
<SelectItem value="4:3">4:3</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-zinc-500">生图模型</label>
|
<label className="text-xs font-medium text-zinc-500">生图模型</label>
|
||||||
<select value={form.imageModel} onChange={(e) => handleChange('imageModel', e.target.value)}
|
<Select value={form.imageModel} onValueChange={(v) => handleChange('imageModel', v)}>
|
||||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm">
|
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||||
<option value="gemini">Gemini</option>
|
<SelectValue />
|
||||||
<option value="mj">Midjourney</option>
|
</SelectTrigger>
|
||||||
<option value="gpt">GPT Image</option>
|
<SelectContent>
|
||||||
<option value="kling">Kling</option>
|
<SelectItem value="gemini">Gemini</SelectItem>
|
||||||
</select>
|
<SelectItem value="mj">Midjourney</SelectItem>
|
||||||
|
<SelectItem value="gpt">GPT Image</SelectItem>
|
||||||
|
<SelectItem value="kling">Kling</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-zinc-500">视频模型</label>
|
<label className="text-xs font-medium text-zinc-500">视频模型</label>
|
||||||
<select value={form.videoModel} onChange={(e) => handleChange('videoModel', e.target.value)}
|
<Select value={form.videoModel} onValueChange={(v) => handleChange('videoModel', v)}>
|
||||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm">
|
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||||
<option value="veo3-fast">Veo3 Fast</option>
|
<SelectValue />
|
||||||
<option value="veo3-fast-frames">Veo3 Frames</option>
|
</SelectTrigger>
|
||||||
<option value="kling">Kling</option>
|
<SelectContent>
|
||||||
<option value="grok">Grok</option>
|
<SelectItem value="veo3-fast">Veo3 Fast</SelectItem>
|
||||||
</select>
|
<SelectItem value="veo3-fast-frames">Veo3 Frames</SelectItem>
|
||||||
|
<SelectItem value="kling">Kling</SelectItem>
|
||||||
|
<SelectItem value="grok">Grok</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-zinc-500">Pipeline</label>
|
<label className="text-xs font-medium text-zinc-500">Pipeline</label>
|
||||||
<select value={form.pipeline} onChange={(e) => handleChange('pipeline', e.target.value)}
|
<Select value={form.pipeline} onValueChange={(v) => handleChange('pipeline', v)}>
|
||||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm">
|
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||||
<option value="image-video">图片+视频</option>
|
<SelectValue />
|
||||||
<option value="single">单图模式</option>
|
</SelectTrigger>
|
||||||
</select>
|
<SelectContent>
|
||||||
|
<SelectItem value="image-video">图片+视频</SelectItem>
|
||||||
|
<SelectItem value="single">单图模式</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-zinc-500">批量数量</label>
|
<label className="text-xs font-medium text-zinc-500">批量数量</label>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { RefreshCw, FolderOpen, Image as ImageIcon, Film } from 'lucide-react';
|
import { RefreshCw, FolderOpen, Image as ImageIcon, Film } from 'lucide-react';
|
||||||
import { useAssets } from '@/hooks/useAssets';
|
import { useAssets } from '@/hooks/useAssets';
|
||||||
|
import { useAccounts } from '@/hooks/useAccounts';
|
||||||
import { AssetProjectGroup } from './AssetProjectGroup';
|
import { AssetProjectGroup } from './AssetProjectGroup';
|
||||||
import { AssetPreview } from './AssetPreview';
|
import { AssetPreview } from './AssetPreview';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -36,15 +37,14 @@ function AssetGroupSkeleton() {
|
|||||||
export function AssetGallery() {
|
export function AssetGallery() {
|
||||||
const [accountFilter, setAccountFilter] = useState('');
|
const [accountFilter, setAccountFilter] = useState('');
|
||||||
const [typeTab, setTypeTab] = useState<TypeTab>('all');
|
const [typeTab, setTypeTab] = useState<TypeTab>('all');
|
||||||
const [accounts, setAccounts] = useState<{ id: string; name: string }[]>([]);
|
|
||||||
const [manifests, setManifests] = useState<ManifestGroup[]>([]);
|
const [manifests, setManifests] = useState<ManifestGroup[]>([]);
|
||||||
const [previewAsset, setPreviewAsset] = useState<Asset | null>(null);
|
const [previewAsset, setPreviewAsset] = useState<Asset | null>(null);
|
||||||
|
const { accounts } = useAccounts();
|
||||||
const { assets, loading, remove } = useAssets({
|
const { assets, loading, remove } = useAssets({
|
||||||
accountId: accountFilter || undefined,
|
accountId: accountFilter || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/accounts').then((r) => r.json()).then(setAccounts).catch(() => {});
|
|
||||||
fetch('/api/assets/manifests').then((r) => r.json()).then(setManifests).catch(() => {});
|
fetch('/api/assets/manifests').then((r) => r.json()).then(setManifests).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { Terminal, Image, Play, FileText, ArrowUp } from 'lucide-react';
|
import { Terminal, Image, Play, FileText, ArrowUp, X, Paperclip } from 'lucide-react';
|
||||||
|
|
||||||
const SLASH_COMMANDS = [
|
const SLASH_COMMANDS = [
|
||||||
{ cmd: '/run', desc: '执行 pipeline 阶段', icon: Play },
|
{ cmd: '/run', desc: '执行 pipeline 阶段', icon: Play },
|
||||||
@@ -9,32 +9,78 @@ const SLASH_COMMANDS = [
|
|||||||
{ cmd: '/help', desc: '显示帮助', icon: Terminal },
|
{ cmd: '/help', desc: '显示帮助', icon: Terminal },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ChatInput({ onSend, disabled }: { onSend: (content: string) => void; disabled?: boolean }) {
|
export interface ImageAttachment {
|
||||||
|
data: string;
|
||||||
|
mimeType: string;
|
||||||
|
preview: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatInput({ onSend, disabled }: { onSend: (content: string, images?: Array<{ data: string; mimeType: string }>) => void; disabled?: boolean }) {
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [showCmds, setShowCmds] = useState(false);
|
const [showCmds, setShowCmds] = useState(false);
|
||||||
const [cmdIdx, setCmdIdx] = useState(0);
|
const [cmdIdx, setCmdIdx] = useState(0);
|
||||||
const ref = useRef<HTMLTextAreaElement>(null);
|
const [images, setImages] = useState<ImageAttachment[]>([]);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
const textRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const matchingCmds = input.startsWith('/')
|
const matchingCmds = input.startsWith('/')
|
||||||
? SLASH_COMMANDS.filter((c) => c.cmd.startsWith(input.split(' ')[0]))
|
? SLASH_COMMANDS.filter((c) => c.cmd.startsWith(input.split(' ')[0]))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Auto-show/hide slash command menu
|
const readFiles = (files: File[]) => {
|
||||||
useEffect(() => {
|
if (files.length === 0) return;
|
||||||
if (input.startsWith('/') && matchingCmds.length > 0) {
|
let loaded = 0;
|
||||||
setShowCmds(true);
|
const results: ImageAttachment[] = [];
|
||||||
setCmdIdx(0);
|
files.forEach((file, i) => {
|
||||||
} else {
|
const reader = new FileReader();
|
||||||
setShowCmds(false);
|
reader.onload = () => {
|
||||||
}
|
const base64 = (reader.result as string).split(',')[1];
|
||||||
}, [input, matchingCmds.length]);
|
results[i] = { data: base64, mimeType: file.type, preview: reader.result as string };
|
||||||
|
loaded++;
|
||||||
|
if (loaded === files.length) {
|
||||||
|
setImages((prev) => [...prev, ...results]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files) readFiles(Array.from(e.target.files));
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!dragging) setDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragging(false);
|
||||||
|
const imageFiles = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'));
|
||||||
|
if (imageFiles.length > 0) readFiles(imageFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeImage = (idx: number) => {
|
||||||
|
setImages((prev) => prev.filter((_, i) => i !== idx));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (!input.trim() || disabled) return;
|
const text = input.trim();
|
||||||
onSend(input.trim());
|
if ((!text && images.length === 0) || disabled) return;
|
||||||
|
const imgs = images.length > 0 ? images.map(({ data, mimeType }) => ({ data, mimeType })) : undefined;
|
||||||
|
onSend(text || '(图片)', imgs);
|
||||||
setInput('');
|
setInput('');
|
||||||
|
setImages([]);
|
||||||
setShowCmds(false);
|
setShowCmds(false);
|
||||||
ref.current?.focus();
|
textRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
@@ -50,25 +96,25 @@ export function ChatInput({ onSend, disabled }: { onSend: (content: string) => v
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const canSend = input.trim().length > 0 && !disabled;
|
const canSend = (input.trim().length > 0 || images.length > 0) && !disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Slash command menu */}
|
{/* Slash command menu */}
|
||||||
{showCmds && matchingCmds.length > 0 && (
|
{showCmds && matchingCmds.length > 0 && (
|
||||||
<div className="mx-6 mb-0.5 bg-white border border-zinc-200 rounded-xl shadow-lg overflow-hidden animate-in slide-in-from-bottom-2">
|
<div className="mx-6 mb-1 bg-white border border-zinc-200 rounded-2xl shadow-xl shadow-zinc-200/50 overflow-hidden animate-in slide-in-from-bottom-2">
|
||||||
{matchingCmds.map((c, i) => {
|
{matchingCmds.map((c, i) => {
|
||||||
const Icon = c.icon;
|
const Icon = c.icon;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={c.cmd}
|
key={c.cmd}
|
||||||
onClick={() => { setInput(c.cmd + ' '); setShowCmds(false); ref.current?.focus(); }}
|
onClick={() => { setInput(c.cmd + ' '); setShowCmds(false); textRef.current?.focus(); }}
|
||||||
className={`w-full flex items-center gap-2.5 px-3.5 py-2 text-sm transition-colors ${
|
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
||||||
i === (cmdIdx % matchingCmds.length) ? 'bg-indigo-50 text-indigo-700' : 'text-zinc-600 hover:bg-zinc-50'
|
i === (cmdIdx % matchingCmds.length) ? 'bg-zinc-50 text-zinc-900' : 'text-zinc-500 hover:bg-zinc-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon size={15} className="text-zinc-400" />
|
<Icon size={15} className="text-zinc-400" />
|
||||||
<span className="font-mono font-semibold text-xs">{c.cmd}</span>
|
<span className="font-mono font-medium text-xs">{c.cmd}</span>
|
||||||
<span className="text-zinc-400 text-xs">{c.desc}</span>
|
<span className="text-zinc-400 text-xs">{c.desc}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -76,44 +122,96 @@ export function ChatInput({ onSend, disabled }: { onSend: (content: string) => v
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input bar */}
|
{/* Input area */}
|
||||||
<div className="px-4 pb-4 pt-2 bg-gradient-to-t from-white via-white to-transparent">
|
<div
|
||||||
|
className="px-5 pb-5 pt-3"
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
<div className={`
|
<div className={`
|
||||||
flex items-end gap-2 bg-zinc-50 rounded-2xl border transition-all duration-200 px-4 py-3
|
bg-white rounded-3xl border transition-all duration-300 relative
|
||||||
${disabled ? 'border-zinc-200 opacity-60' : 'border-zinc-200 focus-within:border-indigo-300 focus-within:shadow-md focus-within:shadow-indigo-50 focus-within:bg-white'}
|
${dragging
|
||||||
|
? 'border-zinc-400 shadow-xl shadow-zinc-200/60'
|
||||||
|
: disabled
|
||||||
|
? 'border-zinc-100 opacity-50'
|
||||||
|
: 'border-zinc-200/80 shadow-lg shadow-zinc-100/50 focus-within:border-zinc-300 focus-within:shadow-xl focus-within:shadow-zinc-200/40'
|
||||||
|
}
|
||||||
`}>
|
`}>
|
||||||
<textarea
|
{/* Drop overlay */}
|
||||||
ref={ref}
|
{dragging && (
|
||||||
value={input}
|
<div className="absolute inset-0 rounded-3xl bg-zinc-50/80 backdrop-blur-sm z-10 flex items-center justify-center pointer-events-none">
|
||||||
onChange={(e) => setInput(e.target.value)}
|
<span className="text-sm text-zinc-400 font-medium">松开以添加图片</span>
|
||||||
onKeyDown={handleKeyDown}
|
</div>
|
||||||
rows={1}
|
)}
|
||||||
placeholder={disabled ? '等待回复中...' : '输入消息,或按 / 查看快捷命令...'}
|
{/* Image preview strip */}
|
||||||
className="flex-1 bg-transparent text-sm resize-none outline-none placeholder:text-zinc-400 text-zinc-700 min-h-[24px] max-h-[120px]"
|
{images.length > 0 && (
|
||||||
disabled={disabled}
|
<div className="flex flex-wrap gap-2.5 px-5 pt-4">
|
||||||
style={{ height: 'auto' }}
|
{images.map((img, i) => (
|
||||||
onInput={(e) => {
|
<div key={i} className="relative group/att">
|
||||||
const el = e.currentTarget;
|
<img src={img.preview} alt="" className="w-20 h-20 object-cover rounded-2xl shadow-sm" />
|
||||||
el.style.height = 'auto';
|
<button
|
||||||
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
onClick={() => removeImage(i)}
|
||||||
}}
|
className="absolute -top-2 -right-2 w-5 h-5 bg-zinc-900 text-white rounded-full flex items-center justify-center opacity-0 group-hover/att:opacity-100 transition-all duration-200 scale-75 group-hover/att:scale-100"
|
||||||
/>
|
>
|
||||||
<button
|
<X size={10} strokeWidth={3} />
|
||||||
onClick={handleSend}
|
</button>
|
||||||
disabled={!canSend}
|
</div>
|
||||||
className={`
|
))}
|
||||||
w-8 h-8 rounded-xl flex items-center justify-center transition-all flex-shrink-0
|
</div>
|
||||||
${canSend
|
)}
|
||||||
? 'bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm'
|
|
||||||
: 'bg-zinc-200 text-zinc-400 cursor-not-allowed'}
|
{/* Input row */}
|
||||||
`}
|
<div className="flex items-end gap-1 px-3 py-3">
|
||||||
>
|
{/* Attachment button */}
|
||||||
<ArrowUp size={16} strokeWidth={2.5} />
|
<button
|
||||||
</button>
|
onClick={() => fileRef.current?.click()}
|
||||||
|
disabled={disabled}
|
||||||
|
className="flex-shrink-0 w-10 h-10 rounded-2xl flex items-center justify-center text-zinc-300 hover:text-zinc-500 hover:bg-zinc-50 active:scale-95 transition-all duration-200 disabled:opacity-40"
|
||||||
|
>
|
||||||
|
<Paperclip size={20} strokeWidth={1.8} />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
ref={textRef}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
rows={1}
|
||||||
|
placeholder={disabled ? '等待回复中...' : '输入消息...'}
|
||||||
|
className="flex-1 bg-transparent text-[15px] resize-none outline-none placeholder:text-zinc-300 text-zinc-800 min-h-[40px] max-h-[140px] py-2 leading-relaxed"
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ height: 'auto' }}
|
||||||
|
onInput={(e) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
el.style.height = 'auto';
|
||||||
|
el.style.height = Math.min(el.scrollHeight, 140) + 'px';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Send button */}
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!canSend}
|
||||||
|
className={`
|
||||||
|
flex-shrink-0 w-10 h-10 rounded-2xl flex items-center justify-center transition-all duration-200
|
||||||
|
${canSend
|
||||||
|
? 'bg-zinc-900 text-white hover:bg-zinc-800 active:scale-90'
|
||||||
|
: 'bg-zinc-100 text-zinc-300'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<ArrowUp size={18} strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-zinc-400 text-center mt-1.5">
|
|
||||||
美图 Agent 可能会产生错误内容,请核实重要信息
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -169,18 +169,18 @@ export function ConfigForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex gap-0 border-b border-zinc-200 px-6 bg-white">
|
<div className="flex gap-1 px-6 pt-4 bg-white">
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab('agent')}
|
onClick={() => setTab('agent')}
|
||||||
className={`px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-150
|
||||||
${tab === 'agent' ? 'border-indigo-600 text-indigo-600' : 'border-transparent text-zinc-500 hover:text-zinc-700'}`}
|
${tab === 'agent' ? 'bg-zinc-900 text-white shadow-sm' : 'text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100'}`}
|
||||||
>
|
>
|
||||||
Agent 配置
|
Agent 配置
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab('skills')}
|
onClick={() => setTab('skills')}
|
||||||
className={`px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-150
|
||||||
${tab === 'skills' ? 'border-indigo-600 text-indigo-600' : 'border-transparent text-zinc-500 hover:text-zinc-700'}`}
|
${tab === 'skills' ? 'bg-zinc-900 text-white shadow-sm' : 'text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100'}`}
|
||||||
>
|
>
|
||||||
工具配置
|
工具配置
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ const SelectTrigger = React.forwardRef<
|
|||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 w-full items-center justify-between rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 shadow-sm transition-colors placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-indigo-200 focus:border-indigo-300 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
'flex h-9 w-full items-center justify-between rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 transition-colors hover:border-zinc-300 focus:outline-none focus:ring-2 focus:ring-indigo-200 focus:border-indigo-300 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDown size={14} className="text-zinc-400" />
|
<ChevronDown size={14} className="text-zinc-400 transition-transform duration-200" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
));
|
));
|
||||||
@@ -35,7 +35,9 @@ const SelectContent = React.forwardRef<
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-50 max-h-72 min-w-[8rem] overflow-hidden rounded-lg border border-zinc-200 bg-white text-zinc-700 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'relative z-50 max-h-72 min-w-[8rem] overflow-hidden rounded-lg border border-zinc-200 bg-white text-zinc-700 shadow-lg',
|
||||||
|
'data-[state=open]:opacity-100 data-[state=open]:scale-100 data-[state=closed]:opacity-0 data-[state=closed]:scale-95',
|
||||||
|
'transition-[opacity,transform] duration-150',
|
||||||
position === 'popper' && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
position === 'popper' && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -62,7 +64,7 @@ const SelectItem = React.forwardRef<
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-indigo-50 focus:text-indigo-700 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
'relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-indigo-50 focus:text-indigo-700 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user