/** * @license * SPDX-License-Identifier: Apache-2.0 */ import { GoogleGenAI, Chat } from "@google/genai"; // --- PASTE YOUR GEMINI API KEY HERE --- const API_KEY = "YOUR_API_KEY"; // ------------------------------------ document.addEventListener('DOMContentLoaded', () => { // Setup screen elements const setupContainer = document.getElementById('setup-container') as HTMLDivElement; const setupForm = document.getElementById('setup-form') as HTMLFormElement; const nameInput = document.getElementById('name-input') as HTMLInputElement; const gradeSelect = document.getElementById('grade-select') as HTMLSelectElement; const boardSelect = document.getElementById('board-select') as HTMLSelectElement; const subjectSelect = document.getElementById('subject-select') as HTMLSelectElement; // Chat screen elements const chatContainer = document.getElementById('chat-container') as HTMLDivElement; const chatTitle = document.getElementById('chat-title') as HTMLHeadingElement; const messageForm = document.getElementById('message-form') as HTMLFormElement; const messageInput = document.getElementById('message-input') as HTMLInputElement; const sendButton = document.getElementById('send-button') as HTMLButtonElement; const messageList = document.getElementById('message-list') as HTMLDivElement; const typingIndicator = document.getElementById('typing-indicator') as HTMLDivElement; // Attachment and Image elements const attachmentButton = document.getElementById('attachment-button') as HTMLButtonElement; const attachmentMenu = document.getElementById('attachment-menu') as HTMLDivElement; const uploadImageButton = document.getElementById('upload-image-button') as HTMLButtonElement; const useCameraButton = document.getElementById('use-camera-button') as HTMLButtonElement; const imageUpload = document.getElementById('image-upload') as HTMLInputElement; const imagePreviewContainer = document.getElementById('image-preview-container') as HTMLDivElement; const imagePreview = document.getElementById('image-preview') as HTMLImageElement; const removeImageButton = document.getElementById('remove-image-button') as HTMLButtonElement; // Camera Modal elements const cameraModal = document.getElementById('camera-modal') as HTMLDivElement; const cameraView = document.getElementById('camera-view') as HTMLVideoElement; const cameraCanvas = document.getElementById('camera-canvas') as HTMLCanvasElement; const captureButton = document.getElementById('capture-button') as HTMLButtonElement; const cancelCameraButton = document.getElementById('cancel-camera-button') as HTMLButtonElement; let ai: GoogleGenAI; let chat: Chat | null = null; let studentName: string = ''; let attachedImage: { mimeType: string, data: string } | null = null; let cameraStream: MediaStream | null = null; /** * Converts a file to a Base64 encoded string. */ const fileToGenerativePart = async (file: File) => { const base64EncodedDataPromise = new Promise((resolve) => { const reader = new FileReader(); reader.onloadend = () => resolve((reader.result as string).split(',')[1]); reader.readAsDataURL(file); }); return { inlineData: { data: await base64EncodedDataPromise, mimeType: file.type }, }; }; /** * Formats the AI's text response into safe HTML. */ const formatResponse = (text: string): string => { let html = ''; let inList = false; const lines = text.split('\n'); for (const line of lines) { let processedLine = line.replace(/\*\*(.*?)\*\*/g, '$1'); if (processedLine.startsWith('* ')) { if (!inList) { html += ''; inList = false; } if (processedLine.trim()) { html += `

${processedLine}

