YaCy Chat

This Chat is private. YaCy does not keep any history — only your browser remembers the current conversation.

Default Dialog Augmentation:
User
Attach Search Results Attach PNG/JPG or text (.txt/.md/.tex)
`); popup.document.close(); } else if (attachment.kind === 'text') { const blob = new Blob([attachmentTextContent(attachment)], { type: attachment.mime || 'text/plain' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); setTimeout(() => URL.revokeObjectURL(url), 2000); } } function closeAttachmentModal() { if (activeModal && activeModal.parentNode) { activeModal.parentNode.removeChild(activeModal); } activeModal = null; } function openAttachmentModal(preview, attachment) { closeAttachmentModal(); if (!preview) return; const backdrop = document.createElement('div'); backdrop.className = 'attachment-modal-backdrop'; backdrop.addEventListener('click', event => { if (event.target === backdrop) closeAttachmentModal(); }); const modal = document.createElement('div'); modal.className = 'attachment-modal'; const header = document.createElement('div'); header.className = 'modal-header'; const title = document.createElement('h4'); title.textContent = preview.name || 'Attachment'; header.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.className = 'close-modal'; closeBtn.innerHTML = ''; closeBtn.addEventListener('click', closeAttachmentModal); header.appendChild(closeBtn); const body = document.createElement('div'); body.className = 'modal-body'; if (preview.kind === 'image' && preview.imageUrl) { const img = document.createElement('img'); img.src = preview.imageUrl; img.alt = preview.name || 'Attachment image'; img.style.maxWidth = '100%'; img.style.display = 'block'; body.appendChild(img); } else { const content = attachmentTextContent(attachment) || preview.text || ''; if (preview.kind === 'markdown' && markdownSupport.enabled) { const div = document.createElement('div'); div.innerHTML = renderMarkdown(content); body.appendChild(div); } else { const pre = document.createElement('pre'); pre.textContent = content; body.appendChild(pre); } } modal.appendChild(header); modal.appendChild(body); backdrop.appendChild(modal); document.body.appendChild(backdrop); activeModal = backdrop; window.addEventListener('keydown', function escHandler(event) { if (event.key === 'Escape') { closeAttachmentModal(); window.removeEventListener('keydown', escHandler); } }, { once: true }); } function renderPreviewBody(container, preview) { if (!container || !preview) return; if (preview.kind === 'image' && preview.imageUrl) { const img = document.createElement('img'); img.src = preview.imageUrl; img.alt = preview.name || 'Attachment image'; img.className = 'preview-image'; container.appendChild(img); return; } const body = document.createElement('div'); body.className = 'preview-body'; if (preview.kind === 'markdown' && markdownSupport.enabled) { body.innerHTML = renderMarkdown(preview.text || ''); } else { body.textContent = preview.text || 'Preview unavailable.'; } container.appendChild(body); } function showAttachmentPopover(anchor, attachment) { if (!anchor || !attachment) return; const preview = makePreviewData(attachment); if (!preview) return; clearTimeout(hidePopoverTimer); closeAttachmentPopover(); const host = anchor.parentElement || anchor; const hostStyle = window.getComputedStyle(host); if (hostStyle.position === 'static') { host.style.position = 'relative'; } const popover = document.createElement('div'); popover.className = 'attachment-preview'; popover.setAttribute('role', 'dialog'); const title = document.createElement('h5'); title.textContent = preview.name || 'Attachment'; popover.appendChild(title); const meta = document.createElement('div'); meta.className = 'meta'; const parts = []; if (preview.mime) parts.push(preview.mime); if (preview.size) parts.push(formatFileSize(preview.size)); meta.textContent = parts.join(' • '); popover.appendChild(meta); renderPreviewBody(popover, preview); const actions = document.createElement('div'); actions.className = 'actions'; actions.appendChild(createActionButton('glyphicon-new-window', 'Full preview', () => openAttachmentModal(preview, attachment))); actions.appendChild(createActionButton('glyphicon-download', 'Download', () => downloadAttachment(attachment))); if (preview.kind === 'image') { actions.appendChild(createActionButton('glyphicon-picture', 'Open tab', () => openInNewTab(attachment))); } if (preview.kind === 'text' || preview.kind === 'markdown') { actions.appendChild(createActionButton('glyphicon-copy', 'Copy snippet', () => copyToClipboard(preview.text || ''))); actions.appendChild(createActionButton('glyphicon-list-alt', 'Copy all', () => copyToClipboard(attachmentTextContent(attachment) || preview.text || ''))); } popover.appendChild(actions); if (preview.note) { const note = document.createElement('div'); note.className = 'preview-note'; note.textContent = preview.note; popover.appendChild(note); } popover.addEventListener('mouseenter', () => clearTimeout(hidePopoverTimer)); popover.addEventListener('mouseleave', () => scheduleHidePopover()); host.appendChild(popover); const rect = anchor.getBoundingClientRect(); const viewportMid = window.innerHeight / 2; const placeAbove = rect.top > viewportMid; if (placeAbove) { popover.classList.add('above'); } else { popover.classList.remove('above'); } activePopover = popover; popoverAnchor = anchor; } function setupAttachmentPreviewTrigger(anchor, attachmentProvider) { if (!anchor) return; const handler = () => { const attachment = typeof attachmentProvider === 'function' ? attachmentProvider() : attachmentProvider; if (!attachment) return; showAttachmentPopover(anchor, attachment); }; anchor.addEventListener('mouseenter', handler); anchor.addEventListener('focus', handler); anchor.addEventListener('mouseleave', () => scheduleHidePopover()); anchor.addEventListener('blur', () => scheduleHidePopover()); } function createAttachmentChip(attachment) { const chip = document.createElement('div'); chip.className = 'attachment-chip'; chip.tabIndex = 0; const icon = document.createElement('span'); const iconClass = attachment?.kind === 'image' ? 'glyphicon-picture' : (attachment?.kind === 'text' ? 'glyphicon-file' : 'glyphicon-paperclip'); icon.className = `glyphicon ${iconClass}`; chip.appendChild(icon); const label = document.createElement('span'); label.className = 'attachment-label'; label.textContent = attachment?.name || 'Attachment'; chip.appendChild(label); setupAttachmentPreviewTrigger(chip, () => attachment); return chip; } async function buildAttachment(file) { const name = file.name || 'attachment'; const mime = (file.type || '').toLowerCase(); const ext = getFileExtension(name); const allowed = isAllowedByMime(mime) || isAllowedByExtension(ext); const kind = detectAttachmentKind(mime, ext); if (!allowed || !kind) return null; if (kind === 'image') { const dataUrl = await readFileAsDataURL(file); return { kind: 'image', name, dataUrl, mime, size: file.size }; } if (kind === 'text') { const textContent = await readFileAsText(file); const dataUrl = textToDataUrl(textContent, mime || 'text/plain'); return { kind: 'text', name, textContent, dataUrl, mime, size: file.size }; } return null; } dom.form.addEventListener('submit', async event => { event.preventDefault(); if (state.busy) return; const prompt = dom.input.value.trim(); if (!prompt) return; ensureSystemMessage(); const userMessage = buildUserMessage(prompt); // add user message to state immediately so edit/trim is available right away const userEntry = { ...userMessage }; state.messages.push(userEntry); const userIndex = state.messages.length - 1; const preview = formatUserPreview(prompt); state.busy = true; dom.sendButton.disabled = true; appendMessage('user', preview, { attachment: inferAttachmentFromContent(userEntry.content), messageIndex: userIndex }); dom.input.value = ''; clearAttachment(); resizeComposer(); const assistantNode = appendMessage('assistant', initialAssistantPlaceholder(userMessage)); try { await streamChat(userMessage, assistantNode, { onFirstToken: showComposer, includeUserInPayload: false, pushUserToState: false, currentUserIndex: userIndex }); } catch (err) { appendMessage('system', `Error: ${err.message}`); showComposer(); } finally { state.busy = false; dom.sendButton.disabled = false; showComposer(); } }); dom.addFileButton.addEventListener('click', () => { if (state.attachment) { clearAttachment({ applyDefault: false }); return; } if (state.searchMode !== SEARCH_DEFAULTS.none) { setSearchModeActive(SEARCH_DEFAULTS.none); } dom.addFileButton.classList.add('button-active'); dom.fileInput.click(); window.setTimeout(() => { window.addEventListener('focus', () => { if (!state.attachment && !dom.fileInput.value) { dom.addFileButton.classList.remove('button-active'); } }, { once: true }); }, 0); }); dom.searchButton?.addEventListener('click', () => { if (state.searchMode !== SEARCH_DEFAULTS.none) { clearAttachment({ applyDefault: false }); return; } const mode = state.searchDefault === SEARCH_DEFAULTS.none ? SEARCH_DEFAULTS.local : state.searchDefault; setSearchModeActive(mode); }); dom.fileInput.addEventListener('change', handleFileChange); dom.clearChatButton?.addEventListener('click', clearChatHistory); dom.downloadChatButton?.addEventListener('click', downloadChat); dom.uploadChatButton?.addEventListener('click', () => dom.uploadChatInput?.click()); dom.uploadChatInput?.addEventListener('change', handleUploadChatFile); dom.toggleSystemButton?.addEventListener('click', () => { state.showSystem = !state.showSystem; renderConversation(); }); dom.searchDefaultOptions?.addEventListener('change', event => { const target = event.target; if (!(target instanceof HTMLInputElement)) return; if (target.name !== 'searchDefault') return; if (!Object.values(SEARCH_OPTIONS).includes(target.value)) return; state.searchDefault = target.value; updateSearchDefaultSlider(); persistSearchDefault(); applySearchDefaultToComposer(); }); window.addEventListener('scroll', closeAttachmentPopover); dom.input.addEventListener('input', resizeComposer); dom.input.addEventListener('input', ensureComposerVisible); dom.input.addEventListener('keydown', event => { if (event.isComposing) return; if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); if (typeof dom.form.requestSubmit === 'function') { dom.form.requestSubmit(dom.sendButton); } else { dom.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); } } }); dom.composerMain?.addEventListener('dragover', event => { event.preventDefault(); }); dom.composerMain?.addEventListener('drop', async event => { event.preventDefault(); const file = event.dataTransfer?.files && event.dataTransfer.files[0]; if (!file) return; try { setAttachmentLoading(true); const attachment = await buildAttachment(file); if (!attachment) { appendMessage('system', 'Please drop a PNG/JPG image or a text file (.txt, .md, .tex, or other plain text).'); return; } setComposerAttachment(attachment); } catch (err) { appendMessage('system', `Failed to read file: ${err.message}`); clearAttachment(); } finally { setAttachmentLoading(false); } }); window.addEventListener('resize', resizeComposer); async function initializeChat() { await loadSearchDefault(); await hydrateConversation(); updateSystemToggleButton(); resizeComposer(); updateClearChatVisibility(); } initializeChat().catch(err => { console.warn('Failed to initialize chat from VFS', err); updateSystemToggleButton(); resizeComposer(); updateClearChatVisibility(); });