Skip to content

Commit 06a0f31

Browse files
authored
Merge pull request #6 from OpenKnots/okcode/ctrl-backtick-terminal
Add image attachment picker to chat composer
2 parents d6754ca + 69813b7 commit 06a0f31

1 file changed

Lines changed: 47 additions & 1 deletion

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,11 @@ import {
9696
ChevronLeftIcon,
9797
ChevronRightIcon,
9898
CircleAlertIcon,
99+
ImagePlusIcon,
99100
ListTodoIcon,
100101
LockIcon,
101102
LockOpenIcon,
103+
PaperclipIcon,
102104
XIcon,
103105
} from "lucide-react";
104106
import { Button } from "./ui/button";
@@ -414,6 +416,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
414416
const attachmentPreviewHandoffTimeoutByMessageIdRef = useRef<Record<string, number>>({});
415417
const sendInFlightRef = useRef(false);
416418
const dragDepthRef = useRef(0);
419+
const fileInputRef = useRef<HTMLInputElement>(null);
417420
const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});
418421
const setMessagesScrollContainerRef = useCallback((element: HTMLDivElement | null) => {
419422
messagesScrollRef.current = element;
@@ -2339,6 +2342,19 @@ export default function ChatView({ threadId }: ChatViewProps) {
23392342
focusComposer();
23402343
};
23412344

2345+
const onFileInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
2346+
const files = Array.from(event.target.files ?? []);
2347+
if (files.length > 0) {
2348+
addComposerImages(files);
2349+
}
2350+
// Reset so the same file can be selected again
2351+
event.target.value = "";
2352+
};
2353+
2354+
const openFilePicker = () => {
2355+
fileInputRef.current?.click();
2356+
};
2357+
23422358
const onRevertToTurnCount = useCallback(
23432359
async (turnCount: number) => {
23442360
const api = readNativeApi();
@@ -3776,16 +3792,33 @@ export default function ChatView({ threadId }: ChatViewProps) {
37763792
className="mx-auto w-full min-w-0 max-w-3xl"
37773793
data-chat-composer-form="true"
37783794
>
3795+
<input
3796+
ref={fileInputRef}
3797+
type="file"
3798+
accept="image/*"
3799+
multiple
3800+
className="hidden"
3801+
onChange={onFileInputChange}
3802+
tabIndex={-1}
3803+
/>
37793804
<div
37803805
className={cn(
3781-
"group rounded-[22px] p-px transition-colors duration-200",
3806+
"group relative rounded-[22px] p-px transition-colors duration-200",
37823807
composerProviderState.composerFrameClassName,
37833808
)}
37843809
onDragEnter={onComposerDragEnter}
37853810
onDragOver={onComposerDragOver}
37863811
onDragLeave={onComposerDragLeave}
37873812
onDrop={onComposerDrop}
37883813
>
3814+
{isDragOverComposer && (
3815+
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-[22px] border-2 border-dashed border-primary/60 bg-primary/5">
3816+
<div className="flex items-center gap-2 rounded-lg bg-background/90 px-4 py-2 text-sm font-medium text-primary shadow-sm">
3817+
<ImagePlusIcon className="size-4" />
3818+
Drop images to attach
3819+
</div>
3820+
</div>
3821+
)}
37893822
<div
37903823
className={cn(
37913824
"rounded-[20px] border bg-card transition-colors duration-200 focus-within:border-ring/45",
@@ -4100,6 +4133,19 @@ export default function ChatView({ threadId }: ChatViewProps) {
41004133
data-chat-composer-actions="right"
41014134
className="flex shrink-0 items-center gap-2"
41024135
>
4136+
{pendingUserInputs.length === 0 && (
4137+
<Button
4138+
variant="ghost"
4139+
size="icon-sm"
4140+
type="button"
4141+
className="text-muted-foreground/70 hover:text-foreground/80"
4142+
onClick={openFilePicker}
4143+
title="Attach images"
4144+
aria-label="Attach images"
4145+
>
4146+
<PaperclipIcon className="size-4" />
4147+
</Button>
4148+
)}
41034149
{activeContextWindow ? (
41044150
<ContextWindowMeter usage={activeContextWindow} />
41054151
) : null}

0 commit comments

Comments
 (0)