`; } } } if (inList) { html += ''; } return html; }; /** * Scrolls the message list to the bottom. */ const scrollToBottom = () => { messageList.scrollTop = messageList.scrollHeight; }; /** * Displays the selected image preview. */ const showImagePreview = (file: File) => { const reader = new FileReader(); reader.onload = async (e) => { imagePreview.src = e.target?.result as string; imagePreviewContainer.classList.remove('hidden'); const part = await fileToGenerativePart(file); attachedImage = { mimeType: part.inlineData.mimeType, data: part.inlineData.data, }; }; reader.readAsDataURL(file); }; /** * Removes the attached image. */ const removeImage = () => { attachedImage = null; imagePreview.src = '#'; imageUpload.value = ''; // Reset file input imagePreviewContainer.classList.add('hidden'); }; /** * Adds a message to the chat window. */ const addMessage = (text: string, sender: 'user' | 'ai' | 'system', imageUrl?: string) => { const messageElement = document.createElement('div'); messageElement.classList.add('message', `${sender}-message`); let content = ''; if (imageUrl) { const img = document.createElement('img'); img.src = imageUrl; content += img.outerHTML; } if (sender === 'ai') { content += formatResponse(text); } else { const p = document.createElement('p'); p.textContent = text; content += p.outerHTML; } messageElement.innerHTML = content; messageList.appendChild(messageElement); scrollToBottom(); }; /** * Initializes a new chat session with the current settings. */ const initializeChat = () => { const grade = gradeSelect.value; const board = boardSelect.value; const subject = subjectSelect.value; const systemInstruction = `You are an expert AI Tutor. Your student's name is ${studentName}. They are in grade ${grade}, following the ${board} curriculum, and are asking about ${subject}. Please provide a clear, concise, and age-appropriate explanation. Explain the concept simply and use analogies if helpful. Do not just give the answer; help the student understand the 'how' and the 'why'. Format your response with markdown-style syntax for bolding (**text**) and bullet points (* point).`; chat = ai.chats.create({ model: 'gemini-2.5-flash', config: { systemInstruction: systemInstruction, } }); }; /** * Handles the form submission for sending a message. */ const handleSendMessage = async (e: Event) => { e.preventDefault(); const userMessage = messageInput.value.trim(); if (!userMessage && !attachedImage) return; const imageUrl = attachedImage ? `data:${attachedImage.mimeType};base64,${attachedImage.data}` : undefined; addMessage(userMessage, 'user', imageUrl); const messageToSend = []; if (attachedImage) { messageToSend.push({ inlineData: attachedImage }); } if (userMessage) { messageToSend.push({ text: userMessage }); } messageInput.value = ''; messageInput.disabled = true; sendButton.disabled = true; removeImage(); typingIndicator.style.display = 'flex'; scrollToBottom(); try { if (!chat) initializeChat(); const result = await chat!.sendMessage({ message: messageToSend }); addMessage(result.text, 'ai'); } catch (err) { console.error(err); addMessage('Sorry, something went wrong. Please try again.', 'ai'); } finally { typingIndicator.style.display = 'none'; messageInput.disabled = false; sendButton.disabled = false; messageInput.focus(); } }; /** * Handles the initial setup form submission. */ const handleSetup = (e: Event) => { e.preventDefault(); studentName = nameInput.value.trim(); if (!studentName) { alert('Please fill in your name.'); return; } if (!API_KEY || API_KEY === "YOUR_API_KEY") { alert("Please provide your Gemini API key in the index.tsx file."); return; } try { ai = new GoogleGenAI({ apiKey: API_KEY }); } catch (error) { alert('Failed to initialize AI. Please check your API key configuration.'); console.error(error); return; } setupContainer.classList.add('hidden'); chatContainer.classList.remove('hidden'); chatTitle.textContent = `AI Tutor for ${studentName}`; initializeChat(); addMessage(`Hello ${studentName}! I'm your AI Tutor. Ask me anything about ${subjectSelect.value}.`, 'ai'); }; /** * Camera functionality */ const openCamera = async () => { try { cameraStream = await navigator.mediaDevices.getUserMedia({ video: true }); cameraView.srcObject = cameraStream; cameraModal.classList.remove('hidden'); } catch (err) { console.error("Error accessing camera: ", err); alert("Could not access the camera. Please ensure you have given permission."); } }; const closeCamera = () => { if (cameraStream) { cameraStream.getTracks().forEach(track => track.stop()); } cameraModal.classList.add('hidden'); }; const captureImage = () => { const context = cameraCanvas.getContext('2d'); cameraCanvas.width = cameraView.videoWidth; cameraCanvas.height = cameraView.videoHeight; context?.drawImage(cameraView, 0, 0, cameraCanvas.width, cameraCanvas.height); cameraCanvas.toBlob(blob => { if (blob) { const file = new File([blob], "camera-shot.jpg", { type: "image/jpeg" }); showImagePreview(file); } }, 'image/jpeg'); closeCamera(); }; // --- Event Listeners --- setupForm.addEventListener('submit', handleSetup); messageForm.addEventListener('submit', handleSendMessage); removeImageButton.addEventListener('click', removeImage); // Attachment menu toggle attachmentButton.addEventListener('click', (e) => { e.stopPropagation(); attachmentMenu.classList.toggle('hidden'); }); document.addEventListener('click', () => { if (!attachmentMenu.classList.contains('hidden')) { attachmentMenu.classList.add('hidden'); } }); // Upload image uploadImageButton.addEventListener('click', () => imageUpload.click()); imageUpload.addEventListener('change', (e) => { const files = (e.target as HTMLInputElement).files; if (files && files[0]) { showImagePreview(files[0]); } }); // Camera events useCameraButton.addEventListener('click', openCamera); captureButton.addEventListener('click', captureImage); cancelCameraButton.addEventListener('click', closeCamera); });