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 实现真正的混合编辑 */}
|
{/* 混合输入区域:使用 contenteditable 实现真正的混合编辑 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative px-4 pt-3 pb-12",
|
"relative px-3 pt-3 pb-12",
|
||||||
isDetailGenerationMode ? "min-h-[72px]" : "min-h-[60px]"
|
isDetailGenerationMode ? "min-h-[72px]" : "min-h-[60px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -10207,79 +10207,81 @@ export function CanvasChatPanel({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<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">
|
||||||
ref={textareaRef as any}
|
<div
|
||||||
contentEditable={!isSubmitting}
|
ref={textareaRef as any}
|
||||||
suppressContentEditableWarning
|
contentEditable={!isSubmitting}
|
||||||
className={cn(
|
suppressContentEditableWarning
|
||||||
"min-h-[40px] max-h-[120px] overflow-y-auto text-sm outline-none empty:before:content-[attr(data-placeholder)]",
|
className={cn(
|
||||||
isVideoWorkspace ? "empty:before:text-muted-foreground/70" : "text-[#111827] empty:before:text-[#9aa1ad] dark:text-zinc-100 dark:empty:before:text-zinc-500"
|
"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) => {
|
data-placeholder={composerPlaceholder}
|
||||||
// 从 contenteditable 中提取纯文本(排除附件标签文本)
|
onInput={(e) => {
|
||||||
const target = e.target as HTMLDivElement;
|
// 从 contenteditable 中提取纯文本(排除附件标签文本)
|
||||||
const clone = target.cloneNode(true) as HTMLDivElement;
|
const target = e.target as HTMLDivElement;
|
||||||
clone.querySelectorAll("[data-attachment-url]").forEach((el) => el.remove());
|
const clone = target.cloneNode(true) as HTMLDivElement;
|
||||||
const textContent = (clone.textContent || "")
|
clone.querySelectorAll("[data-attachment-url]").forEach((el) => el.remove());
|
||||||
.replace(/\u200b/g, " ")
|
const textContent = (clone.textContent || "")
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\u200b/g, " ")
|
||||||
.trim();
|
.replace(/\s+/g, " ")
|
||||||
setInput(textContent);
|
.trim();
|
||||||
if ((!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
|
setInput(textContent);
|
||||||
syncAttachmentsWithEditor(target);
|
if ((!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
|
||||||
}
|
syncAttachmentsWithEditor(target);
|
||||||
}}
|
}
|
||||||
onKeyDown={(e) => {
|
}}
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
onKeyDown={(e) => {
|
||||||
e.preventDefault();
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
// 🆕 详情模式下,Enter 键默认不直接发送,防止误触。除非使用 Meta+Enter 或 Ctrl+Enter(这里先不处理组合键,仅禁用单键发送)
|
e.preventDefault();
|
||||||
// 或者如果用户习惯了 Enter 发送,可以保持,但考虑到详情生成成本较高,也许需要确认?
|
// 🆕 详情模式下,Enter 键默认不直接发送,防止误触。除非使用 Meta+Enter 或 Ctrl+Enter(这里先不处理组合键,仅禁用单键发送)
|
||||||
// 暂且保持一致,如果用户在详情模式下按 Enter,也触发 handleSend(内部会做校验)
|
// 或者如果用户习惯了 Enter 发送,可以保持,但考虑到详情生成成本较高,也许需要确认?
|
||||||
handleSend();
|
// 暂且保持一致,如果用户在详情模式下按 Enter,也触发 handleSend(内部会做校验)
|
||||||
}
|
handleSend();
|
||||||
// 处理 Backspace 删除图片标签(只删除聊天框中的标签,不影响画布)
|
}
|
||||||
if (e.key === 'Backspace' && (!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
|
// 处理 Backspace 删除图片标签(只删除聊天框中的标签,不影响画布)
|
||||||
const selection = window.getSelection();
|
if (e.key === 'Backspace' && (!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
|
||||||
if (selection && selection.rangeCount > 0) {
|
const selection = window.getSelection();
|
||||||
const range = selection.getRangeAt(0);
|
if (selection && selection.rangeCount > 0) {
|
||||||
if (range.collapsed && range.startOffset === 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')) {
|
const prevSibling = range.startContainer.previousSibling as HTMLElement;
|
||||||
e.preventDefault();
|
if (prevSibling && prevSibling.getAttribute?.('data-attachment-url')) {
|
||||||
e.stopPropagation(); // 阻止事件冒泡,防止触发全局删除
|
e.preventDefault();
|
||||||
// 使用 URL 来删除附件,更可靠
|
e.stopPropagation(); // 阻止事件冒泡,防止触发全局删除
|
||||||
removeAttachmentByTagElement(prevSibling);
|
// 使用 URL 来删除附件,更可靠
|
||||||
prevSibling.remove();
|
removeAttachmentByTagElement(prevSibling);
|
||||||
|
prevSibling.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
onPaste={(e) => {
|
||||||
onPaste={(e) => {
|
const clipboardItems = Array.from(e.clipboardData?.items || []);
|
||||||
const clipboardItems = Array.from(e.clipboardData?.items || []);
|
const imageFiles = clipboardItems
|
||||||
const imageFiles = clipboardItems
|
.filter((item) => item.type.startsWith("image/"))
|
||||||
.filter((item) => item.type.startsWith("image/"))
|
.map((item) => item.getAsFile())
|
||||||
.map((item) => item.getAsFile())
|
.filter((file): file is File => Boolean(file));
|
||||||
.filter((file): file is File => Boolean(file));
|
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
void handlePastedImages(imageFiles);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (imageFiles.length > 0) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void handlePastedImages(imageFiles);
|
const text = e.clipboardData.getData('text/plain');
|
||||||
return;
|
document.execCommand('insertText', false, text);
|
||||||
}
|
}}
|
||||||
|
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
||||||
e.preventDefault();
|
/>
|
||||||
const text = e.clipboardData.getData('text/plain');
|
</div>
|
||||||
document.execCommand('insertText', false, text);
|
|
||||||
}}
|
|
||||||
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasAgentUploadSlots && (
|
{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">
|
<div className="flex flex-row gap-2 overflow-x-auto scrollbar-hide">
|
||||||
{agentUploadSlots.map((slot, index) => {
|
{agentUploadSlots.map((slot, index) => {
|
||||||
const slotAttachment = attachments[index];
|
const slotAttachment = attachments[index];
|
||||||
@ -10347,7 +10349,7 @@ export function CanvasChatPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isVideoModel && videoFrameSlots.length > 0 && (
|
{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">
|
<div className="flex flex-row gap-2">
|
||||||
{videoFrameSlots.map((label, index) => {
|
{videoFrameSlots.map((label, index) => {
|
||||||
const slotAttachment = attachments[index];
|
const slotAttachment = attachments[index];
|
||||||
@ -10417,10 +10419,7 @@ export function CanvasChatPanel({
|
|||||||
{/* Bottom Toolbar */}
|
{/* Bottom Toolbar */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute left-2 bottom-2 right-2 flex items-center justify-between gap-3",
|
"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)]"
|
||||||
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"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
@ -10463,13 +10462,13 @@ export function CanvasChatPanel({
|
|||||||
{!isWorkspacePresentation ? (
|
{!isWorkspacePresentation ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative h-8 cursor-pointer rounded-full px-3 text-sm font-medium",
|
"relative h-8 cursor-pointer rounded-full px-3 text-sm font-medium",
|
||||||
isVideoWorkspace
|
isVideoWorkspace
|
||||||
? "border border-border/60 bg-background text-foreground hover:bg-background/90"
|
? "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"
|
: 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"
|
? "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"
|
: activeComposerMode === "detail"
|
||||||
@ -10649,9 +10648,9 @@ export function CanvasChatPanel({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
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
|
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"
|
: "hover:bg-muted dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12 dark:hover:text-white"
|
||||||
)}
|
)}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
@ -10680,7 +10679,7 @@ export function CanvasChatPanel({
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<ImageParamsBar
|
||||||
modelName={activeComposerMode === "agent" ? agentDisplayName : currentModel?.displayName}
|
modelName={activeComposerMode === "agent" ? agentDisplayName : currentModel?.displayName}
|
||||||
initialParams={imageParams}
|
initialParams={imageParams}
|
||||||
@ -10695,7 +10694,7 @@ export function CanvasChatPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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" ? (
|
{!isWorkspacePresentation && activeComposerMode === "image" ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -10751,7 +10750,7 @@ export function CanvasChatPanel({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"h-8 w-8 rounded-full p-0 justify-center [&>svg]:hidden",
|
"h-8 w-8 rounded-full p-0 justify-center [&>svg]:hidden",
|
||||||
isVideoWorkspace
|
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"
|
: "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}
|
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-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"
|
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||||
: canSend
|
: 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"
|
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||||
)}
|
)}
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user