From 60451d888397d74a20c6246d35947af2a812ac83 Mon Sep 17 00:00:00 2001 From: Mostafa-Khairy0 Date: Sun, 2 Nov 2025 20:08:22 +0200 Subject: [PATCH 1/4] feat(ai-panel): add support for pasting images directly into Wave AI input --- frontend/app/aipanel/aipanelinput.tsx | 85 +++++++++++++-------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/frontend/app/aipanel/aipanelinput.tsx b/frontend/app/aipanel/aipanelinput.tsx index 0dad5e4d5d..dc7fc6eff5 100644 --- a/frontend/app/aipanel/aipanelinput.tsx +++ b/frontend/app/aipanel/aipanelinput.tsx @@ -39,9 +39,7 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps useEffect(() => { const inputRefObject: React.RefObject = { current: { - focus: () => { - textareaRef.current?.focus(); - }, + focus: () => textareaRef.current?.focus(), resize: resizeTextarea, }, }; @@ -56,60 +54,58 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps } }; - const handleFocus = useCallback(() => { - model.requestWaveAIFocus(); - }, [model]); + const handleFocus = useCallback(() => model.requestWaveAIFocus(), [model]); - const handleBlur = useCallback((e: React.FocusEvent) => { - if (e.relatedTarget === null) { - return; - } + const handleBlur = useCallback( + (e: React.FocusEvent) => { + if (e.relatedTarget === null) return; + if (waveAIHasFocusWithin(e.relatedTarget)) return; + model.requestNodeFocus(); + }, + [model] + ); - if (waveAIHasFocusWithin(e.relatedTarget)) { - return; - } + useEffect(() => resizeTextarea(), [input, resizeTextarea]); + useEffect(() => { + if (isPanelOpen) resizeTextarea(); + }, [isPanelOpen, resizeTextarea]); - model.requestNodeFocus(); - }, [model]); + const handleUploadClick = () => fileInputRef.current?.click(); - useEffect(() => { - resizeTextarea(); - }, [input, resizeTextarea]); + const processFile = async (file: File) => { + if (!isAcceptableFile(file)) { + console.warn(`Rejected unsupported file type: ${file.type}`); + return; + } - useEffect(() => { - if (isPanelOpen) { - resizeTextarea(); + const sizeError = validateFileSize(file); + if (sizeError) { + model.setError(formatFileSizeError(sizeError)); + return; } - }, [isPanelOpen, resizeTextarea]); - const handleUploadClick = () => { - fileInputRef.current?.click(); + await model.addFile(file); }; const handleFileChange = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); - const acceptableFiles = files.filter(isAcceptableFile); - - for (const file of acceptableFiles) { - const sizeError = validateFileSize(file); - if (sizeError) { - model.setError(formatFileSizeError(sizeError)); - if (e.target) { - e.target.value = ""; - } - return; - } - await model.addFile(file); - } + for (const file of files) await processFile(file); + if (e.target) e.target.value = ""; + }; - if (acceptableFiles.length < files.length) { - console.warn(`${files.length - acceptableFiles.length} files were rejected due to unsupported file types`); - } + const handlePaste = useCallback(async (e: React.ClipboardEvent) => { + const items = e.clipboardData?.items; + if (!items) return; - if (e.target) { - e.target.value = ""; + for (const item of items) { + if (item.type.startsWith("image/")) { + const blob = item.getAsFile(); + if (!blob) continue; + const file = new File([blob], `pasted-image-${Date.now()}.png`, { type: blob.type }); + await processFile(file); + } } - }; + }, []); return (
@@ -128,11 +124,12 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} + onPaste={handlePaste} onFocus={handleFocus} onBlur={handleBlur} placeholder={model.inBuilder ? "What would you like to build..." : "Ask Wave AI anything..."} className={cn( - "w-full text-white px-2 py-2 pr-5 focus:outline-none resize-none overflow-auto", + "w-full text-white px-2 py-2 pr-5 focus:outline-none resize-none overflow-auto", isFocused ? "bg-accent-900/50" : "bg-gray-800" )} style={{ fontSize: "13px" }} From 67f27ee79ad0726d50825060634df19f254e7fe7 Mon Sep 17 00:00:00 2001 From: Mostafa-Khairy0 Date: Sun, 2 Nov 2025 20:52:51 +0200 Subject: [PATCH 2/4] fix memo issus --- frontend/app/aipanel/aipanelinput.tsx | 67 +++++++++++++++------------ 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/frontend/app/aipanel/aipanelinput.tsx b/frontend/app/aipanel/aipanelinput.tsx index dc7fc6eff5..388024f706 100644 --- a/frontend/app/aipanel/aipanelinput.tsx +++ b/frontend/app/aipanel/aipanelinput.tsx @@ -29,7 +29,6 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps const resizeTextarea = useCallback(() => { const textarea = textareaRef.current; if (!textarea) return; - textarea.style.height = "auto"; const scrollHeight = textarea.scrollHeight; const maxHeight = 7 * 24; @@ -72,40 +71,50 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps const handleUploadClick = () => fileInputRef.current?.click(); - const processFile = async (file: File) => { - if (!isAcceptableFile(file)) { - console.warn(`Rejected unsupported file type: ${file.type}`); - return; - } + const processFile = useCallback( + async (file: File) => { + if (!isAcceptableFile(file)) { + console.warn(`Rejected unsupported file type: ${file.type}`); + return; + } - const sizeError = validateFileSize(file); - if (sizeError) { - model.setError(formatFileSizeError(sizeError)); - return; - } + const sizeError = validateFileSize(file); + if (sizeError) { + model.setError(formatFileSizeError(sizeError)); + return; + } - await model.addFile(file); - }; + await model.addFile(file); + }, + [model] + ); - const handleFileChange = async (e: React.ChangeEvent) => { - const files = Array.from(e.target.files || []); - for (const file of files) await processFile(file); - if (e.target) e.target.value = ""; - }; + const handleFileChange = useCallback( + async (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + for (const file of files) await processFile(file); + if (e.target) e.target.value = ""; + }, + [processFile] + ); + + const handlePaste = useCallback( + async (e: React.ClipboardEvent) => { + const items = e.clipboardData?.items; + if (!items) return; - const handlePaste = useCallback(async (e: React.ClipboardEvent) => { - const items = e.clipboardData?.items; - if (!items) return; + for (const item of items) { + if (item.type.startsWith("image/")) { + const blob = item.getAsFile(); + if (!blob) continue; - for (const item of items) { - if (item.type.startsWith("image/")) { - const blob = item.getAsFile(); - if (!blob) continue; - const file = new File([blob], `pasted-image-${Date.now()}.png`, { type: blob.type }); - await processFile(file); + const file = new File([blob], `pasted-image-${Date.now()}.png`, { type: blob.type }); + await processFile(file); + } } - } - }, []); + }, + [processFile] + ); return (
From 7f75c7a5d4482081d8c6fd4ae192e232ef46e4dc Mon Sep 17 00:00:00 2001 From: Mostafa-Khairy0 Date: Sun, 2 Nov 2025 21:05:47 +0200 Subject: [PATCH 3/4] fix issus --- frontend/app/aipanel/aipanelinput.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/frontend/app/aipanel/aipanelinput.tsx b/frontend/app/aipanel/aipanelinput.tsx index 388024f706..6d6c25b785 100644 --- a/frontend/app/aipanel/aipanelinput.tsx +++ b/frontend/app/aipanel/aipanelinput.tsx @@ -84,7 +84,12 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps return; } - await model.addFile(file); + try { + await model.addFile(file); + } catch (error: any) { + console.error("Failed to add file:", error); + model.setError(error?.message || "Failed to add file"); + } }, [model] ); @@ -108,7 +113,14 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps const blob = item.getAsFile(); if (!blob) continue; - const file = new File([blob], `pasted-image-${Date.now()}.png`, { type: blob.type }); + let ext = blob.type.split("/")[1] || "png"; + ext = ext.toLowerCase(); + if (ext === "jpeg") ext = "jpg"; + if (!/^[a-z0-9]+$/.test(ext)) ext = "png"; + + const filename = `pasted-image-${Date.now()}.${ext}`; + const file = new File([blob], filename, { type: blob.type }); + await processFile(file); } } From ab13a7e573e476abd4f93d07dc57358237803044 Mon Sep 17 00:00:00 2001 From: Mostafa-Khairy0 Date: Sun, 2 Nov 2025 21:14:35 +0200 Subject: [PATCH 4/4] fix issus --- frontend/app/aipanel/aipanelinput.tsx | 32 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/frontend/app/aipanel/aipanelinput.tsx b/frontend/app/aipanel/aipanelinput.tsx index 6d6c25b785..af54a35311 100644 --- a/frontend/app/aipanel/aipanelinput.tsx +++ b/frontend/app/aipanel/aipanelinput.tsx @@ -108,24 +108,30 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps const items = e.clipboardData?.items; if (!items) return; - for (const item of items) { - if (item.type.startsWith("image/")) { - const blob = item.getAsFile(); - if (!blob) continue; - - let ext = blob.type.split("/")[1] || "png"; - ext = ext.toLowerCase(); - if (ext === "jpeg") ext = "jpg"; - if (!/^[a-z0-9]+$/.test(ext)) ext = "png"; + const imageItems = Array.from(items).filter((item) => item.type.startsWith("image/")); - const filename = `pasted-image-${Date.now()}.${ext}`; - const file = new File([blob], filename, { type: blob.type }); + if (imageItems.length > 0) { + e.preventDefault(); - await processFile(file); + for (const item of imageItems) { + const blob = item.getAsFile(); + if (blob) { + const mimeType = blob.type; + let ext = mimeType.split("/")[1] || "png"; + if (ext === "jpeg") ext = "jpg"; + const file = new File([blob], `pasted-image.${ext}`, { type: mimeType }); + + try { + await processFile(file); + } catch (error) { + console.error("Error processing pasted image:", error); + model.setError(error instanceof Error ? error.message : "Failed to process image"); + } + } } } }, - [processFile] + [processFile, model] ); return (