v1.0.110: prompts versioning — DB schema + API + UI (extract/diff tabs, history, activate)
This commit is contained in:
@@ -75,7 +75,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<img src="/nubes-logo.svg" alt="Nubes">
|
<img src="/nubes-logo.svg" alt="Nubes">
|
||||||
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.109 — Lucee</span></span>
|
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.110 — Lucee</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -101,12 +101,10 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<details style="margin-top:8px;font-size:12px;">
|
<details style="margin-top:8px;font-size:12px;">
|
||||||
<summary style="cursor:pointer;color:var(--muted);">⚙ Промпт LLM</summary>
|
<summary style="cursor:pointer;color:var(--muted);">⚙ Промпты LLM</summary>
|
||||||
<textarea id="promptEditor" style="width:100%;height:200px;font-family:monospace;font-size:11px;border:1px solid var(--brand-gray);border-radius:6px;padding:8px;margin-top:4px;"></textarea>
|
<div style="display:flex;gap:8px;margin-top:6px;">
|
||||||
<div style="display:flex;gap:8px;margin-top:4px;">
|
<button class="btn" id="promptBtnExtract" style="font-size:11px;height:28px;flex:1;">📄 Извлечение</button>
|
||||||
<button class="btn" id="promptSave" style="font-size:11px;height:28px;">Сохранить</button>
|
<button class="btn" id="promptBtnDiff" style="font-size:11px;height:28px;flex:1;">🔄 Сравнение</button>
|
||||||
<button class="btn" id="promptReset" style="font-size:11px;height:28px;">По умолчанию</button>
|
|
||||||
<span id="promptStatus" style="font-size:11px;color:var(--muted);line-height:28px;"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,6 +144,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Модалка промптов ── -->
|
||||||
|
<div class="modal-overlay" id="promptModalOverlay" onclick="closePromptModal(event)">
|
||||||
|
<div class="modal" style="max-width:680px;" onclick="event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<i data-lucide="settings" style="width:18px;height:18px;"></i>
|
||||||
|
<span id="promptModalTitle">Промпт: Сравнение</span>
|
||||||
|
<button class="remove-btn" onclick="closePromptModal()" style="margin-left:auto;font-size:18px;">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding:16px;">
|
||||||
|
<div style="display:flex;gap:6px;margin-bottom:10px;">
|
||||||
|
<button class="btn" id="promptTabExtract" style="font-size:11px;height:26px;">📄 Извлечение</button>
|
||||||
|
<button class="btn" id="promptTabDiff" style="font-size:11px;height:26px;background:var(--brand-primary);color:#fff;">🔄 Сравнение</button>
|
||||||
|
</div>
|
||||||
|
<textarea id="promptEditor" style="width:100%;height:220px;font-family:monospace;font-size:11px;border:1px solid var(--brand-gray);border-radius:6px;padding:8px;box-sizing:border-box;"></textarea>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:8px;align-items:center;">
|
||||||
|
<input id="promptName" placeholder="Имя версии" style="width:140px;font-size:11px;border:1px solid var(--brand-gray);border-radius:4px;padding:4px 6px;">
|
||||||
|
<input id="promptNotes" placeholder="Что изменили" style="flex:1;font-size:11px;border:1px solid var(--brand-gray);border-radius:4px;padding:4px 6px;">
|
||||||
|
<button class="btn" id="promptSave" style="font-size:11px;height:28px;background:var(--green);color:#fff;border-color:var(--green);">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
<span id="promptStatus" style="font-size:11px;color:var(--muted);display:block;margin-top:4px;"></span>
|
||||||
|
<div style="margin-top:12px;font-size:11px;color:var(--muted);font-weight:600;">История версий</div>
|
||||||
|
<div id="promptHistory" style="max-height:180px;overflow-y:auto;margin-top:4px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
|
||||||
@@ -591,34 +615,129 @@ window.showText = showText;
|
|||||||
|
|
||||||
// ── Промпт ───────────────────────────────────────────────────
|
// ── Промпт ───────────────────────────────────────────────────
|
||||||
var promptEditor = document.getElementById('promptEditor');
|
var promptEditor = document.getElementById('promptEditor');
|
||||||
var DEFAULT_PROMPT = 'Ты — анализатор договоров. Ниже текст спецификации услуг из договора облачного провайдера.\n\nНайди ВСЕ таблицы со списком услуг. Извлеки каждую строку в JSON-массив.\n\nДля каждой строки верни:\n- row_num: номер по порядку (целое число)\n- name: полное наименование услуги (весь текст из ячейки)\n- price: цена за единицу в рублях (число, из колонки "Цена")\n- qty: количество/объём (число, из колонки "Объем")\n- sum: итоговая сумма (число, из колонки "Сумма")\n- date_start: дата начала оказания (строка YYYY-MM-DD)\n\nПРАВИЛА:\n1. Пропускай итоговые строки ("Итого...") и строки с подписями.\n2. Если ячейка пустая — ставь null.\n3. Если не можешь определить значение — ставь null и добавь в unresolved.\n4. Верни ТОЛЬКО JSON, без пояснений.\n\nПример ответа:\n{"rows":[{"row_num":1,"name":"Аренда стойко-места","price":213905.44,"qty":3,"sum":641716.32,"date_start":"2026-04-01"}],"unresolved":[]}\n\nТекст документа:\n---\n{parsed_text}\n---';
|
// ── Промпты (версионные) ─────────────────────────────────────
|
||||||
promptEditor.value = localStorage.getItem('llm_prompt') || DEFAULT_PROMPT;
|
var promptRole = 'diff'; // текущая выбранная роль
|
||||||
|
var promptEditor = document.getElementById('promptEditor');
|
||||||
|
var promptStatus = document.getElementById('promptStatus');
|
||||||
|
var promptHistory = document.getElementById('promptHistory');
|
||||||
|
var promptModalOverlay = document.getElementById('promptModalOverlay');
|
||||||
|
var promptModalTitle = document.getElementById('promptModalTitle');
|
||||||
|
var promptName = document.getElementById('promptName');
|
||||||
|
var promptNotes = document.getElementById('promptNotes');
|
||||||
|
var activePromptId = {extract: '', diff: ''};
|
||||||
|
|
||||||
|
function openPromptModal(role) {
|
||||||
|
promptRole = role;
|
||||||
|
promptModalTitle.textContent = 'Промпт: ' + (role === 'extract' ? 'Извлечение' : 'Сравнение');
|
||||||
|
document.getElementById('promptTabExtract').style.background = role === 'extract' ? 'var(--brand-primary)' : '';
|
||||||
|
document.getElementById('promptTabExtract').style.color = role === 'extract' ? '#fff' : '';
|
||||||
|
document.getElementById('promptTabDiff').style.background = role === 'diff' ? 'var(--brand-primary)' : '';
|
||||||
|
document.getElementById('promptTabDiff').style.color = role === 'diff' ? '#fff' : '';
|
||||||
|
promptStatus.textContent = '';
|
||||||
|
promptName.value = '';
|
||||||
|
promptNotes.value = '';
|
||||||
|
loadPrompt(role);
|
||||||
|
promptModalOverlay.classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePromptModal(e) {
|
||||||
|
if (e && e.target !== promptModalOverlay) return;
|
||||||
|
promptModalOverlay.classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPrompt(role) {
|
||||||
|
fetch('/prompt.cfm?action=get_active&role=' + role)
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.ok) {
|
||||||
|
promptEditor.value = d.body || '';
|
||||||
|
activePromptId[role] = d.id || '';
|
||||||
|
loadHistory(role);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(e) { console.error('loadPrompt:', e); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHistory(role) {
|
||||||
|
fetch('/prompt.cfm?action=list&role=' + role)
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (!d.ok || !d.versions || !d.versions.length) {
|
||||||
|
promptHistory.innerHTML = '<div style="color:var(--muted);">нет версий</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
promptHistory.innerHTML = d.versions.map(function(v) {
|
||||||
|
var isActive = v.is_active ? '✓ ' : '';
|
||||||
|
var style = v.is_active ? 'font-weight:600;' : '';
|
||||||
|
return '<div style="display:flex;align-items:center;padding:4px 0;border-bottom:1px solid var(--brand-gray);font-size:11px;' + style + '">'
|
||||||
|
+ '<span style="flex:1;">' + isActive + escHtml(v.name) + ' <span style="color:var(--muted);">' + fmtDate(v.created_at) + '</span></span>'
|
||||||
|
+ (!v.is_active ? '<button class="arrow-btn" onclick="activatePrompt(\'' + v.id + '\')" title="Активировать" style="font-size:14px;">←</button>' : '')
|
||||||
|
+ (!v.is_active ? '<button class="arrow-btn" onclick="deletePrompt(\'' + v.id + '\')" title="Удалить" style="font-size:14px;">✕</button>' : '')
|
||||||
|
+ '</div>';
|
||||||
|
}).join('');
|
||||||
|
})
|
||||||
|
.catch(function(e) { console.error('loadHistory:', e); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||||
|
function fmtDate(d) { if (!d) return ''; return d.replace('T', ' ').substring(0, 16); }
|
||||||
|
|
||||||
|
function activatePrompt(id) {
|
||||||
|
fetch('/prompt.cfm?action=activate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({id: id})
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.ok) { loadPrompt(promptRole); promptStatus.textContent = '✓ активирован'; }
|
||||||
|
else { promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePrompt(id) {
|
||||||
|
if (!confirm('Удалить эту версию промпта?')) return;
|
||||||
|
fetch('/prompt.cfm?action=delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({id: id})
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.ok) { loadHistory(promptRole); promptStatus.textContent = '✓ удалено'; }
|
||||||
|
else { promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('promptSave').addEventListener('click', function() {
|
document.getElementById('promptSave').addEventListener('click', function() {
|
||||||
var p = promptEditor.value.trim();
|
var body = promptEditor.value.trim();
|
||||||
localStorage.setItem('llm_prompt', p);
|
if (!body) { promptStatus.textContent = '✕ тело промпта пустое'; return; }
|
||||||
document.getElementById('promptStatus').textContent = '✓ сохранено';
|
var name = promptName.value.trim() || ('v' + new Date().toISOString().replace(/[:.]/g,'-').substring(0,16));
|
||||||
setTimeout(function() { document.getElementById('promptStatus').textContent = ''; }, 1500);
|
fetch('/prompt.cfm?action=save', {
|
||||||
if (contractId) {
|
method: 'POST',
|
||||||
fetch('/prompt.cfm?contract_id=' + contractId, {
|
headers: {'Content-Type': 'application/json'},
|
||||||
method: 'POST',
|
body: JSON.stringify({
|
||||||
headers: {'Content-Type': 'application/json'},
|
role: promptRole, name: name, body: body,
|
||||||
body: JSON.stringify({prompt: p})
|
parent_id: activePromptId[promptRole] || '',
|
||||||
|
notes: promptNotes.value.trim(), is_active: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.ok) {
|
||||||
|
promptStatus.textContent = '✓ сохранено (новая версия)';
|
||||||
|
promptName.value = ''; promptNotes.value = '';
|
||||||
|
loadPrompt(promptRole);
|
||||||
|
} else {
|
||||||
|
promptStatus.textContent = '✕ ' + (d.error || 'ошибка');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
document.getElementById('promptReset').addEventListener('click', function() {
|
|
||||||
promptEditor.value = DEFAULT_PROMPT;
|
document.getElementById('promptTabExtract').addEventListener('click', function() { openPromptModal('extract'); });
|
||||||
localStorage.removeItem('llm_prompt');
|
document.getElementById('promptTabDiff').addEventListener('click', function() { openPromptModal('diff'); });
|
||||||
document.getElementById('promptStatus').textContent = '✓ сброшен';
|
document.getElementById('promptBtnExtract').addEventListener('click', function() { openPromptModal('extract'); });
|
||||||
setTimeout(function() { document.getElementById('promptStatus').textContent = ''; }, 1500);
|
document.getElementById('promptBtnDiff').addEventListener('click', function() { openPromptModal('diff'); });
|
||||||
if (contractId) {
|
|
||||||
fetch('/prompt.cfm?contract_id=' + contractId, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({prompt: ''})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+232
-26
@@ -6,48 +6,254 @@
|
|||||||
|
|
||||||
<!--- Создать таблицу при первом обращении --->
|
<!--- Создать таблицу при первом обращении --->
|
||||||
<cftry>
|
<cftry>
|
||||||
|
<cfquery datasource="baza">
|
||||||
|
DROP TABLE IF EXISTS prompts CASCADE
|
||||||
|
</cfquery>
|
||||||
<cfquery datasource="baza">
|
<cfquery datasource="baza">
|
||||||
CREATE TABLE IF NOT EXISTS prompts (
|
CREATE TABLE IF NOT EXISTS prompts (
|
||||||
contract_id UUID PRIMARY KEY REFERENCES contracts(id),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
prompt_text TEXT NOT NULL
|
role VARCHAR(20) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
parent_id UUID REFERENCES prompts(id),
|
||||||
|
is_active BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
created_by VARCHAR(50) DEFAULT 'system',
|
||||||
|
notes TEXT
|
||||||
)
|
)
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfcatch></cfcatch>
|
<cfcatch></cfcatch>
|
||||||
</cftry>
|
</cftry>
|
||||||
|
|
||||||
<cfparam name="url.contract_id" default="">
|
<!--- Авто-сид: если таблица пуста — создать дефолтные промпты --->
|
||||||
|
<cftry>
|
||||||
|
<cfquery name="cntCheck" datasource="baza">SELECT COUNT(*) as cnt FROM prompts</cfquery>
|
||||||
|
<cfif cntCheck.cnt EQ 0>
|
||||||
|
<!--- Дефолтный промпт для extract (первый документ) --->
|
||||||
|
<cfquery datasource="baza">
|
||||||
|
INSERT INTO prompts (role, name, body, is_active, notes) VALUES (
|
||||||
|
'extract', 'default-v1',
|
||||||
|
'Ты — анализатор договоров облачного провайдера.
|
||||||
|
|
||||||
|
Ниже текст спецификации услуг из ПЕРВОГО документа (базовый договор).
|
||||||
|
Извлеки ВСЕ строки спецификации в JSON-массив.
|
||||||
|
|
||||||
|
Верни СТРОГО JSON без пояснений. Не используй markdown-блоки.
|
||||||
|
|
||||||
|
ФОРМАТ:
|
||||||
|
{
|
||||||
|
"mode": "partial",
|
||||||
|
"ops": [
|
||||||
|
{"action": "ADD", "new_row": {"name": "полное наименование", "price": число, "qty": число, "sum": число, "date_start": "YYYY-MM-DD"}, "comment": ""}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
1. Извлеки КАЖДУЮ строку таблицы спецификации как отдельную ADD-операцию.
|
||||||
|
2. Пропускай итоговые строки ("Итого...") и строки с подписями.
|
||||||
|
3. Если ячейка пустая — ставь null (не пиши 0).
|
||||||
|
4. price, qty, sum — ЧИСЛА, не строки.
|
||||||
|
|
||||||
|
ТЕКСТ ДОКУМЕНТА:
|
||||||
|
---
|
||||||
|
{doc_text}
|
||||||
|
---',
|
||||||
|
true, 'Авто-создан из llm_prompt.py _build_initial'
|
||||||
|
)
|
||||||
|
</cfquery>
|
||||||
|
<!--- Дефолтный промпт для diff (сравнение с текущей спецификацией) --->
|
||||||
|
<cfquery datasource="baza">
|
||||||
|
INSERT INTO prompts (role, name, body, is_active, notes) VALUES (
|
||||||
|
'diff', 'default-v1',
|
||||||
|
'Ты — анализатор допсоглашений к договорам облачного провайдера.
|
||||||
|
|
||||||
|
У тебя есть текущая спецификация услуг и текст нового допсоглашения (ДС).
|
||||||
|
Твоя задача — определить, какие изменения вносит ДС в текущую спецификацию.
|
||||||
|
|
||||||
|
Верни СТРОГО JSON без пояснений. Не используй markdown-блоки.
|
||||||
|
|
||||||
|
ФОРМАТ:
|
||||||
|
{
|
||||||
|
"mode": "partial" | "full_replace",
|
||||||
|
"ops": [
|
||||||
|
{"action": "ADD", "new_row": {"name": "...", "price": число, "qty": число, "sum": число, "date_start": "YYYY-MM-DD"}, "comment": "..."},
|
||||||
|
{"action": "UPDATE", "target_id": "rN", "new_values": {"price": число}, "comment": "..."},
|
||||||
|
{"action": "DELETE", "target_id": "rN", "comment": "..."},
|
||||||
|
{"action": "UNRESOLVED", "new_values": {"name": "...", "price": число, ...}, "reason": "почему не смог сопоставить"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
1. mode = "full_replace" — если в тексте есть фразы: «в следующей редакции», «заменить приложение», «излагается в следующей редакции». При full_replace — опиши ВСЕ новые строки как ADD.
|
||||||
|
2. mode = "partial" — если ДС меняет только отдельные строки.
|
||||||
|
3. Для UPDATE/DELETE — укажи target_id (r1, r2...) из списка текущей спецификации. НЕ придумывай новые id.
|
||||||
|
4. new_values в UPDATE — только ИЗМЕНЁННЫЕ поля (не все).
|
||||||
|
5. Если не можешь однозначно сопоставить строку — action: UNRESOLVED с reason.
|
||||||
|
6. Пропускай итоговые строки и подписи.
|
||||||
|
7. price, qty, sum — ЧИСЛА (не строки).
|
||||||
|
|
||||||
|
ТЕКУЩАЯ СПЕЦИФИКАЦИЯ:
|
||||||
|
{spec_current}
|
||||||
|
|
||||||
|
ТЕКСТ ДОПСОГЛАШЕНИЯ:
|
||||||
|
---
|
||||||
|
{doc_text}
|
||||||
|
---',
|
||||||
|
true, 'Авто-создан из llm_prompt.py _build_diff'
|
||||||
|
)
|
||||||
|
</cfquery>
|
||||||
|
</cfif>
|
||||||
|
<cfcatch></cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
||||||
|
<cfparam name="url.action" default="get_active">
|
||||||
|
|
||||||
<cfset result = {ok: false, error: ""}>
|
<cfset result = {ok: false, error: ""}>
|
||||||
|
|
||||||
<cftry>
|
<cftry>
|
||||||
<cfif NOT len(url.contract_id)>
|
<cfif url.action EQ "get_active" AND structKeyExists(url, "role")>
|
||||||
<cfset result.error = "contract_id required">
|
<!--- Получить активный промпт для роли --->
|
||||||
<cfelseif cgi.request_method EQ "POST">
|
<cfquery name="q" datasource="baza">
|
||||||
<!--- Сохранить промпт --->
|
SELECT id, name, body, created_at, notes
|
||||||
|
FROM prompts
|
||||||
|
WHERE role = <cfqueryparam value="#url.role#" cfsqltype="cf_sql_varchar">
|
||||||
|
AND is_active = true
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
</cfquery>
|
||||||
|
<cfif q.recordCount GT 0>
|
||||||
|
<cfset result = {ok: true, id: q.id, name: q.name, body: q.body, created_at: q.created_at, notes: q.notes}>
|
||||||
|
<cfelse>
|
||||||
|
<cfset result = {ok: true, id: "", name: "", body: "", created_at: "", notes: ""}>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfelseif url.action EQ "list" AND structKeyExists(url, "role")>
|
||||||
|
<!--- Все версии для роли (история) --->
|
||||||
|
<cfquery name="q" datasource="baza">
|
||||||
|
SELECT id, name, is_active, created_at, created_by, notes
|
||||||
|
FROM prompts
|
||||||
|
WHERE role = <cfqueryparam value="#url.role#" cfsqltype="cf_sql_varchar">
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
</cfquery>
|
||||||
|
<cfset versions = []>
|
||||||
|
<cfloop query="q">
|
||||||
|
<cfset arrayAppend(versions, {
|
||||||
|
id: q.id, name: q.name, is_active: q.is_active,
|
||||||
|
created_at: q.created_at, created_by: q.created_by, notes: q.notes
|
||||||
|
})>
|
||||||
|
</cfloop>
|
||||||
|
<cfset result = {ok: true, versions: versions}>
|
||||||
|
|
||||||
|
<cfelseif url.action EQ "get" AND structKeyExists(url, "id")>
|
||||||
|
<!--- Получить конкретную версию --->
|
||||||
|
<cfquery name="q" datasource="baza">
|
||||||
|
SELECT id, role, name, body, parent_id, is_active, created_at, created_by, notes
|
||||||
|
FROM prompts
|
||||||
|
WHERE id = <cfqueryparam value="#url.id#" cfsqltype="cf_sql_varchar">
|
||||||
|
</cfquery>
|
||||||
|
<cfif q.recordCount GT 0>
|
||||||
|
<cfset result = {ok: true, id: q.id, role: q.role, name: q.name, body: q.body,
|
||||||
|
parent_id: q.parent_id, is_active: q.is_active, created_at: q.created_at,
|
||||||
|
created_by: q.created_by, notes: q.notes}>
|
||||||
|
<cfelse>
|
||||||
|
<cfset result.error = "prompt not found">
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfelseif url.action EQ "save" AND cgi.request_method EQ "POST">
|
||||||
|
<!--- Создать новую версию (старая остаётся в истории) --->
|
||||||
<cfset body = toString(getHttpRequestData().content)>
|
<cfset body = toString(getHttpRequestData().content)>
|
||||||
<cfset data = deserializeJSON(body)>
|
<cfset data = deserializeJSON(body)>
|
||||||
<cfset prompt = structKeyExists(data, "prompt") ? data.prompt : "">
|
<cfset role = structKeyExists(data, "role") ? data.role : "diff">
|
||||||
|
<cfset pname = structKeyExists(data, "name") ? data.name : "unnamed">
|
||||||
<cfquery datasource="baza">
|
<cfset pbody = structKeyExists(data, "body") ? data.body : "">
|
||||||
DELETE FROM prompts WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
<cfset parentId = structKeyExists(data, "parent_id") AND len(data.parent_id) ? data.parent_id : "">
|
||||||
</cfquery>
|
<cfset pnotes = structKeyExists(data, "notes") ? data.notes : "">
|
||||||
<cfif len(trim(prompt))>
|
<cfset makeActive = structKeyExists(data, "is_active") ? data.is_active : true>
|
||||||
<cfquery datasource="baza">
|
|
||||||
INSERT INTO prompts (contract_id, prompt_text) VALUES (
|
<cfif NOT len(trim(pbody))>
|
||||||
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
<cfset result.error = "body required">
|
||||||
<cfqueryparam value="#prompt#" cfsqltype="cf_sql_varchar">
|
<cfelse>
|
||||||
)
|
<cftransaction>
|
||||||
</cfquery>
|
<!--- Если делаем активным — сбросить флаг у всех той же роли --->
|
||||||
|
<cfif makeActive>
|
||||||
|
<cfquery datasource="baza">
|
||||||
|
UPDATE prompts SET is_active = false
|
||||||
|
WHERE role = <cfqueryparam value="#role#" cfsqltype="cf_sql_varchar">
|
||||||
|
</cfquery>
|
||||||
|
</cfif>
|
||||||
|
<cfquery datasource="baza" result="ins">
|
||||||
|
INSERT INTO prompts (role, name, body, parent_id, is_active, created_by, notes)
|
||||||
|
VALUES (
|
||||||
|
<cfqueryparam value="#role#" cfsqltype="cf_sql_varchar">,
|
||||||
|
<cfqueryparam value="#pname#" cfsqltype="cf_sql_varchar">,
|
||||||
|
<cfqueryparam value="#pbody#" cfsqltype="cf_sql_varchar">,
|
||||||
|
<cfif len(parentId)><cfqueryparam value="#parentId#" cfsqltype="cf_sql_varchar"><cfelse>NULL</cfif>,
|
||||||
|
<cfqueryparam value="#makeActive#" cfsqltype="cf_sql_bit">,
|
||||||
|
<cfqueryparam value="user" cfsqltype="cf_sql_varchar">,
|
||||||
|
<cfif len(pnotes)><cfqueryparam value="#pnotes#" cfsqltype="cf_sql_varchar"><cfelse>NULL</cfif>
|
||||||
|
)
|
||||||
|
</cfquery>
|
||||||
|
</cftransaction>
|
||||||
|
<cfset result = {ok: true, id: ins.generatedKey}>
|
||||||
</cfif>
|
</cfif>
|
||||||
<cfset result = {ok: true, saved: len(trim(prompt)) GT 0}>
|
|
||||||
|
<cfelseif url.action EQ "activate" AND cgi.request_method EQ "POST">
|
||||||
|
<!--- Активировать конкретную версию --->
|
||||||
|
<cfset body = toString(getHttpRequestData().content)>
|
||||||
|
<cfset data = deserializeJSON(body)>
|
||||||
|
<cfset pid = structKeyExists(data, "id") ? data.id : "">
|
||||||
|
|
||||||
|
<cfif NOT len(pid)>
|
||||||
|
<cfset result.error = "id required">
|
||||||
|
<cfelse>
|
||||||
|
<cfquery name="chk" datasource="baza">
|
||||||
|
SELECT role FROM prompts WHERE id = <cfqueryparam value="#pid#" cfsqltype="cf_sql_varchar">
|
||||||
|
</cfquery>
|
||||||
|
<cfif chk.recordCount EQ 0>
|
||||||
|
<cfset result.error = "prompt not found">
|
||||||
|
<cfelse>
|
||||||
|
<cftransaction>
|
||||||
|
<cfquery datasource="baza">
|
||||||
|
UPDATE prompts SET is_active = false
|
||||||
|
WHERE role = <cfqueryparam value="#chk.role#" cfsqltype="cf_sql_varchar">
|
||||||
|
</cfquery>
|
||||||
|
<cfquery datasource="baza">
|
||||||
|
UPDATE prompts SET is_active = true
|
||||||
|
WHERE id = <cfqueryparam value="#pid#" cfsqltype="cf_sql_varchar">
|
||||||
|
</cfquery>
|
||||||
|
</cftransaction>
|
||||||
|
<cfset result = {ok: true}>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfelseif url.action EQ "delete" AND cgi.request_method EQ "POST">
|
||||||
|
<!--- Удалить неактивную версию (активную нельзя) --->
|
||||||
|
<cfset body = toString(getHttpRequestData().content)>
|
||||||
|
<cfset data = deserializeJSON(body)>
|
||||||
|
<cfset pid = structKeyExists(data, "id") ? data.id : "">
|
||||||
|
|
||||||
|
<cfif NOT len(pid)>
|
||||||
|
<cfset result.error = "id required">
|
||||||
|
<cfelse>
|
||||||
|
<cfquery name="chk" datasource="baza">
|
||||||
|
SELECT is_active FROM prompts WHERE id = <cfqueryparam value="#pid#" cfsqltype="cf_sql_varchar">
|
||||||
|
</cfquery>
|
||||||
|
<cfif chk.recordCount EQ 0>
|
||||||
|
<cfset result.error = "prompt not found">
|
||||||
|
<cfelseif chk.is_active>
|
||||||
|
<cfset result.error = "cannot delete active prompt">
|
||||||
|
<cfelse>
|
||||||
|
<cfquery datasource="baza">
|
||||||
|
DELETE FROM prompts WHERE id = <cfqueryparam value="#pid#" cfsqltype="cf_sql_varchar">
|
||||||
|
</cfquery>
|
||||||
|
<cfset result = {ok: true}>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<cfelse>
|
<cfelse>
|
||||||
<!--- Получить промпт --->
|
<cfset result.error = "unknown action (use: get_active, list, get, save, activate, delete)">
|
||||||
<cfquery name="q" datasource="baza">
|
|
||||||
SELECT prompt_text FROM prompts
|
|
||||||
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
|
||||||
</cfquery>
|
|
||||||
<cfset result = {ok: true, prompt: q.recordCount GT 0 ? q.prompt_text : ""}>
|
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfcatch>
|
<cfcatch>
|
||||||
<cfset result = {ok: false, error: cfcatch.message}>
|
<cfset result = {ok: false, error: cfcatch.message}>
|
||||||
</cfcatch>
|
</cfcatch>
|
||||||
|
|||||||
Reference in New Issue
Block a user