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
This commit is contained in:
126
static/js/inline_chat.js
Normal file
126
static/js/inline_chat.js
Normal file
@@ -0,0 +1,126 @@
|
||||
// ── 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();
|
||||
Reference in New Issue
Block a user