From 32810b41528d55307c7edf5b91a6821eccb863e8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 24 Jan 2026 04:13:19 +0400 Subject: [PATCH] refac --- backend/open_webui/models/prompts.py | 25 ++ backend/open_webui/routers/prompts.py | 69 +++++- src/lib/apis/prompts/index.ts | 36 ++- .../workspace/Prompts/PromptEditor.svelte | 225 +++++++++++------- 4 files changed, 270 insertions(+), 85 deletions(-) diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index 537d328c3..2d79cf13f 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -307,12 +307,14 @@ class PromptsTable: # Check if content changed to decide on history creation content_changed = ( prompt.name != form_data.name + or prompt.command != form_data.command or prompt.content != form_data.content or prompt.access_control != form_data.access_control ) # Update prompt fields prompt.name = form_data.name + prompt.command = form_data.command prompt.content = form_data.content prompt.data = form_data.data or prompt.data prompt.meta = form_data.meta or prompt.meta @@ -350,6 +352,29 @@ class PromptsTable: except Exception: return None + def update_prompt_metadata( + self, + prompt_id: str, + name: str, + command: str, + db: Optional[Session] = None, + ) -> Optional[PromptModel]: + """Update only name and command (no history created).""" + try: + with get_db_context(db) as db: + prompt = db.query(Prompt).filter_by(id=prompt_id).first() + if not prompt: + return None + + prompt.name = name + prompt.command = command + prompt.updated_at = int(time.time()) + db.commit() + + return PromptModel.model_validate(prompt) + except Exception: + return None + def update_prompt_version( self, prompt_id: str, diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 263bf7617..639658c59 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -25,6 +25,12 @@ from pydantic import BaseModel class PromptVersionUpdateForm(BaseModel): version_id: str + +class PromptMetadataForm(BaseModel): + name: str + command: str + + router = APIRouter() @@ -209,6 +215,15 @@ async def update_prompt_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) + # Check for command collision if command is being changed + if form_data.command != prompt.command: + existing_prompt = Prompts.get_prompt_by_command(form_data.command, db=db) + if existing_prompt and existing_prompt.id != prompt.id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Command '/{form_data.command}' is already in use by another prompt", + ) + # Use the ID from the found prompt updated_prompt = Prompts.update_prompt_by_id( prompt.id, form_data, user.id, db=db @@ -222,7 +237,59 @@ async def update_prompt_by_id( ) -@router.post("/id/{prompt_id}/set/version", response_model=Optional[PromptModel]) +############################ +# UpdatePromptMetadata +############################ + + +@router.post("/id/{prompt_id}/update/meta", response_model=Optional[PromptModel]) +async def update_prompt_metadata( + prompt_id: str, + form_data: PromptMetadataForm, + user=Depends(get_verified_user), + db: Session = Depends(get_session), +): + """Update prompt name and command only (no history created).""" + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not has_access(user.id, "write", prompt.access_control, db=db) + and user.role != "admin" + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Check for command collision if command is being changed + if form_data.command != prompt.command: + existing_prompt = Prompts.get_prompt_by_command(form_data.command, db=db) + if existing_prompt and existing_prompt.id != prompt.id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Command '/{form_data.command}' is already in use", + ) + + updated_prompt = Prompts.update_prompt_metadata( + prompt.id, form_data.name, form_data.command, db=db + ) + if updated_prompt: + return updated_prompt + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +@router.post("/id/{prompt_id}/update/version", response_model=Optional[PromptModel]) async def set_prompt_version( prompt_id: str, form_data: PromptVersionUpdateForm, diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts index b17079085..791a98bb4 100644 --- a/src/lib/apis/prompts/index.ts +++ b/src/lib/apis/prompts/index.ts @@ -238,6 +238,40 @@ export const updatePromptById = async (token: string, prompt: PromptItem) => { return res; }; +export const updatePromptMetadata = async ( + token: string, + promptId: string, + name: string, + command: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/update/meta`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ name, command }) + }) + .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 setProductionPromptVersion = async ( token: string, promptId: string, @@ -245,7 +279,7 @@ export const setProductionPromptVersion = async ( ) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/set/version`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/update/version`, { method: 'POST', headers: { Accept: 'application/json', diff --git a/src/lib/components/workspace/Prompts/PromptEditor.svelte b/src/lib/components/workspace/Prompts/PromptEditor.svelte index 5bfbd50a1..f2e9cdbf1 100644 --- a/src/lib/components/workspace/Prompts/PromptEditor.svelte +++ b/src/lib/components/workspace/Prompts/PromptEditor.svelte @@ -16,7 +16,8 @@ import { getPromptHistory, setProductionPromptVersion, - deletePromptHistoryVersion + deletePromptHistoryVersion, + updatePromptMetadata } from '$lib/apis/prompts'; import dayjs from 'dayjs'; import localizedFormat from 'dayjs/plugin/localizedFormat'; @@ -53,6 +54,11 @@ let historyHasMore = true; let contentCopied = false; + // For debounced auto-save of name/command + let originalName = ''; + let originalCommand = ''; + let debounceTimer: ReturnType | null = null; + $: if (!edit && !hasManualEdit) { command = name !== '' ? slugify(name) : ''; } @@ -191,6 +197,41 @@ LOCALIZED_DATE: dayjs(dateVal).format('L') }); }; + + const debouncedSaveMetadata = () => { + if (disabled || !edit) return; + + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + debounceTimer = setTimeout(async () => { + // Skip if nothing changed + if (name === originalName && command === originalCommand) return; + + if (!validateCommandString(command)) { + toast.error( + $i18n.t('Only alphanumeric characters and hyphens are allowed in the command string.') + ); + command = originalCommand; + return; + } + + try { + await updatePromptMetadata(localStorage.token, prompt?.id, name, command); + // Update originals on success + originalName = name; + originalCommand = command; + toast.success($i18n.t('Saved')); + } catch (error) { + toast.error(`${error}`); + // Revert on error (collision) + name = originalName; + command = originalCommand; + } + }, 500); + }; + onMount(async () => { if (prompt) { name = prompt.name || ''; @@ -199,6 +240,10 @@ content = prompt.content; accessControl = prompt?.access_control === undefined ? {} : prompt?.access_control; + // Store originals for revert on collision + originalName = name; + originalCommand = command; + if (edit) { await loadHistory(); // Auto-select production version @@ -295,87 +340,101 @@ {#if edit} -
- -
- -
-
-

