Skip to content

Commit bd54d17

Browse files
committed
feat:textarea for video container
1 parent 6887615 commit bd54d17

File tree

2 files changed

+121
-71
lines changed

2 files changed

+121
-71
lines changed

src/components/ChatContainer.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export const ChatContainer = () => {
4747

4848
const resetTextareaHeight = useCallback(() => {
4949
if (textareaRef.current) {
50-
// Reset height to 1 row height, assuming 20px as the line height
5150
textareaRef.current.style.height = "2.5rem";
5251
}
5352
}, []);
@@ -96,6 +95,7 @@ export const ChatContainer = () => {
9695
},
9796
[handleInputChange]
9897
);
98+
9999
return (
100100
<div className="flex flex-col h-[95vh]">
101101
<Card className="flex flex-col flex-1 overflow-hidden">

src/components/VisionContainer.tsx

+120-70
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
"use client";
22
// componnets/VisionContainer.tsx
3+
import React, {
4+
useState,
5+
useRef,
6+
useCallback,
7+
KeyboardEvent,
8+
FormEvent,
9+
} from "react";
310
import { MarkdownViewer } from "@/components/MarkdownViewer";
411
import { Card } from "@/components/ui/card";
5-
import { useState } from "react";
612
import { Loader, Send } from "lucide-react";
713

814
import { useControlContext } from "@/providers/ControlContext";
@@ -16,79 +22,131 @@ export const VisionContainer = () => {
1622
const [prompt, setPrompt] = useState<string>("");
1723
const [userQuestion, setUserQuestion] = useState<string>("");
1824
const [loading, setLoading] = useState<boolean>(false);
25+
const textareaRef = useRef<HTMLTextAreaElement>(null);
1926

20-
const isFormSubmittable = () => {
27+
const isFormSubmittable = useCallback(() => {
2128
return (
2229
prompt.trim() !== "" &&
2330
mediaDataList.some(
2431
(media) => media !== null && media.data !== "" && media.mimeType !== ""
2532
)
2633
);
27-
};
34+
}, [prompt, mediaDataList]);
2835

29-
const handleSubmitForm = async (e: React.FormEvent<HTMLFormElement>) => {
30-
e.preventDefault();
36+
const handleSubmitForm = useCallback(
37+
async (e: React.FormEvent<HTMLFormElement>) => {
38+
e.preventDefault();
3139

32-
if (!isFormSubmittable()) return;
33-
setLoading(true);
34-
setUserQuestion(prompt);
35-
setResult("");
36-
setPrompt("");
40+
if (!isFormSubmittable()) return;
41+
setLoading(true);
42+
setUserQuestion(prompt);
43+
setResult("");
44+
setPrompt("");
45+
resetTextareaHeight();
3746

38-
// Filter out any invalid image data
39-
const validMediaData = mediaDataList.filter(
40-
(data) => data.data !== "" && data.mimeType !== ""
41-
);
47+
// Filter out any invalid image data
48+
const validMediaData = mediaDataList.filter(
49+
(data) => data.data !== "" && data.mimeType !== ""
50+
);
4251

43-
// If there are no valid images and the prompt is empty, do not proceed
44-
if (validMediaData.length === 0) return;
52+
// If there are no valid images and the prompt is empty, do not proceed
53+
if (validMediaData.length === 0) return;
4554

46-
const mediaBase64 = validMediaData.map((data) =>
47-
data.data.replace(/^data:(image|video)\/\w+;base64,/, "")
48-
);
49-
const mediaTypes = validMediaData.map((data) => data.mimeType);
50-
51-
// console.log(mediaBase64.length, mediaTypes);
52-
53-
const body = JSON.stringify({
54-
message: prompt,
55-
media: mediaBase64,
56-
media_types: mediaTypes,
57-
general_settings: generalSettings,
58-
safety_settings: safetySettings,
59-
});
60-
61-
try {
62-
const response = await fetch(`/api/gemini-vision`, {
63-
method: "POST",
64-
body,
65-
headers: {
66-
"Content-Type": "application/json",
67-
},
55+
const mediaBase64 = validMediaData.map((data) =>
56+
data.data.replace(/^data:(image|video)\/\w+;base64,/, "")
57+
);
58+
const mediaTypes = validMediaData.map((data) => data.mimeType);
59+
60+
// console.log(mediaBase64.length, mediaTypes);
61+
62+
const body = JSON.stringify({
63+
message: prompt,
64+
media: mediaBase64,
65+
media_types: mediaTypes,
66+
general_settings: generalSettings,
67+
safety_settings: safetySettings,
6868
});
6969

70-
if (!response.ok) {
71-
throw new Error(`HTTP error! status: ${response.status}`);
72-
}
70+
try {
71+
const response = await fetch(`/api/gemini-vision`, {
72+
method: "POST",
73+
body,
74+
headers: {
75+
"Content-Type": "application/json",
76+
},
77+
});
7378

74-
const reader = response.body?.getReader();
75-
if (reader) {
76-
let accumulator = "";
77-
while (true) {
78-
const { done, value } = await reader.read();
79-
if (done) break;
80-
const text = new TextDecoder().decode(value);
81-
accumulator += text;
82-
setResult(accumulator);
79+
if (!response.ok) {
80+
throw new Error(`HTTP error! status: ${response.status}`);
81+
}
82+
83+
const reader = response.body?.getReader();
84+
if (reader) {
85+
let accumulator = "";
86+
while (true) {
87+
const { done, value } = await reader.read();
88+
if (done) break;
89+
const text = new TextDecoder().decode(value);
90+
accumulator += text;
91+
setResult(accumulator);
92+
}
93+
setLoading(false);
94+
}
95+
} catch (error) {
96+
if (error instanceof Error) {
97+
setResult(`Error: ${error.message}`);
98+
setLoading(false);
8399
}
84-
setLoading(false);
85-
}
86-
} catch (error) {
87-
if (error instanceof Error) {
88-
setResult(`Error: ${error.message}`);
89-
setLoading(false);
90100
}
101+
// };
102+
},
103+
[generalSettings, safetySettings, mediaDataList, prompt]
104+
);
105+
106+
const resetTextareaHeight = useCallback(() => {
107+
if (textareaRef.current) {
108+
textareaRef.current.style.height = "2.5rem";
91109
}
110+
}, []);
111+
112+
const handleKeyPress = useCallback(
113+
(event: KeyboardEvent<HTMLTextAreaElement>) => {
114+
if (event.key === "Enter" && !event.ctrlKey && !event.shiftKey) {
115+
event.preventDefault();
116+
if (isFormSubmittable()) {
117+
setLoading(true);
118+
handleSubmitForm(event as unknown as FormEvent<HTMLFormElement>);
119+
}
120+
} else if (event.key === "Enter") {
121+
// Allow for Ctrl+Enter or Shift+Enter to insert new lines
122+
event.preventDefault();
123+
const textarea = event.currentTarget;
124+
const cursorPosition = textarea.selectionStart;
125+
textarea.value =
126+
textarea.value.slice(0, cursorPosition) +
127+
"\n" +
128+
textarea.value.slice(cursorPosition);
129+
setPrompt(textarea.value);
130+
textarea.selectionStart = cursorPosition + 1;
131+
textarea.selectionEnd = cursorPosition + 1;
132+
adjustTextareaHeight(textarea);
133+
}
134+
},
135+
[setPrompt, isFormSubmittable, handleSubmitForm]
136+
);
137+
138+
const handleTextAreaInput = useCallback(
139+
(event: React.FormEvent<HTMLTextAreaElement>) => {
140+
const target = event.currentTarget;
141+
setPrompt(target.value);
142+
adjustTextareaHeight(target);
143+
},
144+
[setPrompt]
145+
);
146+
147+
const adjustTextareaHeight = (target: HTMLTextAreaElement) => {
148+
target.style.height = "auto";
149+
target.style.height = `${target.scrollHeight}px`;
92150
};
93151

94152
return (
@@ -113,11 +171,14 @@ export const VisionContainer = () => {
113171
onSubmit={handleSubmitForm}
114172
className="flex gap-4 pt-4 border-t border-primary/70 p-2"
115173
>
116-
<input
117-
type="text"
174+
<textarea
175+
ref={textareaRef}
118176
value={prompt}
177+
onInput={handleTextAreaInput}
119178
onChange={(e) => setPrompt(e.target.value)}
120-
className="flex-1 p-2 rounded"
179+
onKeyDown={handleKeyPress}
180+
rows={1}
181+
className="flex-1 p-2 resize-none overflow-hidden min-h-8 rounded"
121182
placeholder="Ask a question about the images"
122183
/>
123184
<div className="m-auto">
@@ -133,17 +194,6 @@ export const VisionContainer = () => {
133194
)}
134195
</Button>
135196
</div>
136-
{/* <button
137-
type="submit"
138-
className="ml-2 p-2 rounded-full border bg-blue-500 hover:bg-blue-600 text-white"
139-
disabled={!isFormSubmittable() || loading}
140-
>
141-
{loading ? (
142-
<Loader className="animate-spin" />
143-
) : (
144-
<Send className="m-auto" />
145-
)}
146-
</button> */}
147197
</form>
148198
</Card>
149199
</div>

0 commit comments

Comments
 (0)