271 lines
7.6 KiB
Svelte
271 lines
7.6 KiB
Svelte
<script lang="ts">
|
|
import { onMount, tick, getContext } from 'svelte';
|
|
|
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
|
import { toast } from 'svelte-sonner';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
|
import AccessControlModal from '../common/AccessControlModal.svelte';
|
|
import { user } from '$lib/stores';
|
|
import { slugify, parseFrontmatter, formatSkillName } from '$lib/utils';
|
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
|
import { updateSkillAccessGrants } from '$lib/apis/skills';
|
|
import { goto } from '$app/navigation';
|
|
|
|
export let onSubmit: Function;
|
|
export let edit = false;
|
|
export let skill = null;
|
|
export let clone = false;
|
|
export let disabled = false;
|
|
|
|
const i18n = getContext('i18n');
|
|
|
|
let loading = false;
|
|
|
|
let name = '';
|
|
let id = '';
|
|
let description = '';
|
|
let content = '';
|
|
|
|
let accessGrants = [];
|
|
let showAccessControlModal = false;
|
|
let hasManualEdit = false;
|
|
let hasManualName = false;
|
|
let hasManualDescription = false;
|
|
let isFrontmatterDetected = false;
|
|
|
|
// Auto-detect frontmatter and fill name/description in create mode
|
|
$: if (!edit && content) {
|
|
const fm = parseFrontmatter(content);
|
|
if (fm.name) {
|
|
isFrontmatterDetected = true;
|
|
if (!hasManualName) {
|
|
name = formatSkillName(fm.name);
|
|
}
|
|
if (!hasManualEdit) {
|
|
id = fm.name;
|
|
}
|
|
} else {
|
|
isFrontmatterDetected = false;
|
|
}
|
|
if (fm.description && !hasManualDescription) {
|
|
description = fm.description;
|
|
}
|
|
} else if (!edit && !content) {
|
|
isFrontmatterDetected = false;
|
|
}
|
|
|
|
$: if (!edit && !hasManualEdit && !isFrontmatterDetected) {
|
|
id = name !== '' ? slugify(name) : '';
|
|
}
|
|
|
|
function handleIdInput(e: Event) {
|
|
hasManualEdit = true;
|
|
}
|
|
|
|
function handleNameInput(e: Event) {
|
|
hasManualName = true;
|
|
}
|
|
|
|
function handleDescriptionInput(e: Event) {
|
|
hasManualDescription = true;
|
|
}
|
|
|
|
const submitHandler = async () => {
|
|
if (disabled) {
|
|
toast.error($i18n.t('You do not have permission to edit this skill.'));
|
|
return;
|
|
}
|
|
loading = true;
|
|
|
|
await onSubmit({
|
|
id,
|
|
name,
|
|
description,
|
|
content,
|
|
is_active: true,
|
|
meta: { tags: [] },
|
|
access_grants: accessGrants
|
|
});
|
|
|
|
loading = false;
|
|
};
|
|
|
|
onMount(async () => {
|
|
if (skill) {
|
|
name = skill.name || '';
|
|
await tick();
|
|
id = skill.id || '';
|
|
description = skill.description || '';
|
|
content = skill.content || '';
|
|
accessGrants = skill?.access_grants === undefined ? [] : skill?.access_grants;
|
|
|
|
if (name) hasManualName = true;
|
|
if (description) hasManualDescription = true;
|
|
if (id) hasManualEdit = true;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<AccessControlModal
|
|
bind:show={showAccessControlModal}
|
|
bind:accessGrants
|
|
accessRoles={['read', 'write']}
|
|
share={$user?.permissions?.sharing?.skills || $user?.role === 'admin'}
|
|
sharePublic={$user?.permissions?.sharing?.public_skills || $user?.role === 'admin'}
|
|
onChange={async () => {
|
|
if (edit && skill?.id) {
|
|
try {
|
|
await updateSkillAccessGrants(localStorage.token, skill.id, accessGrants);
|
|
toast.success($i18n.t('Saved'));
|
|
} catch (error) {
|
|
toast.error(`${error}`);
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
|
<div class="mx-auto w-full md:px-0 h-full">
|
|
<form class=" flex flex-col max-h-[100dvh] h-full" on:submit|preventDefault={submitHandler}>
|
|
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
|
<div class="w-full mb-2 flex flex-col gap-0.5">
|
|
<div class="flex w-full items-center">
|
|
<div class=" shrink-0 mr-2">
|
|
<Tooltip content={$i18n.t('Back')}>
|
|
<button
|
|
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
|
aria-label={$i18n.t('Back')}
|
|
on:click={() => {
|
|
goto('/workspace/skills');
|
|
}}
|
|
type="button"
|
|
>
|
|
<ChevronLeft strokeWidth="2.5" />
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<div class="flex-1">
|
|
<Tooltip content={$i18n.t('e.g. Code Review Guidelines')} placement="top-start">
|
|
<input
|
|
class="w-full text-2xl bg-transparent outline-hidden"
|
|
type="text"
|
|
placeholder={$i18n.t('Skill Name')}
|
|
aria-label={$i18n.t('Skill Name')}
|
|
bind:value={name}
|
|
on:input={handleNameInput}
|
|
required
|
|
{disabled}
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<div class="self-center shrink-0">
|
|
{#if !disabled}
|
|
<button
|
|
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
|
|
type="button"
|
|
on:click={() => (showAccessControlModal = true)}
|
|
>
|
|
<LockClosed strokeWidth="2.5" className="size-3.5" />
|
|
|
|
<div class="text-sm font-medium shrink-0">
|
|
{$i18n.t('Access')}
|
|
</div>
|
|
</button>
|
|
{:else}
|
|
<span
|
|
class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-full"
|
|
>{$i18n.t('Read Only')}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class=" flex gap-2 px-1 items-center">
|
|
{#if edit}
|
|
<div class="text-sm text-gray-500 shrink-0">
|
|
{id}
|
|
</div>
|
|
{:else}
|
|
<Tooltip
|
|
className="w-full"
|
|
content={$i18n.t('e.g. code-review-guidelines')}
|
|
placement="top-start"
|
|
>
|
|
<input
|
|
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
|
|
type="text"
|
|
placeholder={$i18n.t('Skill ID')}
|
|
aria-label={$i18n.t('Skill ID')}
|
|
bind:value={id}
|
|
on:input={handleIdInput}
|
|
required
|
|
disabled={edit}
|
|
/>
|
|
</Tooltip>
|
|
{/if}
|
|
|
|
<Tooltip
|
|
className="w-full self-center items-center flex"
|
|
content={$i18n.t('e.g. Step-by-step instructions for code reviews')}
|
|
placement="top-start"
|
|
>
|
|
<input
|
|
class="w-full text-sm bg-transparent outline-hidden"
|
|
type="text"
|
|
placeholder={$i18n.t('Skill Description')}
|
|
aria-label={$i18n.t('Skill Description')}
|
|
bind:value={description}
|
|
on:input={handleDescriptionInput}
|
|
{disabled}
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
|
<div class="h-full flex flex-col">
|
|
<div
|
|
class="bg-gray-50 dark:bg-gray-900 rounded-xl border border-gray-100/50 dark:border-gray-850/50 flex-1 min-h-0 overflow-hidden flex flex-col"
|
|
>
|
|
{#if disabled}
|
|
<div class="px-4 py-3 overflow-y-auto flex-1">
|
|
<pre class="text-xs whitespace-pre-wrap font-mono">{content}</pre>
|
|
</div>
|
|
{:else}
|
|
<textarea
|
|
class="w-full flex-1 text-xs bg-transparent outline-hidden resize-none font-mono px-4 py-3"
|
|
bind:value={content}
|
|
placeholder={$i18n.t('Enter skill instructions in markdown...')}
|
|
aria-label={$i18n.t('Skill Instructions')}
|
|
required
|
|
/>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pb-3 flex justify-end">
|
|
{#if !disabled}
|
|
<button
|
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex items-center"
|
|
type="submit"
|
|
disabled={loading}
|
|
>
|
|
{$i18n.t(edit ? 'Save' : 'Save & Create')}
|
|
{#if loading}
|
|
<div class="ml-1.5">
|
|
<Spinner />
|
|
</div>
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|