Files
notes/static/js/inline_chat.js
SimolZimol 939cc13689 new file: .dockerignore
new file:   .env.example
	new file:   Dockerfile
	new file:   app.py
	new file:   blueprints/__init__.py
	new file:   blueprints/auth.py
	new file:   blueprints/chat.py
	new file:   blueprints/context.py
	new file:   blueprints/documents.py
	new file:   blueprints/main.py
	new file:   config.py
	new file:   docker-compose.yml
	new file:   models/__init__.py
	new file:   models/chat_session.py
	new file:   models/document.py
	new file:   models/user.py
	new file:   requirements.txt
	new file:   services/__init__.py
	new file:   services/document_parser.py
	new file:   services/llm_service.py
	new file:   services/rag_service.py
	new file:   services/url_scraper.py
	new file:   static/css/style.css
	new file:   static/js/chat.js
	new file:   static/js/inline_chat.js
	new file:   static/js/main.js
	new file:   templates/base.html
	new file:   templates/document_view.html
	new file:   templates/index.html
	new file:   templates/login.html
	new file:   templates/register.html
2026-05-22 16:03:50 +02:00

127 lines
4.1 KiB
JavaScript

// ── Inline chat for document_view.html ───────────────────────────────────────
// Uses a plain <textarea> as read-only editor (CodeMirror CDN bundle is huge;
// we use a lightweight syntax-highlighted <pre> approach instead).
const $ = (sel) => document.querySelector(sel);
const DOC_ID = window.__DOC_ID__;
let selectedText = '';
// ── Load document content ─────────────────────────────────────────────────────
async function loadDoc() {
const res = await fetch(`/api/documents/${DOC_ID}/content`);
if (!res.ok) {
$('#editor-mount').textContent = 'Failed to load document.';
return;
}
const data = await res.json();
$('#doc-name').textContent = data.name;
// Render as selectable pre
const pre = document.createElement('pre');
pre.id = 'doc-content';
pre.className = 'p-6 text-sm text-copilot-text whitespace-pre-wrap leading-relaxed outline-none min-h-full';
pre.textContent = data.content;
$('#editor-mount').appendChild(pre);
}
// ── Selection detection ───────────────────────────────────────────────────────
document.addEventListener('mouseup', (e) => {
if ($('#inline-popup')?.contains(e.target)) return;
const sel = window.getSelection();
const text = sel?.toString().trim();
if (!text || text.length < 5) {
hidePopup();
return;
}
selectedText = text;
showPopup(e.clientX, e.clientY);
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Escape') hidePopup();
});
function showPopup(x, y) {
const popup = $('#inline-popup');
const preview = $('#inline-selection-preview');
const question = $('#inline-question');
const answer = $('#inline-answer');
preview.textContent = selectedText.length > 300
? selectedText.slice(0, 300) + '…'
: selectedText;
question.value = '';
answer.textContent = '';
answer.classList.add('hidden');
$('#inline-loading').classList.add('hidden');
// Position popup near the cursor, stay within viewport
const wrapper = $('#editor-wrapper').getBoundingClientRect();
const popupW = 320;
const popupH = 260;
let left = x - wrapper.left + 12;
let top = y - wrapper.top + 12;
if (left + popupW > wrapper.width) left = wrapper.width - popupW - 8;
if (top + popupH > wrapper.height) top = Math.max(0, y - wrapper.top - popupH - 12);
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
popup.classList.remove('hidden');
setTimeout(() => question.focus(), 50);
}
function hidePopup() {
$('#inline-popup').classList.add('hidden');
selectedText = '';
}
// ── Submit inline question ────────────────────────────────────────────────────
$('#inline-submit').addEventListener('click', askInline);
$('#inline-question').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
askInline();
}
});
$('#inline-close').addEventListener('click', hidePopup);
async function askInline() {
const question = $('#inline-question').value.trim();
if (!question || !selectedText) return;
const loading = $('#inline-loading');
const answer = $('#inline-answer');
const btn = $('#inline-submit');
btn.disabled = true;
loading.classList.remove('hidden');
answer.classList.add('hidden');
try {
const res = await fetch('/api/chat/inline', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ selected_text: selectedText, question }),
});
const data = await res.json();
answer.textContent = res.ok ? data.reply : `Error: ${data.error}`;
answer.classList.remove('hidden');
} catch (err) {
answer.textContent = `Network error: ${err.message}`;
answer.classList.remove('hidden');
} finally {
loading.classList.add('hidden');
btn.disabled = false;
}
}
loadDoc();