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