517 lines
16 KiB
JavaScript
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('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
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();
|
|
});
|
|
}());
|