diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 0b6354b0b6..967d1b59ea 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -635,6 +635,50 @@ async def get_prompt_history( return history +@router.get('/id/{prompt_id}/history/diff') +async def get_prompt_diff( + prompt_id: str, + from_id: str, + to_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Get diff between two versions.""" + prompt = await 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, + ) + + # Check read access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + diff = await PromptHistories.compute_diff(from_id, to_id, prompt.id, db=db) + if not diff: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + return diff + + @router.get('/id/{prompt_id}/history/{history_id}', response_model=PromptHistoryModel) async def get_prompt_history_entry( prompt_id: str, @@ -726,47 +770,3 @@ async def delete_prompt_history_entry( ) return success - - -@router.get('/id/{prompt_id}/history/diff') -async def get_prompt_diff( - prompt_id: str, - from_id: str, - to_id: str, - user=Depends(get_verified_user), - db: AsyncSession = Depends(get_async_session), -): - """Get diff between two versions.""" - prompt = await 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, - ) - - # Check read access - if not ( - user.role == 'admin' - or prompt.user_id == user.id - or await AccessGrants.has_access( - user_id=user.id, - resource_type='prompt', - resource_id=prompt.id, - permission='read', - db=db, - ) - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - - diff = await PromptHistories.compute_diff(from_id, to_id, prompt.id, db=db) - if not diff: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=ERROR_MESSAGES.NOT_FOUND, - ) - - return diff diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index 033b2bd5af..933966cb9f 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -664,7 +664,8 @@ async def diff_skill_file( return { 'binary': False, 'diff': ''.join( - difflib.unified_diff( + line if line.endswith('\n') else line + '\n\\ No newline at end of file\n' + for line in difflib.unified_diff( (files[0] or {}).get('content', '').splitlines(True), (files[1] or {}).get('content', '').splitlines(True), fromfile=f'{from_id[:7]}/{path}', diff --git a/src/lib/components/chat/FileNav/FileEntryRow.svelte b/src/lib/components/chat/FileNav/FileEntryRow.svelte index ea3b61a1ba..eecfa45386 100644 --- a/src/lib/components/chat/FileNav/FileEntryRow.svelte +++ b/src/lib/components/chat/FileNav/FileEntryRow.svelte @@ -339,7 +339,7 @@ {entry.name} {/if} - {#if !writable && !renaming} + {#if !writable && !renaming && variant !== 'workspace'} {$i18n.t('Read-only')} {/if} {#if entry.type === 'file' && entry.size !== undefined && !renaming} diff --git a/src/lib/components/common/Tooltip.svelte b/src/lib/components/common/Tooltip.svelte index 26f41406fc..011714d891 100644 --- a/src/lib/components/common/Tooltip.svelte +++ b/src/lib/components/common/Tooltip.svelte @@ -41,7 +41,7 @@ if (elementId) { tooltipContent = document.getElementById(elementId); } else { - tooltipContent = DOMPurify.sanitize(content); + tooltipContent = allowHTML ? DOMPurify.sanitize(content) : content; } // After the element changes, the old instance must be destroyed, otherwise the detached tippy floating DOM will be left behind diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index e2fb76af85..979c3c1db6 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -258,10 +258,7 @@ }; const createPromptHandler = async (prompt: PromptDraft) => { - const res = await createNewPrompt(localStorage.token, prompt).catch((error) => { - toast.error(`${error}`); - return null; - }); + const res = await createNewPrompt(localStorage.token, prompt); if (res) { toast.success($i18n.t('Prompt created successfully')); @@ -269,6 +266,7 @@ await getPromptList(); await closeCreateModal(); } + return res; }; const cloneHandler = async (prompt) => { diff --git a/src/lib/components/workspace/Prompts/PromptEditor.svelte b/src/lib/components/workspace/Prompts/PromptEditor.svelte index fef9a6219e..57e2330c63 100644 --- a/src/lib/components/workspace/Prompts/PromptEditor.svelte +++ b/src/lib/components/workspace/Prompts/PromptEditor.svelte @@ -1,24 +1,28 @@ - - -
-
-
{$i18n.t('Edit Prompt')}
- + + +
+
+
+ {#if modal} + {$i18n.t(clone ? 'Clone Prompt' : 'Create Prompt')} + {:else} + + {/if} +
+ {#if !disabled} (showAccessControlModal = true)} /> + {:else}{$i18n.t('Read Only')}{/if} + {#if modal}{/if} +
- -
-
-
{$i18n.t('Prompt Content')}
-
- -
- - {:else} - + {/if}
- {#if history.length > 0} -
- {#each history as entry, index} - - {/each} - - {#if historyLoading} -
- -
- {/if} -
- {:else if !historyLoading} -
- {$i18n.t('No history available')} + {#if historical} +
+ {selectedHistoryEntry.id.slice(0, 7)} · {renderDate( + selectedHistoryEntry.created_at + )} + {#if !editingHistory}
+ + {#if !disabled} + + {/if} +
{/if}
{/if} -
-{/snippet} + + {#if !readOnly} +
+ {#if edit}{:else}
{/if} +
+ {#if edit}{/if} + +
+
+ {/if} + +
diff --git a/src/lib/components/workspace/Skills/SkillEditor.svelte b/src/lib/components/workspace/Skills/SkillEditor.svelte index b8da223aa5..9753744a56 100644 --- a/src/lib/components/workspace/Skills/SkillEditor.svelte +++ b/src/lib/components/workspace/Skills/SkillEditor.svelte @@ -6,7 +6,7 @@ import AccessButton from '$lib/components/common/AccessButton.svelte'; import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; - import Check from '$lib/components/icons/Check.svelte'; + import VersionMenuItem from '../common/VersionMenuItem.svelte'; import Download from '$lib/components/icons/Download.svelte'; import Dropdown from '$lib/components/common/Dropdown.svelte'; import DropdownMenu from '$lib/components/common/DropdownMenu.svelte'; @@ -21,6 +21,7 @@ import { updateSkillAccessGrants } from '$lib/apis/skills'; import { goto, beforeNavigate } from '$app/navigation'; import SkillFiles from './SkillFiles.svelte'; + import VersionDiff from '../common/VersionDiff.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import { getSkillHistory, @@ -55,16 +56,16 @@ let versionId: string | null = null; let currentVersionId: string | null = null; let history: any[] = []; + let currentHistoryEntry: any = null; let historyPage = 1; let showHistory = false; let historyLoading = false; let historyError = false; - let currentVersionName = ''; let commitMessage = ''; let conflict = false; let baseline = ''; let historyDiff: any = null; - let fileDiff = ''; + let comparing = false; let discard = false; let discardAction = () => {}; $: historical = edit && versionId !== currentVersionId; @@ -88,6 +89,11 @@ historyError = false; try { history = await getSkillHistory(localStorage.token, id, historyPage); + currentHistoryEntry = + history.find((entry) => entry.id === currentVersionId) || + (currentHistoryEntry?.id === currentVersionId + ? currentHistoryEntry + : await getSkillVersion(localStorage.token, id, currentVersionId!)); } catch (error) { historyError = true; toast.error(skillError(error)); @@ -100,17 +106,23 @@ if (selected !== versionId) confirmDiscard(() => selectVersion(selected)); }; const compareToCurrent = async () => { + if (comparing || historyDiff) return; + const fromId = versionId!; + const toId = currentVersionId!; + comparing = true; try { - historyDiff = await ( + const result = await ( await skillRequest( localStorage.token, - `/id/${id}/history/diff?${new URLSearchParams({ from_id: versionId!, to_id: currentVersionId! })}` + `/id/${id}/history/diff?${new URLSearchParams({ from_id: fromId, to_id: toId })}` ) ).json(); - historyDiff.fromId = versionId; - fileDiff = ''; + if (versionId === fromId && currentVersionId === toId) + historyDiff = { ...result, fromId, toId }; } catch (error) { toast.error(skillError(error)); + } finally { + comparing = false; } }; const selectVersion = async (selected: string) => { @@ -123,7 +135,6 @@ reloadKey++; remember(); historyDiff = null; - fileDiff = ''; } catch (error) { toast.error(skillError(error)); } @@ -132,7 +143,6 @@ try { const latest = await getSkillById(localStorage.token, id); currentVersionId = latest.version_id; - currentVersionName = latest.name; await selectVersion(latest.version_id); conflict = false; } catch (error) { @@ -148,10 +158,8 @@ currentVersionId! ); currentVersionId = restored.version_id; - currentVersionName = restored.name; versionId = restored.version_id; historyDiff = null; - fileDiff = ''; remember(); await loadHistory(); toast.success($i18n.t('Saved')); @@ -224,7 +232,6 @@ }); if (result) { currentVersionId = result.version_id; - currentVersionName = result.name; versionId = result.version_id; commitMessage = ''; conflict = false; @@ -249,7 +256,6 @@ description = skill.description || ''; content = skill.content || ''; currentVersionId = skill.version_id || null; - currentVersionName = skill.name; versionId = currentVersionId; accessGrants = skill?.access_grants === undefined ? [] : skill?.access_grants; } @@ -373,57 +379,6 @@
- {#if historical} -
- {$i18n.t('Read Only')} - - {#if !disabled}{/if} -
- {/if} - {#if historyDiff} -
- - - {#each Object.entries(historyDiff.metadata) as [key, values]}

- {key}: {JSON.stringify(values)} -

{/each} - {#each historyDiff.files as file}{/each} -
{fileDiff}
-
- {/if} {#if conflict}