优化画布视频首尾帧入口与工具栏布局
This commit is contained in:
parent
2aa2aaf0e3
commit
34dbb11dde
@ -300,6 +300,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
const [isSessionMenuOpen, setIsSessionMenuOpen] = useState(false);
|
const [isSessionMenuOpen, setIsSessionMenuOpen] = useState(false);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const videoSlotInputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const hiddenPromptRef = useRef<string | null>(null);
|
const hiddenPromptRef = useRef<string | null>(null);
|
||||||
const autoContextRef = useRef<string | null>(null);
|
const autoContextRef = useRef<string | null>(null);
|
||||||
@ -321,6 +322,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
imageCount: 1,
|
imageCount: 1,
|
||||||
});
|
});
|
||||||
const [videoParams, setVideoParams] = useState<VideoGenerationParams | null>(null);
|
const [videoParams, setVideoParams] = useState<VideoGenerationParams | null>(null);
|
||||||
|
const [videoSlotUploading, setVideoSlotUploading] = useState<number | null>(null);
|
||||||
|
|
||||||
// 🆕 保存图片参数到 localStorage
|
// 🆕 保存图片参数到 localStorage
|
||||||
const handleImageParamsChange = (params: ImageGenerationParams) => {
|
const handleImageParamsChange = (params: ImageGenerationParams) => {
|
||||||
@ -864,6 +866,12 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
const allModels = [SMART_AUTO_MODEL, ...visualModels];
|
const allModels = [SMART_AUTO_MODEL, ...visualModels];
|
||||||
const currentModel = allModels.find((model) => model.id === selectedModelId);
|
const currentModel = allModels.find((model) => model.id === selectedModelId);
|
||||||
const isVideoModel = Boolean(currentModel && VIDEO_PROVIDER_TYPES.has(currentModel.providerType));
|
const isVideoModel = Boolean(currentModel && VIDEO_PROVIDER_TYPES.has(currentModel.providerType));
|
||||||
|
const videoFrameSlots = useMemo(() => {
|
||||||
|
if (!isVideoModel || !videoParams) return [] as string[];
|
||||||
|
if (videoParams.mode === "first-last-frame") return ["首帧", "尾帧"] as string[];
|
||||||
|
if (videoParams.mode === "first-frame") return ["首帧"] as string[];
|
||||||
|
return [] as string[];
|
||||||
|
}, [isVideoModel, videoParams]);
|
||||||
|
|
||||||
// 默认选择模型(根据系统设置)
|
// 默认选择模型(根据系统设置)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -903,6 +911,22 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
}
|
}
|
||||||
}, [isVideoModel, currentModel?.providerType, videoParams]);
|
}, [isVideoModel, currentModel?.providerType, videoParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVideoModel || videoFrameSlots.length === 0) return;
|
||||||
|
const maxAttachments = videoFrameSlots.length;
|
||||||
|
setAttachments((current) => {
|
||||||
|
if (current.length <= maxAttachments) return current;
|
||||||
|
return current.slice(0, maxAttachments);
|
||||||
|
});
|
||||||
|
}, [isVideoModel, videoFrameSlots.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoFrameSlots.length === 0) return;
|
||||||
|
const editableDiv = textareaRef.current as HTMLDivElement | null;
|
||||||
|
if (!editableDiv) return;
|
||||||
|
editableDiv.querySelectorAll("[data-attachment-url]").forEach((element) => element.remove());
|
||||||
|
}, [videoFrameSlots.length]);
|
||||||
|
|
||||||
// 合并 initialMessages 和历史消息
|
// 合并 initialMessages 和历史消息
|
||||||
const mergedInitialMessages = historyData?.messages ?? (activeChatId === chatId ? initialMessages : []);
|
const mergedInitialMessages = historyData?.messages ?? (activeChatId === chatId ? initialMessages : []);
|
||||||
|
|
||||||
@ -1388,6 +1412,96 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}, [nodes, selectedImageNodes]);
|
}, [nodes, selectedImageNodes]);
|
||||||
|
|
||||||
|
const updateAttachmentAtVideoSlot = useCallback(
|
||||||
|
(slotIndex: number, attachment: PickedObjectAttachment | null) => {
|
||||||
|
const slotCount = Math.max(videoFrameSlots.length, 2);
|
||||||
|
setAttachments((current) => {
|
||||||
|
const slots: Array<PickedObjectAttachment | null> = Array.from(
|
||||||
|
{ length: slotCount },
|
||||||
|
(_, index) => current[index] ?? null
|
||||||
|
);
|
||||||
|
slots[slotIndex] = attachment;
|
||||||
|
while (slots.length > 0 && !slots[slots.length - 1]) {
|
||||||
|
slots.pop();
|
||||||
|
}
|
||||||
|
return slots.filter((item): item is PickedObjectAttachment => Boolean(item));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[videoFrameSlots.length]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleVideoSlotFileChange = useCallback(
|
||||||
|
async (slotIndex: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.type.startsWith("image/")) {
|
||||||
|
toast.error("请上传图片文件");
|
||||||
|
event.currentTarget.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 50 * 1024 * 1024) {
|
||||||
|
toast.error("图片大小不能超过 50MB");
|
||||||
|
event.currentTarget.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVideoSlotUploading(slotIndex);
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
const response = await fetch("/api/files/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const imageUrl = data.url as string;
|
||||||
|
|
||||||
|
updateAttachmentAtVideoSlot(slotIndex, {
|
||||||
|
url: imageUrl,
|
||||||
|
name: file.name,
|
||||||
|
contentType: file.type || "image/jpeg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const dimensions = await getImageDimensions(imageUrl);
|
||||||
|
const nodeIndex = nodes.length + slotIndex;
|
||||||
|
const spacing = 20;
|
||||||
|
const x = 100 + (nodeIndex % 3) * (dimensions.width + spacing);
|
||||||
|
const y = 100 + Math.floor(nodeIndex / 3) * (dimensions.height + spacing);
|
||||||
|
addNode({
|
||||||
|
type: "image",
|
||||||
|
position: { x, y },
|
||||||
|
size: { width: dimensions.width, height: dimensions.height },
|
||||||
|
data: {
|
||||||
|
src: imageUrl,
|
||||||
|
status: "completed",
|
||||||
|
title: file.name.replace(/\.[^.]+$/, ""),
|
||||||
|
prompt: file.name.replace(/\.[^.]+$/, ""),
|
||||||
|
metadata: {
|
||||||
|
originalWidth: dimensions.originalWidth,
|
||||||
|
originalHeight: dimensions.originalHeight,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
connections: [],
|
||||||
|
});
|
||||||
|
toast.success(`${slotIndex === 0 ? "首帧" : "尾帧"}上传成功`);
|
||||||
|
} catch (_error) {
|
||||||
|
toast.error("图片上传失败");
|
||||||
|
} finally {
|
||||||
|
setVideoSlotUploading(null);
|
||||||
|
event.currentTarget.value = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addNode, nodes.length, updateAttachmentAtVideoSlot]
|
||||||
|
);
|
||||||
|
|
||||||
// 处理发送(图片已在上传时添加到画布,这里只发送消息)
|
// 处理发送(图片已在上传时添加到画布,这里只发送消息)
|
||||||
const handleSend = useCallback(async () => {
|
const handleSend = useCallback(async () => {
|
||||||
if (!input.trim() && attachments.length === 0) return;
|
if (!input.trim() && attachments.length === 0) return;
|
||||||
@ -1408,7 +1522,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
if (
|
if (
|
||||||
effectiveAttachments.length === 0 &&
|
effectiveAttachments.length === 0 &&
|
||||||
input.trim() &&
|
input.trim() &&
|
||||||
(!videoImageLimit || videoImageLimit.max > 0)
|
(!videoImageLimit || videoImageLimit.max > 0) &&
|
||||||
|
videoFrameSlots.length === 0
|
||||||
) {
|
) {
|
||||||
const recentAttachments = getRecentImageAttachments();
|
const recentAttachments = getRecentImageAttachments();
|
||||||
if (recentAttachments.length > 0) {
|
if (recentAttachments.length > 0) {
|
||||||
@ -1546,6 +1661,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
buildAutoContext,
|
buildAutoContext,
|
||||||
isVideoModel,
|
isVideoModel,
|
||||||
videoParams,
|
videoParams,
|
||||||
|
videoFrameSlots.length,
|
||||||
currentModel?.providerType,
|
currentModel?.providerType,
|
||||||
selectedImageNodes,
|
selectedImageNodes,
|
||||||
]);
|
]);
|
||||||
@ -1761,6 +1877,15 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isLoading = status === 'streaming' || status === 'submitted';
|
const isLoading = status === 'streaming' || status === 'submitted';
|
||||||
|
const currentVideoImageLimit = isVideoModel
|
||||||
|
? getVideoImageLimit(videoParams, currentModel?.providerType)
|
||||||
|
: null;
|
||||||
|
const showVideoAttachmentButton = Boolean(
|
||||||
|
isVideoModel &&
|
||||||
|
currentVideoImageLimit &&
|
||||||
|
currentVideoImageLimit.max > 0 &&
|
||||||
|
videoFrameSlots.length === 0
|
||||||
|
);
|
||||||
const lastUserIndexForMarkers = messages.findLastIndex(m => m.role === "user");
|
const lastUserIndexForMarkers = messages.findLastIndex(m => m.role === "user");
|
||||||
const messagesAfterLastUser = lastUserIndexForMarkers >= 0 ? messages.slice(lastUserIndexForMarkers + 1) : messages;
|
const messagesAfterLastUser = lastUserIndexForMarkers >= 0 ? messages.slice(lastUserIndexForMarkers + 1) : messages;
|
||||||
const hasGenerationStart = messagesAfterLastUser.some(msg =>
|
const hasGenerationStart = messagesAfterLastUser.some(msg =>
|
||||||
@ -2235,7 +2360,12 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
data-canvas-chat-input-anchor="true"
|
data-canvas-chat-input-anchor="true"
|
||||||
>
|
>
|
||||||
{/* 混合输入区域:使用 contenteditable 实现真正的混合编辑 */}
|
{/* 混合输入区域:使用 contenteditable 实现真正的混合编辑 */}
|
||||||
<div className="relative px-4 pt-3 pb-12 min-h-[60px]">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative px-4 pt-3 min-h-[60px]",
|
||||||
|
isVideoModel && videoFrameSlots.length > 0 ? "pb-40" : "pb-12"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={textareaRef as any}
|
ref={textareaRef as any}
|
||||||
contentEditable={!isLoading}
|
contentEditable={!isLoading}
|
||||||
@ -2252,7 +2382,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.trim();
|
.trim();
|
||||||
setInput(textContent);
|
setInput(textContent);
|
||||||
syncAttachmentsWithEditor(target);
|
if (!isVideoModel || videoFrameSlots.length === 0) {
|
||||||
|
syncAttachmentsWithEditor(target);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
@ -2260,7 +2392,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
handleSend();
|
handleSend();
|
||||||
}
|
}
|
||||||
// 处理 Backspace 删除图片标签(只删除聊天框中的标签,不影响画布)
|
// 处理 Backspace 删除图片标签(只删除聊天框中的标签,不影响画布)
|
||||||
if (e.key === 'Backspace') {
|
if (e.key === 'Backspace' && (!isVideoModel || videoFrameSlots.length === 0)) {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection && selection.rangeCount > 0) {
|
if (selection && selection.rangeCount > 0) {
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
@ -2287,6 +2419,60 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isVideoModel && videoFrameSlots.length > 0 && (
|
||||||
|
<div className="px-4 pb-2">
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
{videoFrameSlots.map((label, index) => {
|
||||||
|
const slotAttachment = attachments[index];
|
||||||
|
const isUploading = videoSlotUploading === index;
|
||||||
|
const isDisabled = isLoading || (index > 0 && !attachments[index - 1]);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`canvas-video-slot-${label}-${index}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
videoSlotInputRefs.current[index]?.click();
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"group relative flex h-24 w-24 shrink-0 items-center justify-center overflow-hidden rounded-xl border bg-muted/30 text-muted-foreground transition-colors",
|
||||||
|
!isDisabled && "cursor-pointer hover:border-primary/60 hover:bg-muted/50",
|
||||||
|
isDisabled && "cursor-not-allowed opacity-55"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{slotAttachment ? (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src={slotAttachment.url}
|
||||||
|
alt={slotAttachment.name || label}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-x-0 bottom-0 bg-black/55 px-1 py-0.5 text-center text-[11px] text-white">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute right-1 top-1 rounded-full bg-black/65 px-1 text-[11px] text-white opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
updateAttachmentAtVideoSlot(index, null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
x
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<span className="text-xl leading-none">+</span>
|
||||||
|
<span className="text-sm">{isUploading ? "上传中" : label}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 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">
|
||||||
@ -2299,99 +2485,90 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
{isVideoModel ? (
|
{videoFrameSlots.map((_, index) => (
|
||||||
<>
|
<input
|
||||||
<Button
|
key={`canvas-video-slot-input-${index}`}
|
||||||
type="button"
|
type="file"
|
||||||
variant="ghost"
|
accept="image/*"
|
||||||
className="h-8 rounded-full border border-blue-500 bg-blue-50 px-3 text-blue-600 hover:bg-blue-100"
|
className="hidden"
|
||||||
>
|
ref={(element) => {
|
||||||
<Video className="mr-1 h-4 w-4" />
|
videoSlotInputRefs.current[index] = element;
|
||||||
<span className="text-sm font-medium">视频</span>
|
}}
|
||||||
</Button>
|
onChange={(event) => {
|
||||||
<div className="shrink-0">
|
void handleVideoSlotFileChange(index, event);
|
||||||
<VideoParamsBar
|
}}
|
||||||
modelName={currentModel?.displayName}
|
/>
|
||||||
initialParams={videoParams || undefined}
|
))}
|
||||||
onParamsChange={handleVideoParamsChange}
|
<Button
|
||||||
inline={true}
|
type="button"
|
||||||
providerType={currentModel?.providerType}
|
variant="ghost"
|
||||||
/>
|
className={cn(
|
||||||
</div>
|
"h-8 rounded-full px-3 text-sm font-medium",
|
||||||
</>
|
isVideoModel
|
||||||
) : (
|
? "border border-blue-500 bg-blue-50 text-blue-600 hover:bg-blue-100"
|
||||||
<>
|
: "border border-emerald-500 bg-emerald-50 text-emerald-700 hover:bg-emerald-100"
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
)}
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
{isVideoModel ? (
|
||||||
size="icon"
|
<Video className="mr-1 h-4 w-4" />
|
||||||
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()}
|
<ImagePlus className="mr-1 h-4 w-4" />
|
||||||
disabled={isLoading}
|
)}
|
||||||
>
|
{isVideoModel ? "视频" : "生图"}
|
||||||
<Paperclip className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
|
||||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
{(!isVideoModel || showVideoAttachmentButton) && (
|
||||||
<SelectTrigger className={cn(
|
<Button
|
||||||
"h-8 text-xs transition-colors rounded-full px-2 gap-1.5 w-auto flex-shrink-0",
|
variant="ghost"
|
||||||
selectedModelId === "auto"
|
size="icon"
|
||||||
? "bg-gradient-to-r from-purple-50 to-blue-50 text-purple-600 border-purple-100 hover:from-purple-100 hover:to-blue-100"
|
className="h-8 w-8 rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
: "bg-blue-50 text-blue-600 border-blue-100 hover:bg-blue-100 hover:border-blue-200"
|
onClick={() => fileInputRef.current?.click()}
|
||||||
)}>
|
disabled={isLoading}
|
||||||
<span className="h-5 w-5 overflow-hidden rounded-full border border-blue-200 bg-white">
|
>
|
||||||
{allModels.find((m) => m.id === selectedModelId)?.avatarUrl ? (
|
<Paperclip className="h-4 w-4" />
|
||||||
<img
|
</Button>
|
||||||
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 className="shrink-0">
|
||||||
|
{isVideoModel ? (
|
||||||
|
<VideoParamsBar
|
||||||
|
modelName={currentModel?.displayName}
|
||||||
|
initialParams={videoParams || undefined}
|
||||||
|
onParamsChange={handleVideoParamsChange}
|
||||||
|
inline={true}
|
||||||
|
providerType={currentModel?.providerType}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImageParamsBar
|
||||||
|
modelName={currentModel?.displayName}
|
||||||
|
initialParams={imageParams}
|
||||||
|
onParamsChange={handleImageParamsChange}
|
||||||
|
inline={true}
|
||||||
|
providerType={currentModel?.providerType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{isVideoModel && (
|
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
<SelectTrigger className="h-8 w-8 rounded-full border border-[#cfd4dc] bg-white p-0 justify-center [&>svg]:hidden">
|
||||||
<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">
|
||||||
<span className="h-6 w-6 overflow-hidden rounded-full bg-white flex items-center justify-center">
|
{currentModel?.avatarUrl ? (
|
||||||
{allModels.find((m) => m.id === selectedModelId)?.avatarUrl ? (
|
<img
|
||||||
<img
|
src={currentModel.avatarUrl}
|
||||||
src={allModels.find((m) => m.id === selectedModelId)?.avatarUrl || ""}
|
alt={currentModel.displayName || "模型头像"}
|
||||||
alt={allModels.find((m) => m.id === selectedModelId)?.displayName || "模型头像"}
|
className="h-full w-full object-cover"
|
||||||
className="h-full w-full object-cover"
|
/>
|
||||||
/>
|
) : (
|
||||||
) : (
|
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-[#4b5563]">
|
||||||
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-[#4b5563]">
|
{(currentModel?.displayName || "A").slice(0, 1)}
|
||||||
{(allModels.find((m) => m.id === selectedModelId)?.displayName || "A").slice(0, 1)}
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
</span>
|
||||||
</span>
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
{renderModelSelectContent()}
|
||||||
{renderModelSelectContent()}
|
</Select>
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user