feat(input): add image attachment support to prompt input

- Add imperative handle for programmatic image attachment
- Expose addImage() method via React ref
- Support screenshot integration from preview pane

Enables automatic attachment of screenshots to Claude prompts.
This commit is contained in:
Mufeed VH
2025-06-23 00:30:15 +05:30
parent 36744f13f9
commit 4334045661

View File

@@ -44,6 +44,10 @@ interface FloatingPromptInputProps {
className?: string; className?: string;
} }
export interface FloatingPromptInputRef {
addImage: (imagePath: string) => void;
}
type Model = { type Model = {
id: "sonnet" | "opus"; id: "sonnet" | "opus";
name: string; name: string;
@@ -70,19 +74,21 @@ const MODELS: Model[] = [
* FloatingPromptInput component - Fixed position prompt input with model picker * FloatingPromptInput component - Fixed position prompt input with model picker
* *
* @example * @example
* const promptRef = useRef<FloatingPromptInputRef>(null);
* <FloatingPromptInput * <FloatingPromptInput
* ref={promptRef}
* onSend={(prompt, model) => console.log('Send:', prompt, model)} * onSend={(prompt, model) => console.log('Send:', prompt, model)}
* isLoading={false} * isLoading={false}
* /> * />
*/ */
export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({ export const FloatingPromptInput = React.forwardRef<FloatingPromptInputRef, FloatingPromptInputProps>(({
onSend, onSend,
isLoading = false, isLoading = false,
disabled = false, disabled = false,
defaultModel = "sonnet", defaultModel = "sonnet",
projectPath, projectPath,
className, className,
}) => { }, ref) => {
const [prompt, setPrompt] = useState(""); const [prompt, setPrompt] = useState("");
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel); const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
@@ -97,6 +103,34 @@ export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
const expandedTextareaRef = useRef<HTMLTextAreaElement>(null); const expandedTextareaRef = useRef<HTMLTextAreaElement>(null);
const unlistenDragDropRef = useRef<(() => void) | null>(null); const unlistenDragDropRef = useRef<(() => void) | null>(null);
// Expose a method to add images programmatically
React.useImperativeHandle(
ref,
() => ({
addImage: (imagePath: string) => {
setPrompt(currentPrompt => {
const existingPaths = extractImagePaths(currentPrompt);
if (existingPaths.includes(imagePath)) {
return currentPrompt; // Image already added
}
const mention = `@${imagePath}`;
const newPrompt = currentPrompt + (currentPrompt.endsWith(' ') || currentPrompt === '' ? '' : ' ') + mention + ' ';
// Focus the textarea
setTimeout(() => {
const target = isExpanded ? expandedTextareaRef.current : textareaRef.current;
target?.focus();
target?.setSelectionRange(newPrompt.length, newPrompt.length);
}, 0);
return newPrompt;
});
}
}),
[isExpanded]
);
// Helper function to check if a file is an image // Helper function to check if a file is an image
const isImageFile = (path: string): boolean => { const isImageFile = (path: string): boolean => {
const ext = path.split('.').pop()?.toLowerCase(); const ext = path.split('.').pop()?.toLowerCase();
@@ -410,21 +444,21 @@ export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
</Button> </Button>
</div> </div>
{isLoading ? (
<div className="flex items-center justify-center min-w-[60px] h-10">
<div className="rotating-symbol text-primary text-2xl"></div>
</div>
) : (
<Button <Button
onClick={handleSend} onClick={handleSend}
disabled={!prompt.trim() || isLoading || disabled} disabled={!prompt.trim() || disabled}
size="sm" size="sm"
className="min-w-[80px]" className="min-w-[80px]"
> >
{isLoading ? (
<div className="rotating-symbol text-primary-foreground"></div>
) : (
<>
<Send className="mr-2 h-4 w-4" /> <Send className="mr-2 h-4 w-4" />
Send Send
</>
)}
</Button> </Button>
)}
</div> </div>
</motion.div> </motion.div>
</motion.div> </motion.div>
@@ -541,18 +575,20 @@ export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
</div> </div>
{/* Send Button */} {/* Send Button */}
{isLoading ? (
<div className="flex items-center justify-center min-w-[60px] h-10">
<div className="rotating-symbol text-primary text-2xl"></div>
</div>
) : (
<Button <Button
onClick={handleSend} onClick={handleSend}
disabled={!prompt.trim() || isLoading || disabled} disabled={!prompt.trim() || disabled}
size="default" size="default"
className="min-w-[60px]" className="min-w-[60px]"
> >
{isLoading ? (
<div className="rotating-symbol text-primary-foreground"></div>
) : (
<Send className="h-4 w-4" /> <Send className="h-4 w-4" />
)}
</Button> </Button>
)}
</div> </div>
<div className="mt-2 text-xs text-muted-foreground"> <div className="mt-2 text-xs text-muted-foreground">
@@ -563,4 +599,6 @@ export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
</div> </div>
</> </>
); );
}; });
FloatingPromptInput.displayName = 'FloatingPromptInput';