feat(web): 将原生 select 替换为 shadcn/ui Select 组件并支持图片消息发送
- 在 AccountForm、AssetGallery、ChatView、ConfigForm、PromptEditor 中将 `<select>` 替换为 shadcn/ui 的 Select 组件以统一 UI 风格 - 在 ChatView 和 useChat hook 中支持发送图片附件 - 更新 pi-bridge 和 ws/chat 以处理 agent 调用中的图片数据
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import type { Account } from '@/types';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import type { Account } from '@/types';
|
||||
|
||||
interface Props {
|
||||
@@ -64,41 +65,44 @@ export function AccountForm({ account, onSave, onDelete, onCancel }: Props) {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">画幅</label>
|
||||
<select
|
||||
value={form.defaultFormat}
|
||||
onChange={(e) => handleChange('defaultFormat', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm"
|
||||
>
|
||||
<option value="9:16">9:16 竖屏</option>
|
||||
<option value="16:9">16:9 横屏</option>
|
||||
<option value="1:1">1:1 方形</option>
|
||||
</select>
|
||||
<Select value={form.defaultFormat} onValueChange={(v) => handleChange('defaultFormat', v)}>
|
||||
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="9:16">9:16 竖屏</SelectItem>
|
||||
<SelectItem value="16:9">16:9 横屏</SelectItem>
|
||||
<SelectItem value="1:1">1:1 方形</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">生图模型</label>
|
||||
<select
|
||||
value={form.imageModel}
|
||||
onChange={(e) => handleChange('imageModel', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm"
|
||||
>
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="mj">Midjourney</option>
|
||||
<option value="gpt">GPT Image</option>
|
||||
<option value="kling">Kling</option>
|
||||
</select>
|
||||
<Select value={form.imageModel} onValueChange={(v) => handleChange('imageModel', v)}>
|
||||
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gemini">Gemini</SelectItem>
|
||||
<SelectItem value="mj">Midjourney</SelectItem>
|
||||
<SelectItem value="gpt">GPT Image</SelectItem>
|
||||
<SelectItem value="kling">Kling</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">视频模型</label>
|
||||
<select
|
||||
value={form.videoModel}
|
||||
onChange={(e) => handleChange('videoModel', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm"
|
||||
>
|
||||
<option value="veo3-fast">Veo3 Fast</option>
|
||||
<option value="veo3-fast-frames">Veo3 Fast Frames</option>
|
||||
<option value="kling">Kling</option>
|
||||
<option value="grok">Grok</option>
|
||||
</select>
|
||||
<Select value={form.videoModel} onValueChange={(v) => handleChange('videoModel', v)}>
|
||||
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="veo3-fast">Veo3 Fast</SelectItem>
|
||||
<SelectItem value="veo3-fast-frames">Veo3 Fast Frames</SelectItem>
|
||||
<SelectItem value="kling">Kling</SelectItem>
|
||||
<SelectItem value="grok">Grok</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAssets } from '@/hooks/useAssets';
|
||||
import { AssetProjectGroup } from './AssetProjectGroup';
|
||||
import { AssetPreview } from './AssetPreview';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import type { Asset } from '@/types';
|
||||
|
||||
interface ManifestGroup {
|
||||
@@ -125,16 +126,17 @@ export function AssetGallery() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={accountFilter}
|
||||
onChange={(e) => setAccountFilter(e.target.value)}
|
||||
className="h-8 rounded-lg border border-zinc-100 bg-zinc-50/80 px-2 text-xs hover:border-zinc-200 transition-colors"
|
||||
>
|
||||
<option value="">全部账号</option>
|
||||
{accounts.map((a) => (
|
||||
<option key={a.id} value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<Select value={accountFilter || 'all'} onValueChange={(v) => setAccountFilter(v === 'all' ? '' : v)}>
|
||||
<SelectTrigger className="h-8 w-auto min-w-[100px] text-xs border-zinc-100 bg-zinc-50/80 hover:border-zinc-200">
|
||||
<SelectValue placeholder="全部账号" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部账号</SelectItem>
|
||||
{accounts.map((a) => (
|
||||
<SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="flex-1" />
|
||||
<Button size="sm" variant="outline" className="h-8 text-xs rounded-lg" onClick={handleScan}>
|
||||
|
||||
@@ -126,10 +126,10 @@ export function ChatView() {
|
||||
creatingRef.current = false;
|
||||
}, [createConversation, selectedAccountId]);
|
||||
|
||||
const handleSend = useCallback((content: string) => {
|
||||
const handleSend = useCallback((content: string, images?: Array<{ data: string; mimeType: string }>) => {
|
||||
if (quote) { content = `> ${quote}\n\n${content}`; setQuote(null); }
|
||||
if (conversationId) {
|
||||
send(content);
|
||||
send(content, images);
|
||||
} else {
|
||||
handleSendNew(content);
|
||||
}
|
||||
@@ -154,14 +154,18 @@ export function ChatView() {
|
||||
{accounts.length > 0 && (
|
||||
<div className="flex items-center gap-2 mt-3 justify-center">
|
||||
<span className="text-xs text-zinc-400">当前账号:</span>
|
||||
<select
|
||||
<Select
|
||||
value={selectedAccountId || ''}
|
||||
onChange={(e) => useAppStore.getState().setSelectedAccountId(e.target.value || null)}
|
||||
className="h-7 rounded-lg border border-zinc-100 bg-white px-2.5 text-xs text-zinc-600 hover:border-zinc-200 transition-colors"
|
||||
onValueChange={(v) => useAppStore.getState().setSelectedAccountId(v === 'none' ? null : v)}
|
||||
>
|
||||
<option value="">不指定</option>
|
||||
{accounts.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
|
||||
</select>
|
||||
<SelectTrigger className="h-7 w-auto min-w-[100px] text-xs px-2.5 border-zinc-100 hover:border-zinc-200">
|
||||
<SelectValue placeholder="不指定" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">不指定</SelectItem>
|
||||
{accounts.map((a) => <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{recentConvs.length > 0 && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import { api } from '@/lib/api';
|
||||
import { Save, Check, Loader2, TestTube2 } from 'lucide-react';
|
||||
|
||||
@@ -272,35 +273,47 @@ export function ConfigForm() {
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">生图模型</label>
|
||||
<select value={agentForm.defaultImageModel} onChange={(e) => handleAgentChange('defaultImageModel', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm">
|
||||
<option value="">未设置</option>
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="mj">Midjourney</option>
|
||||
<option value="gpt">GPT Image</option>
|
||||
<option value="kling">Kling</option>
|
||||
</select>
|
||||
<Select value={agentForm.defaultImageModel || 'none'} onValueChange={(v) => handleAgentChange('defaultImageModel', v === 'none' ? '' : v)}>
|
||||
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||
<SelectValue placeholder="未设置" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">未设置</SelectItem>
|
||||
<SelectItem value="gemini">Gemini</SelectItem>
|
||||
<SelectItem value="mj">Midjourney</SelectItem>
|
||||
<SelectItem value="gpt">GPT Image</SelectItem>
|
||||
<SelectItem value="kling">Kling</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">视频模型</label>
|
||||
<select value={agentForm.defaultVideoModel} onChange={(e) => handleAgentChange('defaultVideoModel', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm">
|
||||
<option value="">未设置</option>
|
||||
<option value="veo3-fast">Veo3 Fast</option>
|
||||
<option value="veo3-fast-frames">Veo3 Fast Frames</option>
|
||||
<option value="kling">Kling</option>
|
||||
<option value="grok">Grok</option>
|
||||
</select>
|
||||
<Select value={agentForm.defaultVideoModel || 'none'} onValueChange={(v) => handleAgentChange('defaultVideoModel', v === 'none' ? '' : v)}>
|
||||
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||
<SelectValue placeholder="未设置" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">未设置</SelectItem>
|
||||
<SelectItem value="veo3-fast">Veo3 Fast</SelectItem>
|
||||
<SelectItem value="veo3-fast-frames">Veo3 Fast Frames</SelectItem>
|
||||
<SelectItem value="kling">Kling</SelectItem>
|
||||
<SelectItem value="grok">Grok</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">画幅</label>
|
||||
<select value={agentForm.defaultFormat} onChange={(e) => handleAgentChange('defaultFormat', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm">
|
||||
<option value="">未设置</option>
|
||||
<option value="9:16">9:16 竖屏</option>
|
||||
<option value="16:9">16:9 横屏</option>
|
||||
<option value="1:1">1:1 方形</option>
|
||||
</select>
|
||||
<Select value={agentForm.defaultFormat || 'none'} onValueChange={(v) => handleAgentChange('defaultFormat', v === 'none' ? '' : v)}>
|
||||
<SelectTrigger className="mt-1 w-full h-10 text-sm bg-zinc-50 border-zinc-200">
|
||||
<SelectValue placeholder="未设置" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">未设置</SelectItem>
|
||||
<SelectItem value="9:16">9:16 竖屏</SelectItem>
|
||||
<SelectItem value="16:9">16:9 横屏</SelectItem>
|
||||
<SelectItem value="1:1">1:1 方形</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'prismjs/themes/prism.css';
|
||||
import { useAccounts } from '@/hooks/useAccounts';
|
||||
import { usePrompts } from '@/hooks/usePrompts';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
|
||||
const PROMPT_TYPES = [
|
||||
{ type: 'storyboard', label: '分镜' },
|
||||
@@ -54,15 +55,16 @@ export function PromptEditor({ accountId: accountIdProp }: { accountId?: string
|
||||
<div className="w-48 border-r border-zinc-200 p-3 space-y-3 bg-zinc-50">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">账户</label>
|
||||
<select
|
||||
value={selectedAccount}
|
||||
onChange={(e) => setSelectedAccount(e.target.value)}
|
||||
className="mt-1 w-full h-9 rounded-md border border-zinc-200 bg-white px-2 text-sm text-zinc-700"
|
||||
>
|
||||
{accounts.map((a) => (
|
||||
<option key={a.id} value={a.id}>{a.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<Select value={selectedAccount} onValueChange={setSelectedAccount}>
|
||||
<SelectTrigger className="mt-1 w-full h-9 text-sm">
|
||||
<SelectValue placeholder="选择账户" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{accounts.map((a) => (
|
||||
<SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">模板</label>
|
||||
|
||||
@@ -150,8 +150,8 @@ export function useChat(conversationId: string | null) {
|
||||
}
|
||||
}, [conversationId, connected]);
|
||||
|
||||
const send = useCallback((content: string) => {
|
||||
chatSocket.send('chat', { content });
|
||||
const send = useCallback((content: string, images?: Array<{ data: string; mimeType: string }>) => {
|
||||
chatSocket.send('chat', { content, images });
|
||||
}, []);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user