You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
9.8 KiB
265 lines
9.8 KiB
(function () { |
|
'use strict'; |
|
|
|
function init(root) { |
|
var app = root || document.getElementById('project-search-app'); |
|
if (!app || app.dataset.psChatBound === '1') { |
|
return; |
|
} |
|
app.dataset.psChatBound = '1'; |
|
|
|
var form = app.querySelector('#project-search-form'); |
|
var input = app.querySelector('#project-search-query'); |
|
var messagesEl = app.querySelector('#project-search-messages'); |
|
var statusEl = app.querySelector('#project-search-status'); |
|
var errorEl = app.querySelector('#project-search-error'); |
|
var submitBtn = app.querySelector('#project-search-submit'); |
|
var endBtn = app.querySelector('#project-search-end-chat'); |
|
var askUrl = app.dataset.askUrl; |
|
var endUrl = app.dataset.endUrl; |
|
var csrf = document.querySelector('meta[name="csrf-token"]'); |
|
var pending = false; |
|
|
|
function csrfToken() { |
|
return csrf ? csrf.getAttribute('content') : ''; |
|
} |
|
|
|
function hideError() { |
|
if (!errorEl) return; |
|
errorEl.classList.add('hidden'); |
|
errorEl.textContent = ''; |
|
} |
|
|
|
function showError(message) { |
|
if (!errorEl) return; |
|
errorEl.textContent = message; |
|
errorEl.classList.remove('hidden'); |
|
} |
|
|
|
function setPending(active) { |
|
pending = active; |
|
if (statusEl) { |
|
statusEl.classList.toggle('hidden', !active); |
|
} |
|
if (submitBtn) { |
|
submitBtn.disabled = active || app.dataset.indexAvailable !== '1' || app.dataset.chatAvailable !== '1'; |
|
} |
|
if (input) { |
|
input.disabled = active || app.dataset.indexAvailable !== '1' || app.dataset.chatAvailable !== '1'; |
|
} |
|
} |
|
|
|
function removeEmptyState() { |
|
var empty = messagesEl ? messagesEl.querySelector('[data-chat-empty]') : null; |
|
if (empty) { |
|
empty.remove(); |
|
} |
|
} |
|
|
|
function escapeHtml(text) { |
|
var div = document.createElement('div'); |
|
div.textContent = text; |
|
return div.innerHTML; |
|
} |
|
|
|
function renderMessage(message) { |
|
var isUser = message.role === 'user'; |
|
var html = '<div class="ps-chat__msg ' + (isUser ? 'ps-chat__msg--user' : 'ps-chat__msg--assistant') + '" data-message-id="' + (message.id || '') + '">'; |
|
html += '<div class="ps-chat__msg-label">' + (isUser ? 'Anda' : 'Ollama') + '</div>'; |
|
html += '<div class="ps-chat__bubble">'; |
|
html += '<div class="ps-chat__text whitespace-pre-wrap">' + escapeHtml(message.content || '') + '</div>'; |
|
|
|
if (!isUser && message.results && message.results.length) { |
|
html += '<details class="ps-chat__results mt-3" open>'; |
|
html += '<summary class="text-xs text-cyan-400 cursor-pointer">' + message.results.length + ' folder ditemukan</summary>'; |
|
html += '<ul class="mt-2 space-y-1 max-h-40 overflow-y-auto">'; |
|
message.results.forEach(function (row) { |
|
html += '<li class="text-xs font-mono text-slate-400 break-all">' + escapeHtml(row.path || '') + '</li>'; |
|
}); |
|
html += '</ul></details>'; |
|
} |
|
|
|
html += '</div></div>'; |
|
return html; |
|
} |
|
|
|
function appendMessage(message) { |
|
if (!messagesEl) return; |
|
removeEmptyState(); |
|
messagesEl.insertAdjacentHTML('beforeend', renderMessage(message)); |
|
messagesEl.scrollTop = messagesEl.scrollHeight; |
|
} |
|
|
|
function ensureEndChatButton() { |
|
if (endBtn || !app) return; |
|
var header = app.querySelector('.mt-6.mb-6'); |
|
if (!header) return; |
|
var btn = document.createElement('button'); |
|
btn.type = 'button'; |
|
btn.id = 'project-search-end-chat'; |
|
btn.className = 'rounded-lg border border-slate-600 hover:border-rose-500 hover:text-rose-300 px-4 py-2 text-sm text-slate-300'; |
|
btn.textContent = 'End Chat'; |
|
header.appendChild(btn); |
|
btn.addEventListener('click', endChat); |
|
} |
|
|
|
function getSources() { |
|
if (!form) return ['project']; |
|
var checked = form.querySelectorAll('input[name="sources[]"]:checked'); |
|
return Array.prototype.map.call(checked, function (el) { return el.value; }); |
|
} |
|
|
|
function parseResponse(res) { |
|
var contentType = res.headers.get('content-type') || ''; |
|
|
|
return res.text().then(function (text) { |
|
if (contentType.indexOf('application/json') !== -1) { |
|
try { |
|
return { ok: res.ok, data: JSON.parse(text) }; |
|
} catch (e) { |
|
throw new Error('Respons server tidak valid.'); |
|
} |
|
} |
|
|
|
if (res.status === 504 || res.status === 502) { |
|
throw new Error('Waktu permintaan habis. Ollama membutuhkan waktu lebih lama — coba lagi dalam beberapa detik.'); |
|
} |
|
|
|
if (res.status === 419) { |
|
throw new Error('Sesi habis. Muat ulang halaman dan login kembali.'); |
|
} |
|
|
|
if (res.status === 401 || res.status === 302) { |
|
throw new Error('Sesi login tidak valid. Muat ulang halaman.'); |
|
} |
|
|
|
throw new Error('Server error (HTTP ' + res.status + ').'); |
|
}); |
|
} |
|
|
|
function sendMessage(query) { |
|
if (pending || !askUrl) return; |
|
|
|
hideError(); |
|
setPending(true); |
|
appendMessage({ role: 'user', content: query }); |
|
|
|
fetch(askUrl, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Accept': 'application/json', |
|
'X-CSRF-TOKEN': csrfToken(), |
|
'X-Requested-With': 'XMLHttpRequest', |
|
}, |
|
body: JSON.stringify({ |
|
query: query, |
|
sources: getSources(), |
|
limit: 10, |
|
}), |
|
}) |
|
.then(function (res) { |
|
return parseResponse(res); |
|
}) |
|
.then(function (result) { |
|
if (!result.ok) { |
|
throw new Error(result.data.message || 'Permintaan gagal.'); |
|
} |
|
return result.data; |
|
}) |
|
.then(function (data) { |
|
if (data.assistant_message) { |
|
appendMessage(data.assistant_message); |
|
} |
|
ensureEndChatButton(); |
|
}) |
|
.catch(function (err) { |
|
showError(err.message || 'Gagal memproses pencarian.'); |
|
}) |
|
.finally(function () { |
|
setPending(false); |
|
if (input) { |
|
input.focus(); |
|
} |
|
}); |
|
} |
|
|
|
function endChat() { |
|
if (!endUrl || !window.confirm('Akhiri chat ini dan mulai sesi baru?')) { |
|
return; |
|
} |
|
|
|
hideError(); |
|
setPending(true); |
|
|
|
fetch(endUrl, { |
|
method: 'POST', |
|
headers: { |
|
'Accept': 'application/json', |
|
'X-CSRF-TOKEN': csrfToken(), |
|
'X-Requested-With': 'XMLHttpRequest', |
|
}, |
|
}) |
|
.then(function (res) { |
|
return parseResponse(res); |
|
}) |
|
.then(function (result) { |
|
if (!result.ok) { |
|
throw new Error(result.data.message || 'Gagal mengakhiri chat.'); |
|
} |
|
return result.data; |
|
}) |
|
.then(function () { |
|
if (messagesEl) { |
|
messagesEl.innerHTML = '<div class="ps-chat__empty text-center text-slate-500 text-sm py-12" data-chat-empty>' |
|
+ '<p class="mb-1">Chat diakhiri.</p>' |
|
+ '<p>Mulai percakapan baru dengan pertanyaan di bawah.</p></div>'; |
|
} |
|
var btn = app.querySelector('#project-search-end-chat'); |
|
if (btn) { |
|
btn.remove(); |
|
} |
|
}) |
|
.catch(function (err) { |
|
showError(err.message || 'Gagal mengakhiri chat.'); |
|
}) |
|
.finally(function () { |
|
setPending(false); |
|
}); |
|
} |
|
|
|
if (form) { |
|
form.addEventListener('submit', function (event) { |
|
event.preventDefault(); |
|
if (!input) return; |
|
var query = input.value.trim(); |
|
if (query.length < 3) return; |
|
input.value = ''; |
|
sendMessage(query); |
|
}); |
|
} |
|
|
|
if (input) { |
|
input.addEventListener('keydown', function (event) { |
|
if (event.key === 'Enter' && !event.shiftKey) { |
|
event.preventDefault(); |
|
form.dispatchEvent(new Event('submit', { cancelable: true })); |
|
} |
|
}); |
|
} |
|
|
|
if (endBtn) { |
|
endBtn.addEventListener('click', endChat); |
|
} |
|
} |
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
init(); |
|
}); |
|
|
|
document.addEventListener('turbo:frame-load', function (event) { |
|
if (event.target && event.target.id === 'app-content') { |
|
init(event.target); |
|
} |
|
}); |
|
})();
|
|
|