Connecting...
πŸ“„
Upload a document
or drag and drop here

Blank Document

Start typing in the editor, or select a document template above to auto-generate.

✍️ Electronic Signatures
Your signature will appear here
All changes saved

Start typing your document here, or select a template from the left panel...

`; const blob = new Blob([html], { type: 'application/msword' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${sessionName.replace(/[^a-z0-9]/gi, '-')}.doc`; a.click(); URL.revokeObjectURL(url); } function handleDocumentEdit() { syncToFirebase(); } // ========== FIREBASE & COLLABORATION ========== const firebaseConfig = { apiKey: "AIzaSyCeACvK3MVTOOaWY4SWG1yNnrcA9ILkkaM", authDomain: "termsdocs.firebaseapp.com", databaseURL: "https://termsdocs-default-rtdb.firebaseio.com", projectId: "termsdocs", storageBucket: "termsdocs.firebasestorage.app", messagingSenderId: "62791617178", appId: "1:62791617178:web:ff79f24f40cc98e88aed23", measurementId: "G-901N2Y3CDZ" }; firebase.initializeApp(firebaseConfig); const db = firebase.database(); let userName = ''; let userColor = ''; let roomId = ''; let myUserId = ''; let contentRef, usersRef, myUserRef, metaRef; let isLocalUpdate = false; let saveTimeout = null; const userColors = ['#ef4444', '#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']; // Get or create room ID const urlParams = new URLSearchParams(window.location.search); roomId = urlParams.get('room'); if (!roomId) { roomId = Math.random().toString(36).substring(2, 14); window.history.replaceState({}, '', `?room=${roomId}`); } myUserId = Math.random().toString(36).substring(2, 10); const savedName = localStorage.getItem('terms-law-name'); if (savedName) document.getElementById('userName').value = savedName; // Event listeners document.getElementById('userName').addEventListener('keypress', e => { if (e.key === 'Enter') joinSession(); }); document.getElementById('joinBtn').addEventListener('click', joinSession); document.getElementById('copyBtn').addEventListener('click', copyLink); document.getElementById('newBtn').addEventListener('click', () => { window.location.href = `?room=${Math.random().toString(36).substring(2, 14)}`; }); function joinSession() { userName = document.getElementById('userName').value.trim(); if (!userName) return document.getElementById('userName').focus(); localStorage.setItem('terms-law-name', userName); userColor = userColors[Math.floor(Math.random() * userColors.length)]; document.getElementById('joinModal').classList.add('hidden'); initFirebase(); } function initFirebase() { contentRef = db.ref(`rooms/${roomId}/content`); usersRef = db.ref(`rooms/${roomId}/users`); metaRef = db.ref(`rooms/${roomId}/meta`); myUserRef = usersRef.child(myUserId); const preview = document.getElementById('docPreview'); const sessionNameInput = document.getElementById('sessionName'); myUserRef.set({ name: userName, color: userColor, timestamp: firebase.database.ServerValue.TIMESTAMP }); myUserRef.onDisconnect().remove(); contentRef.on('value', (snapshot) => { if (isLocalUpdate) return; const content = snapshot.val(); if (content !== null && content !== undefined) { preview.innerHTML = content; } }); metaRef.child('name').on('value', (snapshot) => { const name = snapshot.val(); if (name && document.activeElement !== sessionNameInput) { sessionNameInput.value = name; } }); usersRef.on('value', (snapshot) => { updateUsers(snapshot.val() || {}); }); db.ref('.info/connected').on('value', (snapshot) => { const connected = snapshot.val(); document.getElementById('statusDot').classList.toggle('connected', connected); document.getElementById('statusText').textContent = connected ? 'Connected' : 'Connecting...'; if (connected) showToast(`Joined as ${userName}`); }); sessionNameInput.addEventListener('input', () => { metaRef.child('name').set(sessionNameInput.value); }); } function syncToFirebase() { const preview = document.getElementById('docPreview'); const saveStatus = document.getElementById('saveStatus'); saveStatus.textContent = 'Saving...'; saveStatus.className = 'save-status saving'; clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { isLocalUpdate = true; contentRef.set(preview.innerHTML).then(() => { isLocalUpdate = false; saveStatus.textContent = 'All changes saved'; saveStatus.className = 'save-status saved'; }); }, 300); } function updateUsers(users) { const userList = document.getElementById('userList'); userList.innerHTML = ''; const userEntries = Object.entries(users); userEntries.forEach(([id, user]) => { const isYou = id === myUserId; const dot = document.createElement('div'); dot.className = 'user-dot'; dot.style.background = user.color; dot.dataset.name = user.name + (isYou ? ' (you)' : ''); dot.textContent = user.name.charAt(0).toUpperCase(); userList.appendChild(dot); }); } function copyLink() { navigator.clipboard.writeText(window.location.href) .then(() => showToast('Link copied!')) .catch(() => prompt('Copy:', window.location.href)); } function showToast(msg) { const toast = document.getElementById('toast'); toast.textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 3000); } // ========== SIGNATURE FUNCTIONALITY ========== const signatures = { A: { name: '', font: 'dancing', mode: 'type', applied: false, timestamp: null, dataUrl: null, ip: null }, B: { name: '', font: 'dancing', mode: 'type', applied: false, timestamp: null, dataUrl: null, ip: null } }; let signaturesRef = null; let canvasContexts = {}; let isDrawing = false; let lastX = 0; let lastY = 0; let documentId = null; // Get internet time from WorldTimeAPI async function getInternetTime() { try { const response = await fetch('https://worldtimeapi.org/api/ip'); const data = await response.json(); return data.datetime; } catch (e) { // Fallback to local time if API fails, but mark it console.warn('Could not fetch internet time, using local time'); return new Date().toISOString(); } } // Get user's IP address async function getUserIP() { try { const response = await fetch('https://api.ipify.org?format=json'); const data = await response.json(); return data.ip; } catch (e) { return 'Unknown'; } } // Generate unique document ID function generateDocumentId() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let id = 'TL-'; for (let i = 0; i < 8; i++) { id += chars.charAt(Math.floor(Math.random() * chars.length)); } return id + '-' + Date.now().toString(36).toUpperCase(); } function initSignatures() { if (!roomId) return; signaturesRef = db.ref(`rooms/${roomId}/signatures`); // Generate or retrieve document ID signaturesRef.child('documentId').once('value', (snapshot) => { if (snapshot.val()) { documentId = snapshot.val(); } else { documentId = generateDocumentId(); signaturesRef.child('documentId').set(documentId); } }); signaturesRef.on('value', (snapshot) => { const data = snapshot.val(); if (data) { if (data.A) { signatures.A = { ...signatures.A, ...data.A }; updateSignatureUI('A'); } if (data.B) { signatures.B = { ...signatures.B, ...data.B }; updateSignatureUI('B'); } if (data.documentId) { documentId = data.documentId; } // Update verification stamp if any signatures exist if (signatures.A.applied || signatures.B.applied) { updateVerificationStamp(); } } }); // Initialize canvases initSignatureCanvases(); } function initSignatureCanvases() { ['A', 'B'].forEach(party => { const canvas = document.getElementById(`sigCanvas${party}`); if (!canvas) return; const ctx = canvas.getContext('2d'); canvasContexts[party] = ctx; // Set canvas resolution const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * 2; canvas.height = rect.height * 2; ctx.scale(2, 2); // Style settings ctx.strokeStyle = '#1a365d'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; // Mouse events canvas.addEventListener('mousedown', (e) => startDrawing(e, party)); canvas.addEventListener('mousemove', (e) => draw(e, party)); canvas.addEventListener('mouseup', () => stopDrawing(party)); canvas.addEventListener('mouseout', () => stopDrawing(party)); // Touch events for mobile canvas.addEventListener('touchstart', (e) => { e.preventDefault(); const touch = e.touches[0]; startDrawing(touch, party); }); canvas.addEventListener('touchmove', (e) => { e.preventDefault(); const touch = e.touches[0]; draw(touch, party); }); canvas.addEventListener('touchend', () => stopDrawing(party)); }); } function startDrawing(e, party) { isDrawing = true; const canvas = document.getElementById(`sigCanvas${party}`); const rect = canvas.getBoundingClientRect(); lastX = e.clientX - rect.left; lastY = e.clientY - rect.top; } function draw(e, party) { if (!isDrawing) return; const canvas = document.getElementById(`sigCanvas${party}`); const ctx = canvasContexts[party]; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.lineTo(x, y); ctx.stroke(); lastX = x; lastY = y; // Enable apply button when drawing document.getElementById('sigApply' + party).disabled = !document.getElementById('sigName' + party).value.trim(); } function stopDrawing(party) { if (isDrawing) { isDrawing = false; // Save canvas data const canvas = document.getElementById(`sigCanvas${party}`); signatures[party].dataUrl = canvas.toDataURL('image/png'); } } function clearCanvas(party) { const canvas = document.getElementById(`sigCanvas${party}`); const ctx = canvasContexts[party]; const rect = canvas.getBoundingClientRect(); ctx.clearRect(0, 0, rect.width * 2, rect.height * 2); signatures[party].dataUrl = null; } function setSignatureMode(party, mode) { signatures[party].mode = mode; // Update toggle buttons const panel = document.getElementById('sigParty' + party + 'Panel'); panel.querySelectorAll('.sig-toggle-btn').forEach((btn, idx) => { btn.classList.toggle('active', (idx === 0 && mode === 'type') || (idx === 1 && mode === 'draw')); }); // Show/hide containers document.getElementById('sigTypeContainer' + party).classList.toggle('hidden', mode !== 'type'); document.getElementById('sigDrawContainer' + party).classList.toggle('hidden', mode !== 'draw'); // Re-init canvas if switching to draw mode if (mode === 'draw') { setTimeout(() => initSignatureCanvases(), 100); } } function selectSignatureParty(party) { const isA = party === 'partyA'; document.getElementById('sigTabPartyA').classList.toggle('active', isA); document.getElementById('sigTabPartyB').classList.toggle('active', !isA); document.getElementById('sigPartyAPanel').classList.toggle('hidden', !isA); document.getElementById('sigPartyBPanel').classList.toggle('hidden', isA); // Re-init canvases when switching setTimeout(() => initSignatureCanvases(), 100); } function updateSignaturePreview(party) { const nameInput = document.getElementById('sigName' + party); const preview = document.getElementById('sigPreview' + party); const applyBtn = document.getElementById('sigApply' + party); const name = nameInput.value.trim(); if (name) { const fontClass = 'sig-font-' + signatures[party].font; preview.innerHTML = `${name}`; applyBtn.disabled = false; } else { preview.innerHTML = 'Your signature will appear here'; applyBtn.disabled = true; } } function selectSignatureFont(party, font) { signatures[party].font = font; // Update button states const container = document.getElementById('sigParty' + party + 'Panel'); container.querySelectorAll('.sig-font-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.font === font); }); // Update preview updateSignaturePreview(party); } async function applySignature(party) { const nameInput = document.getElementById('sigName' + party); const name = nameInput.value.trim(); if (!name) return; showToast('Applying signature...'); // Get internet time and IP const [internetTime, userIP] = await Promise.all([ getInternetTime(), getUserIP() ]); signatures[party].name = name; signatures[party].applied = true; signatures[party].timestamp = internetTime; signatures[party].ip = userIP; // If draw mode, ensure dataUrl is captured if (signatures[party].mode === 'draw') { const canvas = document.getElementById(`sigCanvas${party}`); signatures[party].dataUrl = canvas.toDataURL('image/png'); } // Save to Firebase if (signaturesRef) { signaturesRef.child(party).set(signatures[party]); } updateSignatureUI(party); updateDocumentSignatures(); updateVerificationStamp(); showToast('Signature applied with verification stamp!'); } function updateSignatureUI(party) { const tab = document.getElementById('sigTabParty' + party); const status = document.getElementById('sigStatus' + party); if (signatures[party].applied) { tab.classList.add('signed'); status.textContent = 'βœ“ Signed'; status.style.color = '#22c55e'; } else { tab.classList.remove('signed'); status.textContent = 'Not signed'; status.style.color = '#9ca3af'; } } function formatTimestamp(isoString) { if (!isoString) return ''; const date = new Date(isoString); return date.toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short' }); } function updateDocumentSignatures() { const preview = document.getElementById('docPreview'); const sigBlocks = preview.querySelectorAll('.signature-party'); ['A', 'B'].forEach((party, idx) => { if (sigBlocks.length > idx && signatures[party].applied) { const sigLine = sigBlocks[idx].querySelector('.signature-line'); if (sigLine) { let sigHtml = ''; if (signatures[party].mode === 'draw' && signatures[party].dataUrl) { sigHtml = `Signature`; } else { const fontClass = 'sig-font-' + signatures[party].font; sigHtml = `
${signatures[party].name}
`; } sigHtml += `
${formatTimestamp(signatures[party].timestamp)}
`; sigLine.innerHTML = sigHtml; } } }); syncToFirebase(); } function updateVerificationStamp() { // Remove existing stamp if any let existingStamp = document.querySelector('.verification-stamp'); if (existingStamp) existingStamp.remove(); // Only add if at least one signature if (!signatures.A.applied && !signatures.B.applied) return; const preview = document.getElementById('docPreview'); const stampHtml = `
Electronic Signature Verification
Document ID: ${documentId || 'Generating...'}
Party A
${signatures.A.applied ? `
${signatures.A.name}
${formatTimestamp(signatures.A.timestamp)}
IP: ${signatures.A.ip || 'N/A'}
` : '
Not yet signed
'}
Party B
${signatures.B.applied ? `
${signatures.B.name}
${formatTimestamp(signatures.B.timestamp)}
IP: ${signatures.B.ip || 'N/A'}
` : '
Not yet signed
'}
`; preview.insertAdjacentHTML('beforeend', stampHtml); } function sendForSignature() { const emailInput = document.getElementById('emailSignInput'); const email = emailInput.value.trim(); if (!email || !email.includes('@')) { showToast('Please enter a valid email address'); return; } const signUrl = window.location.href; const sessionName = document.getElementById('sessionName').value || 'Document'; const subject = encodeURIComponent(`Action Required: Sign "${sessionName}" - Terms.Law`); const body = encodeURIComponent(`Hello, You have been requested to review and electronically sign a legal document. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ πŸ“„ Document: ${sessionName} πŸ”— Document ID: ${documentId || 'Will be assigned'} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Please click the secure link below to access the document: ${signUrl} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ HOW TO SIGN: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. Click the link above to open the document 2. Review the document carefully 3. Scroll down to the "Electronic Signatures" section 4. Click on "Party B" tab 5. Enter your full legal name 6. Choose to TYPE or DRAW your signature 7. Click "Apply Signature" Your signature will be timestamped and verified by Terms.Law. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ This is an official electronic signature request. By signing, you agree to the terms contained in the document. Questions? Contact the sender of this email. Powered by Terms.Law Docs `); window.open(`mailto:${email}?subject=${subject}&body=${body}`); document.getElementById('emailSentBadge').classList.remove('hidden'); if (signaturesRef) { signaturesRef.child('emailSent').set({ email: email, sentAt: new Date().toISOString(), sentBy: userName, documentId: documentId }); } showToast('Signature request email opened!'); } // Initialize signatures after Firebase is ready setTimeout(initSignatures, 1500); window.addEventListener('beforeunload', () => { if (myUserRef) myUserRef.remove(); }); // ======================================== // AI DOCUMENT CHAT // ======================================== const aiChat = { messages: [], conversationHistory: [], isLoading: false, isOpen: true, isMinimized: false, selectedModel: 'llama' // 'llama' or 'sonnet' }; const aiChatApiUrls = [ 'https://terms.law/api/document-chat', 'https://template-generator-aob3.vercel.app/api/document-chat', '/api/document-chat' ]; // Direct Groq API fallback (for local testing when server APIs are unavailable) // Get a free key at https://console.groq.com/keys const GROQ_API_KEY_FALLBACK = localStorage.getItem('groq_api_key') || ''; async function callGroqDirectly(message, documentText, documentType, conversationHistory) { if (!GROQ_API_KEY_FALLBACK) { throw new Error('No Groq API key - set via localStorage.setItem("groq_api_key", "your-key")'); } const systemPrompt = `You are an expert legal document analyst helping users understand contracts and legal agreements. CURRENT DOCUMENT: Type: ${documentType} ${documentText ? `Content:\n${documentText.substring(0, 6000)}` : 'No document loaded.'} Be direct, reference specific clauses, use **bold** for key terms. Note this is AI assistance, not legal advice.`; const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${GROQ_API_KEY_FALLBACK}` }, body: JSON.stringify({ model: 'llama-3.3-70b-versatile', messages: [ { role: 'system', content: systemPrompt }, ...conversationHistory.slice(-6), { role: 'user', content: message } ], max_tokens: 1500, temperature: 0.3 }) }); if (!response.ok) throw new Error('Groq API failed'); const data = await response.json(); return { response: data.choices[0].message.content }; } function initAiChat() { const panel = document.getElementById('aiChatPanel'); const fab = document.getElementById('aiChatFab'); // Add welcome message addAiMessage('assistant', "Hi! I'm your document assistant. I can help you understand this document, identify risks, or answer questions. Try asking:\n\nβ€’ \"Summarize this document\"\nβ€’ \"What are the risks?\"\nβ€’ \"Should I sign this?\"\nβ€’ \"Explain [specific clause]\""); } function toggleAiChat() { const panel = document.getElementById('aiChatPanel'); const fab = document.getElementById('aiChatFab'); if (aiChat.isOpen) { panel.style.display = 'none'; fab.classList.add('show'); aiChat.isOpen = false; } else { panel.style.display = 'flex'; fab.classList.remove('show'); aiChat.isOpen = true; } } function minimizeAiChat() { const panel = document.getElementById('aiChatPanel'); aiChat.isMinimized = !aiChat.isMinimized; panel.classList.toggle('minimized', aiChat.isMinimized); const toggleBtn = document.getElementById('aiChatToggleBtn'); toggleBtn.textContent = aiChat.isMinimized ? '+' : 'βˆ’'; } function selectAiModel(model) { aiChat.selectedModel = model; document.querySelectorAll('.model-switch-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.model === model); }); // Show indicator in header const subtitle = document.querySelector('.ai-chat-subtitle'); subtitle.textContent = model === 'sonnet' ? 'Claude Sonnet 4 (Premium)' : 'Llama 3.3 (Free)'; } function addAiMessage(type, content) { aiChat.messages.push({ type, content }); renderAiMessages(); } function renderAiMessages() { const container = document.getElementById('aiChatMessages'); let html = aiChat.messages.map(msg => { const formattedContent = formatAiMessage(msg.content); return `
${formattedContent}
`; }).join(''); if (aiChat.isLoading) { html += `
`; } container.innerHTML = html; container.scrollTop = container.scrollHeight; } function formatAiMessage(content) { return content .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/^### (.*$)/gim, '$1
') .replace(/^## (.*$)/gim, '$1
') .replace(/^# (.*$)/gim, '$1
') .replace(/^- (.*$)/gim, 'β€’ $1
') .replace(/^\d+\. (.*$)/gim, 'β€’ $1
') .replace(/\n\n/g, '

') .replace(/\n/g, '
'); } function getDocumentText() { const preview = document.getElementById('docPreview'); if (!preview) return ''; return preview.innerText || preview.textContent || ''; } function getDocumentType() { const select = document.getElementById('documentType'); if (!select) return 'Legal Document'; return select.options[select.selectedIndex]?.text || 'Legal Document'; } async function sendAiMessage() { const input = document.getElementById('aiChatInput'); const message = input.value.trim(); if (!message || aiChat.isLoading) return; input.value = ''; addAiMessage('user', message); aiChat.isLoading = true; renderAiMessages(); try { const documentText = getDocumentText(); const documentType = getDocumentType(); let response = null; for (const url of aiChatApiUrls) { try { response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, documentText, documentType, model: aiChat.selectedModel, conversationHistory: aiChat.conversationHistory.slice(-6) }) }); if (response.ok) break; response = null; } catch (e) { response = null; } } let data = null; if (!response) { // Try direct Groq fallback for local testing if (GROQ_API_KEY_FALLBACK && aiChat.selectedModel === 'llama') { data = await callGroqDirectly(message, documentText, documentType, aiChat.conversationHistory); } else { throw new Error('All API endpoints failed. For local testing, set your Groq key: localStorage.setItem("groq_api_key", "your-key-here")'); } } else { data = await response.json(); } // Update conversation history aiChat.conversationHistory.push( { role: 'user', content: message }, { role: 'assistant', content: data.response } ); addAiMessage('assistant', data.response); } catch (error) { console.error('AI Chat error:', error); if (error.message.includes('Groq key')) { addAiMessage('assistant', 'API unavailable. To enable locally:\n\n1. Get free key at console.groq.com\n2. Open browser console (F12)\n3. Run: `localStorage.setItem("groq_api_key", "your-key")`\n4. Refresh page'); } else { addAiMessage('assistant', 'Sorry, I encountered an error. Please try again.'); } } finally { aiChat.isLoading = false; renderAiMessages(); } } function sendQuickAiQuestion(question) { const input = document.getElementById('aiChatInput'); input.value = question; sendAiMessage(); } function handleAiKeyPress(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAiMessage(); } } // Initialize AI chat on load setTimeout(initAiChat, 500);
AI
Document Assistant
Llama 3.3 (Free)