Files
www-data 2f2e8869d6 Reorganize PHP internals and static assets
Move shared PHP code into private/, move JavaScript files into js/, and block direct access to private/. Remove unused API key and cache artifacts from the working tree.
2026-05-26 11:32:36 +02:00

517 lines
16 KiB
JavaScript

(function () {
'use strict';
let promptData = { personal: {}, default: {}, can_edit_defaults: false };
let currentKind = null;
let currentPrompt = null;
let currentVersions = [];
let versionIndex = 0;
let uiText = {
prompt_not_found: 'Prompt not found',
no_previous_versions: 'No previous versions stored.',
no_lines_for_view: 'No lines for this view.',
restore_version_confirm: 'Restore version',
delete_version_confirm: 'Delete version',
default_prompt_prefix: 'Default prompt: ',
prompt_prefix: 'Prompt: ',
created: 'created',
updated: 'updated',
default_prompt: 'default prompt',
version: 'version',
showing_version: 'showing version',
of: 'of',
old: 'old',
new: 'new'
};
function byId(id) {
return document.getElementById(id);
}
function readPromptData() {
const el = byId('promptDataJson');
const textEl = byId('promptTextJson');
if (!el) {
return;
}
try {
promptData = JSON.parse(el.textContent || '{}');
} catch (e) {
console.error('Could not parse prompt data JSON', e);
promptData = { personal: {}, default: {} };
}
if (!textEl) {
return;
}
try {
uiText = Object.assign(uiText, JSON.parse(textEl.textContent || '{}'));
} catch (e) {
console.error('Could not parse prompt text JSON', e);
}
}
function esc(s) {
return String(s)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function fmtTs(ts) {
if (!ts) {
return '-';
}
const d = new Date(ts * 1000);
return d.toISOString().replace('T', ' ').slice(0, 19);
}
function promptActionUrl(kind) {
const url = new URL(window.location.href);
url.pathname = '/prompts';
url.searchParams.set('mode', kind === 'default' ? 'defaults' : 'personal');
return url.pathname + url.search;
}
function setEditing(editing) {
const shell = byId('promptModalForm');
const mayEdit = currentKind !== 'default' || promptData.can_edit_defaults;
editing = editing && mayEdit;
shell.classList.toggle('edit-mode', editing);
shell.classList.toggle('can-edit', mayEdit);
shell.classList.toggle('read-only-default', currentKind === 'default' && !mayEdit);
byId('modalName').readOnly = !editing;
byId('modalContent').readOnly = !editing;
byId('modalDefaultKey').readOnly = !editing;
byId('modalLanguage').disabled = !editing;
}
function openEditModal() {
if (!currentPrompt) {
return;
}
if (currentKind === 'default' && !promptData.can_edit_defaults) {
return;
}
setEditing(true);
byId('promptModalBackdrop').classList.add('open');
renderVersionPane();
}
function closeEditModal() {
byId('promptModalBackdrop').classList.remove('open');
if (currentPrompt) {
resetEditorFields();
renderVersionPane();
}
}
function resetEditorFields() {
byId('modalName').value = currentPrompt.name;
byId('modalLanguage').value = currentPrompt.language;
byId('modalContent').value = currentPrompt.content;
byId('modalDefaultKey').value = currentPrompt.default_key || '';
setEditing(false);
}
function openPromptEditor(kind, id) {
currentKind = kind;
currentPrompt = promptData[kind] ? promptData[kind][id] : null;
if (!currentPrompt) {
alert(uiText.prompt_not_found);
return;
}
currentVersions = currentPrompt.versions || [];
currentVersions.sort(function (a, b) {
return b.version_no - a.version_no;
});
/*
* The newest stored version is often the current snapshot. If possible,
* start by showing the one before that.
*/
versionIndex = currentVersions.length > 1 ? 1 : 0;
byId('modalTitle').textContent =
(kind === 'default' ? uiText.default_prompt_prefix : uiText.prompt_prefix) +
currentPrompt.name;
byId('viewerTitle').textContent =
(kind === 'default' ? uiText.default_prompt_prefix : uiText.prompt_prefix) +
currentPrompt.name;
byId('modalMeta').textContent =
uiText.created + ' ' + fmtTs(currentPrompt.created_at) +
' · ' + uiText.updated + ' ' + fmtTs(currentPrompt.updated_at) +
(kind === 'default' && !promptData.can_edit_defaults ? ' · ' + uiText.default_prompt : '');
byId('viewerMeta').textContent = byId('modalMeta').textContent;
byId('viewerContent').textContent = currentPrompt.content;
byId('defaultKeyRow').style.display = kind === 'default' ? 'block' : 'none';
if (kind === 'default') {
byId('modalAction').value = 'update_default';
byId('modalDefaultId').value = currentPrompt.id;
byId('modalPromptId').value = '';
} else {
byId('modalAction').value = 'update_prompt';
byId('modalPromptId').value = currentPrompt.id;
byId('modalDefaultId').value = '';
}
byId('promptModalForm').action = promptActionUrl(kind);
byId('modalAuxForm').action = promptActionUrl(kind);
byId('promptViewer').classList.remove('is-empty');
byId('promptViewer').classList.toggle('is-default-prompt', kind === 'default');
byId('promptModalBackdrop').classList.toggle('is-default-prompt', kind === 'default');
byId('editPromptButton').style.display =
kind === 'default' && !promptData.can_edit_defaults ? 'none' : '';
document.querySelectorAll('.prompt-select').forEach(function (button) {
button.classList.toggle(
'selected',
button.dataset.kind === kind && button.dataset.id === String(id)
);
});
resetEditorFields();
renderVersionPane();
}
function currentSelectedVersion() {
if (currentVersions.length === 0) {
return null;
}
if (versionIndex < 0) {
versionIndex = 0;
}
if (versionIndex >= currentVersions.length) {
versionIndex = currentVersions.length - 1;
}
return currentVersions[versionIndex];
}
function olderVersion() {
if (currentVersions.length === 0) {
return;
}
if (versionIndex < currentVersions.length - 1) {
versionIndex++;
renderVersionPane();
}
}
function newerVersion() {
if (currentVersions.length === 0) {
return;
}
const minIndex = currentVersions.length > 1 ? 1 : 0;
if (versionIndex > minIndex) {
versionIndex--;
renderVersionPane();
}
}
function renderVersionPane() {
const version = currentSelectedVersion();
const contentEl = byId('versionContent');
const metaEl = byId('selectedVersionMeta');
const indicatorEl = byId('versionIndicator');
if (!version) {
contentEl.innerHTML = '<span class="diff-muted">' +
esc(uiText.no_previous_versions) +
'</span>';
metaEl.textContent = '';
indicatorEl.textContent = '';
return;
}
metaEl.textContent =
uiText.version + ' ' + version.version_no +
' · ' + fmtTs(version.created_at) +
(version.note ? ' · ' + version.note : '');
indicatorEl.textContent =
uiText.showing_version + ' ' + version.version_no +
' (' + (versionIndex + 1) + ' ' + uiText.of + ' ' + currentVersions.length + ')';
const currentText = byId('modalContent').value;
const oldText = version.content;
const mode = byId('diffMode').value;
if (mode === 'plain') {
contentEl.innerHTML = esc(oldText);
return;
}
contentEl.innerHTML = renderLineDiff(oldText, currentText, mode);
}
function linesOf(s) {
return String(s).split(/\r?\n/);
}
function lcsTable(a, b) {
const m = a.length;
const n = b.length;
const dp = Array.from({ length: m + 1 }, function () {
return Array(n + 1).fill(0);
});
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
if (a[i] === b[j]) {
dp[i][j] = dp[i + 1][j + 1] + 1;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
return dp;
}
function diffOps(oldLines, newLines) {
const dp = lcsTable(oldLines, newLines);
const ops = [];
let i = 0;
let j = 0;
while (i < oldLines.length && j < newLines.length) {
if (oldLines[i] === newLines[j]) {
ops.push({ type: 'same', oldLine: oldLines[i], newLine: newLines[j] });
i++;
j++;
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
ops.push({ type: 'deleted', oldLine: oldLines[i] });
i++;
} else {
ops.push({ type: 'added', newLine: newLines[j] });
j++;
}
}
while (i < oldLines.length) {
ops.push({ type: 'deleted', oldLine: oldLines[i] });
i++;
}
while (j < newLines.length) {
ops.push({ type: 'added', newLine: newLines[j] });
j++;
}
return pairChangedLines(ops);
}
function pairChangedLines(ops) {
const paired = [];
for (let k = 0; k < ops.length; k++) {
const a = ops[k];
const b = ops[k + 1];
if (a && b && a.type === 'deleted' && b.type === 'added') {
paired.push({
type: 'changed',
oldLine: a.oldLine,
newLine: b.newLine
});
k++;
} else if (a && b && a.type === 'added' && b.type === 'deleted') {
paired.push({
type: 'changed',
oldLine: b.oldLine,
newLine: a.newLine
});
k++;
} else {
paired.push(a);
}
}
return paired;
}
function renderLineDiff(oldText, newText, mode) {
const ops = diffOps(linesOf(oldText), linesOf(newText));
const out = [];
for (const op of ops) {
if (mode !== 'all' && mode !== op.type) {
continue;
}
if (op.type === 'same') {
out.push('<span class="diff-line diff-same">' + esc(op.oldLine) + '</span>');
} else if (op.type === 'added') {
out.push('<span class="diff-line diff-added">+ ' + esc(op.newLine) + '</span>');
} else if (op.type === 'deleted') {
out.push('<span class="diff-line diff-deleted">- ' + esc(op.oldLine) + '</span>');
} else if (op.type === 'changed') {
out.push(
'<span class="diff-line diff-changed">~ ' +
esc(uiText.old) +
': ' +
esc(op.oldLine) +
'</span>'
);
out.push(
'<span class="diff-line diff-changed">~ ' +
esc(uiText.new) +
': ' +
esc(op.newLine) +
'</span>'
);
}
}
if (out.length === 0) {
return '<span class="diff-muted">' + esc(uiText.no_lines_for_view) + '</span>';
}
return out.join('');
}
function configureAux(action, versionNo) {
const aux = byId('modalAuxForm');
byId('auxAction').value = action;
byId('auxVersionNo').value = versionNo || '';
byId('auxVersionNote').value = 'modal snapshot';
if (currentKind === 'default') {
byId('auxDefaultId').value = currentPrompt.id;
byId('auxPromptId').value = '';
} else {
byId('auxPromptId').value = currentPrompt.id;
byId('auxDefaultId').value = '';
}
return aux;
}
function storeSnapshot() {
if (!currentPrompt) {
return;
}
const action = currentKind === 'default'
? 'create_default_version'
: 'create_version';
configureAux(action, '').submit();
}
function restoreSelectedVersion() {
const version = currentSelectedVersion();
if (!currentPrompt || !version) {
return;
}
if (!confirm(uiText.restore_version_confirm + ' ' + version.version_no + '?')) {
return;
}
const action = currentKind === 'default'
? 'restore_default_version'
: 'restore_version';
configureAux(action, version.version_no).submit();
}
function deleteSelectedVersion() {
const version = currentSelectedVersion();
if (!currentPrompt || !version) {
return;
}
if (!confirm(uiText.delete_version_confirm + ' ' + version.version_no + '?')) {
return;
}
const action = currentKind === 'default'
? 'delete_default_version'
: 'delete_version';
configureAux(action, version.version_no).submit();
}
function bindEvents() {
document.querySelectorAll('.prompt-tab').forEach(function (tab) {
tab.addEventListener('click', function () {
const tabName = tab.dataset.tab;
document.querySelectorAll('.prompt-tab').forEach(function (other) {
const active = other === tab;
other.classList.toggle('active', active);
other.setAttribute('aria-selected', active ? 'true' : 'false');
});
document.querySelectorAll('.prompt-tab-panel').forEach(function (panel) {
panel.classList.toggle('active', panel.id === 'tab-' + tabName);
});
});
});
document.querySelectorAll('.js-open-prompt').forEach(function (button) {
button.addEventListener('click', function () {
openPromptEditor(button.dataset.kind, button.dataset.id);
});
});
byId('editPromptButton').addEventListener('click', function () {
openEditModal();
});
byId('cancelEditButton').addEventListener('click', function () {
closeEditModal();
});
byId('versionNewerButton').addEventListener('click', newerVersion);
byId('versionOlderButton').addEventListener('click', olderVersion);
byId('diffMode').addEventListener('change', renderVersionPane);
byId('modalContent').addEventListener('input', renderVersionPane);
byId('snapshotButton').addEventListener('click', storeSnapshot);
byId('restoreVersionButton').addEventListener('click', restoreSelectedVersion);
byId('deleteVersionButton').addEventListener('click', deleteSelectedVersion);
byId('promptModalBackdrop').addEventListener('click', function (ev) {
if (ev.target === byId('promptModalBackdrop')) {
closeEditModal();
}
});
document.addEventListener('keydown', function (ev) {
if (ev.key === 'Escape') {
closeEditModal();
}
});
setEditing(false);
}
document.addEventListener('DOMContentLoaded', function () {
readPromptData();
bindEvents();
});
}());