feat(llm): переход на реляционные узлы промпта, db_cli context и очистка от регулярок
This commit is contained in:
@@ -2,18 +2,10 @@
|
||||
===============================================================================
|
||||
FILE: modules/web_api/static/js/chat.js
|
||||
ROLE: Клиентский интерфейс диалога, Drag-and-Drop вложений, генерация динамических
|
||||
кнопок подтверждений и рендеринг Generative UI интерактивного виджета задач.
|
||||
|
||||
AI-CONTEXT-ANCHORS:
|
||||
- ANCHOR[FILE_UPLOAD_UTILS]: Выбор, превью и очистка прикрепленных файлов.
|
||||
- ANCHOR[INTERACTIVE_BUTTONS_CORE]: Деактивация и быстрая отправка нажатых кнопок.
|
||||
- ANCHOR[INTERACTIVE_TASK_WIDGET_JS]: Рендерер и REST-обработчики виджета задач.
|
||||
- ANCHOR[CHAT_SEND_PIPELINE]: Главный метод sendMessage и вставка сообщений в DOM.
|
||||
- ANCHOR[DRAG_DROP_HANDLERS]: Обработка перетаскивания файлов в окно чата.
|
||||
кнопок подтверждений и блокировка ввода при активном действии.
|
||||
===============================================================================
|
||||
*/
|
||||
|
||||
// --- [SECTION 1: FILE UPLOAD HELPERS] --- # ANCHOR[FILE_UPLOAD_UTILS]
|
||||
function updateInputHeight(el) {
|
||||
if (!el) return;
|
||||
el.style.height = "24px";
|
||||
@@ -51,7 +43,29 @@ function clearAttachedFile() {
|
||||
if (previewContainer) previewContainer.classList.add("hidden");
|
||||
}
|
||||
|
||||
// --- [SECTION 2: ACTION BUTTONS CORE] --- # ANCHOR[INTERACTIVE_BUTTONS_CORE]
|
||||
function setInputLocked(isLocked) {
|
||||
const input = document.getElementById("user-input");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
if (input) {
|
||||
input.disabled = isLocked;
|
||||
if (isLocked) {
|
||||
input.placeholder = "Выберите действие с помощью кнопок выше...";
|
||||
input.classList.add("cursor-not-allowed", "opacity-60");
|
||||
} else {
|
||||
input.placeholder = "Команда, вопрос или перетащите файл сюда...";
|
||||
input.classList.remove("cursor-not-allowed", "opacity-60");
|
||||
}
|
||||
}
|
||||
if (sendBtn) {
|
||||
sendBtn.disabled = isLocked;
|
||||
if (isLocked) {
|
||||
sendBtn.classList.add("opacity-40", "cursor-not-allowed");
|
||||
} else {
|
||||
sendBtn.classList.remove("opacity-40", "cursor-not-allowed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function disableAllActionButtons() {
|
||||
const allBtnContainers = document.querySelectorAll(".action-buttons-container");
|
||||
allBtnContainers.forEach(container => {
|
||||
@@ -64,6 +78,7 @@ function disableAllActionButtons() {
|
||||
|
||||
function handleActionButtonClick(text) {
|
||||
disableAllActionButtons();
|
||||
setInputLocked(false);
|
||||
const input = document.getElementById("user-input");
|
||||
if (input) {
|
||||
input.value = text;
|
||||
@@ -71,7 +86,7 @@ function handleActionButtonClick(text) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- [SECTION 3: INTERACTIVE TASK WIDGET] --- # ANCHOR[INTERACTIVE_TASK_WIDGET_JS]
|
||||
// --- [INTERACTIVE TASK WIDGET] ---
|
||||
let activeWidgetTasksMap = new Map();
|
||||
|
||||
function renderTaskItemsHtml(tasks, filterStatus) {
|
||||
@@ -134,7 +149,6 @@ function renderInteractiveTaskCard(tasks) {
|
||||
|
||||
return `
|
||||
<div class="task-widget-card mt-3 bg-white border border-slate-200 rounded-2xl p-3.5 shadow-sm space-y-3" id="${widgetId}">
|
||||
<!-- Вкладки фильтров -->
|
||||
<div class="flex items-center gap-1 pb-2 border-b border-slate-100 overflow-x-auto no-scrollbar text-xs font-semibold">
|
||||
<button type="button" onclick="filterTaskWidget('${widgetId}', 'ALL', this)" class="widget-tab-btn px-2.5 py-1 rounded-lg bg-indigo-50 text-indigo-600 font-bold border border-indigo-200">Все</button>
|
||||
<button type="button" onclick="filterTaskWidget('${widgetId}', 'IN_PROGRESS', this)" class="widget-tab-btn px-2.5 py-1 rounded-lg text-slate-500 hover:text-slate-700">В работе</button>
|
||||
@@ -142,12 +156,10 @@ function renderInteractiveTaskCard(tasks) {
|
||||
<button type="button" onclick="filterTaskWidget('${widgetId}', 'COMPLETED', this)" class="widget-tab-btn px-2.5 py-1 rounded-lg text-slate-500 hover:text-slate-700">Завершенные</button>
|
||||
</div>
|
||||
|
||||
<!-- Список элементов задач -->
|
||||
<div class="widget-items-container space-y-2 max-h-72 overflow-y-auto pr-1">
|
||||
${itemsHtml}
|
||||
</div>
|
||||
|
||||
<!-- Быстрое создание новой задачи -->
|
||||
<div class="pt-2 border-t border-slate-100 flex items-center gap-2">
|
||||
<input type="text" placeholder="Новая задача..." class="flex-1 bg-slate-50 border border-slate-200 rounded-xl px-3 py-1.5 text-xs text-slate-800 focus:outline-none focus:border-indigo-600 focus:bg-white transition" onkeydown="if(event.key==='Enter') addTaskInline('${widgetId}', this)">
|
||||
<button type="button" onclick="addTaskInline('${widgetId}', this.previousElementSibling)" class="bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white px-3 py-1.5 rounded-xl text-xs font-semibold shadow-sm transition flex items-center gap-1">
|
||||
@@ -192,7 +204,6 @@ async function toggleTaskStatusInline(taskId, isChecked) {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// Обновляем статус задачи во всех локальных виджетах
|
||||
for (let [wId, state] of activeWidgetTasksMap.entries()) {
|
||||
const targetTask = state.tasks.find(t => t.task_id === taskId);
|
||||
if (targetTask) {
|
||||
@@ -256,7 +267,6 @@ async function addTaskInline(widgetId, inputEl) {
|
||||
|
||||
if (res.ok) {
|
||||
inputEl.value = "";
|
||||
// Мгновенная выгрузка свежего списка без обращения к LLM
|
||||
const tasksRes = await fetch("/api/v1/tasks", {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
@@ -278,7 +288,7 @@ async function addTaskInline(widgetId, inputEl) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- [SECTION 4: CHAT SEND PIPELINE] --- # ANCHOR[CHAT_SEND_PIPELINE]
|
||||
// --- [CHAT SEND PIPELINE] ---
|
||||
async function sendMessage(e) {
|
||||
if (e && e.preventDefault) e.preventDefault();
|
||||
|
||||
@@ -386,6 +396,15 @@ async function sendMessage(e) {
|
||||
${buttonsMarkup}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Если выведены кнопки подтверждения превью — блокируем ввод с клавиатуры
|
||||
if (actionData.type === "PROMPT_PREVIEW") {
|
||||
setInputLocked(true);
|
||||
} else {
|
||||
setInputLocked(false);
|
||||
}
|
||||
} else {
|
||||
setInputLocked(false);
|
||||
}
|
||||
|
||||
let interactiveWidgetHtml = "";
|
||||
@@ -421,8 +440,9 @@ async function sendMessage(e) {
|
||||
`;
|
||||
chatWindow.insertAdjacentHTML("beforeend", errorHtml);
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
setInputLocked(false);
|
||||
} finally {
|
||||
if (sendBtn) {
|
||||
if (sendBtn && !document.getElementById("user-input")?.disabled) {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.classList.remove("opacity-50");
|
||||
}
|
||||
@@ -439,7 +459,6 @@ function escapeHtml(text) {
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// --- [SECTION 5: DRAG & DROP AND KEYBOARD LISTENERS] --- # ANCHOR[DRAG_DROP_HANDLERS]
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const input = document.getElementById("user-input");
|
||||
const dropZone = document.getElementById("chat-window")?.parentElement;
|
||||
@@ -452,6 +471,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (input.disabled) return;
|
||||
const text = input.value.trim();
|
||||
if (text) {
|
||||
if (localHistory.length === 0 || localHistory[0] !== text) {
|
||||
|
||||
Reference in New Issue
Block a user