- Updated OpenRouter integration to accept an array of image URLs - Updated ImagePicker to use the `multiple` attribute natively - Added `appendImagesDeduped` for handling client-side image deduplication - Enhanced clipboard pasting to extract multiple images at once - Rendered multiple images in a horizontal thumbnail strip in the AIToolbar - Added tests to cover multi-image logic and AI request mapping
80 lines
2.0 KiB
TypeScript
80 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import type { VariantProps } from "class-variance-authority";
|
|
import { ImageIcon } from "lucide-react";
|
|
import type React from "react";
|
|
import { useImperativeHandle, useRef } from "react";
|
|
import { Button, type buttonVariants } from "@/components/ui/button";
|
|
import { IMAGE_ACCEPT } from "@/lib/constants";
|
|
|
|
interface ImagePickerProps extends VariantProps<typeof buttonVariants> {
|
|
/** Called with ALL selected files (array of 1..N). */
|
|
onFilesSelect?: (files: File[]) => void;
|
|
className?: string;
|
|
children?: React.ReactNode;
|
|
disabled?: boolean;
|
|
/** Allow selecting multiple images at once (native OS picker multi-select). */
|
|
multiple?: boolean;
|
|
/** Expose an imperative trigger so parents can open the file dialog via ref */
|
|
triggerRef?: React.Ref<{ open: () => void }>;
|
|
}
|
|
|
|
export function ImagePicker({
|
|
onFilesSelect,
|
|
className,
|
|
children,
|
|
variant = "ghost",
|
|
size = "icon",
|
|
disabled = false,
|
|
multiple = false,
|
|
triggerRef,
|
|
}: ImagePickerProps) {
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// Expose `.open()` to parent through triggerRef
|
|
useImperativeHandle(triggerRef, () => ({
|
|
open() {
|
|
fileInputRef.current?.click();
|
|
},
|
|
}));
|
|
|
|
const handleButtonClick = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const fileList = event.target.files;
|
|
if (fileList && fileList.length > 0 && onFilesSelect) {
|
|
onFilesSelect(Array.from(fileList));
|
|
}
|
|
// Reset so the same file(s) can be re-selected
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = "";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
name="image-upload"
|
|
accept={IMAGE_ACCEPT}
|
|
multiple={multiple}
|
|
onChange={handleFileChange}
|
|
className="hidden"
|
|
/>
|
|
<Button
|
|
onClick={handleButtonClick}
|
|
variant={variant}
|
|
size={size}
|
|
className={className}
|
|
disabled={disabled}
|
|
aria-label="Attach image"
|
|
>
|
|
{children || <ImageIcon className="h-4 w-4" />}
|
|
</Button>
|
|
</>
|
|
);
|
|
}
|