diff --git a/web-app/components.json b/web-app/components.json new file mode 100644 index 0000000..7a2e4d8 --- /dev/null +++ b/web-app/components.json @@ -0,0 +1,15 @@ +{ + "style": "default", + "tailwind": { + "config": "tailwind.config.js", + "css": "src/app/globals.css", + "baseColor": "zinc", + "cssVariables": true + }, + "rsc": false, + "tsx": false, + "aliases": { + "utils": "~/lib/utils", + "components": "~/components" + } +} diff --git a/web-app/package-lock.json b/web-app/package-lock.json index 9cba0a3..a0bc9b4 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -13,6 +13,7 @@ "@vitejs/plugin-react": "^5.0.4", "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", + "class-variance-authority": "^0.7.1", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", @@ -26,10 +27,12 @@ "react-markdown": "^10.1.0", "react-router": "^7.9.4", "react-router-dom": "^7.9.4", + "shadcn-ui": "^0.9.5", "vite-jsconfig-paths": "^2.0.1" }, "devDependencies": { "@eslint/js": "^9.38.0", + "daisyui": "^5.3.7", "eslint": "^9.38.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", @@ -2607,12 +2610,33 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2735,6 +2759,16 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/daisyui": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.7.tgz", + "integrity": "sha512-0+8PaSGift0HlIQABCeZzWOBV5Nx/vsI2TihB9hbaEyZENPlZZz+se2JnAH5rz9gBYTyDLB7NJup8hkREr6WBw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -7558,6 +7592,30 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shadcn-ui": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/shadcn-ui/-/shadcn-ui-0.9.5.tgz", + "integrity": "sha512-dsBQWpdLLYCdSdmvOmu53nJhhWnQD1OiblhuhkI4rPYxPKTyfbmZ2NTJHWMu1fXN9PTfN6IVK5vvh+BrjHJx2g==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1" + }, + "bin": { + "shadcn-ui": "dist/index.js" + } + }, + "node_modules/shadcn-ui/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/web-app/package.json b/web-app/package.json index 4606845..84361bb 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -18,6 +18,7 @@ "@vitejs/plugin-react": "^5.0.4", "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", + "class-variance-authority": "^0.7.1", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", @@ -31,11 +32,13 @@ "react-markdown": "^10.1.0", "react-router": "^7.9.4", "react-router-dom": "^7.9.4", + "shadcn-ui": "^0.9.5", "vite-jsconfig-paths": "^2.0.1" }, "packageManager": ">=npm@10.9.0", "devDependencies": { "@eslint/js": "^9.38.0", + "daisyui": "^5.3.7", "eslint": "^9.38.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", diff --git a/web-app/src/app/index.jsx b/web-app/src/app/index.jsx index 86a06e6..ece1d51 100644 --- a/web-app/src/app/index.jsx +++ b/web-app/src/app/index.jsx @@ -5,6 +5,7 @@ function App() { return (
+
); } diff --git a/web-app/src/components/layouts/chat-layout.jsx b/web-app/src/components/layouts/chat-layout.jsx index 78dff4f..ce45310 100644 --- a/web-app/src/components/layouts/chat-layout.jsx +++ b/web-app/src/components/layouts/chat-layout.jsx @@ -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 ( -
+
diff --git a/web-app/src/components/ui/button/delete-button.jsx b/web-app/src/components/ui/button/delete-button.jsx index 61b4df0..c75c306 100644 --- a/web-app/src/components/ui/button/delete-button.jsx +++ b/web-app/src/components/ui/button/delete-button.jsx @@ -5,7 +5,7 @@ export default function FlameButton({ onClick }) { return ( diff --git a/web-app/src/components/ui/button/down-button.jsx b/web-app/src/components/ui/button/down-button.jsx index c5a5d9f..b51dc3f 100644 --- a/web-app/src/components/ui/button/down-button.jsx +++ b/web-app/src/components/ui/button/down-button.jsx @@ -6,7 +6,7 @@ export default function DownButton({ onClick }) { return ( diff --git a/web-app/src/components/ui/button/schematic-button.jsx b/web-app/src/components/ui/button/schematic-button.jsx index 027d00e..c7e3c00 100644 --- a/web-app/src/components/ui/button/schematic-button.jsx +++ b/web-app/src/components/ui/button/schematic-button.jsx @@ -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 ( - - - +
+ + + {filesList.length > 0 && ( +
+ {filesList.map((f, i) => ( +
+ {f.name} + +
+ ))} +
+ )} +
); } diff --git a/web-app/src/components/ui/chat/chat-header.jsx b/web-app/src/components/ui/chat/chat-header.jsx index 89b5738..2a75129 100644 --- a/web-app/src/components/ui/chat/chat-header.jsx +++ b/web-app/src/components/ui/chat/chat-header.jsx @@ -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 (
-
-
+
+
+

{title}

+
diff --git a/web-app/src/components/ui/chat/message-input.jsx b/web-app/src/components/ui/chat/message-input.jsx index 951a70a..8e48f34 100644 --- a/web-app/src/components/ui/chat/message-input.jsx +++ b/web-app/src/components/ui/chat/message-input.jsx @@ -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 (
-