feat(llm): двухфазный жизненный цикл сессий, двусторонний Diff, Topic Drift Guard и актуализация документации
This commit is contained in:
@@ -4,7 +4,7 @@ FILE: modules/web_api/static/js/chat/core.js
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: web_api / static / js / chat
|
||||
ROLE: Клиентский интерфейс диалога, Drag-and-Drop вложений, блокировка ввода
|
||||
при активных кнопках, вызов онлайн-редактора и отправка правок.
|
||||
при активных кнопках, динамический вызов инлайн-редактора и двусторонний Diff.
|
||||
===============================================================================
|
||||
*/
|
||||
|
||||
@@ -79,11 +79,6 @@ function disableAllActionButtons() {
|
||||
}
|
||||
|
||||
function handleActionButtonClick(text) {
|
||||
if (text === "action:open_editor") {
|
||||
openInlinePromptEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
disableAllActionButtons();
|
||||
setInputLocked(false);
|
||||
const input = document.getElementById("user-input");
|
||||
@@ -93,22 +88,123 @@ function handleActionButtonClick(text) {
|
||||
}
|
||||
}
|
||||
|
||||
function openInlinePromptEditor() {
|
||||
const editorContainer = document.getElementById("inline-prompt-editor-container");
|
||||
if (editorContainer) {
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// --- [INLINE PROMPT EDITOR & FULL DIFF BUILDER] ---
|
||||
function openInlinePromptEditor(btnEl) {
|
||||
const cardContainer = btnEl.closest(".chat-message-card");
|
||||
if (!cardContainer) return;
|
||||
|
||||
const editorContainer = cardContainer.querySelector(".inline-prompt-editor-container");
|
||||
const diffView = cardContainer.querySelector(".prompt-preview-diff-view");
|
||||
const textarea = cardContainer.querySelector(".inline-prompt-textarea");
|
||||
|
||||
if (editorContainer && textarea) {
|
||||
editorContainer.classList.remove("hidden");
|
||||
const textarea = document.getElementById("inline-prompt-textarea");
|
||||
if (textarea) textarea.focus();
|
||||
|
||||
if (diffView) {
|
||||
const targetHeight = Math.max(diffView.offsetHeight, 320);
|
||||
textarea.style.height = `${targetHeight}px`;
|
||||
}
|
||||
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function closeInlinePromptEditor() {
|
||||
const editorContainer = document.getElementById("inline-prompt-editor-container");
|
||||
function closeInlinePromptEditor(btnEl) {
|
||||
const cardContainer = btnEl.closest(".chat-message-card");
|
||||
if (!cardContainer) return;
|
||||
const editorContainer = cardContainer.querySelector(".inline-prompt-editor-container");
|
||||
if (editorContainer) editorContainer.classList.add("hidden");
|
||||
}
|
||||
|
||||
async function saveManualPromptDraft() {
|
||||
const textarea = document.getElementById("inline-prompt-textarea");
|
||||
function parsePromptIntoMap(text) {
|
||||
const map = new Map();
|
||||
if (!text) return map;
|
||||
const lines = text.split("\n");
|
||||
for (let line of lines) {
|
||||
let trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
const match = trimmed.match(/^(\d+)[\.\s]+(\d+)[\.\s\:\-]*(.*)$/);
|
||||
if (match) {
|
||||
const key = `${match[1]}.${match[2]}`;
|
||||
map.set(key, { sec: parseInt(match[1]), itm: parseInt(match[2]), content: match[3].trim(), full: trimmed });
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildHighlightedPromptHtml(newRawText, originalBaselineText) {
|
||||
const origMap = parsePromptIntoMap(originalBaselineText);
|
||||
const newMap = parsePromptIntoMap(newRawText);
|
||||
|
||||
const formattedLines = [];
|
||||
const rawLines = newRawText.split("\n");
|
||||
const handledNewKeys = new Set();
|
||||
|
||||
let currentSection = null;
|
||||
|
||||
for (let line of rawLines) {
|
||||
let trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
formattedLines.push("");
|
||||
continue;
|
||||
}
|
||||
|
||||
const secMatch = trimmed.match(/^(\d+)[\.\s\:\-]+([^\d].*)$/);
|
||||
const subMatch = trimmed.match(/^(\d+)[\.\s]+(\d+)[\.\s\:\-]*(.*)$/);
|
||||
|
||||
if (subMatch) {
|
||||
const key = `${subMatch[1]}.${subMatch[2]}`;
|
||||
handledNewKeys.add(key);
|
||||
currentSection = parseInt(subMatch[1]);
|
||||
const cleanContent = subMatch[3].trim();
|
||||
const fullItemStr = `${subMatch[1]}.${subMatch[2]}. ${cleanContent}`;
|
||||
|
||||
if (!origMap.has(key) || origMap.get(key).content !== cleanContent) {
|
||||
// Добавленный или изменённый пункт
|
||||
formattedLines.push(` <span class="text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300">${escapeHtml(fullItemStr)}</span>`);
|
||||
} else {
|
||||
formattedLines.push(` ${escapeHtml(fullItemStr)}`);
|
||||
}
|
||||
} else if (secMatch && !anyLower(secMatch[2].slice(0, 15))) {
|
||||
// Заголовок раздела
|
||||
currentSection = parseInt(secMatch[1]);
|
||||
formattedLines.push(escapeHtml(trimmed));
|
||||
} else {
|
||||
formattedLines.push(escapeHtml(trimmed));
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем удаленные пункты (были в оригинале, но отсутствуют в новом тексте)
|
||||
for (let [origKey, origObj] of origMap.entries()) {
|
||||
if (!handledNewKeys.has(origKey)) {
|
||||
const strikeMarkup = ` <span class="line-through text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300 opacity-80">${origKey}. ${escapeHtml(origObj.content)} [УДАЛЕНИЕ]</span>`;
|
||||
formattedLines.push(strikeMarkup);
|
||||
}
|
||||
}
|
||||
|
||||
return formattedLines.join("\n");
|
||||
}
|
||||
|
||||
function anyLower(str) {
|
||||
return /[а-яa-z]/.test(str);
|
||||
}
|
||||
|
||||
async function saveManualPromptDraft(btnEl) {
|
||||
const cardContainer = btnEl.closest(".chat-message-card");
|
||||
if (!cardContainer) return;
|
||||
|
||||
const textarea = cardContainer.querySelector(".inline-prompt-textarea");
|
||||
const previewTextEl = cardContainer.querySelector(".prompt-preview-diff-view");
|
||||
if (!textarea) return;
|
||||
|
||||
const newText = textarea.value.trim();
|
||||
@@ -130,28 +226,18 @@ async function saveManualPromptDraft() {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
closeInlinePromptEditor();
|
||||
const previewTextEl = document.getElementById("prompt-preview-diff-view");
|
||||
closeInlinePromptEditor(btnEl);
|
||||
if (previewTextEl) {
|
||||
previewTextEl.innerText = newText;
|
||||
const baseline = cardContainer.dataset.originalBaseline || "";
|
||||
const highlightedHtml = buildHighlightedPromptHtml(newText, baseline);
|
||||
previewTextEl.innerHTML = highlightedHtml;
|
||||
}
|
||||
alert("✓ Правки сохранены в черновике! Нажмите «Подтвердить» для применения в БД.");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Ошибка сохранения черновика: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// --- [CHAT SEND PIPELINE] ---
|
||||
async function sendMessage(e) {
|
||||
if (e && e.preventDefault) e.preventDefault();
|
||||
@@ -250,8 +336,10 @@ async function sendMessage(e) {
|
||||
iconMarkup = '<i class="fa-solid fa-pen-to-square text-xs"></i>';
|
||||
}
|
||||
|
||||
const clickHandler = isEdit ? "openInlinePromptEditor(this)" : `handleActionButtonClick('${escapeHtml(btn.value)}')`;
|
||||
|
||||
return `
|
||||
<button type="button" onclick="handleActionButtonClick('${escapeHtml(btn.value)}')"
|
||||
<button type="button" onclick="${clickHandler}"
|
||||
class="${btnClasses} font-semibold px-3 py-1.5 rounded-xl text-xs flex items-center gap-1.5 transition cursor-pointer">
|
||||
${iconMarkup}
|
||||
<span>${escapeHtml(btn.label)}</span>
|
||||
@@ -265,21 +353,25 @@ async function sendMessage(e) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Блокируем ввод при наличии кнопок
|
||||
setInputLocked(true);
|
||||
} else {
|
||||
setInputLocked(false);
|
||||
}
|
||||
|
||||
let previewEditorHtml = "";
|
||||
if (actionData && actionData.type === "PROMPT_PREVIEW" && actionData.raw_draft) {
|
||||
let rawDraftText = "";
|
||||
let baselineText = "";
|
||||
|
||||
if (actionData && actionData.type === "PROMPT_PREVIEW") {
|
||||
rawDraftText = actionData.raw_draft || "";
|
||||
baselineText = actionData.baseline_prompt || rawDraftText;
|
||||
previewEditorHtml = `
|
||||
<div id="inline-prompt-editor-container" class="hidden mt-3 p-3 bg-slate-50 border border-slate-300 rounded-xl space-y-2">
|
||||
<div class="inline-prompt-editor-container hidden mt-3 p-3 bg-slate-50 border border-slate-300 rounded-xl space-y-2">
|
||||
<p class="text-[11px] font-bold text-slate-700 uppercase"><i class="fa-solid fa-pen-to-square mr-1"></i> Ручное редактирование текста промпта:</p>
|
||||
<textarea id="inline-prompt-textarea" rows="10" class="w-full bg-white border border-slate-300 rounded-lg p-2.5 text-xs font-mono text-slate-800 focus:outline-none focus:border-indigo-600">${escapeHtml(actionData.raw_draft)}</textarea>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" onclick="closeInlinePromptEditor()" class="px-3 py-1.5 rounded-lg text-xs text-slate-600 bg-slate-200 hover:bg-slate-300">Свернуть</button>
|
||||
<button type="button" onclick="saveManualPromptDraft()" class="px-3 py-1.5 rounded-lg text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-700 shadow-sm">Сохранить правки</button>
|
||||
<textarea class="inline-prompt-textarea w-full bg-white border border-slate-300 rounded-lg p-3 text-xs font-mono text-slate-800 focus:outline-none focus:border-indigo-600 resize-y shadow-inner leading-relaxed">${escapeHtml(rawDraftText)}</textarea>
|
||||
<div class="flex justify-end gap-2 pt-1">
|
||||
<button type="button" onclick="closeInlinePromptEditor(this)" class="px-3 py-1.5 rounded-lg text-xs text-slate-600 bg-slate-200 hover:bg-slate-300 transition">Свернуть</button>
|
||||
<button type="button" onclick="saveManualPromptDraft(this)" class="px-3.5 py-1.5 rounded-lg text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-700 shadow-sm transition">Сохранить правки</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -291,11 +383,11 @@ async function sendMessage(e) {
|
||||
}
|
||||
|
||||
const botMsgHtml = `
|
||||
<div class="bg-white border border-slate-200 rounded-2xl p-3.5 shadow-sm max-w-2xl mb-3">
|
||||
<div class="chat-message-card bg-white border border-slate-200 rounded-2xl p-3.5 shadow-sm max-w-2xl mb-3" data-original-baseline="${escapeHtml(baselineText)}">
|
||||
<p class="text-[11px] font-bold text-indigo-600 uppercase tracking-wider mb-1">
|
||||
<i class="fa-solid fa-robot mr-1"></i> ${assistantTitle}
|
||||
</p>
|
||||
<div class="text-slate-800 text-xs sm:text-sm whitespace-pre-wrap leading-relaxed" id="prompt-preview-diff-view">${replyText}</div>
|
||||
<div class="prompt-preview-diff-view text-slate-800 text-xs sm:text-sm whitespace-pre-wrap leading-relaxed">${replyText}</div>
|
||||
${previewEditorHtml}
|
||||
${interactiveWidgetHtml}
|
||||
${actionButtonsHtml}
|
||||
|
||||
Reference in New Issue
Block a user