Skip to content

Commit f33cfd0

Browse files
Adding Day #78
1 parent 09c8d9f commit f33cfd0

File tree

9 files changed

+866
-1
lines changed

9 files changed

+866
-1
lines changed
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Day #78
2+
3+
### Google Gemini Clone
4+
Build your own Generative AI App using Google Gemini API with Vanilla JavaScript. Create AI App like Google Gemini or ChatGPT step by step tutorial.
5+
6+
In this tutorial ([Open in Youtube](https://youtu.be/Klcpzw_nvLU)), you will learn to create your own Generative AI app like Google Gemini or ChatGPT using vanilla JavaScript. In this step-by-step tutorial, we will create a Gemini clone app using the Gemini API for absolutely free. You can build this Google Gemini app for your college project or personal portfolio.
7+
8+
- Introduction to AI-Powered Chat Apps: Learn how to create a generative AI app similar to Google Gemini or ChatGPT using vanilla JavaScript. 🤖
9+
- Connecting with the Gemini API: Discover how to use the Gemini API to add AI capabilities to your app, allowing for natural language conversations and responses. 🌐
10+
- Step-by-Step JavaScript Implementation: Follow our detailed guide, from setting up your project to implementing AI-powered chat features using vanilla JavaScript. 💻
11+
- User-Friendly Interface: Design a sleek, responsive interface for an engaging chat experience, ensuring smooth interactions and real-time responses. 🌟
12+
- Optimizing Performance: Learn how to keep your AI chat app efficient, running smoothly across different devices and platforms. 📱
13+
14+
## Warning
15+
You need to get your own **API Key** (in video we showed how!) and replace it in script.js file on line 12 :
16+
17+
```javascript
18+
const GOOGLE_API_KEY = "YOUR_API_KEY";
19+
```
20+
21+
# Screenshot
22+
Here we have project screenshot :
23+
24+
![screenshot](screenshot.png)
96.6 KB
Loading
Loading
1.35 MB
Loading
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
8+
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
9+
<link rel="stylesheet" href="style.css">
10+
<title>Day #78 - Google Gemini Clone | AsmrProg</title>
11+
</head>
12+
13+
<body>
14+
15+
<nav class="navbar">
16+
<h3 class="navbar__logo">AsmrProg Gemini</h3>
17+
<button class="navbar__button" id="themeToggler"><i class='bx bx-sun'></i></button>
18+
</nav>
19+
20+
<header class="header">
21+
<div class="header__title">
22+
<h1>Hello, There!</h1>
23+
<h2>How can I help you today?</h2>
24+
</div>
25+
<div class="suggests">
26+
<div class="suggests__item">
27+
<p class="suggests__item-text">
28+
Give tips on helping kids finish their homework on time
29+
</p>
30+
<div class="suggests__item-icon">
31+
<i class='bx bx-stopwatch'></i>
32+
</div>
33+
</div>
34+
<div class="suggests__item">
35+
<p class="suggests__item-text">
36+
Help me write an out-of-office email
37+
</p>
38+
<div class="suggests__item-icon">
39+
<i class='bx bx-edit-alt'></i>
40+
</div>
41+
</div>
42+
<div class="suggests__item">
43+
<p class="suggests__item-text">
44+
Give me phrases to learn a new language
45+
</p>
46+
<div class="suggests__item-icon">
47+
<i class='bx bx-compass'></i>
48+
</div>
49+
</div>
50+
<div class="suggests__item">
51+
<p class="suggests__item-text">
52+
Show me how to build something by hand
53+
</p>
54+
<div class="suggests__item-icon">
55+
<i class='bx bx-wrench'></i>
56+
</div>
57+
</div>
58+
</div>
59+
</header>
60+
61+
<section class="chats"></section>
62+
63+
<section class="prompt">
64+
<form action="#" class="prompt__form" novalidate>
65+
<div class="prompt__input-wrapper">
66+
<input type="text" placeholder="Enter a prompt here" class="prompt__form-input" required>
67+
<button class="prompt__form-button" id="sendButton">
68+
<i class='bx bx-send'></i>
69+
</button>
70+
<button class="prompt__form-button" id="deleteButton">
71+
<i class='bx bx-trash'></i>
72+
</button>
73+
</div>
74+
</form>
75+
<p class="prompt__disclaim">
76+
Gemini may display inaccurate info, including about people, so double-check its responses.
77+
</p>
78+
</section>
79+
80+
81+
82+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
83+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
84+
<script src="script.js"></script>
85+
</body>
86+
87+
</html>
80.8 KB
Loading
+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
const messageForm = document.querySelector(".prompt__form");
2+
const chatHistoryContainer = document.querySelector(".chats");
3+
const suggestionItems = document.querySelectorAll(".suggests__item");
4+
5+
const themeToggleButton = document.getElementById("themeToggler");
6+
const clearChatButton = document.getElementById("deleteButton");
7+
8+
// State variables
9+
let currentUserMessage = null;
10+
let isGeneratingResponse = false;
11+
12+
const GOOGLE_API_KEY = "YOUR_API_KEY";
13+
const API_REQUEST_URL = `https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent?key=${GOOGLE_API_KEY}`;
14+
15+
// Load saved data from local storage
16+
const loadSavedChatHistory = () => {
17+
const savedConversations = JSON.parse(localStorage.getItem("saved-api-chats")) || [];
18+
const isLightTheme = localStorage.getItem("themeColor") === "light_mode";
19+
20+
document.body.classList.toggle("light_mode", isLightTheme);
21+
themeToggleButton.innerHTML = isLightTheme ? '<i class="bx bx-moon"></i>' : '<i class="bx bx-sun"></i>';
22+
23+
chatHistoryContainer.innerHTML = '';
24+
25+
// Iterate through saved chat history and display messages
26+
savedConversations.forEach(conversation => {
27+
// Display the user's message
28+
const userMessageHtml = `
29+
30+
<div class="message__content">
31+
<img class="message__avatar" src="assets/profile.png" alt="User avatar">
32+
<p class="message__text">${conversation.userMessage}</p>
33+
</div>
34+
35+
`;
36+
37+
const outgoingMessageElement = createChatMessageElement(userMessageHtml, "message--outgoing");
38+
chatHistoryContainer.appendChild(outgoingMessageElement);
39+
40+
// Display the API response
41+
const responseText = conversation.apiResponse?.candidates?.[0]?.content?.parts?.[0]?.text;
42+
const parsedApiResponse = marked.parse(responseText); // Convert to HTML
43+
const rawApiResponse = responseText; // Plain text version
44+
45+
const responseHtml = `
46+
47+
<div class="message__content">
48+
<img class="message__avatar" src="assets/gemini.svg" alt="Gemini avatar">
49+
<p class="message__text"></p>
50+
<div class="message__loading-indicator hide">
51+
<div class="message__loading-bar"></div>
52+
<div class="message__loading-bar"></div>
53+
<div class="message__loading-bar"></div>
54+
</div>
55+
</div>
56+
<span onClick="copyMessageToClipboard(this)" class="message__icon hide"><i class='bx bx-copy-alt'></i></span>
57+
58+
`;
59+
60+
const incomingMessageElement = createChatMessageElement(responseHtml, "message--incoming");
61+
chatHistoryContainer.appendChild(incomingMessageElement);
62+
63+
const messageTextElement = incomingMessageElement.querySelector(".message__text");
64+
65+
// Display saved chat without typing effect
66+
showTypingEffect(rawApiResponse, parsedApiResponse, messageTextElement, incomingMessageElement, true); // 'true' skips typing
67+
});
68+
69+
document.body.classList.toggle("hide-header", savedConversations.length > 0);
70+
};
71+
72+
// create a new chat message element
73+
const createChatMessageElement = (htmlContent, ...cssClasses) => {
74+
const messageElement = document.createElement("div");
75+
messageElement.classList.add("message", ...cssClasses);
76+
messageElement.innerHTML = htmlContent;
77+
return messageElement;
78+
}
79+
80+
// Show typing effect
81+
const showTypingEffect = (rawText, htmlText, messageElement, incomingMessageElement, skipEffect = false) => {
82+
const copyIconElement = incomingMessageElement.querySelector(".message__icon");
83+
copyIconElement.classList.add("hide"); // Initially hide copy button
84+
85+
if (skipEffect) {
86+
// Display content directly without typing
87+
messageElement.innerHTML = htmlText;
88+
hljs.highlightAll();
89+
addCopyButtonToCodeBlocks();
90+
copyIconElement.classList.remove("hide"); // Show copy button
91+
isGeneratingResponse = false;
92+
return;
93+
}
94+
95+
const wordsArray = rawText.split(' ');
96+
let wordIndex = 0;
97+
98+
const typingInterval = setInterval(() => {
99+
messageElement.innerText += (wordIndex === 0 ? '' : ' ') + wordsArray[wordIndex++];
100+
if (wordIndex === wordsArray.length) {
101+
clearInterval(typingInterval);
102+
isGeneratingResponse = false;
103+
messageElement.innerHTML = htmlText;
104+
hljs.highlightAll();
105+
addCopyButtonToCodeBlocks();
106+
copyIconElement.classList.remove("hide");
107+
}
108+
}, 75);
109+
};
110+
111+
// Fetch API response based on user input
112+
const requestApiResponse = async (incomingMessageElement) => {
113+
const messageTextElement = incomingMessageElement.querySelector(".message__text");
114+
115+
try {
116+
const response = await fetch(API_REQUEST_URL, {
117+
method: "POST",
118+
headers: { "Content-Type": "application/json" },
119+
body: JSON.stringify({
120+
contents: [{ role: "user", parts: [{ text: currentUserMessage }] }]
121+
}),
122+
});
123+
124+
const responseData = await response.json();
125+
if (!response.ok) throw new Error(responseData.error.message);
126+
127+
const responseText = responseData?.candidates?.[0]?.content?.parts?.[0]?.text;
128+
if (!responseText) throw new Error("Invalid API response.");
129+
130+
const parsedApiResponse = marked.parse(responseText);
131+
const rawApiResponse = responseText;
132+
133+
showTypingEffect(rawApiResponse, parsedApiResponse, messageTextElement, incomingMessageElement);
134+
135+
// Save conversation in local storage
136+
let savedConversations = JSON.parse(localStorage.getItem("saved-api-chats")) || [];
137+
savedConversations.push({
138+
userMessage: currentUserMessage,
139+
apiResponse: responseData
140+
});
141+
localStorage.setItem("saved-api-chats", JSON.stringify(savedConversations));
142+
} catch (error) {
143+
isGeneratingResponse = false;
144+
messageTextElement.innerText = error.message;
145+
messageTextElement.closest(".message").classList.add("message--error");
146+
} finally {
147+
incomingMessageElement.classList.remove("message--loading");
148+
}
149+
};
150+
151+
// Add copy button to code blocks
152+
const addCopyButtonToCodeBlocks = () => {
153+
const codeBlocks = document.querySelectorAll('pre');
154+
codeBlocks.forEach((block) => {
155+
const codeElement = block.querySelector('code');
156+
let language = [...codeElement.classList].find(cls => cls.startsWith('language-'))?.replace('language-', '') || 'Text';
157+
158+
const languageLabel = document.createElement('div');
159+
languageLabel.innerText = language.charAt(0).toUpperCase() + language.slice(1);
160+
languageLabel.classList.add('code__language-label');
161+
block.appendChild(languageLabel);
162+
163+
const copyButton = document.createElement('button');
164+
copyButton.innerHTML = `<i class='bx bx-copy'></i>`;
165+
copyButton.classList.add('code__copy-btn');
166+
block.appendChild(copyButton);
167+
168+
copyButton.addEventListener('click', () => {
169+
navigator.clipboard.writeText(codeElement.innerText).then(() => {
170+
copyButton.innerHTML = `<i class='bx bx-check'></i>`;
171+
setTimeout(() => copyButton.innerHTML = `<i class='bx bx-copy'></i>`, 2000);
172+
}).catch(err => {
173+
console.error("Copy failed:", err);
174+
alert("Unable to copy text!");
175+
});
176+
});
177+
});
178+
};
179+
180+
// Show loading animation during API request
181+
const displayLoadingAnimation = () => {
182+
const loadingHtml = `
183+
184+
<div class="message__content">
185+
<img class="message__avatar" src="assets/gemini.svg" alt="Gemini avatar">
186+
<p class="message__text"></p>
187+
<div class="message__loading-indicator">
188+
<div class="message__loading-bar"></div>
189+
<div class="message__loading-bar"></div>
190+
<div class="message__loading-bar"></div>
191+
</div>
192+
</div>
193+
<span onClick="copyMessageToClipboard(this)" class="message__icon hide"><i class='bx bx-copy-alt'></i></span>
194+
195+
`;
196+
197+
const loadingMessageElement = createChatMessageElement(loadingHtml, "message--incoming", "message--loading");
198+
chatHistoryContainer.appendChild(loadingMessageElement);
199+
200+
requestApiResponse(loadingMessageElement);
201+
};
202+
203+
// Copy message to clipboard
204+
const copyMessageToClipboard = (copyButton) => {
205+
const messageContent = copyButton.parentElement.querySelector(".message__text").innerText;
206+
207+
navigator.clipboard.writeText(messageContent);
208+
copyButton.innerHTML = `<i class='bx bx-check'></i>`; // Confirmation icon
209+
setTimeout(() => copyButton.innerHTML = `<i class='bx bx-copy-alt'></i>`, 1000); // Revert icon after 1 second
210+
};
211+
212+
// Handle sending chat messages
213+
const handleOutgoingMessage = () => {
214+
currentUserMessage = messageForm.querySelector(".prompt__form-input").value.trim() || currentUserMessage;
215+
if (!currentUserMessage || isGeneratingResponse) return; // Exit if no message or already generating response
216+
217+
isGeneratingResponse = true;
218+
219+
const outgoingMessageHtml = `
220+
221+
<div class="message__content">
222+
<img class="message__avatar" src="assets/profile.png" alt="User avatar">
223+
<p class="message__text"></p>
224+
</div>
225+
226+
`;
227+
228+
const outgoingMessageElement = createChatMessageElement(outgoingMessageHtml, "message--outgoing");
229+
outgoingMessageElement.querySelector(".message__text").innerText = currentUserMessage;
230+
chatHistoryContainer.appendChild(outgoingMessageElement);
231+
232+
messageForm.reset(); // Clear input field
233+
document.body.classList.add("hide-header");
234+
setTimeout(displayLoadingAnimation, 500); // Show loading animation after delay
235+
};
236+
237+
// Toggle between light and dark themes
238+
themeToggleButton.addEventListener('click', () => {
239+
const isLightTheme = document.body.classList.toggle("light_mode");
240+
localStorage.setItem("themeColor", isLightTheme ? "light_mode" : "dark_mode");
241+
242+
// Update icon based on theme
243+
const newIconClass = isLightTheme ? "bx bx-moon" : "bx bx-sun";
244+
themeToggleButton.querySelector("i").className = newIconClass;
245+
});
246+
247+
// Clear all chat history
248+
clearChatButton.addEventListener('click', () => {
249+
if (confirm("Are you sure you want to delete all chat history?")) {
250+
localStorage.removeItem("saved-api-chats");
251+
252+
// Reload chat history to reflect changes
253+
loadSavedChatHistory();
254+
255+
currentUserMessage = null;
256+
isGeneratingResponse = false;
257+
}
258+
});
259+
260+
// Handle click on suggestion items
261+
suggestionItems.forEach(suggestion => {
262+
suggestion.addEventListener('click', () => {
263+
currentUserMessage = suggestion.querySelector(".suggests__item-text").innerText;
264+
handleOutgoingMessage();
265+
});
266+
});
267+
268+
// Prevent default from submission and handle outgoing message
269+
messageForm.addEventListener('submit', (e) => {
270+
e.preventDefault();
271+
handleOutgoingMessage();
272+
});
273+
274+
// Load saved chat history on page load
275+
loadSavedChatHistory();

0 commit comments

Comments
 (0)