diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts index 4129ea62a..7cc8c1f92 100644 --- a/src/lib/apis/prompts/index.ts +++ b/src/lib/apis/prompts/index.ts @@ -2,9 +2,45 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; type PromptItem = { command: string; - title: string; + name: string; // Changed from title content: string; + data?: object | null; + meta?: object | null; access_control?: null | object; + version_id?: string | null; // Active version + commit_message?: string | null; // For history tracking +}; + +type PromptHistoryItem = { + id: string; + prompt_id: string; + parent_id: string | null; + snapshot: { + name: string; + content: string; + command: string; + data: object; + meta: object; + access_control: object | null; + }; + user_id: string; + commit_message: string | null; + created_at: number; + user?: { + id: string; + name: string; + email: string; + }; +}; + +type PromptDiff = { + from_id: string; + to_id: string; + from_snapshot: object; + to_snapshot: object; + content_diff: string[]; + name_changed: boolean; + access_control_changed: boolean; }; export const createNewPrompt = async (token: string, prompt: PromptItem) => { @@ -19,7 +55,7 @@ export const createNewPrompt = async (token: string, prompt: PromptItem) => { }, body: JSON.stringify({ ...prompt, - command: `/${prompt.command}` + command: prompt.command.startsWith('/') ? prompt.command.slice(1) : prompt.command }) }) .then(async (res) => { @@ -104,6 +140,8 @@ export const getPromptList = async (token: string = '') => { export const getPromptByCommand = async (token: string, command: string) => { let error = null; + command = command.charAt(0) === '/' ? command.slice(1) : command; + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}`, { method: 'GET', headers: { @@ -136,7 +174,9 @@ export const getPromptByCommand = async (token: string, command: string) => { export const updatePromptByCommand = async (token: string, prompt: PromptItem) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${prompt.command}/update`, { + const command = prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/update`, { method: 'POST', headers: { Accept: 'application/json', @@ -145,7 +185,7 @@ export const updatePromptByCommand = async (token: string, prompt: PromptItem) = }, body: JSON.stringify({ ...prompt, - command: `/${prompt.command}` + command: command }) }) .then(async (res) => { @@ -169,6 +209,41 @@ export const updatePromptByCommand = async (token: string, prompt: PromptItem) = return res; }; +export const setProductionPromptVersion = async ( + token: string, + command: string, + version_id: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/set/version`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + version_id: version_id + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const deletePromptByCommand = async (token: string, command: string) => { let error = null; @@ -202,3 +277,200 @@ export const deletePromptByCommand = async (token: string, command: string) => { return res; }; + +//////////////////////////// +// Prompt History APIs +//////////////////////////// + +export const getPromptHistory = async ( + token: string, + command: string, + limit: number = 50, + offset: number = 0 +): Promise => { + let error = null; + + command = command.charAt(0) === '/' ? command.slice(1) : command; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/prompts/command/${command}/history?limit=${limit}&offset=${offset}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deletePromptHistoryVersion = async ( + token: string, + command: string, + historyId: string +): Promise => { + let error = null; + + command = command.charAt(0) === '/' ? command.slice(1) : command; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/prompts/command/${command}/history/${historyId}`, + { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return false; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptHistoryEntry = async ( + token: string, + command: string, + historyId: string +): Promise => { + let error = null; + + command = command.charAt(0) === '/' ? command.slice(1) : command; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/prompts/command/${command}/history/${historyId}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const restorePromptFromHistory = async ( + token: string, + command: string, + historyId: string, + commitMessage?: string +) => { + let error = null; + + command = command.charAt(0) === '/' ? command.slice(1) : command; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/prompts/command/${command}/history/${historyId}/restore`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + commit_message: commitMessage + }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptDiff = async ( + token: string, + command: string, + fromId: string, + toId: string +): Promise => { + let error = null; + + command = command.charAt(0) === '/' ? command.slice(1) : command; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/prompts/command/${command}/history/diff?from_id=${fromId}&to_id=${toId}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + + diff --git a/src/lib/components/chat/MessageInput/Commands/Prompts.svelte b/src/lib/components/chat/MessageInput/Commands/Prompts.svelte index 5df3c4691..0da053993 100644 --- a/src/lib/components/chat/MessageInput/Commands/Prompts.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Prompts.svelte @@ -14,7 +14,7 @@ $: filteredItems = prompts .filter((p) => p.command.toLowerCase().includes(query.toLowerCase())) - .sort((a, b) => a.title.localeCompare(b.title)); + .sort((a, b) => a.name.localeCompare(b.name)); $: if (query) { selectedPromptIdx = 0; @@ -42,7 +42,7 @@ {#if filteredItems.length > 0}
{#each filteredItems as promptItem, promptIdx} - + diff --git a/src/lib/components/common/Badge.svelte b/src/lib/components/common/Badge.svelte index c1513f8b6..8327a420c 100644 --- a/src/lib/components/common/Badge.svelte +++ b/src/lib/components/common/Badge.svelte @@ -12,8 +12,8 @@
{content}
diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 4a4d1b55a..4dd969405 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -343,7 +343,7 @@
-
{prompt.title}
+
{prompt.name}
{prompt.command}
diff --git a/src/lib/components/workspace/Prompts/PromptEditor.svelte b/src/lib/components/workspace/Prompts/PromptEditor.svelte index 9dc3a1a56..ddb56dada 100644 --- a/src/lib/components/workspace/Prompts/PromptEditor.svelte +++ b/src/lib/components/workspace/Prompts/PromptEditor.svelte @@ -4,12 +4,25 @@ import Textarea from '$lib/components/common/Textarea.svelte'; import { toast } from 'svelte-sonner'; import Tooltip from '$lib/components/common/Tooltip.svelte'; - import AccessControl from '../common/AccessControl.svelte'; import LockClosed from '$lib/components/icons/LockClosed.svelte'; import AccessControlModal from '../common/AccessControlModal.svelte'; import { user } from '$lib/stores'; - import { slugify } from '$lib/utils'; + import { slugify, formatDate } from '$lib/utils'; import Spinner from '$lib/components/common/Spinner.svelte'; + import Modal from '$lib/components/common/Modal.svelte'; + import XMark from '$lib/components/icons/XMark.svelte'; + import { + getPromptHistory, + updatePromptByCommand, + setProductionPromptVersion, + deletePromptHistoryVersion + } from '$lib/apis/prompts'; + import dayjs from 'dayjs'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; + import PromptHistoryMenu from './PromptHistoryMenu.svelte'; + import Badge from '$lib/components/common/Badge.svelte'; + + dayjs.extend(localizedFormat); export let onSubmit: Function; export let edit = false; @@ -20,22 +33,25 @@ const i18n = getContext('i18n'); let loading = false; + let showEditModal = false; - let title = ''; + let name = ''; let command = ''; let content = ''; + let commitMessage = ''; let accessControl = {}; - let showAccessControlModal = false; - let hasManualEdit = false; + let history: any[] = []; + let historyLoading = false; + let selectedHistoryEntry: any = null; + $: if (!edit && !hasManualEdit) { - command = title !== '' ? slugify(title) : ''; + command = name !== '' ? slugify(name) : ''; } - // Track manual edits function handleCommandInput(e: Event) { hasManualEdit = true; } @@ -49,11 +65,15 @@ if (validateCommandString(command)) { await onSubmit({ - title, + name, command, content, - access_control: accessControl + access_control: accessControl, + commit_message: commitMessage || undefined }); + showEditModal = false; + commitMessage = ''; + await loadHistory(); } else { toast.error( $i18n.t('Only alphanumeric characters and hyphens are allowed in the command string.') @@ -64,22 +84,79 @@ }; const validateCommandString = (inputString) => { - // Regular expression to match only alphanumeric characters, hyphen, and underscore const regex = /^[a-zA-Z0-9-_]+$/; - - // Test the input string against the regular expression return regex.test(inputString); }; + const loadHistory = async () => { + if (!prompt?.command || !edit) return; + historyLoading = true; + try { + history = await getPromptHistory(localStorage.token, prompt.command); + } catch (error) { + console.error('Failed to load history:', error); + history = []; + } + historyLoading = false; + }; + + const setAsProduction = async (historyEntry: any) => { + if (disabled) { + toast.error($i18n.t('You do not have permission to edit this prompt.')); + return; + } + + try { + await setProductionPromptVersion(localStorage.token, prompt.command, historyEntry.id); + // Update local prompt object to trigger reactivity + prompt = { ...prompt, version_id: historyEntry.id }; + toast.success($i18n.t('Production version updated')); + } catch (error) { + toast.error(`${error}`); + } + }; + + const handleDeleteHistory = async (historyId: string) => { + if (disabled) return; + + try { + await deletePromptHistoryVersion(localStorage.token, prompt.command, historyId); + toast.success($i18n.t('Version deleted')); + // Reload history + await loadHistory(); + // Reset selection if deleted entry was selected + if (selectedHistoryEntry?.id === historyId) { + selectedHistoryEntry = history.length > 0 ? history[0] : null; + } + } catch (error) { + toast.error(`${error}`); + } + }; + + const renderDate = (timestamp: number) => { + const dateVal = timestamp * 1000; + return $i18n.t(formatDate(dateVal), { + LOCALIZED_TIME: dayjs(dateVal).format('LT'), + LOCALIZED_DATE: dayjs(dateVal).format('L') + }); + }; onMount(async () => { if (prompt) { - title = prompt.title; + name = prompt.name || ''; await tick(); - command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command; content = prompt.content; - accessControl = prompt?.access_control === undefined ? {} : prompt?.access_control; + + if (edit) { + await loadHistory(); + // Auto-select production version + if (prompt.version_id && history.length > 0) { + selectedHistoryEntry = history.find((h) => h.id === prompt.version_id) || history[0]; + } else if (history.length > 0) { + selectedHistoryEntry = history[0]; + } + } } }); @@ -92,124 +169,328 @@ sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin'} /> -
-
{ - submitHandler(); - }} - > -
- + +
+
+
{$i18n.t('Edit Prompt')}
+ -
- {/if} -
- -
-
/
- -
-
- + +
-
-
-
{$i18n.t('Prompt Content')}
+ +
+ +
+
+ +
+
+
/
+ +
+
+
-
-
+
+
+
{$i18n.t('Prompt Content')}
+
+ +