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' => '' . h($default->defaultKey()) . '', '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, ));