feat(file input): adding feature to add and remove files

Next up is to create a seperate menu that will show all the files.
Probably as a seperate frame. Needs to be scrollable and can use similar
ui as before
This commit is contained in:
JK-le-dev 2025-10-19 04:32:32 -05:00
commit 7785047976
11 changed files with 182 additions and 48 deletions

View file

@ -5,6 +5,7 @@ function App() {
return (
<div className="dark min-h-screen bg-gray-950 text-white flex justify-center pt-12">
<ChatLayout />
<div></div>
</div>
);
}

View file

@ -3,20 +3,6 @@ import ChatHeader from "src/components/ui/chat/chat-header";
import ChatWindow from "src/components/ui/chat/chat-window";
import MessageInput from "src/components/ui/chat/message-input";
import { GoogleGenAI } from "@google/genai"
const ai = new GoogleGenAI({ apiKey: import.meta.env.GEMINI_API_KEY })
async function AIRepsponse(userInputArray) {
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: userInputArray
})
return response.text
}
let userInput = []
export default function ChatLayout() {
const [messages, setMessages] = useState([
{
@ -25,23 +11,21 @@ export default function ChatLayout() {
},
]);
async function handleSend(text) {
userInput.push(text)
const res = await AIRepsponse(userInput)
function handleSend(text) {
const userMsg = { role: "user", content: text };
setMessages((s) => [...s, userMsg]);
// fake assistant reply after short delay
setTimeout(() => {
setMessages((s) => [
...s,
{ role: "assistant", content: res },
{ role: "assistant", content: `You said: ${text}` },
]);
}, 600);
}
return (
<div className="w-full max-w-4xl gap-4 p-4">
<div className="flex flex-col flex-start w-full max-w-3xl gap-4 p-4">
<ChatHeader />
<ChatWindow messages={messages} />
<MessageInput onSend={handleSend} />

View file

@ -5,7 +5,7 @@ export default function FlameButton({ onClick }) {
return (
<motion.button
onClick={onClick}
className="bg-gray-700 p-2 rounded-2xl"
className="bg-gray-700 cursor-pointer p-2 rounded-2xl border-2 border-gray-600"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>

View file

@ -6,7 +6,7 @@ export default function DownButton({ onClick }) {
return (
<motion.button
onClick={onClick}
className="bg-gray-700 p-2 rounded-2xl"
className="bg-gray-700 p-2 rounded-2xl file-input border-2 border-gray-600"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>

View file

@ -1,16 +1,71 @@
import React from "react";
import React, { useState, useRef } from "react";
import { X } from "lucide-react";
import { motion } from "motion/react";
import { FilePlus2 } from "lucide-react";
export default function SchematicButton({ onClick }) {
export default function SchematicButton({ onFiles }) {
const [filesList, setFilesList] = useState([]);
const inputRef = useRef(null);
function handleFiles(e) {
const files = Array.from(e.target.files || []);
if (files.length === 0) return;
setFilesList((s) => [...s, ...files]);
if (onFiles) onFiles(files);
if (inputRef.current) inputRef.current.value = null;
}
function removeFile(index) {
setFilesList((s) => {
const copy = [...s];
copy.splice(index, 1);
return copy;
});
}
return (
<motion.button
onClick={onClick}
className=" bg-gray-700 p-2 rounded-2xl"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<FilePlus2 />
</motion.button>
<div className="flex items-center gap-2">
<label className="relative inline-block">
<motion.input
ref={inputRef}
type="file"
accept="image/*,application/pdf"
multiple
onChange={handleFiles}
className="file-input hidden"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
/>
<motion.div
className="bg-gray-700 p-2 rounded-2xl cursor-pointer border-2 border-gray-600"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => inputRef.current && inputRef.current.click()}
>
<FilePlus2 />
</motion.div>
</label>
{filesList.length > 0 && (
<div className="flex gap-2 items-center max-w-xs flex-wrap">
{filesList.map((f, i) => (
<div
key={i}
className="flex items-center gap-2 bg-gray-800 text-sm text-slate-200 px-2 py-1 rounded"
>
<span className="truncate max-w-[10rem]">{f.name}</span>
<button
onClick={() => removeFile(i)}
className="bg-gray-900 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs border-2 border-gray-700"
aria-label="Remove file"
>
<X />
</button>
</div>
))}
</div>
)}
</div>
);
}

View file

@ -1,13 +1,17 @@
import React from "react";
import DeleteButton from "src/components/ui/button/delete-button";
import SchematicButton from "../button/schematic-button";
export default function ChatHeader({ title = "Title of Chat" }) {
return (
<div className="w-full flex justify-center">
<header className="text-slate-100 fixed top-4 ">
<div>
<header className="text-slate-100 fixed top-4 max-w-3xl w-full px-4">
<div className="flex justify-between items-center gap-4">
<SchematicButton></SchematicButton>
<h1 className="text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
{title}
</h1>
<DeleteButton></DeleteButton>
</div>
</header>
</div>

View file

@ -1,12 +1,17 @@
import React, { useState } from "react";
import React, { useState, useRef, useEffect } from "react";
import DeleteButton from "src/components/ui/button/delete-button";
import DownButton from "src/components/ui/button/down-button";
import SchematicButton from "src/components/ui/button/schematic-button";
import { motion } from "motion/react";
import { BotMessageSquare } from "lucide-react";
export default function MessageInput({ onSend }) {
const [text, setText] = useState("");
const textareaRef = useRef(null);
useEffect(() => {
// ensure correct initial height
if (textareaRef.current) textareaRef.current.style.height = "auto";
}, []);
function handleSubmit(e) {
e.preventDefault();
@ -17,29 +22,37 @@ export default function MessageInput({ onSend }) {
return (
<div className="w-full flex justify-center">
<footer className="fixed bottom-6 max-w-2xl w-full px-4">
<div className="flex flex-col gap-2">
<div className="flex justify-between">
<div className="flex gap-2">
<SchematicButton></SchematicButton>
<DeleteButton></DeleteButton>
<footer className="fixed bottom-6 max-w-3xl w-full px-4">
<div className="flex flex-col gap-4">
<div className="flex justify-between items-end">
<div className="flex">
<DownButton></DownButton>
</div>
<DownButton></DownButton>
</div>
<form
onSubmit={handleSubmit}
className="bg-gray-900 rounded-2xl border-2 border-gray-800 shadow-lg shadow-indigo-600"
>
<div className="flex p-2 shadow-xl">
<input
<div className="flex p-2 shadow-xl items-center">
<textarea
ref={textareaRef}
value={text}
onChange={(e) => setText(e.target.value)}
onChange={(e) => {
setText(e.target.value);
// auto-resize
const ta = textareaRef.current;
if (ta) {
ta.style.height = "auto";
ta.style.height = `${ta.scrollHeight}px`;
}
}}
placeholder="Type a message..."
className="flex-1 mx-2 rounded-md shadow-2sx border-none focus:border-none focus:outline-none"
rows={1}
className="flex-1 mx-2 rounded-md shadow-2sx border-none focus:border-none focus:outline-none resize-none overflow-auto max-h-40"
/>
<motion.button
type="submit"
className="flex gap-2 px-4 py-2 bg-gray-700 rounded-xl ml-4"
className="flex gap-2 px-4 py-2 bg-gray-700 rounded-xl ml-4 items-center"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>

View file

@ -1,4 +1,5 @@
@import "tailwindcss";
@plugin "daisyui";
.dark {
--paragraph: 235, 236, 239;