style: refresh canvas composer UI to mirror Lovart dialog
This commit is contained in:
parent
6b90b25d5c
commit
4a3342c795
@ -9883,7 +9883,7 @@ export function CanvasChatPanel({
|
||||
{/* 混合输入区域:使用 contenteditable 实现真正的混合编辑 */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative px-4 pt-3 pb-12",
|
||||
"relative px-3 pt-3 pb-12",
|
||||
isDetailGenerationMode ? "min-h-[72px]" : "min-h-[60px]"
|
||||
)}
|
||||
>
|
||||
@ -10207,79 +10207,81 @@ export function CanvasChatPanel({
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
ref={textareaRef as any}
|
||||
contentEditable={!isSubmitting}
|
||||
suppressContentEditableWarning
|
||||
className={cn(
|
||||
"min-h-[40px] max-h-[120px] overflow-y-auto text-sm outline-none empty:before:content-[attr(data-placeholder)]",
|
||||
isVideoWorkspace ? "empty:before:text-muted-foreground/70" : "text-[#111827] empty:before:text-[#9aa1ad] dark:text-zinc-100 dark:empty:before:text-zinc-500"
|
||||
)}
|
||||
data-placeholder={composerPlaceholder}
|
||||
onInput={(e) => {
|
||||
// 从 contenteditable 中提取纯文本(排除附件标签文本)
|
||||
const target = e.target as HTMLDivElement;
|
||||
const clone = target.cloneNode(true) as HTMLDivElement;
|
||||
clone.querySelectorAll("[data-attachment-url]").forEach((el) => el.remove());
|
||||
const textContent = (clone.textContent || "")
|
||||
.replace(/\u200b/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
setInput(textContent);
|
||||
if ((!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
|
||||
syncAttachmentsWithEditor(target);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
// 🆕 详情模式下,Enter 键默认不直接发送,防止误触。除非使用 Meta+Enter 或 Ctrl+Enter(这里先不处理组合键,仅禁用单键发送)
|
||||
// 或者如果用户习惯了 Enter 发送,可以保持,但考虑到详情生成成本较高,也许需要确认?
|
||||
// 暂且保持一致,如果用户在详情模式下按 Enter,也触发 handleSend(内部会做校验)
|
||||
handleSend();
|
||||
}
|
||||
// 处理 Backspace 删除图片标签(只删除聊天框中的标签,不影响画布)
|
||||
if (e.key === 'Backspace' && (!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
if (range.collapsed && range.startOffset === 0) {
|
||||
// 检查前一个兄弟节点是否是图片标签
|
||||
const prevSibling = range.startContainer.previousSibling as HTMLElement;
|
||||
if (prevSibling && prevSibling.getAttribute?.('data-attachment-url')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // 阻止事件冒泡,防止触发全局删除
|
||||
// 使用 URL 来删除附件,更可靠
|
||||
removeAttachmentByTagElement(prevSibling);
|
||||
prevSibling.remove();
|
||||
<div className="rounded-[22px] border border-[#d7deeb] bg-[#f8fafc] px-4 py-2.5 shadow-[0_12px_24px_-18px_rgba(15,23,42,0.35)] transition-colors focus-within:border-[#a6c8ff] focus-within:bg-white dark:border-zinc-600 dark:bg-zinc-900/60 dark:focus-within:bg-zinc-900">
|
||||
<div
|
||||
ref={textareaRef as any}
|
||||
contentEditable={!isSubmitting}
|
||||
suppressContentEditableWarning
|
||||
className={cn(
|
||||
"min-h-[40px] max-h-[120px] overflow-y-auto text-sm leading-6 outline-none empty:before:content-[attr(data-placeholder)]",
|
||||
isVideoWorkspace ? "empty:before:text-muted-foreground/70" : "text-[#111827] empty:before:text-[#9aa1ad] dark:text-zinc-100 dark:empty:before:text-zinc-500"
|
||||
)}
|
||||
data-placeholder={composerPlaceholder}
|
||||
onInput={(e) => {
|
||||
// 从 contenteditable 中提取纯文本(排除附件标签文本)
|
||||
const target = e.target as HTMLDivElement;
|
||||
const clone = target.cloneNode(true) as HTMLDivElement;
|
||||
clone.querySelectorAll("[data-attachment-url]").forEach((el) => el.remove());
|
||||
const textContent = (clone.textContent || "")
|
||||
.replace(/\u200b/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
setInput(textContent);
|
||||
if ((!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
|
||||
syncAttachmentsWithEditor(target);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
// 🆕 详情模式下,Enter 键默认不直接发送,防止误触。除非使用 Meta+Enter 或 Ctrl+Enter(这里先不处理组合键,仅禁用单键发送)
|
||||
// 或者如果用户习惯了 Enter 发送,可以保持,但考虑到详情生成成本较高,也许需要确认?
|
||||
// 暂且保持一致,如果用户在详情模式下按 Enter,也触发 handleSend(内部会做校验)
|
||||
handleSend();
|
||||
}
|
||||
// 处理 Backspace 删除图片标签(只删除聊天框中的标签,不影响画布)
|
||||
if (e.key === 'Backspace' && (!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
if (range.collapsed && range.startOffset === 0) {
|
||||
// 检查前一个兄弟节点是否是图片标签
|
||||
const prevSibling = range.startContainer.previousSibling as HTMLElement;
|
||||
if (prevSibling && prevSibling.getAttribute?.('data-attachment-url')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // 阻止事件冒泡,防止触发全局删除
|
||||
// 使用 URL 来删除附件,更可靠
|
||||
removeAttachmentByTagElement(prevSibling);
|
||||
prevSibling.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPaste={(e) => {
|
||||
const clipboardItems = Array.from(e.clipboardData?.items || []);
|
||||
const imageFiles = clipboardItems
|
||||
.filter((item) => item.type.startsWith("image/"))
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file): file is File => Boolean(file));
|
||||
}}
|
||||
onPaste={(e) => {
|
||||
const clipboardItems = Array.from(e.clipboardData?.items || []);
|
||||
const imageFiles = clipboardItems
|
||||
.filter((item) => item.type.startsWith("image/"))
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file): file is File => Boolean(file));
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
e.preventDefault();
|
||||
void handlePastedImages(imageFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
e.preventDefault();
|
||||
void handlePastedImages(imageFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData('text/plain');
|
||||
document.execCommand('insertText', false, text);
|
||||
}}
|
||||
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
||||
/>
|
||||
const text = e.clipboardData.getData('text/plain');
|
||||
document.execCommand('insertText', false, text);
|
||||
}}
|
||||
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasAgentUploadSlots && (
|
||||
<div className="mb-11 px-4 pb-1">
|
||||
<div className="mb-11 px-3 pb-1">
|
||||
<div className="flex flex-row gap-2 overflow-x-auto scrollbar-hide">
|
||||
{agentUploadSlots.map((slot, index) => {
|
||||
const slotAttachment = attachments[index];
|
||||
@ -10347,7 +10349,7 @@ export function CanvasChatPanel({
|
||||
)}
|
||||
|
||||
{isVideoModel && videoFrameSlots.length > 0 && (
|
||||
<div className="mb-11 px-4 pb-1">
|
||||
<div className="mb-11 px-3 pb-1">
|
||||
<div className="flex flex-row gap-2">
|
||||
{videoFrameSlots.map((label, index) => {
|
||||
const slotAttachment = attachments[index];
|
||||
@ -10417,10 +10419,7 @@ export function CanvasChatPanel({
|
||||
{/* Bottom Toolbar */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-2 bottom-2 right-2 flex items-center justify-between gap-3",
|
||||
isVideoWorkspace &&
|
||||
"rounded-[24px] border border-border/60 bg-background/88 px-3 py-2 shadow-sm backdrop-blur-xl supports-[backdrop-filter]:bg-background/80",
|
||||
!isVideoWorkspace && "px-1"
|
||||
"absolute inset-x-2 bottom-2 z-10 flex items-center justify-between gap-3 rounded-[24px] border border-[#d7deeb] bg-[#fbfdff]/95 px-3 py-2 shadow-[0_10px_28px_-20px_rgba(15,23,42,0.32)] backdrop-blur-xl supports-[backdrop-filter]:bg-[#fbfdff]/90 dark:border-zinc-700 dark:bg-zinc-900/90 dark:shadow-[0_10px_28px_-20px_rgba(2,6,23,0.7)]"
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
@ -10463,13 +10462,13 @@ export function CanvasChatPanel({
|
||||
{!isWorkspacePresentation ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"relative h-8 cursor-pointer rounded-full px-3 text-sm font-medium",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background text-foreground hover:bg-background/90"
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"relative h-8 cursor-pointer rounded-full px-3 text-sm font-medium",
|
||||
isVideoWorkspace
|
||||
? "border border-[#cfd8e6] bg-[#f3f7ff] text-[#2f4f9e] shadow-[0_2px_8px_-4px_rgba(47,79,158,0.35)] hover:bg-[#e8f0ff] dark:border-zinc-500/60 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
||||
: activeComposerMode === "video"
|
||||
? "border border-blue-500 bg-blue-50 text-blue-600 hover:bg-blue-100 dark:border-blue-400/40 dark:bg-blue-500/15 dark:text-blue-200 dark:hover:bg-blue-500/20"
|
||||
: activeComposerMode === "detail"
|
||||
@ -10649,9 +10648,9 @@ export function CanvasChatPanel({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full text-muted-foreground transition-colors hover:text-foreground mr-1",
|
||||
"h-8 w-8 rounded-full text-muted-foreground transition-colors hover:text-foreground mr-1 border border-transparent hover:border-[#cfd8e6] hover:bg-[#eef4ff] dark:hover:border-zinc-500 dark:hover:bg-zinc-700/70",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background hover:bg-background/90"
|
||||
? "bg-background"
|
||||
: "hover:bg-muted dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12 dark:hover:text-white"
|
||||
)}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
@ -10680,7 +10679,7 @@ export function CanvasChatPanel({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-w-0 shrink pl-1 border-l border-border/40 dark:border-white/10 ml-1">
|
||||
<div className="min-w-0 shrink pl-2 border-l border-[#d9e1f3]/70 dark:border-white/10 ml-1">
|
||||
<ImageParamsBar
|
||||
modelName={activeComposerMode === "agent" ? agentDisplayName : currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
@ -10695,7 +10694,7 @@ export function CanvasChatPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2 pl-2 border-l border-border/40 dark:border-white/10">
|
||||
<div className="flex shrink-0 items-center gap-2 pl-2 border-l border-[#d9e1f3]/70 dark:border-white/10">
|
||||
{!isWorkspacePresentation && activeComposerMode === "image" ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@ -10751,7 +10750,7 @@ export function CanvasChatPanel({
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full p-0 justify-center [&>svg]:hidden",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background"
|
||||
? "border border-[#cfd8e6] bg-white/85 text-zinc-700 hover:bg-white dark:border-white/20 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-100"
|
||||
: "border border-[#cfd4dc] bg-white dark:border-white/20 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-100"
|
||||
)}
|
||||
title={getToolbarItem("chatModelSelector").label}
|
||||
@ -10834,7 +10833,7 @@ export function CanvasChatPanel({
|
||||
? "bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white shadow-md hover:shadow-lg hover:opacity-90 dark:from-violet-500 dark:to-fuchsia-500"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
: canSend
|
||||
? "bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
||||
? "bg-[#2f80ff] text-white shadow-sm shadow-[#2f80ff]/30 hover:bg-[#2570e8]"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
)}
|
||||
onClick={handleSend}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user