| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Own AI Assistant</title> |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| <style> |
| body { |
| font-family: 'Inter', sans-serif; |
| background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%); |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| min-height: 100vh; |
| margin: 0; |
| color: #e2e8f0; |
| } |
| .chat-container { |
| display: flex; |
| flex-direction: column; |
| width: 100%; |
| max-width: 800px; |
| height: 90vh; |
| background-color: #202a36; |
| border-radius: 1.5rem; |
| box-shadow: 0 12px 24px rgba(0, 0, 0, 0.5); |
| overflow: hidden; |
| border: 1px solid rgba(74, 85, 104, 0.3); |
| } |
| .chat-header { |
| padding: 1.5rem 2rem; |
| border-bottom: 1px solid #3a4759; |
| text-align: center; |
| font-size: 2rem; |
| font-weight: 700; |
| color: #e2e8f0; |
| background: linear-gradient(90deg, #2d3748, #3a4759); |
| position: relative; |
| z-index: 10; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .messages-area { |
| flex-grow: 1; |
| overflow-y: auto; |
| padding: 1.5rem; |
| display: flex; |
| flex-direction: column; |
| gap: 0.75rem; |
| scroll-behavior: smooth; |
| } |
| .message-bubble { |
| max-width: 85%; |
| padding: 0.85rem 1.2rem; |
| border-radius: 1.2rem; |
| line-height: 1.6; |
| word-wrap: break-word; |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
| opacity: 0; |
| transform: translateY(10px); |
| animation: fadeInSlideUp 0.3s ease-out forwards; |
| } |
| .user-message { |
| background-color: #4299e1; |
| color: white; |
| align-self: flex-end; |
| border-bottom-right-radius: 0.4rem; |
| } |
| .ai-message { |
| background-color: #3a4759; |
| color: #e2e8f0; |
| align-self: flex-start; |
| border-bottom-left-radius: 0.4rem; |
| } |
| @keyframes fadeInSlideUp { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| .input-area { |
| padding: 1.5rem; |
| border-top: 1px solid #3a4759; |
| background-color: #2d3748; |
| display: flex; |
| flex-direction: column; |
| gap: 0.75rem; |
| } |
| textarea { |
| resize: none; |
| flex-grow: 1; |
| min-height: 2.75rem; |
| max-height: 8rem; |
| overflow-y: auto; |
| background-color: #202a36; |
| color: #e2e8f0; |
| border: 1px solid #4a5568; |
| border-radius: 0.75rem; |
| padding: 0.75rem 1rem; |
| } |
| textarea:focus { |
| border-color: #4299e1; |
| box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.4); |
| outline: none; |
| } |
| |
| .spinner { |
| border: 3px solid rgba(255, 255, 255, 0.2); |
| border-left-color: #4299e1; |
| border-radius: 50%; |
| width: 20px; |
| height: 20px; |
| animation: spin 1s linear infinite; |
| } |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| .loading-message { |
| align-self: flex-start; |
| background-color: #3a4759; |
| color: #e2e8f0; |
| padding: 0.75rem 1rem; |
| border-radius: 0.75rem; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
| } |
| .file-input-group { |
| display: flex; |
| gap: 0.75rem; |
| width: 100%; |
| } |
| .file-input-group label { |
| cursor: pointer; |
| background-color: #4a5568; |
| color: #e2e8f0; |
| padding: 0.6rem 1rem; |
| border-radius: 0.75rem; |
| transition: background-color 0.2s, transform 0.1s; |
| font-size: 0.9rem; |
| display: inline-flex; |
| align-items: center; |
| gap: 0.35rem; |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
| } |
| .file-input-group label:hover { |
| background-color: #5a6a7e; |
| transform: translateY(-1px); |
| } |
| .file-input-group input[type="file"] { |
| display: none; |
| } |
| .clear-file-button { |
| background-color: #e53e3e; |
| color: white; |
| padding: 0.4rem 0.75rem; |
| border-radius: 0.75rem; |
| font-size: 0.8rem; |
| margin-left: 0.5rem; |
| cursor: pointer; |
| transition: background-color 0.2s, transform 0.1s; |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
| } |
| .clear-file-button:hover { |
| background-color: #c53030; |
| transform: translateY(-1px); |
| } |
| .file-name-display { |
| font-size: 0.875rem; |
| color: #a0aec0; |
| margin-left: 0.5rem; |
| display: flex; |
| align-items: center; |
| flex-grow: 1; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| .send-button { |
| background: linear-gradient(90deg, #4299e1, #3182ce); |
| color: white; |
| font-weight: 600; |
| padding: 0.75rem 1.5rem; |
| border-radius: 0.75rem; |
| transition: transform 0.2s, box-shadow 0.2s; |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); |
| } |
| .send-button:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); |
| } |
| .send-button:disabled { |
| background: #4a5568; |
| cursor: not-allowed; |
| transform: none; |
| box-shadow: none; |
| opacity: 0.7; |
| } |
| .new-chat-button { |
| background-color: #63b3ed; |
| color: #1a202c; |
| padding: 0.5rem 1rem; |
| border-radius: 0.75rem; |
| font-size: 0.9rem; |
| font-weight: 600; |
| transition: background-color 0.2s, transform 0.1s; |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
| display: flex; |
| align-items: center; |
| gap: 0.25rem; |
| } |
| .new-chat-button:hover { |
| background-color: #4299e1; |
| transform: translateY(-1px); |
| } |
| </style> |
| </head> |
| <body class="p-4"> |
| <div class="chat-container"> |
| <div class="chat-header"> |
| <span>Own AI Assistant</span> |
| <button id="newChatButton" class="new-chat-button"> |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg> |
| New Chat |
| </button> |
| </div> |
| <div id="messagesArea" class="messages-area"> |
| |
| <div class="ai-message message-bubble"> |
| Hello! How can I assist you today? You can type a message, or upload an image, a text document, or a PDF. |
| </div> |
| </div> |
| <div class="input-area flex-col items-stretch"> |
| <div class="file-input-group w-full"> |
| <label for="imageUpload" class="flex-shrink-0"> |
| <svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg> |
| Image |
| </label> |
| <input type="file" id="imageUpload" accept="image/*"> |
|
|
| <label for="documentUpload" class="flex-shrink-0"> |
| <svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg> |
| Document |
| </label> |
| <input type="file" id="documentUpload" accept=".txt, .pdf, .docx"> |
| |
| <span id="fileNameDisplay" class="file-name-display flex-grow"></span> |
| <button id="clearFileButton" class="clear-file-button hidden">Clear</button> |
| </div> |
| |
| <div id="filePreview" class="w-full mb-2 hidden"> |
| |
| </div> |
|
|
| <div class="flex w-full gap-3"> |
| <textarea |
| id="messageInput" |
| class="p-3 border rounded-lg focus:outline-none transition duration-200" |
| placeholder="Type your message..." |
| rows="1" |
| oninput="this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px';" |
| ></textarea> |
| <button |
| id="sendButton" |
| class="send-button flex items-center justify-center" |
| > |
| Send |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="messageBox" class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center hidden"> |
| <div class="bg-white p-6 rounded-lg shadow-xl max-w-sm w-full text-gray-800"> |
| <h3 class="text-lg font-bold mb-4" id="messageBoxTitle"></h3> |
| <p class="text-gray-700 mb-6" id="messageBoxContent"></p> |
| <button id="messageBoxClose" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg w-full"> |
| OK |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const messagesArea = document.getElementById('messagesArea'); |
| const messageInput = document.getElementById('messageInput'); |
| const sendButton = document.getElementById('sendButton'); |
| const imageUpload = document.getElementById('imageUpload'); |
| const documentUpload = document.getElementById('documentUpload'); |
| const filePreview = document.getElementById('filePreview'); |
| const fileNameDisplay = document.getElementById('fileNameDisplay'); |
| const clearFileButton = document.getElementById('clearFileButton'); |
| const messageBox = document.getElementById('messageBox'); |
| const messageBoxTitle = document.getElementById('messageBoxTitle'); |
| const messageBoxContent = document.getElementById('messageBoxContent'); |
| const messageBoxClose = document.getElementById('messageBoxClose'); |
| const newChatButton = document.getElementById('newChatButton'); |
| |
| let sessionId = localStorage.getItem('aiChatSessionId'); |
| if (!sessionId) { |
| sessionId = crypto.randomUUID(); |
| localStorage.setItem('aiChatSessionId', sessionId); |
| console.log("New session ID generated:", sessionId); |
| } else { |
| console.log("Existing session ID loaded:", sessionId); |
| } |
| |
| let uploadedFile = null; |
| let uploadedFileType = null; |
| let uploadedFileBase64 = null; |
| let uploadedFileText = null; |
| |
| |
| function showMessage(title, content) { |
| messageBoxTitle.textContent = title; |
| messageBoxContent.textContent = content; |
| messageBox.classList.remove('hidden'); |
| } |
| |
| |
| messageBoxClose.addEventListener('click', () => { |
| messageBox.classList.add('hidden'); |
| }); |
| |
| |
| function addMessage(content, sender, isTyping = false) { |
| const messageDiv = document.createElement('div'); |
| messageDiv.classList.add('message-bubble'); |
| if (sender === 'user') { |
| messageDiv.classList.add('user-message'); |
| } else if (sender === 'ai') { |
| messageDiv.classList.add('ai-message'); |
| } else if (sender === 'loading') { |
| messageDiv.classList.add('loading-message'); |
| messageDiv.innerHTML = `<div class="spinner"></div><span>Thinking...</span>`; |
| messageDiv.id = 'loadingMessage'; |
| } |
| |
| if (isTyping) { |
| |
| } else if (typeof content === 'string') { |
| messageDiv.innerHTML = marked.parse(content); |
| } else if (typeof content === 'object' && content.type === 'image') { |
| |
| |
| const img = document.createElement('img'); |
| img.src = content.src; |
| img.alt = "Uploaded image"; |
| img.classList.add('message-image-preview'); |
| messageDiv.appendChild(img); |
| if (content.text) { |
| const textDiv = document.createElement('div'); |
| textDiv.innerHTML = marked.parse(content.text); |
| messageDiv.appendChild(textDiv); |
| } |
| } else if (typeof content === 'object' && content.type === 'document') { |
| |
| const docInfoDiv = document.createElement('div'); |
| docInfoDiv.innerHTML = `<strong>Document Uploaded:</strong> ${content.name}<br>`; |
| if (content.textPreview) { |
| docInfoDiv.innerHTML += `<pre class="whitespace-pre-wrap text-sm max-h-32 overflow-y-auto bg-gray-800 p-2 rounded-md">${content.textPreview.substring(0, 500)}...</pre>`; |
| } else { |
| docInfoDiv.innerHTML += `<em>(Content too large or not plain text for preview)</em>`; |
| } |
| if (content.text) { |
| const textDiv = document.createElement('div'); |
| textDiv.innerHTML = marked.parse(content.text); |
| messageDiv.appendChild(textDiv); |
| } |
| messageDiv.appendChild(docInfoDiv); |
| } |
| |
| messagesArea.appendChild(messageDiv); |
| messagesArea.scrollTop = messagesArea.scrollHeight; |
| return messageDiv; |
| } |
| |
| |
| function clearFileSelection() { |
| uploadedFile = null; |
| uploadedFileType = null; |
| uploadedFileBase64 = null; |
| uploadedFileText = null; |
| imageUpload.value = ''; |
| documentUpload.value = ''; |
| filePreview.innerHTML = ''; |
| filePreview.classList.add('hidden'); |
| fileNameDisplay.textContent = ''; |
| clearFileButton.classList.add('hidden'); |
| console.log("DEBUG: File selection cleared."); |
| } |
| |
| |
| imageUpload.addEventListener('change', (event) => { |
| console.log("DEBUG: Image input change event triggered."); |
| console.log("DEBUG: event.target.files:", event.target.files); |
| |
| const file = event.target.files[0]; |
| |
| if (!file) { |
| console.log("DEBUG: No image file selected or file input was cancelled."); |
| clearFileSelection(); |
| return; |
| } |
| |
| console.log("DEBUG: Selected image file:", file); |
| if (!file.type.startsWith('image/')) { |
| showMessage("Invalid File Type", "Please upload an image file."); |
| imageUpload.value = ''; |
| console.error("DEBUG: Invalid image file type:", file.type); |
| return; |
| } |
| if (file.size > 5 * 1024 * 1024) { |
| showMessage("File Too Large", "Please upload an image smaller than 5MB."); |
| imageUpload.value = ''; |
| console.error("DEBUG: Image file too large:", file.size); |
| return; |
| } |
| |
| uploadedFile = file; |
| uploadedFileType = 'image'; |
| fileNameDisplay.textContent = `Image: ${file.name} (Ready to send)`; |
| clearFileButton.classList.remove('hidden'); |
| |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| uploadedFileBase64 = e.target.result.split(',')[1]; |
| console.log("DEBUG: Image FileReader loaded. Base64 length:", uploadedFileBase64.length); |
| filePreview.innerHTML = ''; |
| filePreview.classList.add('hidden'); |
| }; |
| reader.onerror = (e) => { |
| console.error("DEBUG: FileReader error for image:", e); |
| showMessage("File Read Error", "Could not read the image file."); |
| clearFileSelection(); |
| }; |
| reader.readAsDataURL(file); |
| }); |
| |
| |
| documentUpload.addEventListener('change', (event) => { |
| console.log("DEBUG: Document input change event triggered."); |
| console.log("DEBUG: event.target.files:", event.target.files); |
| |
| const file = event.target.files[0]; |
| |
| if (!file) { |
| console.log("DEBUG: No document file selected or file input was cancelled."); |
| clearFileSelection(); |
| return; |
| } |
| |
| console.log("DEBUG: Selected document file:", file); |
| uploadedFile = file; |
| fileNameDisplay.textContent = `Document: ${file.name} (Ready to send)`; |
| clearFileButton.classList.remove('hidden'); |
| |
| |
| if (file.type === 'text/plain') { |
| uploadedFileType = 'text_document'; |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| uploadedFileText = e.target.result; |
| console.log("DEBUG: Text Document FileReader loaded. Text length:", uploadedFileText.length); |
| filePreview.innerHTML = ''; |
| filePreview.classList.add('hidden'); |
| }; |
| reader.onerror = (e) => { |
| console.error("DEBUG: FileReader error for text document:", e); |
| showMessage("File Read Error", "Could not read the text document file."); |
| clearFileSelection(); |
| }; |
| reader.readAsText(file); |
| } else if (file.type === 'application/pdf') { |
| uploadedFileType = 'pdf_document'; |
| if (file.size > 10 * 1024 * 1024) { |
| showMessage("File Too Large", "Please upload a PDF smaller than 10MB."); |
| documentUpload.value = ''; |
| console.error("DEBUG: PDF file too large:", file.size); |
| clearFileSelection(); |
| return; |
| } |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| uploadedFileBase64 = e.target.result.split(',')[1]; |
| console.log("DEBUG: PDF FileReader loaded. Base64 length:", uploadedFileBase64.length); |
| filePreview.innerHTML = ''; |
| filePreview.classList.add('hidden'); |
| }; |
| reader.onerror = (e) => { |
| console.error("DEBUG: FileReader error for PDF:", e); |
| showMessage("File Read Error", "Could not read the PDF file."); |
| clearFileSelection(); |
| }; |
| reader.readAsDataURL(file); |
| } else { |
| |
| uploadedFileType = 'unsupported_document'; |
| showMessage("Unsupported Document", "Only plain text (.txt) and PDF (.pdf) files are directly processed. For DOCX, etc., advanced backend parsing is required."); |
| console.warn("DEBUG: Unsupported document type selected:", file.type); |
| uploadedFileText = null; |
| uploadedFileBase64 = null; |
| filePreview.innerHTML = ''; |
| filePreview.classList.add('hidden'); |
| } |
| }); |
| |
| clearFileButton.addEventListener('click', clearFileSelection); |
| |
| |
| async function sendMessage() { |
| const userQuery = messageInput.value.trim(); |
| |
| if (!userQuery && !uploadedFile) { |
| showMessage("Input Required", "Please enter your message or upload a file."); |
| console.warn("DEBUG: Attempted to send empty message with no file."); |
| return; |
| } |
| |
| |
| const requestBody = { |
| session_id: sessionId, |
| query: userQuery, |
| }; |
| |
| |
| let displayContent = userQuery; |
| if (uploadedFileType === 'image' && uploadedFileBase64) { |
| requestBody.image_data = { |
| mimeType: uploadedFile.type, |
| data: uploadedFileBase64 |
| }; |
| displayContent = `Image uploaded: ${uploadedFile.name}. ${userQuery ? `\n\n${userQuery}` : ''}`; |
| console.log("DEBUG: Prepared image data for requestBody. MimeType:", requestBody.image_data.mimeType, "Data length:", requestBody.image_data.data.length); |
| } else if (uploadedFileType === 'text_document' && uploadedFileText) { |
| requestBody.document_text = uploadedFileText; |
| displayContent = `Document uploaded: ${uploadedFile.name}. ${userQuery ? `\n\n${userQuery}` : ''}`; |
| console.log("DEBUG: Prepared text document text for requestBody. Text length:", requestBody.document_text.length); |
| } else if (uploadedFileType === 'pdf_document' && uploadedFileBase64) { |
| requestBody.pdf_data = { |
| mimeType: uploadedFile.type, |
| data: uploadedFileBase64 |
| }; |
| displayContent = `PDF uploaded: ${uploadedFile.name}. ${userQuery ? `\n\n${userQuery}` : ''}`; |
| console.log("DEBUG: Prepared PDF data for requestBody. MimeType:", requestBody.pdf_data.mimeType, "Data length:", requestBody.pdf_data.data.length); |
| } else if (uploadedFileType === 'unsupported_document' && uploadedFile) { |
| |
| requestBody.query = `${userQuery}\n\nUser uploaded a document named "${uploadedFile.name}". Please note: Content extraction for this file type is not supported in this demo.`; |
| displayContent = `Document uploaded: ${uploadedFile.name} (unsupported type). ${userQuery ? `\n\n${userQuery}` : ''}`; |
| console.log("DEBUG: Prepared unsupported document type as text query:", requestBody.query); |
| } |
| |
| console.log("DEBUG: Sending requestBody:", requestBody); |
| |
| |
| addMessage(displayContent, 'user'); |
| messageInput.value = ''; |
| messageInput.style.height = 'auto'; |
| |
| |
| clearFileSelection(); |
| |
| |
| const loadingMessageElement = addMessage('', 'loading', true); |
| sendButton.disabled = true; |
| messageInput.disabled = true; |
| imageUpload.disabled = true; |
| documentUpload.disabled = true; |
| |
| try { |
| const response = await fetch('/generate', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(requestBody) |
| }); |
| |
| |
| if (loadingMessageElement && loadingMessageElement.parentNode) { |
| loadingMessageElement.parentNode.removeChild(loadingMessageElement); |
| } |
| |
| if (!response.ok) { |
| const errorData = await response.json(); |
| throw new Error(errorData.error || `HTTP error! status: ${response.status}`); |
| } |
| |
| const result = await response.json(); |
| if (result.solution) { |
| addMessage(result.solution, 'ai'); |
| if (result.session_id && result.session_id !== sessionId) { |
| sessionId = result.session_id; |
| localStorage.setItem('aiChatSessionId', sessionId); |
| console.log("DEBUG: Session ID updated by backend:", sessionId); |
| } |
| } else { |
| addMessage("I'm sorry, I couldn't generate a response.", 'ai'); |
| } |
| |
| } catch (error) { |
| console.error("DEBUG: Error generating solution:", error); |
| |
| if (loadingMessageElement && loadingMessageElement.parentNode) { |
| loadingMessageElement.parentNode.removeChild(loadingMessageElement); |
| } |
| showMessage("Generation Error", `An error occurred: ${error.message}. Please try again.`); |
| addMessage(`An error occurred: ${error.message}`, 'ai'); |
| } finally { |
| sendButton.disabled = false; |
| messageInput.disabled = false; |
| imageUpload.disabled = false; |
| documentUpload.disabled = false; |
| messageInput.focus(); |
| } |
| } |
| |
| |
| sendButton.addEventListener('click', sendMessage); |
| messageInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| sendMessage(); |
| } |
| }); |
| |
| |
| newChatButton.addEventListener('click', () => { |
| if (confirm("Are you sure you want to start a new chat? This will clear the current conversation history.")) { |
| localStorage.removeItem('aiChatSessionId'); |
| sessionId = crypto.randomUUID(); |
| localStorage.setItem('aiChatSessionId', sessionId); |
| messagesArea.innerHTML = ` |
| <div class="ai-message message-bubble"> |
| Hello! How can I assist you today? You can type a message, or upload an image, a text document, or a PDF. |
| </div> |
| `; |
| clearFileSelection(); |
| messageInput.value = ''; |
| messageInput.style.height = 'auto'; |
| messageInput.focus(); |
| console.log("New chat started. New session ID:", sessionId); |
| } |
| }); |
| |
| |
| const script = document.createElement('script'); |
| script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'; |
| script.onload = () => console.log('Marked.js loaded.'); |
| document.head.appendChild(script); |
| |
| </script> |
| </body> |
| </html> |
|
|