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
127 lines
4.1 KiB
JavaScript
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();
|