Spaces:
Sleeping
Sleeping
| document.addEventListener("DOMContentLoaded", () => { | |
| // Auto-resize textarea | |
| const textarea = document.getElementById("user-input") | |
| textarea.addEventListener("input", autoResizeTextarea) | |
| // Feature button selection | |
| const featureButtons = document.querySelectorAll(".feature-btn") | |
| const featureOptions = document.getElementById("feature-options") | |
| const fileUploadLabel = document.getElementById("file-upload-label") | |
| featureButtons.forEach((button) => { | |
| button.addEventListener("click", () => { | |
| // Remove active class from all buttons | |
| featureButtons.forEach((btn) => btn.classList.remove("active")) | |
| // Add active class to clicked button | |
| button.classList.add("active") | |
| // Show/hide file upload button based on feature | |
| const feature = button.dataset.feature | |
| if (feature === "document" || feature === "image") { | |
| fileUploadLabel.classList.remove("hidden") | |
| } else { | |
| fileUploadLabel.classList.add("hidden") | |
| } | |
| // Update feature options | |
| updateFeatureOptions(feature) | |
| // Save active feature to localStorage | |
| localStorage.setItem("activeFeature", feature) | |
| }) | |
| }) | |
| // Restore active feature from localStorage | |
| const activeFeature = localStorage.getItem("activeFeature") || "chat" | |
| document.querySelector(`.feature-btn[data-feature="${activeFeature}"]`)?.click() | |
| // File input handling | |
| const fileInput = document.getElementById("file-upload") | |
| fileInput.addEventListener("change", handleFileSelection) | |
| // Chat form submission | |
| const chatForm = document.getElementById("chat-form") | |
| chatForm.addEventListener("submit", handleChatSubmit) | |
| // History panel toggle | |
| const historyButton = document.createElement("button") | |
| historyButton.id = "historyButton" | |
| historyButton.className = "absolute left-4 top-4 p-2 rounded-full bg-white/20 hover:bg-white/30 transition-colors" | |
| historyButton.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| ` | |
| document.querySelector("header .container").appendChild(historyButton) | |
| historyButton.addEventListener("click", toggleHistoryPanel) | |
| document.getElementById("closeHistory").addEventListener("click", toggleHistoryPanel) | |
| // Theme toggle functionality | |
| const themeToggle = document.getElementById("theme-toggle") | |
| const sunIcon = document.getElementById("sun-icon") | |
| const moonIcon = document.getElementById("moon-icon") | |
| // Check for saved theme preference or use system preference | |
| const savedTheme = localStorage.getItem("theme") | |
| const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches | |
| if (savedTheme === "dark" || (!savedTheme && systemPrefersDark)) { | |
| document.documentElement.classList.add("dark") | |
| sunIcon.classList.remove("hidden") | |
| moonIcon.classList.add("hidden") | |
| } | |
| // Toggle theme when button is clicked | |
| themeToggle.addEventListener("click", () => { | |
| const isDark = document.documentElement.classList.toggle("dark") | |
| // Toggle icons | |
| if (isDark) { | |
| sunIcon.classList.remove("hidden") | |
| moonIcon.classList.add("hidden") | |
| localStorage.setItem("theme", "dark") | |
| } else { | |
| sunIcon.classList.add("hidden") | |
| moonIcon.classList.remove("hidden") | |
| localStorage.setItem("theme", "light") | |
| } | |
| // Show notification | |
| showNotification(`Switched to ${isDark ? "dark" : "light"} mode`, "info") | |
| }) | |
| // Load history from localStorage | |
| loadHistory() | |
| }) | |
| // Auto-resize textarea | |
| function autoResizeTextarea() { | |
| this.style.height = "auto" | |
| this.style.height = this.scrollHeight + "px" | |
| } | |
| // Update feature options based on selected feature | |
| function updateFeatureOptions(feature) { | |
| const optionsContainer = document.getElementById("feature-options") | |
| // Clear previous options | |
| optionsContainer.innerHTML = "" | |
| switch (feature) { | |
| case "chat": | |
| // No special options for chat | |
| optionsContainer.classList.add("hidden") | |
| break | |
| case "translate": | |
| optionsContainer.classList.remove("hidden") | |
| optionsContainer.innerHTML = ` | |
| <div class="language-selector"> | |
| <div> | |
| <label for="sourceLanguage" class="block text-sm font-medium text-gray-700 mb-1">From:</label> | |
| <select id="sourceLanguage" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 bg-white text-gray-900"> | |
| <option value="ar">Arabic</option> | |
| <option value="en" selected>English</option> | |
| <option value="fr">French</option> | |
| <option value="es">Spanish</option> | |
| <option value="zh">Chinese</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="targetLanguage" class="block text-sm font-medium text-gray-700 mb-1">To:</label> | |
| <select id="targetLanguage" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 bg-white text-gray-900"> | |
| <option value="ar" selected>Arabic</option> | |
| <option value="en">English</option> | |
| <option value="fr">French</option> | |
| <option value="es">Spanish</option> | |
| <option value="zh">Chinese</option> | |
| </select> | |
| </div> | |
| </div> | |
| ` | |
| break | |
| case "qa": | |
| optionsContainer.classList.remove("hidden") | |
| optionsContainer.innerHTML = ` | |
| <div class="mb-2"> | |
| <label for="contextInput" class="block text-sm font-medium text-gray-700 mb-1">Reference Text:</label> | |
| <textarea id="contextInput" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 bg-white text-gray-900 resize-none" rows="2" placeholder="Enter reference text here..."></textarea> | |
| </div> | |
| ` | |
| // Auto-resize for the new textarea | |
| document.getElementById("contextInput").addEventListener("input", autoResizeTextarea) | |
| break | |
| default: | |
| optionsContainer.classList.add("hidden") | |
| break | |
| } | |
| } | |
| // Handle file selection | |
| function handleFileSelection() { | |
| const fileInput = document.getElementById("file-upload") | |
| const userInput = document.getElementById("user-input") | |
| if (fileInput.files.length > 0) { | |
| const file = fileInput.files[0] | |
| // Create file preview element | |
| const filePreview = document.createElement("div") | |
| filePreview.className = "file-preview" | |
| filePreview.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" /> | |
| </svg> | |
| <span class="file-preview-name">${file.name}</span> | |
| <span class="file-preview-remove" onclick="removeFile()">×</span> | |
| ` | |
| // Add file preview after textarea | |
| const inputContainer = userInput.parentElement | |
| // Remove any existing file preview | |
| const existingPreview = inputContainer.querySelector(".file-preview") | |
| if (existingPreview) { | |
| existingPreview.remove() | |
| } | |
| inputContainer.appendChild(filePreview) | |
| // Update placeholder text | |
| userInput.placeholder = "Add a message about this file..." | |
| } | |
| } | |
| // Remove selected file | |
| const removeFile = () => { | |
| const fileInput = document.getElementById("file-upload") | |
| const userInput = document.getElementById("user-input") | |
| const filePreview = document.querySelector(".file-preview") | |
| // Reset file input | |
| fileInput.value = "" | |
| // Remove file preview | |
| if (filePreview) { | |
| filePreview.remove() | |
| } | |
| // Reset placeholder | |
| userInput.placeholder = "Type your message..." | |
| } | |
| window.removeFile = removeFile | |
| // Handle chat form submission | |
| function handleChatSubmit(e) { | |
| e.preventDefault() | |
| const userInput = document.getElementById("user-input") | |
| const fileInput = document.getElementById("file-upload") | |
| const activeFeature = document.querySelector(".feature-btn.active").dataset.feature | |
| // Get user message | |
| const userMessage = userInput.value.trim() | |
| // Check if there's a message or file | |
| if (!userMessage && fileInput.files.length === 0) { | |
| showNotification("Please enter a message or select a file", "warning") | |
| return | |
| } | |
| // Add user message to chat | |
| if (userMessage) { | |
| addMessageToChat("user", userMessage) | |
| } | |
| // Process based on active feature | |
| processRequest(activeFeature, userMessage, fileInput.files[0]) | |
| // Clear input and file | |
| userInput.value = "" | |
| userInput.style.height = "auto" | |
| if (fileInput.files.length > 0) { | |
| removeFile() | |
| } | |
| // Scroll to bottom | |
| scrollToBottom() | |
| } | |
| // Process request based on feature | |
| function processRequest(feature, message, file) { | |
| // Show loading indicator | |
| showLoadingIndicator() | |
| // Determine which API endpoint to use | |
| let endpoint | |
| const formData = new FormData() | |
| switch (feature) { | |
| case "summarize": | |
| endpoint = "summarize" | |
| formData.append("text", message) | |
| break | |
| case "translate": | |
| endpoint = "translate" | |
| formData.append("text", message) | |
| formData.append("source_lang", document.getElementById("sourceLanguage").value) | |
| formData.append("target_lang", document.getElementById("targetLanguage").value) | |
| break | |
| case "qa": | |
| endpoint = "qa/" | |
| // Use fetch with JSON instead of FormData for QA | |
| const context = document.getElementById("contextInput").value.trim() | |
| if (!context) { | |
| hideLoadingIndicator() | |
| showNotification("Please enter reference text", "warning") | |
| return | |
| } | |
| // We'll handle this differently below | |
| break | |
| case "code": | |
| endpoint = "generate_code/" | |
| formData.append("prompt", message) | |
| break | |
| case "document": | |
| if (!file) { | |
| hideLoadingIndicator() | |
| showNotification("Please select a document file", "warning") | |
| return | |
| } | |
| endpoint = "extract_text" | |
| formData.append("file", file) | |
| break | |
| case "image": | |
| if (!file) { | |
| hideLoadingIndicator() | |
| showNotification("Please select an image file", "warning") | |
| return | |
| } | |
| endpoint = "image_caption" | |
| formData.append("file", file) | |
| break | |
| default: | |
| // For regular chat, we'll simulate a response | |
| setTimeout(() => { | |
| const responses = [ | |
| "I'm here to help! What would you like to know?", | |
| "That's an interesting question. Let me think about it...", | |
| "I can assist with summarizing text, translating languages, answering questions, generating code, and analyzing documents and images. What would you like me to help you with?", | |
| "I understand. Is there anything specific you'd like me to explain further?", | |
| "I'm your AI assistant. I'm designed to be helpful, harmless, and honest.", | |
| ] | |
| const randomResponse = responses[Math.floor(Math.random() * responses.length)] | |
| addMessageToChat("assistant", randomResponse) | |
| hideLoadingIndicator() | |
| scrollToBottom() | |
| // Add to history | |
| addToHistory("Chat", message.substring(0, 30) + (message.length > 30 ? "..." : "")) | |
| }, 1000) | |
| return | |
| } | |
| // Special handling for QA | |
| if (feature === "qa") { | |
| const context = document.getElementById("contextInput").value.trim() | |
| const question = message | |
| fetch("https://soltane777-textgeneration.hf.space/qa/", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ context: context, question: question }), | |
| }) | |
| .then((response) => { | |
| if (!response.ok) { | |
| throw new Error(`Server responded with status: ${response.status}`) | |
| } | |
| return response.json() | |
| }) | |
| .then((data) => { | |
| const answer = data.answer || "Sorry, I couldn't find an answer to that question." | |
| addMessageToChat("assistant", answer) | |
| addToHistory("Question Answered", question.substring(0, 30) + (question.length > 30 ? "..." : "")) | |
| }) | |
| .catch((error) => { | |
| console.error("Error:", error) | |
| addMessageToChat("assistant", `Error: ${error.message || "Failed to get an answer"}`) | |
| showNotification("Error getting answer: " + (error.message || "Unknown error"), "error") | |
| }) | |
| .finally(() => { | |
| hideLoadingIndicator() | |
| scrollToBottom() | |
| }) | |
| return | |
| } | |
| // Process other requests | |
| fetch(`https://soltane777-textgeneration.hf.space/${endpoint}`, { | |
| method: "POST", | |
| body: formData, | |
| }) | |
| .then((response) => { | |
| if (!response.ok) { | |
| throw new Error(`Server responded with status: ${response.status}`) | |
| } | |
| return response.json() | |
| }) | |
| .then((data) => { | |
| let result | |
| switch (feature) { | |
| case "summarize": | |
| result = data.summary || "Sorry, I couldn't summarize that text." | |
| addToHistory("Summarization", "Text summarized") | |
| break | |
| case "translate": | |
| const sourceLang = getLanguageName(document.getElementById("sourceLanguage").value) | |
| const targetLang = getLanguageName(document.getElementById("targetLanguage").value) | |
| result = data.translation || "Sorry, I couldn't translate that text." | |
| addToHistory("Translation", `${sourceLang} to ${targetLang}`) | |
| break | |
| case "code": | |
| result = formatCodeResponse(data) | |
| addToHistory("Code Generation", message.substring(0, 30) + (message.length > 30 ? "..." : "")) | |
| break | |
| case "document": | |
| result = data.text || "Sorry, I couldn't extract text from that document." | |
| addToHistory("Text Extraction", "Extracted from file") | |
| break | |
| case "image": | |
| result = data.caption || "Sorry, I couldn't analyze that image." | |
| addToHistory("Image Analysis", "Image analyzed") | |
| break | |
| default: | |
| result = JSON.stringify(data) | |
| } | |
| addMessageToChat("assistant", result) | |
| }) | |
| .catch((error) => { | |
| console.error("Error:", error) | |
| addMessageToChat("assistant", `Error: ${error.message || "Failed to process request"}`) | |
| showNotification("Error: " + (error.message || "Unknown error"), "error") | |
| }) | |
| .finally(() => { | |
| hideLoadingIndicator() | |
| scrollToBottom() | |
| }) | |
| } | |
| // Format code response | |
| function formatCodeResponse(data) { | |
| if (!data.code && !data[0]) { | |
| return "Sorry, I couldn't generate code for that prompt." | |
| } | |
| const code = data.code || data[0].generated_text || JSON.stringify(data) | |
| // Format as code block | |
| return "\`\`\`\n" + code + "\n\`\`\`" | |
| } | |
| // Add message to chat | |
| function addMessageToChat(sender, message) { | |
| const chatMessages = document.getElementById("chat-messages") | |
| const messageDiv = document.createElement("div") | |
| if (sender === "user") { | |
| messageDiv.className = "flex justify-end mb-4" | |
| messageDiv.innerHTML = ` | |
| <div class="max-w-3xl user-message p-4 shadow-sm"> | |
| <p class="text-gray-800 whitespace-pre-wrap">${formatMessage(message)}</p> | |
| </div> | |
| ` | |
| } else { | |
| messageDiv.className = "flex items-start mb-4" | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0 bg-purple-600 rounded-full p-2 mr-3"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> | |
| </svg> | |
| </div> | |
| <div class="max-w-3xl assistant-message p-4 shadow-sm"> | |
| <div class="text-gray-800 whitespace-pre-wrap">${formatMessage(message)}</div> | |
| <div class="message-actions"> | |
| <button class="message-action-btn copy-btn" onclick="copyToClipboard(this)"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> | |
| </svg> | |
| Copy | |
| </button> | |
| </div> | |
| </div> | |
| ` | |
| } | |
| chatMessages.appendChild(messageDiv) | |
| } | |
| // Format message with code blocks and links | |
| function formatMessage(message) { | |
| // Replace code blocks | |
| message = message.replace(/\`\`\`([\s\S]*?)\`\`\`/g, (match, code) => { | |
| return `<pre class="code-content">${escapeHtml(code)}</pre>` | |
| }) | |
| // Replace inline code | |
| message = message.replace(/`([^`]+)`/g, (match, code) => { | |
| return `<code class="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">${escapeHtml(code)}</code>` | |
| }) | |
| // Replace URLs with links | |
| message = message.replace(/(https?:\/\/[^\s]+)/g, (url) => { | |
| return `<a href="${url}" target="_blank" class="text-purple-600 hover:underline">${url}</a>` | |
| }) | |
| return message | |
| } | |
| // Escape HTML | |
| function escapeHtml(unsafe) { | |
| return unsafe | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'") | |
| } | |
| // Copy to clipboard | |
| window.copyToClipboard = (button) => { | |
| const messageDiv = button.closest(".assistant-message") | |
| const textContent = messageDiv.querySelector(".whitespace-pre-wrap").innerText | |
| navigator.clipboard | |
| .writeText(textContent) | |
| .then(() => { | |
| // Change button text temporarily | |
| const originalText = button.innerHTML | |
| button.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
| </svg> | |
| Copied! | |
| ` | |
| setTimeout(() => { | |
| button.innerHTML = originalText | |
| }, 2000) | |
| showNotification("Text copied to clipboard!") | |
| }) | |
| .catch((err) => { | |
| console.error("Failed to copy text: ", err) | |
| showNotification("Failed to copy text: " + err.message, "error") | |
| }) | |
| } | |
| // Show loading indicator | |
| function showLoadingIndicator() { | |
| const chatMessages = document.getElementById("chat-messages") | |
| // Create loading message | |
| const loadingDiv = document.createElement("div") | |
| loadingDiv.id = "loading-indicator" | |
| loadingDiv.className = "flex items-start mb-4" | |
| loadingDiv.innerHTML = ` | |
| <div class="flex-shrink-0 bg-purple-600 rounded-full p-2 mr-3"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> | |
| </svg> | |
| </div> | |
| <div class="max-w-3xl assistant-message p-4 shadow-sm flex items-center"> | |
| <div class="spinner mr-3"></div> | |
| <p class="text-gray-600">Thinking...</p> | |
| </div> | |
| ` | |
| chatMessages.appendChild(loadingDiv) | |
| scrollToBottom() | |
| } | |
| // Hide loading indicator | |
| function hideLoadingIndicator() { | |
| const loadingIndicator = document.getElementById("loading-indicator") | |
| if (loadingIndicator) { | |
| loadingIndicator.remove() | |
| } | |
| } | |
| // Scroll to bottom of chat | |
| function scrollToBottom() { | |
| const chatMessages = document.getElementById("chat-messages") | |
| chatMessages.scrollTop = chatMessages.scrollHeight | |
| } | |
| // Show notification | |
| function showNotification(message, type = "success") { | |
| const container = document.getElementById("notification-container") | |
| // Create notification element | |
| const notification = document.createElement("div") | |
| // Set class based on notification type | |
| if (type === "success") { | |
| notification.className = | |
| "notification mb-2 bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded shadow-md" | |
| } else if (type === "error") { | |
| notification.className = "notification mb-2 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-md" | |
| } else if (type === "warning") { | |
| notification.className = | |
| "notification mb-2 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 rounded shadow-md" | |
| } else if (type === "info") { | |
| notification.className = | |
| "notification mb-2 bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 rounded shadow-md" | |
| } | |
| notification.innerHTML = message | |
| // Add to container | |
| container.appendChild(notification) | |
| // Remove after 3 seconds | |
| setTimeout(() => { | |
| notification.style.opacity = "0" | |
| notification.style.transform = "translateY(-20px)" | |
| notification.style.transition = "opacity 0.5s, transform 0.5s" | |
| setTimeout(() => { | |
| notification.remove() | |
| }, 500) | |
| }, 3000) | |
| } | |
| // History panel functions | |
| function toggleHistoryPanel() { | |
| const historyPanel = document.getElementById("historyPanel") | |
| if (historyPanel.classList.contains("translate-x-full")) { | |
| historyPanel.classList.remove("translate-x-full") | |
| } else { | |
| historyPanel.classList.add("translate-x-full") | |
| } | |
| } | |
| function addToHistory(action, details) { | |
| // Get existing history or initialize empty array | |
| const history = JSON.parse(localStorage.getItem("aiAppHistory") || "[]") | |
| // Add new item | |
| history.unshift({ | |
| action, | |
| details, | |
| timestamp: new Date().toISOString(), | |
| }) | |
| // Keep only the last 20 items | |
| if (history.length > 20) { | |
| history.pop() | |
| } | |
| // Save back to localStorage | |
| localStorage.setItem("aiAppHistory", JSON.stringify(history)) | |
| // Update UI if history panel exists | |
| loadHistory() | |
| } | |
| function loadHistory() { | |
| const historyItemsContainer = document.getElementById("historyItems") | |
| if (!historyItemsContainer) return | |
| // Clear existing items | |
| historyItemsContainer.innerHTML = "" | |
| // Get history from localStorage | |
| const history = JSON.parse(localStorage.getItem("aiAppHistory") || "[]") | |
| if (history.length === 0) { | |
| historyItemsContainer.innerHTML = `<p class="text-gray-500 text-center">No history yet</p>` | |
| return | |
| } | |
| // Add each history item | |
| history.forEach((item) => { | |
| const historyItem = document.createElement("div") | |
| historyItem.className = "bg-gray-100 p-3 rounded-lg" | |
| const date = new Date(item.timestamp) | |
| const formattedDate = date.toLocaleDateString() + " " + date.toLocaleTimeString() | |
| historyItem.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <h4 class="font-medium text-gray-800">${item.action}</h4> | |
| <span class="text-xs text-gray-500">${formattedDate}</span> | |
| </div> | |
| <p class="text-sm text-gray-600 mt-1">${item.details}</p> | |
| ` | |
| historyItemsContainer.appendChild(historyItem) | |
| }) | |
| } | |
| // Helper function to get language name from code | |
| function getLanguageName(code) { | |
| const languages = { | |
| ar: "Arabic", | |
| en: "English", | |
| fr: "French", | |
| es: "Spanish", | |
| zh: "Chinese", | |
| } | |
| return languages[code] || code | |
| } |