Spaces:
Runtime error
Runtime error
Download templates/index.html from roshnn24/Figr-DataScience-Assignment: direct link, hf CLI and curl.
- Browser
- Download file 36 kB
-
https://huggingface.co/spaces/roshnn24/Figr-DataScience-Assignment/resolve/main/templates/index.html
- Command line
-
hf download hf://spaces/roshnn24/Figr-DataScience-Assignment/templates/index.html
-
curl -L -o index.html https://huggingface.co/spaces/roshnn24/Figr-DataScience-Assignment/resolve/main/templates/index.html
36 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Chat Interface</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/default.min.css" rel="stylesheet"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet"> | |
| <style> | |
| .chat-area { | |
| height: calc(100vh - 200px); | |
| overflow-y: auto; | |
| background-color: #1a1a1a; | |
| color: #e6e6e6; | |
| padding: 20px; | |
| gap: 15px; /* Space between messages */ | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .message-bubble { | |
| max-width: 80%; | |
| margin: 10px; | |
| padding: 15px; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); | |
| transition: all 0.2s ease; | |
| } | |
| .assistant-message { | |
| background-color: #797D7F; /* Tailwind's gray-800 - a nice light shade for dark theme */ | |
| margin-right: auto; | |
| max-width: 80%; | |
| margin: 10px; | |
| padding: 15px; | |
| border-radius: 10px; | |
| border: 1px solid #4a5568; /* Subtle border for definition */ | |
| color: #e5e7eb; /* Light text color for contrast */ | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
| } | |
| /* For user messages (to maintain contrast) */ | |
| .user-message { | |
| background-color: #283747; /* Slightly darker than assistant messages */ | |
| margin-left: auto; | |
| max-width: 80%; | |
| margin: 10px; | |
| padding: 15px; | |
| border-radius: 10px; | |
| border: 1px solid #4a5568; | |
| color: #e5e7eb; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
| } | |
| .chat-list { | |
| max-height: calc(100vh - 100px); | |
| overflow-y: auto; | |
| background-color: #1a1a1a; | |
| border-right: 1px solid #4a5568; | |
| color: #e6e6e6; | |
| } | |
| .chat-item { | |
| padding: 10px 15px; | |
| border-bottom: 1px solid #2d3748; | |
| color: #e6e6e6; | |
| } | |
| .chat-item:hover { | |
| background-color: #2d3748; | |
| transition: background-color 0.2s ease; | |
| } | |
| .active-chat { | |
| background-color: #3b82f6; | |
| color: #ffffff; | |
| } | |
| .copy-button { | |
| position: absolute; | |
| right: 10px; | |
| top: 5px; | |
| padding: 4px 8px; | |
| background-color: #3b82f6; | |
| color: #ffffff; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| opacity: 0.9; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| border: 1px solid #60a5fa; | |
| } | |
| .copy-button:hover { | |
| opacity: 1; | |
| background-color: #60a5fa; | |
| } | |
| .code-block-wrapper { | |
| position: relative; | |
| margin: 1em 0; | |
| background-color: #1e293b; | |
| border: 1px solid #4a5568; | |
| border-radius: 6px; | |
| } | |
| .copy-tooltip { | |
| position: absolute; | |
| background-color: #2d3748; | |
| color: #ffffff; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| right: 10px; | |
| top: -25px; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| border: 1px solid #4a5568; | |
| } | |
| .copy-tooltip.show { | |
| opacity: 1; | |
| } | |
| .message-footer { | |
| display: flex; | |
| justify-content: flex-end; | |
| margin-top: 10px; | |
| padding-top: 8px; | |
| border-top: 1px solid #4a5568; | |
| } | |
| .retry-button { | |
| display: inline-flex; | |
| align-items: center; | |
| padding: 6px 12px; | |
| background-color: #2d3748; | |
| color: #e6e6e6; | |
| border: 1px solid #4a5568; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .retry-button:hover { | |
| background-color: #4a5568; | |
| color: #ffffff; | |
| } | |
| .retry-button svg { | |
| width: 14px; | |
| height: 14px; | |
| margin-right: 4px; | |
| } | |
| .retry-button.loading { | |
| opacity: 0.7; | |
| cursor: not-allowed; | |
| } | |
| .code-block-wrapper { | |
| position: relative; | |
| margin: 1em 0; | |
| padding-top: 40px; | |
| background-color: #1e293b; | |
| border: 1px solid #4a5568; | |
| border-radius: 6px; | |
| } | |
| .test-button { | |
| position: absolute; | |
| top: 5px; | |
| right: 120px; | |
| background-color: #3b82f6; | |
| color: #ffffff; | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| border: 1px solid #60a5fa; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .test-button:hover { | |
| background-color: #60a5fa; | |
| } | |
| .test-results { | |
| margin-top: 8px; | |
| padding: 12px; | |
| background-color: #1e293b; | |
| border-left: 4px solid #4a5568; | |
| font-family: monospace; | |
| white-space: pre-wrap; | |
| display: none; | |
| color: #e6e6e6; | |
| } | |
| .test-results.show { | |
| display: block; | |
| } | |
| .test-results.error { | |
| border-left-color: #ef4444; | |
| background-color: #2d1f1f; | |
| } | |
| .test-results.success { | |
| border-left-color: #10b981; | |
| background-color: #1a2e1f; | |
| } | |
| .loading-spinner { | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| border: 2px solid #ffffff; | |
| border-radius: 50%; | |
| border-top-color: transparent; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .upload-button { | |
| position: relative; | |
| overflow: hidden; | |
| display: inline-block; | |
| background-color: #3b82f6; | |
| color: #ffffff; | |
| border: 1px solid #60a5fa; | |
| border-radius: 4px; | |
| transition: all 0.2s ease; | |
| padding: 6px 12px; | |
| } | |
| .upload-button:hover { | |
| background-color: #60a5fa; | |
| } | |
| .upload-button input[type=file] { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| min-width: 100%; | |
| min-height: 100%; | |
| opacity: 0; | |
| cursor: pointer; | |
| } | |
| /* New Chat button specific styling */ | |
| .new-chat-button { | |
| background-color: #3b82f6; | |
| color: #ffffff; | |
| padding: 8px 16px; | |
| border-radius: 6px; | |
| border: none; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| font-weight: 500; | |
| } | |
| .new-chat-button:hover { | |
| background-color: #60a5fa; | |
| } | |
| /* Chat title styles */ | |
| .chat-title { | |
| color: #e6e6e6; | |
| font-weight: 600; | |
| margin-bottom: 4px; | |
| } | |
| /* Chat date styles */ | |
| .chat-date { | |
| color: #9ca3af; | |
| font-size: 0.875rem; | |
| } | |
| /* Message input area */ | |
| .message-input { | |
| background-color: #1e293b; | |
| border: 1px solid #4a5568; | |
| color: #e6e6e6; | |
| border-radius: 6px; | |
| padding: 12px; | |
| width: 100%; | |
| margin-bottom: 10px; | |
| } | |
| .message-input:focus { | |
| outline: none; | |
| border-color: #60a5fa; | |
| box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); | |
| } | |
| /* Send button */ | |
| .send-button { | |
| background-color: #3b82f6; | |
| color: #ffffff; | |
| padding: 8px 16px; | |
| border-radius: 6px; | |
| border: none; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| font-weight: 500; | |
| } | |
| .send-button:hover { | |
| background-color: #60a5fa; | |
| } | |
| .header-container { | |
| text-align: center; | |
| padding: 1rem 0; | |
| margin-bottom: 2rem; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .title-wrapper { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| padding: 0.5rem 1.5rem; | |
| background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(147, 197, 253, 0.1)); | |
| border-radius: 12px; | |
| border: 1px solid rgba(59, 130, 246, 0.2); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .title { | |
| font-size: 1.75rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #60A5FA, #3B82F6); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| position: relative; | |
| } | |
| .logo-icon { | |
| width: 32px; | |
| height: 32px; | |
| animation: pulse 2s infinite; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| background-color: #10B981; | |
| border-radius: 50%; | |
| position: relative; | |
| display: inline-block; | |
| margin-left: 0.5rem; | |
| animation: blink 2s infinite; | |
| } | |
| /* Floating particles */ | |
| .particle { | |
| position: absolute; | |
| width: 4px; | |
| height: 4px; | |
| background: rgba(59, 130, 246, 0.2); | |
| border-radius: 50%; | |
| pointer-events: none; | |
| } | |
| .particle:nth-child(1) { animation: float-1 8s infinite; } | |
| .particle:nth-child(2) { animation: float-2 10s infinite; } | |
| .particle:nth-child(3) { animation: float-3 7s infinite; } | |
| .particle:nth-child(4) { animation: float-4 9s infinite; } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.1); } | |
| 100% { transform: scale(1); } | |
| } | |
| @keyframes blink { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.4; } | |
| 100% { opacity: 1; } | |
| } | |
| @keyframes float-1 { | |
| 0%, 100% { transform: translate(0, 0); } | |
| 50% { transform: translate(20px, -20px); } | |
| } | |
| @keyframes float-2 { | |
| 0%, 100% { transform: translate(0, 0); } | |
| 50% { transform: translate(-15px, -25px); } | |
| } | |
| @keyframes float-3 { | |
| 0%, 100% { transform: translate(0, 0); } | |
| 50% { transform: translate(25px, -15px); } | |
| } | |
| @keyframes float-4 { | |
| 0%, 100% { transform: translate(0, 0); } | |
| 50% { transform: translate(-20px, -10px); } | |
| } | |
| .model-label { | |
| position: fixed; | |
| top: 1rem; | |
| right: 1rem; | |
| background-color: #f3f4f6; | |
| color: #4b5563; | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 9999px; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); | |
| z-index: 50; | |
| } | |
| .loading-indicator { | |
| position: sticky; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: rgba(17, 24, 39, 0.95); | |
| padding: 1rem; | |
| border-top: 1px solid #4b5563; | |
| z-index: 50; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .loading-indicator.hidden { | |
| display: none; | |
| } | |
| .loading-bar { | |
| width: 200px; | |
| height: 4px; | |
| background: #374151; | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| .loading-progress { | |
| width: 40%; | |
| height: 100%; | |
| background: #3b82f6; | |
| border-radius: 2px; | |
| animation: moveProgress 1.5s infinite ease-in-out; | |
| } | |
| .loading-text { | |
| color: #9ca3af; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| } | |
| @keyframes moveProgress { | |
| 0% { | |
| transform: translateX(-100%); | |
| } | |
| 50% { | |
| transform: translateX(100%); | |
| } | |
| 100% { | |
| transform: translateX(-100%); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900"> | |
| <div class="model-label">Mistral:7b</div> | |
| <div class="container mx-auto px-4 py-8"> | |
| <h1 class="text-2xl font-bold text-blue-500 text-center mb-6">Figr AI Code Assistant</h1> | |
| <div class="flex gap-4"> | |
| <!-- Sidebar with chat list --> | |
| <div class="w-1/4 bg-gray-800 rounded-lg shadow-lg p-4"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-bold text-gray-100">Chats</h2> | |
| <button onclick="createNewChat()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"> | |
| New Chat | |
| </button> | |
| </div> | |
| <div id="chatList" class="chat-list"> | |
| <!-- Chat items will be dynamically added here --> | |
| </div> | |
| </div> | |
| <!-- Main chat area --> | |
| <div class="w-3/4 bg-gray-800 rounded-lg shadow-lg p-4"> | |
| <div id="chatArea" class="chat-area mb-4"> | |
| <!-- Messages will be dynamically added here --> | |
| </div> | |
| <!-- Input area --> | |
| <div class="flex gap-2"> | |
| <textarea | |
| id="userInput" | |
| class="w-full p-2 border rounded-lg resize-none bg-gray-700 text-gray-100" | |
| rows="3" | |
| placeholder="Type your message here..." | |
| ></textarea> | |
| <div class="flex flex-col gap-2"> | |
| <label class="relative cursor-pointer bg-gray-700 hover:bg-gray-600 p-2 rounded-lg flex items-center justify-center"> | |
| <input type="file" | |
| id="fileInput" | |
| accept=".py" | |
| class="hidden" | |
| onchange="handleFileUpload(event)"/> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-400"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="17 8 12 3 7 8"></polyline> | |
| <line x1="12" y1="3" x2="12" y2="15"></line> | |
| </svg> | |
| </label> | |
| <button | |
| onclick="sendMessage()" | |
| class="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600" | |
| > | |
| Send | |
| </button> | |
| </div> | |
| </div> | |
| <div id="loadingIndicator" class="loading-indicator hidden"> | |
| <div class="loading-bar"> | |
| <div class="loading-progress"></div> | |
| </div> | |
| <div class="loading-text">Generating...</div> | |
| </div> | |
| <!-- Important Information --> | |
| <div class="mt-4"> | |
| <h3 class="text-lg font-bold mb-2 text-gray-100">Important Information</h3> | |
| <div id="importantInfo" class="bg-gray-800 p-4 rounded-lg text-gray-100 border border-gray-700"> | |
| <!-- Important info will be added here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentSessionId = null; | |
| // Initialize marked with syntax highlighting | |
| marked.setOptions({ | |
| highlight: function(code, lang) { | |
| return hljs.highlight(code, {language: lang || 'plaintext'}).value; | |
| } | |
| }); | |
| const renderer = new marked.Renderer(); | |
| renderer.code = function(code, language) { | |
| const highlightedCode = language ? hljs.highlight(code, {language}).value : hljs.highlightAuto(code).value; | |
| return ` | |
| <div class="code-block-wrapper"> | |
| <button class="test-button">Test Code</button> | |
| <button class="copy-button">Copy Code</button> | |
| <pre><code class="hljs ${language || ''}">${highlightedCode}</code></pre> | |
| <div class="test-results"></div> | |
| </div> | |
| `; | |
| }; | |
| marked.setOptions({ renderer }); | |
| // Create new chat | |
| async function createNewChat() { | |
| try { | |
| const response = await fetch('/api/new-chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| currentSessionId = data.chat.id; | |
| await loadChatList(); | |
| clearChatArea(); | |
| } | |
| } catch (error) { | |
| console.error('Error creating new chat:', error); | |
| } | |
| } | |
| // Load chat list | |
| async function loadChatList() { | |
| try { | |
| const response = await fetch('/api/chat-list'); | |
| const data = await response.json(); | |
| const chatListElement = document.getElementById('chatList'); | |
| chatListElement.innerHTML = ''; | |
| data.chats.forEach(chat => { | |
| const chatElement = document.createElement('div'); | |
| chatElement.className = `chat-item p-3 cursor-pointer rounded ${ | |
| chat.id === currentSessionId ? 'active-chat' : '' | |
| }`; | |
| chatElement.onclick = () => loadChat(chat.id); | |
| chatElement.innerHTML = ` | |
| <div class="font-medium">${chat.title || 'New Chat'}</div> | |
| <div class="text-sm text-gray-500">${new Date(chat.date).toLocaleDateString()}</div> | |
| `; | |
| chatListElement.appendChild(chatElement); | |
| }); | |
| } catch (error) { | |
| console.error('Error loading chat list:', error); | |
| } | |
| } | |
| // Load specific chat | |
| async function loadChat(sessionId) { | |
| currentSessionId = sessionId; | |
| try { | |
| const response = await fetch(`/api/chat-history?sessionId=${sessionId}`); | |
| const data = await response.json(); | |
| displayChatHistory(data.history); | |
| displayImportantInfo(data.important_info); | |
| await loadChatList(); // Refresh chat list to update active state | |
| } catch (error) { | |
| console.error('Error loading chat:', error); | |
| } | |
| } | |
| // Display chat history | |
| function displayChatHistory(history) { | |
| const chatArea = document.getElementById('chatArea'); | |
| chatArea.innerHTML = ''; | |
| history.forEach(message => { | |
| displayMessage(message.role, message.content); | |
| }); | |
| scrollToBottom(); | |
| } | |
| // Display important information | |
| function displayImportantInfo(info) { | |
| const infoElement = document.getElementById('importantInfo'); | |
| infoElement.innerHTML = info.length > 0 | |
| ? info.map(item => `<p>• ${item}</p>`).join('') | |
| : '<p>No important information yet</p>'; | |
| } | |
| // Send message | |
| async function sendMessage() { | |
| const userInput = document.getElementById('userInput'); | |
| const message = userInput.value.trim(); | |
| const loadingIndicator = document.getElementById('loadingIndicator'); | |
| if (!message) return; | |
| if (!currentSessionId) { | |
| await createNewChat(); | |
| } | |
| displayMessage('user', message); | |
| userInput.value = ''; | |
| scrollToBottom(); | |
| // Show loading indicator | |
| loadingIndicator.classList.remove('hidden'); | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| message: message, | |
| sessionId: currentSessionId | |
| }) | |
| }); | |
| const data = await response.json(); | |
| // Hide loading indicator | |
| loadingIndicator.classList.add('hidden'); | |
| if (data.success) { | |
| displayMessage('assistant', data.response); | |
| displayImportantInfo(data.important_info); | |
| await loadChatList(); | |
| } else { | |
| displayMessage('assistant', 'Error: ' + data.response); | |
| } | |
| } catch (error) { | |
| // Hide loading indicator on error too | |
| loadingIndicator.classList.add('hidden'); | |
| console.error('Error sending message:', error); | |
| displayMessage('assistant', 'Error sending message'); | |
| } | |
| scrollToBottom(); | |
| } | |
| async function handleFileUpload(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| if (!file.name.endsWith('.py')) { | |
| alert('Please upload only Python (.py) files'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const response = await fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| // Add the file content and analysis to the chat | |
| const message = `I've uploaded a Python file named "${data.filename}". Here's the code:\n\n\`\`\`python\n${data.content}\n\`\`\``; | |
| document.getElementById('userInput').value = message; | |
| await sendMessage(); | |
| // Send the analysis as an assistant message | |
| const analysisMessage = data.analysis; | |
| displayMessage('assistant', analysisMessage); | |
| // Clear the file input | |
| event.target.value = ''; | |
| } else { | |
| alert(data.error || 'Error uploading file'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| alert('Error uploading file'); | |
| } | |
| } | |
| // Display a single message | |
| function displayMessage(role, content) { | |
| const chatArea = document.getElementById('chatArea'); | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message-bubble ${role}-message`; | |
| // Create a div for message content | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'message-content'; | |
| // Custom renderer for marked to wrap code blocks (same as before) | |
| const renderer = new marked.Renderer(); | |
| renderer.code = function(code, language) { | |
| const highlightedCode = language ? hljs.highlight(code, {language}).value : hljs.highlightAuto(code).value; | |
| // Simplified HTML structure with both buttons | |
| const buttonsHtml = ` | |
| <button class="test-button">Test Code</button> | |
| <button class="copy-button">Copy Code</button> | |
| `; | |
| return ` | |
| <div class="code-block-wrapper"> | |
| ${buttonsHtml} | |
| <pre><code class="hljs ${language || ''}">${highlightedCode}</code></pre> | |
| <div class="test-results"></div> | |
| </div> | |
| `; | |
| }; | |
| marked.setOptions({ renderer }); | |
| // Parse markdown and render HTML | |
| contentDiv.innerHTML = marked.parse(content); | |
| messageDiv.appendChild(contentDiv); | |
| // Immediately initialize code blocks after adding content | |
| messageDiv.querySelectorAll('.code-block-wrapper').forEach(wrapper => { | |
| // Ensure buttons exist | |
| if (!wrapper.querySelector('.test-button') || !wrapper.querySelector('.copy-button')) { | |
| const buttonsDiv = document.createElement('div'); | |
| buttonsDiv.innerHTML = ` | |
| <button class="test-button">Test Code</button> | |
| <button class="copy-button">Copy Code</button> | |
| `; | |
| wrapper.insertBefore(buttonsDiv, wrapper.firstChild); | |
| } | |
| const copyButton = wrapper.querySelector('.copy-button'); | |
| const testButton = wrapper.querySelector('.test-button'); | |
| const codeElement = wrapper.querySelector('code'); | |
| const resultsElement = wrapper.querySelector('.test-results') || (() => { | |
| const div = document.createElement('div'); | |
| div.className = 'test-results'; | |
| wrapper.appendChild(div); | |
| return div; | |
| })(); | |
| // Copy button handler | |
| copyButton.addEventListener('click', async () => { | |
| try { | |
| await navigator.clipboard.writeText(codeElement.textContent); | |
| copyButton.textContent = 'Copied!'; | |
| setTimeout(() => { | |
| copyButton.textContent = 'Copy Code'; | |
| }, 2000); | |
| } catch (err) { | |
| console.error('Failed to copy:', err); | |
| } | |
| }); | |
| // Test button handler | |
| testButton.addEventListener('click', async () => { | |
| testButton.textContent = 'Testing...'; | |
| testButton.disabled = true; | |
| try { | |
| const response = await fetch('/api/test-code', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| code: codeElement.textContent | |
| }) | |
| }); | |
| const data = await response.json(); | |
| resultsElement.textContent = data.output || 'Code executed successfully with no output'; | |
| resultsElement.className = `test-results show ${data.success ? 'success' : 'error'}`; | |
| } catch (error) { | |
| resultsElement.textContent = `Error: ${error.message}`; | |
| resultsElement.className = 'test-results show error'; | |
| } finally { | |
| testButton.textContent = 'Test Code'; | |
| testButton.disabled = false; | |
| } | |
| }); | |
| }); | |
| // Add retry button for assistant messages | |
| if (role === 'assistant') { | |
| const footer = document.createElement('div'); | |
| footer.className = 'message-footer'; | |
| footer.innerHTML = ` | |
| <button class="retry-button"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"> | |
| <path d="M2.5 2v6h6M21.5 22v-6h-6"/> | |
| <path d="M22 11.5A10 10 0 003.2 7.2M2 12.5a10 10 0 0018.8 4.2"/> | |
| </svg> | |
| Retry | |
| </button> | |
| `; | |
| messageDiv.appendChild(footer); | |
| // Add click handler for retry button | |
| const retryButton = footer.querySelector('.retry-button'); | |
| const lastUserMessage = findLastUserMessage(); | |
| if (lastUserMessage) { | |
| retryButton.addEventListener('click', () => retryMessage(lastUserMessage, messageDiv)); | |
| } | |
| } | |
| // Add copy button functionality | |
| addCopyButtons(contentDiv); | |
| chatArea.appendChild(messageDiv); | |
| } | |
| // Helper function to find the last user message before an assistant message | |
| function findLastUserMessage() { | |
| const chatArea = document.getElementById('chatArea'); | |
| const messages = chatArea.querySelectorAll('.message-bubble'); | |
| let lastUserMessage = null; | |
| for (let i = messages.length - 1; i >= 0; i--) { | |
| if (messages[i].classList.contains('user-message')) { | |
| const contentDiv = messages[i].querySelector('.message-content'); | |
| if (contentDiv) { | |
| lastUserMessage = contentDiv.textContent; | |
| break; | |
| } | |
| } | |
| } | |
| return lastUserMessage; | |
| } | |
| // Clear chat area | |
| function clearChatArea() { | |
| document.getElementById('chatArea').innerHTML = ''; | |
| document.getElementById('importantInfo').innerHTML = '<p>No important information yet</p>'; | |
| document.getElementById('userInput').value = ''; | |
| } | |
| // Scroll chat area to bottom | |
| function scrollToBottom() { | |
| const chatArea = document.getElementById('chatArea'); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| } | |
| // Handle Enter key in textarea | |
| document.getElementById('userInput').addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| // Initialize | |
| window.onload = async function() { | |
| await loadChatList(); | |
| }; | |
| async function copyCode(codeElement, tooltipElement) { | |
| try { | |
| await navigator.clipboard.writeText(codeElement.textContent); | |
| tooltipElement.textContent = 'Copied!'; | |
| tooltipElement.classList.add('show'); | |
| setTimeout(() => { | |
| tooltipElement.classList.remove('show'); | |
| }, 1500); | |
| } catch (err) { | |
| console.error('Failed to copy code:', err); | |
| tooltipElement.textContent = 'Failed to copy'; | |
| tooltipElement.classList.add('show'); | |
| setTimeout(() => { | |
| tooltipElement.classList.remove('show'); | |
| }, 1500); | |
| } | |
| } | |
| async function retryMessage(originalMessage, messageElement) { | |
| if (!currentSessionId) return; | |
| const retryButton = messageElement.querySelector('.retry-button'); | |
| // Show loading state | |
| retryButton.classList.add('loading'); | |
| retryButton.innerHTML = ` | |
| <svg class="animate-spin" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path> | |
| </svg> | |
| Regenerating... | |
| `; | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| message: originalMessage, | |
| sessionId: currentSessionId | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| // Replace the old message content with new content | |
| const contentDiv = messageElement.querySelector('.message-content'); | |
| contentDiv.innerHTML = marked.parse(data.response); | |
| // Reapply syntax highlighting | |
| contentDiv.querySelectorAll('pre code').forEach((block) => { | |
| hljs.highlightBlock(block); | |
| }); | |
| // Add copy buttons to new code blocks | |
| addCopyButtons(contentDiv); | |
| // Update important info | |
| displayImportantInfo(data.important_info); | |
| } else { | |
| alert('Error regenerating response: ' + data.response); | |
| } | |
| } catch (error) { | |
| console.error('Error regenerating message:', error); | |
| alert('Error regenerating response'); | |
| } finally { | |
| // Restore retry button | |
| retryButton.classList.remove('loading'); | |
| retryButton.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"> | |
| <path d="M2.5 2v6h6M21.5 22v-6h-6"/> | |
| <path d="M22 11.5A10 10 0 003.2 7.2M2 12.5a10 10 0 0018.8 4.2"/> | |
| </svg> | |
| Retry | |
| `; | |
| } | |
| } | |
| // Helper function to add copy buttons to code blocks | |
| function addCopyButtons(element) { | |
| element.querySelectorAll('.code-block-wrapper').forEach(wrapper => { | |
| const copyButton = wrapper.querySelector('.copy-button'); | |
| const codeElement = wrapper.querySelector('code'); | |
| const tooltipElement = wrapper.querySelector('.copy-tooltip'); | |
| copyButton.addEventListener('click', () => copyCode(codeElement, tooltipElement)); | |
| copyButton.addEventListener('mouseenter', () => { | |
| tooltipElement.textContent = 'Copy'; | |
| tooltipElement.classList.add('show'); | |
| }); | |
| copyButton.addEventListener('mouseleave', () => { | |
| if (tooltipElement.textContent === 'Copy') { | |
| tooltipElement.classList.remove('show'); | |
| } | |
| }); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |