Spaces:
Runtime error
Runtime error
| // Initialize the app container. | |
| const app = document.getElementById('app'); | |
| // Add retry count with 600 second timeout (checking every 2 seconds = 300 attempts). | |
| let retryCount = 0; | |
| const MAX_RETRIES = 300; // 600 seconds / 2 second interval = 300 attempts | |
| // Add function to get JWT token | |
| async function getAuthToken() { | |
| try { | |
| const response = await fetch('/api/auth-token'); | |
| if (!response.ok) { | |
| throw new Error('Failed to fetch auth token'); | |
| } | |
| const { token } = await response.json(); | |
| return token; | |
| } catch (error) { | |
| console.error('Error getting auth token:', error); | |
| throw error; | |
| } | |
| } | |
| async function checkServicesStatus() { | |
| try { | |
| const token = await getAuthToken(); | |
| const response = await fetch('/inference/api/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Token token=${token}` | |
| }, | |
| body: JSON.stringify({ | |
| model: 'gemma-2b', | |
| messages: [{role: 'user', content: 'hi'}], | |
| nutrient_dont_send_defaults: true | |
| }) | |
| }); | |
| if (response.ok) { | |
| window.servicesReady = true; | |
| const translationControls = document.getElementById('translationControls'); | |
| const statusIndicator = document.getElementById('statusIndicator'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| if (translationControls) { | |
| translationControls.style.display = 'block'; | |
| } | |
| if (statusIndicator) { | |
| statusIndicator.style.display = 'none'; | |
| } | |
| if (loadingOverlay) { | |
| loadingOverlay.classList.add('hidden'); | |
| } | |
| return true; | |
| } | |
| throw new Error('Services not ready'); | |
| } catch (error) { | |
| retryCount++; | |
| const statusIndicator = document.getElementById('statusIndicator'); | |
| if (statusIndicator) { | |
| if (retryCount >= MAX_RETRIES) { | |
| statusIndicator.innerHTML = '❌ Failed to initialize AI services. Try restarting the space.'; | |
| statusIndicator.style.color = '#dc3545'; | |
| } else { | |
| statusIndicator.innerHTML = `<span class="spinner"></span> Initializing AI services...`; | |
| // Schedule next check only if we haven't exceeded retries | |
| setTimeout(checkServicesStatus, 2000); | |
| } | |
| } | |
| console.log('Waiting for services...', error); | |
| return false; | |
| } | |
| } | |
| // Start the first check | |
| checkServicesStatus(); | |
| // Load Document Authoring SDK. | |
| const script = document.createElement('script'); | |
| script.src = 'https://document-authoring-cdn.pspdfkit.com/releases/document-authoring-1.0.26-umd.js'; | |
| script.crossOrigin = true; | |
| document.head.appendChild(script); | |
| script.onload = async () => { | |
| // Fetch license key from nginx endpoint. | |
| const licenseKeyResponse = await fetch('/api/license-key'); | |
| if (!licenseKeyResponse.ok) { | |
| throw new Error('Failed to fetch license key'); | |
| } | |
| const { licenseKey } = await licenseKeyResponse.json(); | |
| const docAuthSystem = await DocAuth.createDocAuthSystem({licenseKey}); | |
| const editor = await docAuthSystem.createEditor( | |
| document.getElementById("editor"), | |
| { | |
| document: await docAuthSystem.importDOCX(fetch('./Sample.docx')), | |
| } | |
| ); | |
| // Helper function to extract text runs with their paths. | |
| function flattenTextRuns(obj, path = [], result = []) { | |
| for (let key in obj) { | |
| const newPath = [...path, key]; | |
| if (typeof obj[key] === "string" && key === "text" && obj[key].length > 0) { | |
| // Trim any whitespace between text and delimiters. | |
| result.push({ text: obj[key].trim(), path: newPath }); | |
| } else if (typeof obj[key] === "object") { | |
| flattenTextRuns(obj[key], newPath, result); | |
| } | |
| } | |
| return result; | |
| } | |
| // Helper function to group adjacent text runs | |
| function groupAdjacentRuns(textRuns, maxChunkSize = 1000) { | |
| const groups = []; | |
| let currentGroup = []; | |
| let currentSize = 0; | |
| for (let run of textRuns) { | |
| if (currentSize + run.text.length > maxChunkSize && currentGroup.length > 0) { | |
| groups.push([...currentGroup]); | |
| currentGroup = []; | |
| currentSize = 0; | |
| } | |
| currentGroup.push(run); | |
| currentSize += run.text.length; | |
| } | |
| if (currentGroup.length > 0) { | |
| groups.push(currentGroup); | |
| } | |
| return groups; | |
| } | |
| async function translate(content, targetLang, sourceLang = 'English') { | |
| try { | |
| const token = await getAuthToken(); | |
| const response = await fetch('/inference/api/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Token token=${token}` | |
| }, | |
| body: JSON.stringify({ | |
| messages: [ | |
| { | |
| role: "user", | |
| content: `You are a translator. Translate ONLY the exact text between ||| delimiters. Each segment must be translated separately and maintain its own delimiters. The word "Nutrient" must stay EXACTLY as "Nutrient". | |
| Rules: | |
| 1. Do NOT translate proper nouns | |
| 2. Keep original capitalization and punctuation | |
| 3. Preserve ALL spaces exactly as they appear in the original text | |
| 4. For Spanish, use proper inverted punctuation (¡ ¿) | |
| 5. Keep each segment separate - do not combine segments | |
| Examples of correct translations: | |
| Complex sentence split across segments: | |
| |||John's report shows that||| | |
| |||John muestra que||| | |
| |||the project will be||| | |
| |||el proyecto estará||| | |
| |||completed by Friday.||| | |
| |||terminado el viernes.||| | |
| Punctuation and spacing: | |
| |||Hello, world!||| | |
| |||¡Hola, mundo!||| | |
| Mixed content with numbers: | |
| |||Room 101 is empty.||| | |
| |||La habitación 101 está vacía.||| | |
| Now translate this text from ${sourceLang} to ${targetLang}: | |
| ${content}` | |
| } | |
| ], | |
| model: "gemma-2b", | |
| nutrient_dont_send_defaults: true | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Translation request failed'); | |
| } | |
| const data = await response.json(); | |
| return data.choices[0].message.content; | |
| } catch (error) { | |
| console.error("Translation error:", error); | |
| throw new Error("Failed to generate translation"); | |
| } | |
| } | |
| // Function to update nested object value by path. | |
| function setNestedValue(obj, path, value) { | |
| let current = obj; | |
| for (let i = 0; i < path.length - 1; i++) { | |
| current = current[path[i]]; | |
| } | |
| current[path[path.length - 1]] = value; | |
| } | |
| async function translateRecursive(docJson, targetLang, sourceLang = 'English') { | |
| const textRuns = flattenTextRuns(docJson); | |
| const groups = groupAdjacentRuns(textRuns); | |
| for (let group of groups) { | |
| const content = group.map(run => `|||${run.text}|||`).join('\n'); | |
| const translatedContent = await translate(content, targetLang, sourceLang); | |
| const translations = translatedContent | |
| .split('|||') | |
| .filter(t => t.trim()) | |
| .map(t => t.trim()); | |
| group.forEach((run, index) => { | |
| if (translations[index]) { | |
| const originalText = run.text; | |
| let translatedText = translations[index]; | |
| if (originalText.startsWith(' ')) { | |
| translatedText = ' ' + translatedText; | |
| } | |
| if (originalText.endsWith(' ')) { | |
| translatedText = translatedText + ' '; | |
| } | |
| setNestedValue(docJson, run.path, translatedText); | |
| } | |
| }); | |
| } | |
| } | |
| // Add language selection handlers. | |
| const sourceLanguageSelect = document.getElementById('sourceLanguageSelect'); | |
| const targetLanguageSelect = document.getElementById('targetLanguageSelect'); | |
| // Update target language options when source language changes. | |
| sourceLanguageSelect.addEventListener('change', () => { | |
| const selectedSource = sourceLanguageSelect.value; | |
| const languages = ['English', 'Spanish', 'French', 'German']; | |
| // Clear existing options. | |
| targetLanguageSelect.innerHTML = ''; | |
| // Add all languages except the selected source language. | |
| languages.forEach(lang => { | |
| if (lang !== selectedSource) { | |
| const option = document.createElement('option'); | |
| option.value = lang; | |
| option.textContent = lang; | |
| targetLanguageSelect.appendChild(option); | |
| } | |
| }); | |
| }); | |
| const translateButton = document.getElementById('translateButton'); | |
| translateButton.addEventListener('click', async () => { | |
| try { | |
| const sourceLanguage = sourceLanguageSelect.value; | |
| const targetLanguage = targetLanguageSelect.value; | |
| translateButton.disabled = true; | |
| translateButton.innerHTML = '<span class="spinner"></span> Translating...'; | |
| const jsonDoc = await editor.currentDocument().saveDocument(); | |
| await translateRecursive(jsonDoc.container.document, targetLanguage, sourceLanguage); | |
| const translatedDoc = await docAuthSystem.loadDocument(jsonDoc); | |
| editor.setCurrentDocument(translatedDoc); | |
| } catch (error) { | |
| alert('Translation failed: ' + error.message); | |
| } finally { | |
| translateButton.disabled = false; | |
| translateButton.innerHTML = 'Translate Document'; | |
| } | |
| }); | |
| }; | |