Spaces:
Sleeping
Sleeping
// 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'; | |
} | |
}); | |
}; | |