This commit is contained in:
Timothy Jaeryang Baek 2026-10-09 16:35:59 +04:00
parent 24ee1cb168
commit dc1203d34c
3 changed files with 24 additions and 17 deletions

View file

@ -16,7 +16,7 @@
let locale = '';
let meta: { i18n?: Record<string, Record<string, string>>; [key: string]: any } = {};
import { user } from '$lib/stores';
import { slugify } from '$lib/utils';
import { slugify, parseFrontmatter, formatSkillName } from '$lib/utils';
import Spinner from '$lib/components/common/Spinner.svelte';
import { updateSkillAccessGrants } from '$lib/apis/skills';
import { goto, beforeNavigate } from '$app/navigation';
@ -221,6 +221,12 @@
let accessGrants: any[] = [];
let showAccessControlModal = false;
const handleContentChange = (path: string, content: string) => {
if (edit || path !== 'SKILL.md') return;
const fields = parseFrontmatter(content);
if (!name && typeof fields.name === 'string') name = formatSkillName(fields.name);
if (!description && typeof fields.description === 'string') description = fields.description;
};
$: if (!edit && !clone && name) {
id = slugify(name);
}
@ -454,6 +460,7 @@
bind:dirty={fileDirty}
initialPath={new URLSearchParams(location.search).get('path') || 'SKILL.md'}
onSave={submitHandler}
onContentChange={handleContentChange}
canExport={edit &&
($user?.role === 'admin' || $user?.permissions?.workspace?.skills_export)}
>

View file

@ -43,6 +43,7 @@
export let reloadKey = 0;
export let initialPath = 'SKILL.md';
export let onSave: () => Promise<void> = async () => {};
export let onContentChange: (path: string, content: string) => void = () => {};
const i18n = getContext<any>('i18n');
type Draft = SkillFileSummary & {
sourcePath?: string;
@ -139,6 +140,7 @@
if (loadedPath && codeEditor) value = codeEditor.getValue();
const file = files.find((f) => f.path === loadedPath);
if (file && !readOnly && value !== loadedValue) {
onContentChange(loadedPath, value);
file.content = value;
file.changed = true;
file.size = new TextEncoder().encode(value).length;
@ -158,7 +160,10 @@
};
};
$: if (!$mobile) showFileDrawer = false;
$: if (loadedPath && value !== loadedValue && !readOnly) dirty = true;
$: if (loadedPath && value !== loadedValue && !readOnly) {
dirty = true;
onContentChange(loadedPath, value);
}
const blobFor = async (file: Draft) => {
if (file.content !== undefined)
return new Blob([

View file

@ -1,6 +1,7 @@
import type { Writable } from 'svelte/store';
import { v4 as uuidv4 } from 'uuid';
import sha256 from 'js-sha256';
import { parse as parseYaml } from 'yaml';
import DOMPurify, { type UponSanitizeAttributeHookEvent } from 'dompurify';
import { WEBUI_BASE_URL } from '$lib/constants';
import type { FileNavOpenRequest } from '$lib/stores';
@ -2465,22 +2466,16 @@ export const getCodeBlockContents = (content: string): object => {
}))
};
};
export const parseFrontmatter = (content) => {
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (match) {
const frontmatter = {};
match[1].split('\n').forEach((line) => {
const [key, ...value] = line.split(':');
if (key && value) {
frontmatter[key.trim()] = value
.join(':')
.trim()
.replace(/^["']|["']$/g, '');
}
});
return frontmatter;
export const parseFrontmatter = (content: string): Record<string, unknown> => {
const match = content.match(/^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
if (!match) return {};
try {
const fields = parseYaml(match[1]);
return fields && typeof fields === 'object' && !Array.isArray(fields) ? fields : {};
} catch {
// Incomplete frontmatter is normal while typing.
return {};
}
return {};
};
export const formatSkillName = (name) => {