Skip to content

Commit

Permalink
Improve web images processing
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaapple committed Dec 19, 2024
1 parent e6de18f commit 03fe599
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 65 deletions.
164 changes: 164 additions & 0 deletions frontend/components/modal/web-images-model.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { X } from 'lucide-react';

export interface WebImageFile {
url: string;
type: 'image';
name: string;
size: number;
}

interface WebImageUrlModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onImagesAdded: (images: WebImageFile[]) => void;
}

const WebImageModal: React.FC<WebImageUrlModalProps> = ({ open, onOpenChange, onImagesAdded }) => {
const [imageUrls, setImageUrls] = useState<string[]>(['']);
const [previews, setPreviews] = useState<string[]>([]);
const [urlValidities, setUrlValidities] = useState<boolean[]>([false]);

const isValidImageUrl = (url: string): boolean => {
try {
const urlObj = new URL(url);
const validExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
return validExtensions.some((ext) => urlObj.pathname.toLowerCase().endsWith(ext));
} catch {
return false;
}
};

const checkImageValidity = async (url: string): Promise<boolean> => {
if (!isValidImageUrl(url)) return false;

return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = url;
});
};

const updateUrlInput = async (index: number, value: string) => {
const newUrls = [...imageUrls];
newUrls[index] = value;
setImageUrls(newUrls);

if (value.trim()) {
const isValid = await checkImageValidity(value);
const newValidities = [...urlValidities];
newValidities[index] = isValid;
setUrlValidities(newValidities);

const newPreviews = [...previews];
if (isValid) {
newPreviews[index] = value;
} else {
newPreviews[index] = '';
}
setPreviews(newPreviews);
}
};

const addUrlInput = () => {
setImageUrls((prev) => [...prev, '']);
setUrlValidities((prev) => [...prev, false]);
setPreviews((prev) => [...prev, '']);
};

const removeUrlInput = (index: number) => {
const newUrls = imageUrls.filter((_, i) => i !== index);
const newValidities = urlValidities.filter((_, i) => i !== index);
const newPreviews = previews.filter((_, i) => i !== index);

setImageUrls(newUrls);
setUrlValidities(newValidities);
setPreviews(newPreviews);
};

const handleImageUrlSubmit = async () => {
const validUrls = imageUrls.filter((_, index) => urlValidities[index]);

if (validUrls.length === 0) {
toast.error('Please enter valid image URLs');
return;
}

try {
const newUploadedFiles: WebImageFile[] = validUrls.map((url) => ({
url,
type: 'image',
name: `web-image-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
size: 1,
}));

onImagesAdded(newUploadedFiles);

setImageUrls(['']);
setUrlValidities([false]);
setPreviews([]);
onOpenChange(false);

toast.success(`${validUrls.length} image(s) added successfully`);
} catch (error) {
toast.error('Failed to add image URLs');
}
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xl mx-auto">
<DialogHeader>
<DialogTitle>Add Public Web Image URLs</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
{imageUrls.map((url, index) => (
<div key={index} className="flex space-x-2 items-center">
<div className="flex-grow">
<Input
value={url}
onChange={(e) => updateUrlInput(index, e.target.value)}
placeholder="Enter image URL"
className={`
${urlValidities[index] ? 'border-green-500' : url.trim() ? 'border-red-500' : ''}
`}
/>
</div>
<Button className="size-6" variant="ghost" size="icon" onClick={() => removeUrlInput(index)}>
<X />
</Button>
</div>
))}
<Button variant="outline" onClick={addUrlInput} className="w-full">
+ Add Another Image
</Button>
<p className="text-xs text-muted-foreground">Supported Image Formats: JPG, PNG, WebP, GIF</p>

{previews.some((preview) => preview) && (
<div className="grid grid-cols-4 gap-4 mt-4">
{previews.map(
(preview, index) =>
preview && <img key={index} src={preview} alt={`Preview ${index + 1}`} className="w-full h-24 object-cover rounded" />,
)}
</div>
)}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="button" onClick={handleImageUrlSubmit} disabled={!urlValidities.some((validity) => validity)}>
Add Images
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

export default WebImageModal;
73 changes: 42 additions & 31 deletions frontend/components/search/search-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

import React, { KeyboardEvent, useMemo, useRef, useState } from 'react';
import { useSigninModal } from '@/hooks/use-signin-modal';
import { SendHorizontal, FileTextIcon, Database, Image as ImageIcon } from 'lucide-react';
import { useIndexModal } from '@/hooks/use-index-modal';
import { SendHorizontal, FileTextIcon, Database, Image as ImageIcon, Link } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { ModelSelection } from '@/components/search/model-selection';
import { SourceSelection } from '@/components/search/source-selection';
import TextareaAutosize from 'react-textarea-autosize';
import { useUIStore, useUserStore } from '@/lib/store';
import { toast } from 'sonner';
import { Icons } from '@/components/shared/icons';
import Image from 'next/image';
import { type FileRejection, useDropzone } from 'react-dropzone';
import { useUploadFile } from '@/hooks/use-upload-file';
import { useUpgradeModal } from '@/hooks/use-upgrade-modal';
Expand All @@ -22,6 +20,7 @@ import dynamic from 'next/dynamic';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { SearchType } from '@/lib/types';
import WebImageModal, { WebImageFile } from '@/components/modal/web-images-model';

interface Props {
handleSearch: (key: string, attachments?: string[]) => void;
Expand All @@ -46,10 +45,27 @@ const SearchBar: React.FC<Props> = ({
}) => {
const [content, setContent] = useState<string>('');
const signInModal = useSigninModal();
const indexModal = useIndexModal();
const upgradeModal = useUpgradeModal();
const user = useUserStore((state) => state.user);

const [showImageUrlModal, setShowImageUrlModal] = useState(false);
const handleImagesAdded = (images: WebImageFile[]) => {
try {
setUploadedFiles((prev) => [...prev, ...images]);
const newPreviewFiles: FileWithPreview[] = images.map(
(image) =>
({
name: image.name,
type: 'image/jpeg',
preview: image.url,
}) as FileWithPreview,
);
setFiles((prev) => [...prev, ...newPreviewFiles]);
} catch (error) {
toast.error('Failed to add images');
}
};

const checkEmptyInput = () => {
if (isUploading) {
return true;
Expand All @@ -76,7 +92,6 @@ const SearchBar: React.FC<Props> = ({
return;
}
if (uploadedFiles && uploadedFiles.length > 0) {
console.log('uploadedFiles', uploadedFiles);
const fileUrls = uploadedFiles.map((file) => file.url);
handleSearch(content, fileUrls);
setFiles([]);
Expand Down Expand Up @@ -148,7 +163,6 @@ const SearchBar: React.FC<Props> = ({
}

const processedImageFiles = await processImageFiles(acceptedFiles);

const newFiles = processedImageFiles.map(
(file) =>
Object.assign(file, {
Expand Down Expand Up @@ -214,7 +228,7 @@ const SearchBar: React.FC<Props> = ({
{files.map((file, index) => (
<div key={index}>
{file.type.startsWith('image/') ? (
<Image
<img
src={file.preview}
alt={file.name}
width={100}
Expand Down Expand Up @@ -244,30 +258,6 @@ const SearchBar: React.FC<Props> = ({
></TextareaAutosize>
<div className="flex relative">
<div className="absolute left-0 bottom-0 mb-1 ml-2 mt-6 flex items-center space-x-4">
{/* {showIndexButton && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={t('index-tip')}
className="text-gray-500 hover:text-primary hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg p-2 flex items-center space-x-1"
data-umami-event="Index Button Click"
onClick={() => {
if (!user) {
signInModal.onOpen();
} else {
indexModal.onOpen();
}
}}
>
<Database className="dark:text-white" size={20} strokeWidth={2} />
<span className="font-serif text-sm font-semibold dark:text-white">{t('index-button')}</span>
</button>
</TooltipTrigger>
<TooltipContent>{t('index-tip')}</TooltipContent>
</Tooltip>
)} */}

<Tooltip>
<TooltipTrigger asChild>
<div>
Expand Down Expand Up @@ -305,6 +295,25 @@ const SearchBar: React.FC<Props> = ({
</TooltipTrigger>
<TooltipContent>{searchType === SearchType.SEARCH ? t('attach-tip') : t('image-tip')}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div>
<button
type="button"
disabled={isUploading}
data-umami-event="Attach Button Click"
className="text-gray-500 hover:text-primary hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg p-2 flex items-center"
onClick={() => setShowImageUrlModal(true)}
>
<div className="flex items-center dark:text-white space-x-1">
<Link size={20} strokeWidth={2} />
<span className="font-semibold font-serif text-sm">Web Image</span>
</div>
</button>
</div>
</TooltipTrigger>
<TooltipContent>Attach Web Image</TooltipContent>
</Tooltip>
</div>
<div className="absolute right-0 bottom-0 mb-1 mr-2 mt-6 flex items-center space-x-4">
<Tooltip>
Expand All @@ -326,6 +335,8 @@ const SearchBar: React.FC<Props> = ({
</div>
</div>

<WebImageModal open={showImageUrlModal} onOpenChange={setShowImageUrlModal} onImagesAdded={handleImagesAdded} />

<div className="mt-4 grid grid-cols-1 md:grid-cols-4 gap-2">
{showShadcnUI && (
<div className="flex items-center space-x-2 mb-1">
Expand Down
22 changes: 11 additions & 11 deletions frontend/components/search/search-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import React, { memo, useMemo } from 'react';
import AnswerSection from '@/components/search/answer-section';
import QuestionSection from '@/components/search/question-section';
import ActionButtons from '@/components/search/action-buttons';
import { extractAllImageUrls } from '@/lib/shared-utils';
import VideoGallery from '@/components/search/video-gallery';
import ExpandableSection from '@/components/search/expandable-section';
import MindMap from '@/components/search/mindmap';
Expand Down Expand Up @@ -36,17 +35,18 @@ const SearchMessage = memo(
const isUser = role === 'user';

const message = props.message;
const attachments = message.attachments;

const attachments = useMemo(() => {
let initialAttachments = message.attachments ?? [];
if (isUser) {
const imageUrls = extractAllImageUrls(content);
if (imageUrls.length > 0) {
initialAttachments = initialAttachments.concat(imageUrls);
}
}
return initialAttachments;
}, [message.attachments, isUser, content]);
// const attachments = useMemo(() => {
// let initialAttachments = message.attachments ?? [];
// if (isUser) {
// const imageUrls = extractAllImageUrls(content);
// if (imageUrls.length > 0) {
// initialAttachments = initialAttachments.concat(imageUrls);
// }
// }
// return initialAttachments;
// }, [message.attachments, isUser, content]);

const t = useTranslations('SearchMessage');
const { showMindMap } = useUIStore();
Expand Down
22 changes: 11 additions & 11 deletions frontend/components/search/search-window.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ImageSource, Message, SearchType, TextSource, User, VideoSource } from
import { LoaderCircle } from 'lucide-react';
import { useScrollAnchor } from '@/hooks/use-scroll-anchor';
import { toast } from 'sonner';
import { isProUser, extractAllImageUrls, generateId } from '@/lib/shared-utils';
import { isProUser, generateId } from '@/lib/shared-utils';
import { useUpgradeModal } from '@/hooks/use-upgrade-modal';
import { useSearchStore } from '@/lib/store/local-history';
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom';
Expand Down Expand Up @@ -110,16 +110,16 @@ export default function SearchWindow({ id, initialMessages, user, isReadOnly = f
messageValue = 'Please generate the same UI as the image';
}

const imageUrls = extractAllImageUrls(messageValue);
if (imageUrls.length > 1 && user && !isProUser(user)) {
toast.error(t('multi-image-free-limit'));
upgradeModal.onOpen();
return;
}
if (imageUrls.length > 5) {
toast.error(t('multi-image-pro-limit'));
return;
}
// const imageUrls = extractAllImageUrls(messageValue);
// if (imageUrls.length > 1 && user && !isProUser(user)) {
// toast.error(t('multi-image-free-limit'));
// upgradeModal.onOpen();
// return;
// }
// if (imageUrls.length > 5) {
// toast.error(t('multi-image-pro-limit'));
// return;
// }

setInput('');
setIsLoading(true);
Expand Down
2 changes: 1 addition & 1 deletion frontend/hooks/use-upload-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async function uploadSingleFile(file: File): Promise<UploadedFile> {
}

export function useUploadFile() {
const [uploadedFiles, setUploadedFiles] = React.useState<UploadedFile[]>();
const [uploadedFiles, setUploadedFiles] = React.useState<UploadedFile[]>([]);
const [isUploading, setIsUploading] = React.useState(false);
const { compressImage, compressionError } = useImageCompression({});

Expand Down
Loading

0 comments on commit 03fe599

Please sign in to comment.