21ff42074d
## Summary This PR introduces a Skills system for AI agents, inspired by the [Agent Skills specification](https://agentskills.io/specification). ## Changes ### Backend - **SkillEntity**: New database entity with migration for storing skills - **V2 Sync Mechanism**: Implemented FlatSkill, builders, validators, and action handlers following the v2 flat entity pattern - **Standard Skills**: Pre-defined skills (workflow-building, data-manipulation, dashboard-building, metadata-building, research, code-interpreter, xlsx, pdf, docx, pptx) - **GraphQL API**: CRUD operations for skills with proper guards and permissions - **Workspace Cache**: Integrated skills into the workspace cache system ### Frontend - **Skills Table**: Searchable table in AI settings showing all skills - **Skill Form**: Create/edit page with Label (primary), Description, and Content (markdown editor) - **API Name**: Following existing patterns, name is derived from label with advanced settings toggle for custom API names - **Standard vs Custom**: Standard skills are read-only, custom skills can be edited/deleted ## Key Design Decisions - Skills are stored in the database (Salesforce-like approach) rather than files - Name is derived from Label by default (isLabelSyncedWithName pattern) - Skills reference functions/files via @ mentions in markdown content rather than explicit relations - Standard skills are synced from code, custom skills are created via UI ## Screenshots Skills table and form UI follow existing settings patterns. ## Testing - [x] Lint passes - [x] Typecheck passes - [ ] CI tests
135 lines
3.8 KiB
TypeScript
135 lines
3.8 KiB
TypeScript
import { ResizableImage } from '@/advanced-text-editor/extensions/resizable-image/ResizableImage';
|
|
import { UploadImageExtension } from '@/advanced-text-editor/extensions/resizable-image/UploadImageExtension';
|
|
import { SlashCommand } from '@/advanced-text-editor/extensions/slash-command/SlashCommand';
|
|
import { getInitialAdvancedTextEditorContent } from '@/workflow/workflow-variables/utils/getInitialAdvancedTextEditorContent';
|
|
import { VariableTag } from '@/workflow/workflow-variables/utils/variableTag';
|
|
import { t } from '@lingui/core/macro';
|
|
import { Bold } from '@tiptap/extension-bold';
|
|
import { Document } from '@tiptap/extension-document';
|
|
import { HardBreak } from '@tiptap/extension-hard-break';
|
|
import { Heading } from '@tiptap/extension-heading';
|
|
import { Italic } from '@tiptap/extension-italic';
|
|
import { Link } from '@tiptap/extension-link';
|
|
import { ListKit } from '@tiptap/extension-list';
|
|
import { Paragraph } from '@tiptap/extension-paragraph';
|
|
import { Strike } from '@tiptap/extension-strike';
|
|
import { Text } from '@tiptap/extension-text';
|
|
import { Underline } from '@tiptap/extension-underline';
|
|
import { Dropcursor, Placeholder, UndoRedo } from '@tiptap/extensions';
|
|
import { type Editor, useEditor } from '@tiptap/react';
|
|
import { marked } from 'marked';
|
|
import { type DependencyList, useMemo } from 'react';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
export type AdvancedTextEditorContentType = 'json' | 'markdown';
|
|
|
|
type UseAdvancedTextEditorProps = {
|
|
placeholder: string | undefined;
|
|
readonly: boolean | undefined;
|
|
defaultValue: string | undefined | null;
|
|
onUpdate: (editor: Editor) => void;
|
|
onFocus?: (editor: Editor) => void;
|
|
onBlur?: (editor: Editor) => void;
|
|
onImageUpload?: (file: File) => Promise<string>;
|
|
onImageUploadError?: (error: Error, file: File) => void;
|
|
enableSlashCommand?: boolean;
|
|
contentType?: AdvancedTextEditorContentType;
|
|
};
|
|
|
|
export const useAdvancedTextEditor = (
|
|
{
|
|
placeholder,
|
|
readonly,
|
|
defaultValue,
|
|
onUpdate,
|
|
onFocus,
|
|
onBlur,
|
|
onImageUpload,
|
|
onImageUploadError,
|
|
enableSlashCommand,
|
|
contentType = 'json',
|
|
}: UseAdvancedTextEditorProps,
|
|
dependencies?: DependencyList,
|
|
) => {
|
|
const isMarkdownMode = contentType === 'markdown';
|
|
|
|
const extensions = useMemo(
|
|
() => [
|
|
Document,
|
|
Paragraph,
|
|
Text,
|
|
Placeholder.configure({
|
|
placeholder: placeholder ?? t`Enter text or Type '/' for commands`,
|
|
}),
|
|
VariableTag,
|
|
HardBreak.configure({
|
|
keepMarks: false,
|
|
}),
|
|
UndoRedo,
|
|
Bold,
|
|
Italic,
|
|
Strike,
|
|
Underline,
|
|
Heading.configure({
|
|
levels: [1, 2, 3],
|
|
}),
|
|
Link.configure({
|
|
openOnClick: false,
|
|
}),
|
|
ResizableImage,
|
|
Dropcursor,
|
|
ListKit,
|
|
UploadImageExtension.configure({
|
|
onImageUpload,
|
|
onImageUploadError,
|
|
}),
|
|
...(!readonly && enableSlashCommand !== false ? [SlashCommand] : []),
|
|
],
|
|
[
|
|
placeholder,
|
|
onImageUpload,
|
|
onImageUploadError,
|
|
readonly,
|
|
enableSlashCommand,
|
|
],
|
|
);
|
|
|
|
const getEditorContent = () => {
|
|
if (!isDefined(defaultValue)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (isMarkdownMode) {
|
|
// Convert markdown to HTML, then TipTap will parse the HTML
|
|
return marked.parse(defaultValue, { async: false }) as string;
|
|
}
|
|
|
|
return getInitialAdvancedTextEditorContent(defaultValue);
|
|
};
|
|
|
|
const editor = useEditor(
|
|
{
|
|
extensions,
|
|
content: getEditorContent(),
|
|
editable: !readonly,
|
|
onUpdate: ({ editor }) => {
|
|
onUpdate(editor);
|
|
},
|
|
onFocus: ({ editor }) => {
|
|
onFocus?.(editor);
|
|
},
|
|
onBlur: ({ editor }) => {
|
|
onBlur?.(editor);
|
|
},
|
|
editorProps: {
|
|
scrollThreshold: 60,
|
|
scrollMargin: 60,
|
|
},
|
|
injectCSS: false,
|
|
},
|
|
dependencies,
|
|
);
|
|
|
|
return editor;
|
|
};
|