v1.0.169: prompts CRUD via VM API — /api/prompts, no more prompt.cfm dependency
This commit is contained in:
+21
-24
@@ -583,12 +583,12 @@ function closePromptModal(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadPrompt(role) {
|
function loadPrompt(role) {
|
||||||
fetch('/prompt.cfm?action=get_active&role=' + role)
|
fetch(VM_API + '/api/prompts?role=' + role)
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(d) {
|
.then(function(d) {
|
||||||
if (d.OK) {
|
if (d.ok) {
|
||||||
promptEditor.value = d.BODY || '';
|
promptEditor.value = d.body || '';
|
||||||
activePromptId[role] = d.ID || '';
|
activePromptId[role] = d.id || '';
|
||||||
loadHistory(role);
|
loadHistory(role);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -596,52 +596,50 @@ function loadPrompt(role) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadHistory(role) {
|
function loadHistory(role) {
|
||||||
fetch('/prompt.cfm?action=list&role=' + role)
|
fetch(VM_API + '/api/prompts/list?role=' + role)
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(d) {
|
.then(function(d) {
|
||||||
if (!d.OK || !d.VERSIONS || !d.VERSIONS.length) {
|
if (!d.ok || !d.versions || !d.versions.length) {
|
||||||
promptHistory.innerHTML = '<div style="color:var(--muted);">нет версий</div>';
|
promptHistory.innerHTML = '<div style="color:var(--muted);">нет версий</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
promptHistory.innerHTML = d.VERSIONS.map(function(v) {
|
promptHistory.innerHTML = d.versions.map(function(v) {
|
||||||
var isActive = v.IS_ACTIVE ? '✓ ' : '';
|
var isActive = v.is_active ? '✓ ' : '';
|
||||||
var style = v.IS_ACTIVE ? 'font-weight:600;' : '';
|
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 + '">'
|
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>'
|
+ '<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="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>' : '')
|
+ (!v.is_active ? '<button class="arrow-btn" onclick="deletePrompt(\'' + v.id + '\')" title="Удалить" style="font-size:14px;">✕</button>' : '')
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
})
|
})
|
||||||
.catch(function(e) { console.error('loadHistory:', e); });
|
.catch(function(e) { console.error('loadHistory:', e); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// escHtml/fmtDate — вынесены в app_utils.js
|
|
||||||
|
|
||||||
function activatePrompt(id) {
|
function activatePrompt(id) {
|
||||||
fetch('/prompt.cfm?action=activate', {
|
fetch(VM_API + '/api/prompts/activate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({id: id})
|
body: JSON.stringify({id: id})
|
||||||
})
|
})
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(d) {
|
.then(function(d) {
|
||||||
if (d.OK) { loadPrompt(promptRole); promptStatus.textContent = '✓ активирован'; }
|
if (d.ok) { loadPrompt(promptRole); promptStatus.textContent = '✓ активирован'; }
|
||||||
else { promptStatus.textContent = '✕ ' + (d.ERROR || 'ошибка'); }
|
else { promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePrompt(id) {
|
function deletePrompt(id) {
|
||||||
if (!confirm('Удалить эту версию промпта?')) return;
|
if (!confirm('Удалить эту версию промпта?')) return;
|
||||||
fetch('/prompt.cfm?action=delete', {
|
fetch(VM_API + '/api/prompts/delete', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({id: id})
|
body: JSON.stringify({id: id})
|
||||||
})
|
})
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(d) {
|
.then(function(d) {
|
||||||
if (d.OK) { loadHistory(promptRole); promptStatus.textContent = '✓ удалено'; }
|
if (d.ok) { loadHistory(promptRole); promptStatus.textContent = '✓ удалено'; }
|
||||||
else { promptStatus.textContent = '✕ ' + (d.ERROR || 'ошибка'); }
|
else { promptStatus.textContent = '✕ ' + (d.error || 'ошибка'); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,23 +647,22 @@ document.getElementById('promptSave').addEventListener('click', function() {
|
|||||||
var body = promptEditor.value.trim();
|
var body = promptEditor.value.trim();
|
||||||
if (!body) { promptStatus.textContent = '✕ тело промпта пустое'; return; }
|
if (!body) { promptStatus.textContent = '✕ тело промпта пустое'; return; }
|
||||||
var name = promptName.value.trim() || ('v' + new Date().toISOString().replace(/[:.]/g,'-').substring(0,16));
|
var name = promptName.value.trim() || ('v' + new Date().toISOString().replace(/[:.]/g,'-').substring(0,16));
|
||||||
fetch('/prompt.cfm?action=save', {
|
fetch(VM_API + '/api/prompts/save', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
role: promptRole, name: name, body: body,
|
role: promptRole, name: name, body: body,
|
||||||
parent_id: activePromptId[promptRole] || '',
|
|
||||||
notes: promptNotes.value.trim(), is_active: true
|
notes: promptNotes.value.trim(), is_active: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(d) {
|
.then(function(d) {
|
||||||
if (d.OK) {
|
if (d.ok) {
|
||||||
promptStatus.textContent = '✓ сохранено (новая версия)';
|
promptStatus.textContent = '✓ сохранено (новая версия)';
|
||||||
promptName.value = ''; promptNotes.value = '';
|
promptName.value = ''; promptNotes.value = '';
|
||||||
loadPrompt(promptRole);
|
loadPrompt(promptRole);
|
||||||
} else {
|
} else {
|
||||||
promptStatus.textContent = '✕ ' + (d.ERROR || 'ошибка');
|
promptStatus.textContent = '✕ ' + (d.error || 'ошибка');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self._handle_api_groups(parsed)
|
self._handle_api_groups(parsed)
|
||||||
elif parsed.path == "/api/batch-progress":
|
elif parsed.path == "/api/batch-progress":
|
||||||
self._handle_api_batch_progress(parsed)
|
self._handle_api_batch_progress(parsed)
|
||||||
|
elif parsed.path == "/api/prompts":
|
||||||
|
self._handle_api_prompts_get(parsed)
|
||||||
|
elif parsed.path == "/api/prompts/list":
|
||||||
|
self._handle_api_prompts_list(parsed)
|
||||||
elif parsed.path.startswith("/api/documents/"):
|
elif parsed.path.startswith("/api/documents/"):
|
||||||
self._handle_api_document(parsed)
|
self._handle_api_document(parsed)
|
||||||
else:
|
else:
|
||||||
@@ -68,6 +72,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self._handle_apply_groups()
|
self._handle_apply_groups()
|
||||||
elif self.path == "/api/cleanup":
|
elif self.path == "/api/cleanup":
|
||||||
self._handle_cleanup()
|
self._handle_cleanup()
|
||||||
|
elif self.path == "/api/prompts/save":
|
||||||
|
self._handle_api_prompts_save()
|
||||||
|
elif self.path == "/api/prompts/activate":
|
||||||
|
self._handle_api_prompts_activate()
|
||||||
|
elif self.path == "/api/prompts/delete":
|
||||||
|
self._handle_api_prompts_delete()
|
||||||
else:
|
else:
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
|
|
||||||
@@ -193,6 +203,83 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
execute("DELETE FROM contracts")
|
execute("DELETE FROM contracts")
|
||||||
self._json({"ok": True, "message": "all data cleaned"})
|
self._json({"ok": True, "message": "all data cleaned"})
|
||||||
|
|
||||||
|
# ── GET /api/cleanup ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _handle_cleanup(self):
|
||||||
|
"""Полная очистка всех данных (кроме prompts)."""
|
||||||
|
from db.connection import execute
|
||||||
|
execute("DELETE FROM spec_current")
|
||||||
|
execute("DELETE FROM spec_events")
|
||||||
|
execute("DELETE FROM supplements")
|
||||||
|
execute("DELETE FROM upload_chunks")
|
||||||
|
execute("DELETE FROM documents")
|
||||||
|
execute("DELETE FROM contracts")
|
||||||
|
self._json({"ok": True, "message": "all data cleaned"})
|
||||||
|
|
||||||
|
# ── Prompts CRUD ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _handle_api_prompts_get(self, parsed):
|
||||||
|
from db import prompts as db_p
|
||||||
|
params = parse_qs(parsed.query)
|
||||||
|
role = params.get("role", [None])[0]
|
||||||
|
if not role:
|
||||||
|
self._json({"ok": False, "error": "role required"}, 400)
|
||||||
|
return
|
||||||
|
p = db_p.get_active(role)
|
||||||
|
if p:
|
||||||
|
self._json({"ok": True, "id": p["id"], "role": p["role"], "name": p["name"],
|
||||||
|
"body": p["body"], "is_active": p["is_active"], "notes": p.get("notes"),
|
||||||
|
"created_at": str(p.get("created_at", ""))})
|
||||||
|
else:
|
||||||
|
self._json({"ok": False, "error": "not found"}, 404)
|
||||||
|
|
||||||
|
def _handle_api_prompts_list(self, parsed):
|
||||||
|
from db import prompts as db_p
|
||||||
|
params = parse_qs(parsed.query)
|
||||||
|
role = params.get("role", [None])[0]
|
||||||
|
if not role:
|
||||||
|
self._json({"ok": False, "error": "role required"}, 400)
|
||||||
|
return
|
||||||
|
versions = db_p.list_by_role(role)
|
||||||
|
self._json({"ok": True, "versions": versions})
|
||||||
|
|
||||||
|
def _handle_api_prompts_save(self):
|
||||||
|
from db import prompts as db_p
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = json.loads(self.rfile.read(length))
|
||||||
|
role = body.get("role", "")
|
||||||
|
name = body.get("name", "v" + __import__("datetime").datetime.now().isoformat()[:16])
|
||||||
|
prompt_body = body.get("body", "")
|
||||||
|
notes = body.get("notes", "")
|
||||||
|
is_active = body.get("is_active", True)
|
||||||
|
if not role or not prompt_body:
|
||||||
|
self._json({"ok": False, "error": "role and body required"}, 400)
|
||||||
|
return
|
||||||
|
p = db_p.save_new_version(role, name, prompt_body, notes, is_active)
|
||||||
|
self._json({"ok": True, "id": p["id"]})
|
||||||
|
|
||||||
|
def _handle_api_prompts_activate(self):
|
||||||
|
from db import prompts as db_p
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = json.loads(self.rfile.read(length))
|
||||||
|
pid = body.get("id", "")
|
||||||
|
if not pid:
|
||||||
|
self._json({"ok": False, "error": "id required"}, 400)
|
||||||
|
return
|
||||||
|
ok = db_p.activate(pid)
|
||||||
|
self._json({"ok": ok})
|
||||||
|
|
||||||
|
def _handle_api_prompts_delete(self):
|
||||||
|
from db import prompts as db_p
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = json.loads(self.rfile.read(length))
|
||||||
|
pid = body.get("id", "")
|
||||||
|
if not pid:
|
||||||
|
self._json({"ok": False, "error": "id required"}, 400)
|
||||||
|
return
|
||||||
|
ok = db_p.delete_prompt(pid)
|
||||||
|
self._json({"ok": ok})
|
||||||
|
|
||||||
# ── /app.js (static) ───────────────────────────────────────────────────
|
# ── /app.js (static) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
def _handle_app_js(self):
|
def _handle_app_js(self):
|
||||||
|
|||||||
@@ -38,6 +38,47 @@ def seed_defaults():
|
|||||||
_ensure_classify_prompt()
|
_ensure_classify_prompt()
|
||||||
|
|
||||||
|
|
||||||
|
def list_by_role(role):
|
||||||
|
"""List all versions for a role, newest first."""
|
||||||
|
return query(
|
||||||
|
"SELECT id, role, name, is_active, created_by, notes, created_at FROM prompts WHERE role=%s ORDER BY created_at DESC",
|
||||||
|
(role,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_new_version(role, name, body, notes="", is_active=True):
|
||||||
|
"""Save new prompt version. Deactivates all others for this role, inserts new one."""
|
||||||
|
if is_active:
|
||||||
|
execute("UPDATE prompts SET is_active=false WHERE role=%s", (role,))
|
||||||
|
return execute_returning(
|
||||||
|
"""INSERT INTO prompts (role, name, body, is_active, notes)
|
||||||
|
VALUES (%s, %s, %s, %s, %s) RETURNING *""",
|
||||||
|
(role, name, body, is_active, notes),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def activate(prompt_id):
|
||||||
|
"""Activate a prompt version (deactivates others for same role)."""
|
||||||
|
row = query("SELECT role FROM prompts WHERE id=%s", (prompt_id,))
|
||||||
|
if not row:
|
||||||
|
return False
|
||||||
|
role = row[0]["role"]
|
||||||
|
execute("UPDATE prompts SET is_active=false WHERE role=%s", (role,))
|
||||||
|
execute("UPDATE prompts SET is_active=true WHERE id=%s", (prompt_id,))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def delete_prompt(prompt_id):
|
||||||
|
"""Delete a prompt version (cannot delete active one)."""
|
||||||
|
row = query("SELECT is_active FROM prompts WHERE id=%s", (prompt_id,))
|
||||||
|
if not row:
|
||||||
|
return False
|
||||||
|
if row[0]["is_active"]:
|
||||||
|
return False # cannot delete active
|
||||||
|
execute("DELETE FROM prompts WHERE id=%s", (prompt_id,))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _ensure_classify_prompt():
|
def _ensure_classify_prompt():
|
||||||
"""Ensure classify prompt exists (idempotent)."""
|
"""Ensure classify prompt exists (idempotent)."""
|
||||||
existing = query("SELECT id FROM prompts WHERE role = 'classify' AND is_active = true LIMIT 1")
|
existing = query("SELECT id FROM prompts WHERE role = 'classify' AND is_active = true LIMIT 1")
|
||||||
|
|||||||
@@ -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.168 — Lucee</span></span>
|
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.169 — Lucee</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|||||||
Reference in New Issue
Block a user