{name}

-
/{command}
+
+ +
+
+ +
+ / +
-
- {#if !disabled} - - - {:else} - {$i18n.t('Read Only')} - {/if} +
+
+ {#if !disabled} + + + {:else} + {$i18n.t('Read Only')} + {/if} +
+
+ +
+ + -
- - - - -
-
-
-
- {$i18n.t('Prompt Content')} -
- {#if selectedHistoryEntry} - - {selectedHistoryEntry.id.slice(0, 7)} - - {/if} + +
+
+
+
+ {$i18n.t('Prompt Content')}
- - {#if selectedHistoryEntry && !disabled} -
- {#if selectedHistoryEntry.id === prompt?.version_id} - - {:else} - - {/if} - handleDeleteHistory(selectedHistoryEntry.id)} - onClose={() => {}} - /> -
+ {#if selectedHistoryEntry} + + {selectedHistoryEntry.id.slice(0, 7)} + {/if}
-
+ + {#if selectedHistoryEntry && !disabled} +
+ {#if selectedHistoryEntry.id === prompt?.version_id} + + {:else} + + {/if} + handleDeleteHistory(selectedHistoryEntry.id)} + onClose={() => {}} + /> +
+ {/if} +
+ +
+ +
+
+ +
{selectedHistoryEntry?.snapshot
 								?.content || content}
- - -
- {@render historySection()} -
@@ -477,8 +536,8 @@ {/if} {#snippet historySection()} -
-
+
+
{$i18n.t('History')}
{#if historyLoading} @@ -486,7 +545,7 @@
{#if history.length > 0} -
+
{#each history as entry, index}
@@ -513,7 +572,7 @@ {entry.user.name} (e.target.src = '/user.png')} /> {entry.user.name}