Skip to content

Commit 4334045

Browse files
committed
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.
1 parent 36744f1 commit 4334045

1 file changed

Lines changed: 67 additions & 29 deletions

File tree

src/components/FloatingPromptInput.tsx

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ interface FloatingPromptInputProps {
4444
className?: string;
4545
}
4646

47+
export interface FloatingPromptInputRef {
48+
addImage: (imagePath: string) => void;
49+
}
50+
4751
type Model = {
4852
id: "sonnet" | "opus";
4953
name: string;
@@ -70,19 +74,21 @@ const MODELS: Model[] = [
7074
* FloatingPromptInput component - Fixed position prompt input with model picker
7175
*
7276
* @example
77+
* const promptRef = useRef<FloatingPromptInputRef>(null);
7378
* <FloatingPromptInput
79+
* ref={promptRef}
7480
* onSend={(prompt, model) => console.log('Send:', prompt, model)}
7581
* isLoading={false}
7682
* />
7783
*/
78-
export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
84+
export const FloatingPromptInput = React.forwardRef<FloatingPromptInputRef, FloatingPromptInputProps>(({
7985
onSend,
8086
isLoading = false,
8187
disabled = false,
8288
defaultModel = "sonnet",
8389
projectPath,
8490
className,
85-
}) => {
91+
}, ref) => {
8692
const [prompt, setPrompt] = useState("");
8793
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel);
8894
const [isExpanded, setIsExpanded] = useState(false);
@@ -96,6 +102,34 @@ export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
96102
const textareaRef = useRef<HTMLTextAreaElement>(null);
97103
const expandedTextareaRef = useRef<HTMLTextAreaElement>(null);
98104
const unlistenDragDropRef = useRef<(() => void) | null>(null);
105+
106+
// Expose a method to add images programmatically
107+
React.useImperativeHandle(
108+
ref,
109+
() => ({
110+
addImage: (imagePath: string) => {
111+
setPrompt(currentPrompt => {
112+
const existingPaths = extractImagePaths(currentPrompt);
113+
if (existingPaths.includes(imagePath)) {
114+
return currentPrompt; // Image already added
115+
}
116+
117+
const mention = `@${imagePath}`;
118+
const newPrompt = currentPrompt + (currentPrompt.endsWith(' ') || currentPrompt === '' ? '' : ' ') + mention + ' ';
119+
120+
// Focus the textarea
121+
setTimeout(() => {
122+
const target = isExpanded ? expandedTextareaRef.current : textareaRef.current;
123+
target?.focus();
124+
target?.setSelectionRange(newPrompt.length, newPrompt.length);
125+
}, 0);
126+
127+
return newPrompt;
128+
});
129+
}
130+
}),
131+
[isExpanded]
132+
);
99133

100134
// Helper function to check if a file is an image
101135
const isImageFile = (path: string): boolean => {
@@ -410,21 +444,21 @@ export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
410444
</Button>
411445
</div>
412446

413-
<Button
414-
onClick={handleSend}
415-
disabled={!prompt.trim() || isLoading || disabled}
416-
size="sm"
417-
className="min-w-[80px]"
418-
>
419-
{isLoading ? (
420-
<div className="rotating-symbol text-primary-foreground"></div>
421-
) : (
422-
<>
423-
<Send className="mr-2 h-4 w-4" />
424-
Send
425-
</>
426-
)}
427-
</Button>
447+
{isLoading ? (
448+
<div className="flex items-center justify-center min-w-[60px] h-10">
449+
<div className="rotating-symbol text-primary text-2xl"></div>
450+
</div>
451+
) : (
452+
<Button
453+
onClick={handleSend}
454+
disabled={!prompt.trim() || disabled}
455+
size="sm"
456+
className="min-w-[80px]"
457+
>
458+
<Send className="mr-2 h-4 w-4" />
459+
Send
460+
</Button>
461+
)}
428462
</div>
429463
</motion.div>
430464
</motion.div>
@@ -541,18 +575,20 @@ export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
541575
</div>
542576

543577
{/* Send Button */}
544-
<Button
545-
onClick={handleSend}
546-
disabled={!prompt.trim() || isLoading || disabled}
547-
size="default"
548-
className="min-w-[60px]"
549-
>
550-
{isLoading ? (
551-
<div className="rotating-symbol text-primary-foreground"></div>
552-
) : (
578+
{isLoading ? (
579+
<div className="flex items-center justify-center min-w-[60px] h-10">
580+
<div className="rotating-symbol text-primary text-2xl"></div>
581+
</div>
582+
) : (
583+
<Button
584+
onClick={handleSend}
585+
disabled={!prompt.trim() || disabled}
586+
size="default"
587+
className="min-w-[60px]"
588+
>
553589
<Send className="h-4 w-4" />
554-
)}
555-
</Button>
590+
</Button>
591+
)}
556592
</div>
557593

558594
<div className="mt-2 text-xs text-muted-foreground">
@@ -563,4 +599,6 @@ export const FloatingPromptInput: React.FC<FloatingPromptInputProps> = ({
563599
</div>
564600
</>
565601
);
566-
};
602+
});
603+
604+
FloatingPromptInput.displayName = 'FloatingPromptInput';

0 commit comments

Comments
 (0)