From d76c1cefa784d143dde8b0e78476e698879bdf4f Mon Sep 17 00:00:00 2001 From: Lee Zheng Jing Date: Sat, 10 Aug 2024 15:08:30 +0800 Subject: [PATCH 1/2] Update README.md with script instructions --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index 7059bf8..d56e725 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,50 @@ An AI journaling app, with the key feature of a completion similar to copilot, b 2. Run `yarn install` & `yarn generate` 3. Run `yarn dev` 4. Access the dev build at `localhost:3000` + +## Building with Dockerfile and Deploying to Google Cloud Run + +This project is setup to build with Dockerfile, and deploy to Google Cloud Run. For easy build and development, there is already a buildndeploy.ps1 for Windows and buildndeploy.sh for Linux. + +Before running the buildndeploy script, make sure you have done the following prerequisites: + +1. Install Docker Desktop +2. Install Google Cloud SDK +3. Authenticate with Google Cloud SDK + Run the following command to authenticate with Google Cloud SDK: + +```powershell +gcloud auth login +``` + +4. Enable Google Cloud Run API +5. Create a Google Cloud Run service +6. Create a Google Cloud Run service account +7. Assign the Google Cloud Run service account the necessary roles +8. Run the following commands to set the docker configuration for Google Cloud Run: + +```powershell +gcloud auth configure-docker +``` + +9. Update the buildndeploy script with your Google Cloud Project ID and Google Cloud Run service name +10. Create a .env file in the root directory and follow the .env.example file to set the necessary environment variables +11. Make the `buildndeploy.sh` script executable. + +```sh +chmod +x ./buildndeploy.sh +``` + +12. Run the buildndeploy script: + +For Windows: + +```powershell +.\buildndeploy.ps1 +``` + +For Linux: + +```bash +./buildndeploy.sh +``` \ No newline at end of file From 52223651b256fcaec2261e96dfc4ee92a2b47614 Mon Sep 17 00:00:00 2001 From: leezhengjing Date: Mon, 12 Aug 2024 00:13:05 +0800 Subject: [PATCH 2/2] Add completion effect --- src/app/(journal)/entry/page.tsx | 147 +++++++++++++++++++------------ src/app/api/completion/route.ts | 32 +++++-- src/components/ui/textarea.tsx | 4 +- src/types/index.ts | 2 +- tailwind.config.ts | 7 ++ 5 files changed, 127 insertions(+), 65 deletions(-) diff --git a/src/app/(journal)/entry/page.tsx b/src/app/(journal)/entry/page.tsx index 1cfbdb2..cdaaf3a 100644 --- a/src/app/(journal)/entry/page.tsx +++ b/src/app/(journal)/entry/page.tsx @@ -1,25 +1,64 @@ "use client"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { Sender } from "@/types"; +import { Sender, GeminiMessage } from "@/types"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; const JournalPage = () => { const router = useRouter(); - const [text, setText] = useState(""); const [loading, setLoading] = useState(false); const [streaming, setStreaming] = useState(false); - const [responseText, setResponseText] = useState(""); + const [messages, setMessages] = useState([]); + const [responseText, setResponseText] = useState(""); // State for the response text + const textareaRef = useRef(null); + const debounceTimeout = useRef(null); + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.focus(); // Set focus to the textarea when the component mounts + } + }, []); const handleTextChange = (e: React.ChangeEvent) => { setText(e.target.value); + setResponseText(""); // Clear response text when user starts typing + + // Clear previous debounce timeout + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + // Set new debounce timeout + debounceTimeout.current = setTimeout(() => { + handleGenerate(); // Call the function to generate AI response after 3 seconds of inactivity + }, 3000); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Tab" && !loading && responseText) { + e.preventDefault(); + setText((prev) => prev + responseText); // Append the response text to the user's input + setResponseText(""); // Clear the response text after appending + } }; const handleGenerate = async () => { + if (text.trim() === "") return; + setLoading(true); - setResponseText(""); // Clear previous response + + // Trim the text to the last 400 characters or less + const trimmedText = text.slice(-400); + + const newMessage: GeminiMessage = { + role: "user", + parts: [{ text: trimmedText }], + }; + + const newMessages = [...messages, newMessage]; + setMessages(newMessages); try { const response = await fetch("/api/completion", { @@ -27,57 +66,55 @@ const JournalPage = () => { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ text }), + body: JSON.stringify({ messages: newMessages }), }); if (!response.ok) { setLoading(false); - throw new Error("Failed to save the entry"); - } - - const data = response.body; - - if (!data) { - return; + throw new Error("Failed to generate the response"); } setLoading(false); setStreaming(true); - const reader = data.getReader(); + const reader = response.body?.getReader(); const decoder = new TextDecoder(); let done = false; - while (!done) { + let systemResponse = ""; + + while (!done && reader) { const { value, done: doneReading } = await reader.read(); done = doneReading; - const chunkValue = decoder.decode(value, { stream: true }); - setResponseText((prev) => prev + chunkValue); + systemResponse += decoder.decode(value, { stream: true }); } + setResponseText(systemResponse); // Update responseText with the bot's response + + const botMessage: GeminiMessage = { + role: "model", + parts: [{ text: systemResponse }], + }; + + setMessages((prev) => [...prev, botMessage]); setStreaming(false); } catch (error) { - console.error("Failed to save the entry:", error); + console.error("Failed to generate the response:", error); + setLoading(false); } }; const handleSave = async () => { - if (text === "") { - console.error("Text is empty"); + if (text === "" && messages.length === 0) { + console.error("No content to save"); return; } setLoading(true); - const contents = [ - { - sender: Sender.USER, - content: text, - }, - { - sender: Sender.BOT, - content: responseText, - }, - ]; + const contents = messages.map((message) => ({ + sender: message.role === "user" ? Sender.USER : Sender.MODEL, + content: message.parts.map((part) => part.text).join(" "), + })); try { const response = await fetch("/api/journal", { @@ -92,43 +129,45 @@ const JournalPage = () => { setLoading(false); throw new Error("Failed to save the entry"); } - setLoading(false); + setLoading(false); router.push("/journals"); } catch (error) { console.error("Failed to save the entry:", error); + setLoading(false); } }; return ( -
-
+
+
-

Entry

+

Entry

- - {streaming && ( -
Streaming response...
- )} - {responseText && ( -
{responseText}
- )} -
- -