mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-10 03:27:57 +00:00
refac
This commit is contained in:
parent
178de36662
commit
37138282fb
11 changed files with 874 additions and 738 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}',
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@
|
|||
{entry.name}
|
||||
</span>
|
||||
{/if}
|
||||
{#if !writable && !renaming}
|
||||
{#if !writable && !renaming && variant !== 'workspace'}
|
||||
<span class="text-[0.625rem] text-gray-400 shrink-0">{$i18n.t('Read-only')}</span>
|
||||
{/if}
|
||||
{#if entry.type === 'file' && entry.size !== undefined && !renaming}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#if historical}
|
||||
<div class="flex w-full flex-wrap items-center gap-3 px-1 py-2 text-xs text-gray-500">
|
||||
<span>{$i18n.t('Read Only')}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={compareToCurrent}>{$i18n.t('Compare to current')}</button
|
||||
>
|
||||
{#if !disabled}<button
|
||||
type="button"
|
||||
class="hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={restore}>{$i18n.t('Restore as new version')}</button
|
||||
>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if historyDiff}
|
||||
<div
|
||||
class="max-h-56 shrink-0 overflow-auto rounded-lg border border-gray-100 p-2 text-xs dark:border-gray-800"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="mb-2 text-gray-500"
|
||||
on:click={() => {
|
||||
historyDiff = null;
|
||||
fileDiff = '';
|
||||
}}>{$i18n.t('Close')}</button
|
||||
>
|
||||
|
||||
{#each Object.entries(historyDiff.metadata) as [key, values]}<p>
|
||||
{key}: {JSON.stringify(values)}
|
||||
</p>{/each}
|
||||
{#each historyDiff.files as file}<button
|
||||
type="button"
|
||||
class="block py-1"
|
||||
on:click={async () => {
|
||||
try {
|
||||
const result = await (
|
||||
await skillRequest(
|
||||
localStorage.token,
|
||||
`/id/${id}/history/diff/file?${new URLSearchParams({ from_id: historyDiff.fromId, to_id: currentVersionId!, path: file.path })}`
|
||||
)
|
||||
).json();
|
||||
fileDiff = result.binary ? 'Binary file changed' : result.diff;
|
||||
} catch (error) {
|
||||
toast.error(skillError(error));
|
||||
}
|
||||
}}>{file.status}: {file.path}</button
|
||||
>{/each}
|
||||
<pre class="whitespace-pre-wrap">{fileDiff}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
{#if conflict}<div
|
||||
role="alert"
|
||||
class="flex flex-wrap gap-3 bg-amber-50 p-3 text-xs text-amber-900 dark:bg-amber-950 dark:text-amber-100"
|
||||
|
|
@ -439,7 +394,24 @@
|
|||
<div
|
||||
class="min-h-0 flex-1 overflow-hidden rounded-2xl border border-gray-100 bg-gray-50/60 dark:border-white/5 dark:bg-white/[0.03]"
|
||||
>
|
||||
{#if ready}<SkillFiles
|
||||
{#if historyDiff}
|
||||
<VersionDiff
|
||||
diff={historyDiff}
|
||||
onClose={() => (historyDiff = null)}
|
||||
loadFileDiff={async (path) => {
|
||||
try {
|
||||
return await (
|
||||
await skillRequest(
|
||||
localStorage.token,
|
||||
`/id/${id}/history/diff/file?${new URLSearchParams({ from_id: historyDiff.fromId, to_id: historyDiff.toId, path })}`
|
||||
)
|
||||
).json();
|
||||
} catch (error) {
|
||||
throw new Error(skillError(error));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if ready}<SkillFiles
|
||||
bind:this={fileEditor}
|
||||
skillId={id}
|
||||
{versionId}
|
||||
|
|
@ -477,21 +449,17 @@
|
|||
<ChevronDown className="size-3 shrink-0" />
|
||||
</button>
|
||||
<div slot="content">
|
||||
<DropdownMenu className="w-56 max-w-[calc(100vw-2rem)]">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={versionId === currentVersionId}
|
||||
on:click={() => chooseVersion(currentVersionId!)}
|
||||
title={currentVersionId || ''}
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate text-left"
|
||||
>{currentVersionName} · {$i18n.t('Current')}</span
|
||||
>
|
||||
{#if versionId === currentVersionId}<Check
|
||||
className="size-3.5 text-blue-500"
|
||||
/>{/if}
|
||||
</button>
|
||||
<DropdownMenu
|
||||
className="w-56 max-w-[calc(100vw-2rem)] max-h-80 overflow-y-auto"
|
||||
>
|
||||
<VersionMenuItem
|
||||
entry={currentHistoryEntry?.id === currentVersionId
|
||||
? currentHistoryEntry
|
||||
: null}
|
||||
status={$i18n.t('Current')}
|
||||
selected={versionId === currentVersionId}
|
||||
onSelect={() => chooseVersion(currentVersionId!)}
|
||||
/>
|
||||
{#if historyLoading || historyError || history.some((entry) => entry.id !== currentVersionId)}
|
||||
<hr class="border-gray-100 dark:border-gray-800" />
|
||||
{/if}
|
||||
|
|
@ -503,20 +471,11 @@
|
|||
<button type="button" on:click={loadHistory}>{$i18n.t('Retry')}</button>
|
||||
{:else}
|
||||
{#each history.filter((entry) => entry.id !== currentVersionId) as entry}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={entry.id === versionId}
|
||||
title={`${entry.id} · ${new Date(entry.created_at * 1000).toLocaleString()} · ${entry.user?.name || entry.user_id}`}
|
||||
on:click={() => chooseVersion(entry.id)}
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate text-left"
|
||||
>{entry.commit_message || entry.id.slice(0, 7)}</span
|
||||
>
|
||||
{#if entry.id === versionId}<Check
|
||||
className="size-3.5 text-blue-500"
|
||||
/>{/if}
|
||||
</button>
|
||||
<VersionMenuItem
|
||||
{entry}
|
||||
selected={entry.id === versionId}
|
||||
onSelect={() => chooseVersion(entry.id)}
|
||||
/>
|
||||
{:else}
|
||||
{#if historyPage > 1}
|
||||
<div class="px-2 py-2 text-xs text-gray-500">
|
||||
|
|
@ -565,6 +524,30 @@
|
|||
</SkillFiles>{/if}
|
||||
</div>
|
||||
|
||||
{#if historical}
|
||||
<div
|
||||
class="flex w-full shrink-0 flex-wrap items-center gap-2 px-1 py-2 text-xs text-gray-500"
|
||||
>
|
||||
<span class="mr-auto">{$i18n.t('Read Only')}</span>
|
||||
<div class="ml-auto flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-7 items-center rounded-lg bg-gray-100 px-2.5 text-xs text-gray-700 disabled:opacity-60 dark:bg-gray-850 dark:text-gray-200"
|
||||
disabled={comparing || !!historyDiff}
|
||||
on:click={compareToCurrent}
|
||||
>{#if comparing}<Spinner className="mr-1.5 size-3" />{/if}{$i18n.t(
|
||||
'Compare to current'
|
||||
)}</button
|
||||
>
|
||||
{#if !disabled}<button
|
||||
type="button"
|
||||
class="flex h-7 items-center rounded-lg bg-gray-900 px-2.5 text-xs text-white transition hover:bg-black dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white"
|
||||
on:click={restore}>{$i18n.t('Restore as new version')}</button
|
||||
>{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !readOnly}
|
||||
<div class="flex shrink-0 justify-end gap-3 py-2">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
import Folder from '$lib/components/icons/Folder.svelte';
|
||||
import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import FileCodeEditor from '$lib/components/chat/FileNav/FileCodeEditor.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import {
|
||||
|
|
@ -651,6 +653,15 @@
|
|||
<div class="h-full min-h-0 w-full overflow-auto p-1.5">
|
||||
<div class="mb-1 flex w-full items-center gap-1">
|
||||
<div class="min-w-0 flex-1">{@render version()}</div>
|
||||
{#if readOnly}
|
||||
<Tooltip content={$i18n.t('Read Only')}>
|
||||
<span
|
||||
class="flex items-center px-0.5 text-gray-400"
|
||||
role="img"
|
||||
aria-label={$i18n.t('Read Only')}><LockClosed className="size-3" /></span
|
||||
>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if !readOnly || canExport}
|
||||
<Dropdown closeOnSelect align="end">
|
||||
<button
|
||||
|
|
|
|||
184
src/lib/components/workspace/common/VersionDiff.svelte
Normal file
184
src/lib/components/workspace/common/VersionDiff.svelte
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import DiffBlock from '$lib/components/chat/Messages/DiffBlock.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let currentLabel = 'Current';
|
||||
export let loadFileDiff: (path: string) => Promise<{ binary?: boolean; diff?: string }>;
|
||||
export let diff: {
|
||||
fromId: string;
|
||||
toId: string;
|
||||
metadata: Record<string, { before: unknown; after: unknown }>;
|
||||
files: { path: string; status: 'added' | 'deleted' | 'modified'; binary: boolean }[];
|
||||
};
|
||||
export let onClose: () => void;
|
||||
const i18n = getContext<any>('i18n');
|
||||
let selectedPath: string | null = null;
|
||||
let results: Record<
|
||||
string,
|
||||
{ loading?: boolean; error?: string; binary?: boolean; diff?: string }
|
||||
> = {};
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === 'object' && !Array.isArray(value);
|
||||
const displayValue = (value: unknown): string => {
|
||||
if (value == null) return '';
|
||||
if (Array.isArray(value)) return value.map(displayValue).join(', ');
|
||||
if (isObject(value))
|
||||
return Object.entries(value)
|
||||
.map(([key, item]) => `${key}: ${displayValue(item)}`)
|
||||
.join(', ');
|
||||
return String(value);
|
||||
};
|
||||
function changes(
|
||||
path: string[],
|
||||
before: unknown,
|
||||
after: unknown
|
||||
): { path: string[]; before: string; after: string }[] {
|
||||
if (
|
||||
(isObject(before) || isObject(after)) &&
|
||||
(before == null || isObject(before)) &&
|
||||
(after == null || isObject(after))
|
||||
) {
|
||||
const a = isObject(before) ? before : {};
|
||||
const b = isObject(after) ? after : {};
|
||||
return [...new Set([...Object.keys(a), ...Object.keys(b)])].flatMap((key) =>
|
||||
changes([...path, key], a[key], b[key])
|
||||
);
|
||||
}
|
||||
const a = displayValue(before);
|
||||
const b = displayValue(after);
|
||||
return a === b ? [] : [{ path, before: a, after: b }];
|
||||
}
|
||||
$: metadata = Object.entries(diff.metadata).flatMap(([key, value]) =>
|
||||
changes(key === 'meta' ? [] : [key], value.before, value.after)
|
||||
);
|
||||
const labels: Record<string, string> = {
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
tags: 'Tags',
|
||||
i18n: 'Translations'
|
||||
};
|
||||
const statuses = { added: 'Added', deleted: 'Deleted', modified: 'Modified' };
|
||||
|
||||
async function loadFile(path: string) {
|
||||
if (results[path]?.loading || results[path]?.diff !== undefined || results[path]?.binary)
|
||||
return;
|
||||
results = { ...results, [path]: { loading: true } };
|
||||
try {
|
||||
results = { ...results, [path]: await loadFileDiff(path) };
|
||||
} catch (error) {
|
||||
results = {
|
||||
...results,
|
||||
[path]: { error: error instanceof Error ? error.message : String(error) }
|
||||
};
|
||||
}
|
||||
}
|
||||
function selectFile(path: string) {
|
||||
selectedPath = selectedPath === path ? null : path;
|
||||
if (selectedPath) loadFile(path);
|
||||
}
|
||||
onMount(() => {
|
||||
if (diff.files.length) selectFile(diff.files[0].path);
|
||||
});
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="flex h-full min-h-0 flex-col bg-white text-xs dark:bg-gray-900"
|
||||
aria-label={$i18n.t('Compare to current')}
|
||||
>
|
||||
<div class="flex shrink-0 items-center gap-2 bg-gray-50/60 px-3 py-1.5 dark:bg-black">
|
||||
<span class="min-w-0 flex-1 font-medium">{$i18n.t('Compare to current')}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-500 hover:text-gray-900 dark:hover:text-gray-100"
|
||||
aria-label={$i18n.t('Close')}
|
||||
on:click={onClose}><XMark className="size-3.5" /></button
|
||||
>
|
||||
</div>
|
||||
<div class="grid shrink-0 grid-cols-2 border-b border-gray-100 dark:border-white/5">
|
||||
<div class="min-w-0 px-3 py-2 text-gray-500">
|
||||
<span class="text-red-600 dark:text-red-400">−</span>
|
||||
{$i18n.t('Selected version')} <span class="font-mono">{diff.fromId.slice(0, 7)}</span>
|
||||
</div>
|
||||
<div class="min-w-0 px-3 py-2 text-gray-500">
|
||||
<span class="text-green-600 dark:text-green-400">+</span>
|
||||
{$i18n.t(currentLabel)} <span class="font-mono">{diff.toId.slice(0, 7)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
{#each metadata as change}
|
||||
<div class="border-b border-gray-100 dark:border-white/5">
|
||||
<div class="px-3 py-1.5 font-medium text-gray-600 dark:text-gray-300">
|
||||
{change.path.map((key) => $i18n.t(labels[key] || key)).join(' / ')}
|
||||
</div>
|
||||
<div class="grid grid-cols-2">
|
||||
<div
|
||||
class="min-w-0 whitespace-pre-wrap break-words bg-red-50 px-3 py-2 text-red-800 dark:bg-red-950/30 dark:text-red-200"
|
||||
>
|
||||
{change.before || '—'}
|
||||
</div>
|
||||
<div
|
||||
class="min-w-0 whitespace-pre-wrap break-words bg-green-50 px-3 py-2 text-green-800 dark:bg-green-950/30 dark:text-green-200"
|
||||
>
|
||||
{change.after || '—'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#each diff.files as file, index}
|
||||
<div class="border-b border-gray-100 dark:border-white/5">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left"
|
||||
aria-expanded={selectedPath === file.path}
|
||||
aria-controls={`version-diff-file-${index}`}
|
||||
on:click={() => selectFile(file.path)}
|
||||
>
|
||||
<span class:rotate-[-90deg]={selectedPath !== file.path}
|
||||
><ChevronDown className="size-3 shrink-0 text-gray-400" /></span
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate font-mono" title={file.path}>{file.path}</span>
|
||||
<span
|
||||
class="shrink-0 rounded px-1.5 py-0.5 text-[0.625rem] {file.status === 'added'
|
||||
? 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-400'
|
||||
: file.status === 'deleted'
|
||||
? 'bg-red-50 text-red-700 dark:bg-red-950/40 dark:text-red-400'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-gray-850 dark:text-gray-400'}"
|
||||
>{$i18n.t(statuses[file.status])}</span
|
||||
>
|
||||
</button>
|
||||
{#if selectedPath === file.path}
|
||||
<div id={`version-diff-file-${index}`} class="min-w-0 [&_.diff-block]:text-xs">
|
||||
{#if results[file.path]?.loading}
|
||||
<div class="flex items-center gap-2 px-3 py-3 text-gray-500" role="status">
|
||||
<Spinner className="size-3" />{$i18n.t('Loading...')}
|
||||
</div>
|
||||
{:else if results[file.path]?.error}
|
||||
<div class="px-3 py-3 text-gray-500" role="alert">
|
||||
{results[file.path].error}<button
|
||||
type="button"
|
||||
class="ml-2 underline"
|
||||
on:click={() => loadFile(file.path)}>{$i18n.t('Retry')}</button
|
||||
>
|
||||
</div>
|
||||
{:else if results[file.path]?.binary}
|
||||
<p class="px-3 py-3 text-gray-500">
|
||||
{$i18n.t('Binary file — text comparison is unavailable.')}
|
||||
</p>
|
||||
{:else if results[file.path]?.diff}
|
||||
<DiffBlock code={results[file.path].diff} />
|
||||
{:else}
|
||||
<p class="px-3 py-3 text-gray-500">{$i18n.t('No text differences')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if !metadata.length && !diff.files.length}
|
||||
<p class="px-3 py-4 text-gray-500">{$i18n.t('No differences')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
49
src/lib/components/workspace/common/VersionMenuItem.svelte
Normal file
49
src/lib/components/workspace/common/VersionMenuItem.svelte
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { user } from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
|
||||
export let entry: {
|
||||
id: string;
|
||||
commit_message?: string | null;
|
||||
created_at?: number;
|
||||
user_id?: string;
|
||||
user?: { id?: string; name?: string } | null;
|
||||
} | null = null;
|
||||
export let status = '';
|
||||
export let selected = false;
|
||||
export let onSelect: () => void;
|
||||
const i18n = getContext<any>('i18n');
|
||||
$: authorId = entry?.user?.id || entry?.user_id;
|
||||
$: message = entry?.commit_message || entry?.id.slice(0, 7) || status;
|
||||
$: author =
|
||||
entry?.user?.name ||
|
||||
($user?.id === authorId ? $user.name : authorId ? $i18n.t('Unknown user') : '');
|
||||
$: date = entry?.created_at ? new Date(entry.created_at * 1000) : null;
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-label={status || message}
|
||||
aria-checked={selected}
|
||||
class="text-left"
|
||||
title={entry
|
||||
? `${entry.id}${date ? ` · ${date.toLocaleString($i18n.language)}` : ''}`
|
||||
: undefined}
|
||||
on:click={onSelect}
|
||||
>
|
||||
<Tooltip as="span" className="flex shrink-0" content={author} allowHTML={false}>
|
||||
<ProfileImage
|
||||
src={authorId ? `/api/v1/users/${authorId}/profile/image` : '/user.png'}
|
||||
className="size-4 shrink-0 rounded-full!"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip as="span" className="block min-w-0 flex-1" content={message} allowHTML={false}>
|
||||
<span class="block truncate text-xs">{message}</span>
|
||||
</Tooltip>
|
||||
{#if status && entry}<span class="shrink-0 text-[0.625rem] text-gray-400">{status}</span>{/if}
|
||||
{#if selected}<Check className="size-3.5 shrink-0 text-blue-500" />{/if}
|
||||
</button>
|
||||
|
|
@ -17,12 +17,7 @@
|
|||
$: promptId = $page.params.id;
|
||||
|
||||
const onSubmit = async (_prompt) => {
|
||||
console.log(_prompt);
|
||||
const updatedPrompt = await updatePromptById(localStorage.token, _prompt).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
const updatedPrompt = await updatePromptById(localStorage.token, _prompt);
|
||||
if (updatedPrompt) {
|
||||
toast.success($i18n.t('Prompt updated successfully'));
|
||||
// Update local prompt state to reflect the new version
|
||||
|
|
@ -37,6 +32,7 @@
|
|||
updatedPrompt?.access_grants === undefined ? [] : updatedPrompt?.access_grants
|
||||
};
|
||||
}
|
||||
return updatedPrompt;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue