Files
racket-chatgpt-bootstrap/prompts.php
T
www-data 475765e31f Move rendering into private templates
Add an explicit template renderer with HTML views and partials for the app, bootstrap, package, and catalog pages. Move shared reporting setup into config/reporting.php and relocate stylesheet assets under css/.
2026-05-26 12:50:26 +02:00

526 lines
20 KiB
PHP

<?php
/*
* prompts.php
*
* User/admin prompt administration page.
*
* Normal users:
* - manage personal prompts
* - copy global default prompts to their own prompts
*
* Admin users:
* - same as normal users
* - manage global default prompts
*/
require_once __DIR__ . '/private/auth.php';
require_once __DIR__ . '/private/header.php';
require_once __DIR__ . '/private/languagestore.php';
require_once __DIR__ . '/private/promptstore.php';
require_once __DIR__ . '/private/usersettings.php';
require_once __DIR__ . '/private/viewdata.php';
require_once __DIR__ . '/config/reporting.php';
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
$auth = new RacketSandboxAuth($DB_FILE);
$user = $auth->requireLoginHtml();
$store = new PromptStore($DB_FILE);
$languageStore = new LanguageStore($DB_FILE);
$userSettings = new UserSettingsStore($DB_FILE);
$message = '';
$error = '';
function h($s)
{
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function t($key, $fallback = null, $values = array())
{
global $languageStore, $language;
return $languageStore->translateFormat($key, $language, $values, $fallback);
}
function post_value($name, $default = '')
{
return $_POST[$name] ?? $default;
}
function get_value($name, $default = '')
{
return $_GET[$name] ?? $default;
}
function fmt_time($ts)
{
if ($ts === null) {
return '-';
}
return date('Y-m-d H:i:s', (int)$ts);
}
function prompt_to_array($prompt)
{
return array(
'id' => $prompt->id(),
'name' => $prompt->name(),
'language' => $prompt->language(),
'content' => $prompt->content(),
'default_key' => $prompt->defaultKey(),
'is_default' => $prompt->isDefault(),
'created_at' => $prompt->createdAt(),
'updated_at' => $prompt->updatedAt(),
);
}
function version_to_array($version)
{
return array(
'id' => $version->id(),
'prompt_id' => $version->promptId(),
'version_no' => $version->versionNo(),
'name' => $version->name(),
'language' => $version->language(),
'content' => $version->content(),
'note' => $version->note(),
'created_at' => $version->createdAt(),
);
}
function resolve_user_language($userSettings, $userId, $allowedLanguages)
{
$language = isset($_GET['lang'])
? (string)$_GET['lang']
: (string)$userSettings->get($userId, 'language', 'en');
if (!in_array($language, $allowedLanguages, true)) {
$language = 'en';
}
$userSettings->set($userId, 'language', $language);
return $language;
}
$language = resolve_user_language(
$userSettings,
$user->id(),
$store->supportedLanguages()
);
seed_template_translations($languageStore, 'prompts.html');
$mode = get_value('mode', 'personal');
if ($mode !== 'personal' && $mode !== 'defaults') {
$mode = 'personal';
}
if ($mode === 'defaults' && !$user->isAdmin()) {
$mode = 'personal';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = post_value('action');
try {
/*
* User actions.
*/
if ($action === 'logout') {
$auth->logout();
header('Location: /login.php');
exit;
} elseif ($action === 'copy_all_defaults') {
$language = post_value('language', 'en');
$created = $store->copyAllDefaultsToUser($user->id(), $language);
$message = 'Default prompts copied to your prompts: ' . $created;
$mode = 'personal';
} elseif ($action === 'copy_default') {
$defaultId = (int)post_value('default_id');
$store->copyDefaultPromptToUser($user->id(), $defaultId);
$message = 'Default prompt copied to your prompts.';
$mode = 'personal';
} elseif ($action === 'create_prompt') {
$language = post_value('language', 'en');
$store->createPrompt(
$user->id(),
post_value('name'),
$language,
post_value('content')
);
$message = 'Prompt created.';
$mode = 'personal';
} elseif ($action === 'update_prompt') {
$promptId = (int)post_value('prompt_id');
$language = post_value('language', 'en');
$store->updatePrompt(
$user->id(),
$promptId,
post_value('name'),
$language,
post_value('content'),
isset($_POST['create_version']),
post_value('version_note')
);
$message = 'Prompt updated.';
$mode = 'personal';
} elseif ($action === 'create_version') {
$promptId = (int)post_value('prompt_id');
$store->createVersion($user->id(), $promptId, post_value('version_note'));
$message = 'Version stored.';
$mode = 'personal';
} elseif ($action === 'restore_version') {
$promptId = (int)post_value('prompt_id');
$versionNo = (int)post_value('version_no');
$store->restoreVersion($user->id(), $promptId, $versionNo);
$message = 'Version restored.';
$mode = 'personal';
} elseif ($action === 'delete_version') {
$promptId = (int)post_value('prompt_id');
$versionNo = (int)post_value('version_no');
$store->deleteVersion($user->id(), $promptId, $versionNo);
$message = 'Version deleted.';
$mode = 'personal';
} elseif ($action === 'delete_prompt') {
$promptId = (int)post_value('prompt_id');
$store->deletePrompt($user->id(), $promptId);
$message = 'Prompt deleted.';
$mode = 'personal';
}
/*
* Admin-only default prompt actions.
*/
elseif ($action === 'create_default' ||
$action === 'update_default' ||
$action === 'create_default_version' ||
$action === 'restore_default_version' ||
$action === 'delete_default_version' ||
$action === 'delete_default') {
if (!$user->isAdmin()) {
throw new Exception('Admin rights required.');
}
$mode = 'defaults';
if ($action === 'create_default') {
$language = post_value('language', 'en');
$store->createDefaultPrompt(
post_value('default_key'),
post_value('name'),
$language,
post_value('content')
);
$message = 'Default prompt created.';
} elseif ($action === 'update_default') {
$defaultId = (int)post_value('default_id');
$language = post_value('language', 'en');
$store->updateDefaultPrompt(
$defaultId,
post_value('default_key'),
post_value('name'),
$language,
post_value('content'),
isset($_POST['create_version']),
post_value('version_note')
);
$message = 'Default prompt updated.';
} elseif ($action === 'create_default_version') {
$defaultId = (int)post_value('default_id');
$store->createDefaultVersion($defaultId, post_value('version_note'));
$message = 'Default prompt version stored.';
} elseif ($action === 'restore_default_version') {
$defaultId = (int)post_value('default_id');
$versionNo = (int)post_value('version_no');
$store->restoreDefaultVersion($defaultId, $versionNo);
$message = 'Default prompt version restored.';
} elseif ($action === 'delete_default_version') {
$defaultId = (int)post_value('default_id');
$versionNo = (int)post_value('version_no');
$store->deleteDefaultVersion($defaultId, $versionNo);
$message = 'Default prompt version deleted.';
} elseif ($action === 'delete_default') {
$defaultId = (int)post_value('default_id');
$store->deleteDefaultPrompt($defaultId);
$message = 'Default prompt deleted.';
}
} elseif ($action !== '') {
throw new Exception(t('prompts.unknown_action', 'Unknown action: {{action}}', array('action' => $action)));
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
$personalPrompts = $store->listPrompts($user->id(), $language);
$defaultPrompts = $store->listDefaultPrompts($language);
/*
* Full prompt data for the modal editor.
*/
$allPersonalPrompts = $store->listPrompts($user->id(), null);
$allDefaultPrompts = $store->listDefaultPrompts(null);
$promptData = array(
'personal' => array(),
'default' => array(),
'can_edit_defaults' => $user->isAdmin(),
);
foreach ($allPersonalPrompts as $p) {
$promptData['personal'][$p->id()] = prompt_to_array($p);
$promptData['personal'][$p->id()]['versions'] = array();
foreach ($store->listVersions($user->id(), $p->id()) as $v) {
$promptData['personal'][$p->id()]['versions'][] = version_to_array($v);
}
}
foreach ($allDefaultPrompts as $p) {
$promptData['default'][$p->id()] = prompt_to_array($p);
$promptData['default'][$p->id()]['versions'] = array();
if ($user->isAdmin()) {
foreach ($store->listDefaultVersions($p->id()) as $v) {
$promptData['default'][$p->id()]['versions'][] = version_to_array($v);
}
}
}
$headerLanguages = array();
foreach ($store->supportedLanguages() as $lang) {
$headerLanguages[$lang] = $store->languageLabel($lang);
}
$headerNavItems = array(
array(
'label' => t('prompts.back', 'Back to Racket sandbox'),
'url' => '/',
),
);
if ($user->isAdmin()) {
$headerNavItems[] = array(
'label' => t('app.user_management', 'User management'),
'url' => '/users?lang=' . rawurlencode($language),
'separator_before' => true,
);
$headerNavItems[] = array(
'label' => t('app.configuration', 'Configuration'),
'url' => '/admin-config?lang=' . rawurlencode($language),
'separator_before' => true,
);
}
$styleVersion = @filemtime(__DIR__ . '/css/styles.css') ?: time();
$promptEditorVersion = @filemtime(__DIR__ . '/js/prompt-editor.js') ?: time();
header('Content-Type: text/html; charset=utf-8');
$languageOptionsHtml = '';
$languageOptionsPlainHtml = '';
foreach ($store->supportedLanguages() as $lang) {
$languageOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
'value' => $lang,
'selected' => $lang === $language ? ' selected' : '',
'label' => $store->languageLabel($lang),
)) . "\n";
$languageOptionsPlainHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
'value' => $lang,
'selected' => '',
'label' => $store->languageLabel($lang),
)) . "\n";
}
$defaultAdminNoticeHtml = '';
if ($user->isAdmin()) {
$defaultAdminNoticeHtml = RacketSandboxTemplate::renderFile('partials/prompt-admin-notice.html', array(
'badge' => t('prompts.default_admin_badge', 'Admin default prompts'),
'hint' => t('prompts.default_admin_hint', 'You are editing global default prompts. Users can copy these to their own prompts.'),
));
}
$defaultPromptsHtml = '';
foreach ($defaultPrompts as $default) {
$adminDeleteHtml = '';
if ($user->isAdmin()) {
$adminDeleteHtml = RacketSandboxTemplate::renderFile('partials/prompt-default-delete.html', array(
'language_url' => rawurlencode($language),
'confirm_json' => json_encode(t('prompts.delete_default_confirm', 'Delete default prompt {{name}}?', array(
'name' => $default->name(),
)), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
'name' => $default->name(),
'id' => $default->id(),
'delete_label' => t('prompts.delete', 'delete'),
));
}
$defaultPromptsHtml .= RacketSandboxTemplate::renderFile('partials/prompt-default-item.html', array(
'language_url' => rawurlencode($language),
'id' => $default->id(),
'name' => $default->name(),
'default_key' => $default->defaultKey(),
'metadata' => t('prompts.default_metadata', '{{default_key}} · {{updated_at}}', array(
'default_key' => '<code>' . h($default->defaultKey()) . '</code>',
'updated_at' => fmt_time($default->updatedAt()),
)),
'copy_label' => t('prompts.copy', 'copy'),
'admin_delete_html' => $adminDeleteHtml,
)) . "\n";
}
$noDefaultsHtml = count($defaultPrompts) === 0
? RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
'class' => 'empty-state',
'text' => t('prompts.no_defaults', 'No default prompts for this language yet.'),
))
: '';
$createDefaultHtml = '';
if ($user->isAdmin()) {
$createDefaultHtml = RacketSandboxTemplate::renderFile('partials/prompt-create-default.html', array(
'language_url' => rawurlencode($language),
'create_default_label' => t('prompts.create_default', 'Create default prompt'),
'default_key_label' => t('prompts.default_key', 'Default key'),
'name_label' => t('prompts.name', 'Name'),
'language_label' => t('prompts.language', 'Language'),
'language_options_html' => $languageOptionsHtml,
'prompt_content_label' => t('prompts.prompt_content', 'Prompt content'),
));
}
$personalPromptsHtml = '';
foreach ($personalPrompts as $prompt) {
$personalPromptsHtml .= RacketSandboxTemplate::renderFile('partials/prompt-personal-item.html', array(
'language_url' => rawurlencode($language),
'id' => $prompt->id(),
'name' => $prompt->name(),
'metadata' => t('prompts.personal_metadata', '{{language}} · {{updated_at}}', array(
'language' => $store->languageLabel($prompt->language()),
'updated_at' => fmt_time($prompt->updatedAt()),
)),
'confirm_json' => json_encode(t('prompts.delete_prompt_confirm', 'Delete prompt {{name}}?', array(
'name' => $prompt->name(),
)), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
'delete_label' => t('prompts.delete', 'delete'),
)) . "\n";
}
$noPersonalHtml = count($personalPrompts) === 0
? RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
'class' => 'empty-state',
'text' => t('prompts.no_personal', 'No personal prompts yet for this language.'),
))
: '';
$promptTextJson = json_encode(array(
'prompt_not_found' => t('prompts.prompt_not_found', 'Prompt not found'),
'no_previous_versions' => t('prompts.no_previous_versions', 'No previous versions stored.'),
'no_lines_for_view' => t('prompts.no_lines_for_view', 'No lines for this view.'),
'restore_version_confirm' => t('prompts.restore_version_confirm', 'Restore version'),
'delete_version_confirm' => t('prompts.delete_version_confirm', 'Delete version'),
'default_prompt_prefix' => t('prompts.default_prompt_prefix', 'Default prompt: '),
'prompt_prefix' => t('prompts.prompt_prefix', 'Prompt: '),
'created' => t('prompts.created', 'created'),
'updated' => t('prompts.updated', 'updated'),
'default_prompt' => t('prompts.default_prompt', 'default prompt'),
'version' => t('prompts.version', 'version'),
'showing_version' => t('prompts.showing_version', 'showing version'),
'of' => t('prompts.of', 'of'),
'old' => t('prompts.old', 'old'),
'new' => t('prompts.new', 'new'),
), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$headerHtml = app_header_html(array(
'title' => t('prompts.title', 'Prompt administration'),
'nav_items' => $headerNavItems,
'user' => $user,
'admin_label' => t('app.admin', 'admin'),
'language_label' => t('prompts.language', 'Language'),
'language' => $language,
'languages' => $headerLanguages,
'language_action' => '/prompts',
'language_hidden' => array('mode' => $mode),
'logout_action' => '/prompts?lang=' . rawurlencode($language) . '&mode=' . rawurlencode($mode),
'logout_label' => t('app.logout', 'Logout'),
'message' => $message,
'error' => $error,
));
echo RacketSandboxTemplate::renderFile('prompts.html', array(
'language' => $language,
'language_url' => rawurlencode($language),
'mode_url' => rawurlencode($mode),
'title' => t('prompts.title', 'Prompt administration'),
'style_version' => $styleVersion,
'prompt_editor_version' => $promptEditorVersion,
'header_html' => $headerHtml,
'defaults_tab_active_class' => $mode === 'defaults' ? 'active' : '',
'defaults_tab_selected' => $mode === 'defaults' ? 'true' : 'false',
'personal_tab_active_class' => $mode !== 'defaults' ? 'active' : '',
'personal_tab_selected' => $mode !== 'defaults' ? 'true' : 'false',
'available_defaults_label' => t('prompts.available_defaults', 'Available default prompts'),
'your_prompts_label' => t('prompts.your_prompts', 'Your prompts'),
'default_admin_notice_html' => $defaultAdminNoticeHtml,
'copy_all_label' => t('prompts.copy_all', 'Copy all'),
'default_prompts_html' => $defaultPromptsHtml,
'no_defaults_html' => $noDefaultsHtml,
'create_default_html' => $createDefaultHtml,
'personal_prompts_html' => $personalPromptsHtml,
'no_personal_html' => $noPersonalHtml,
'create_personal_label' => t('prompts.create_personal', 'Create personal prompt'),
'name_label' => t('prompts.name', 'Name'),
'language_label' => t('prompts.language', 'Language'),
'language_options_html' => $languageOptionsHtml,
'language_options_plain_html' => $languageOptionsPlainHtml,
'prompt_content_label' => t('prompts.prompt_content', 'Prompt content'),
'select_prompt_label' => t('prompts.select_prompt', 'Select a prompt on the left to view it.'),
'prompt_label' => t('prompts.prompt', 'Prompt'),
'edit_label' => t('prompts.edit', 'Edit'),
'back_label' => t('prompts.back', 'Back to Racket sandbox'),
'close_label' => t('prompts.close', 'Close'),
'newer_previous_label' => t('prompts.newer_previous', 'newer previous version'),
'older_previous_label' => t('prompts.older_previous', 'older previous version'),
'diff_view_label' => t('prompts.diff_view', 'Diff view:'),
'diff_plain_label' => t('prompts.diff_plain', 'text, no diff'),
'diff_all_label' => t('prompts.diff_all', 'all diff'),
'diff_same_label' => t('prompts.diff_same', 'unchanged only'),
'diff_added_label' => t('prompts.diff_added', 'additions only'),
'diff_deleted_label' => t('prompts.diff_deleted', 'deletions only'),
'diff_changed_label' => t('prompts.diff_changed', 'changes only'),
'default_key_label' => t('prompts.default_key', 'Default key'),
'previous_version_label' => t('prompts.previous_version', 'Previous version'),
'store_version_label' => t('prompts.store_version', 'store this edit as a new version'),
'version_note_label' => t('prompts.version_note', 'Version note:'),
'save_label' => t('prompts.save', 'Save'),
'store_snapshot_label' => t('prompts.store_snapshot', 'Store snapshot'),
'restore_version_label' => t('prompts.restore_version', 'Restore selected version'),
'delete_version_label' => t('prompts.delete_version', 'Delete selected version'),
'prompt_data_json' => json_encode($promptData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
'prompt_text_json' => $promptTextJson,
));