Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions demo/gptPlugin/PlaygroundGPT.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import type {StoryFn} from '@storybook/react';
import {PlaygroundGPT} from './PlaygroundGPT';

export default {
title: 'Markdown Editor / YFM examples',
title: 'Experiments / GPT',
component: PlaygroundGPT,
};

type PlaygroundStoryProps = {};
export const Playground: StoryFn<PlaygroundStoryProps> = (props) => <PlaygroundGPT {...props} />;

Playground.storyName = 'GPT';
Playground.storyName = 'Playground GPT';
35 changes: 24 additions & 11 deletions demo/gptPlugin/PlaygroundGPT.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
type MarkupString,
gptExtension,
logger,
mGptExtension,
mGptToolbarItem,
markupToolbarConfigs,
wGptToolbarItem,
wysiwygToolbarConfigs,
} from '../../src';
Expand Down Expand Up @@ -34,30 +37,40 @@ const wCommandMenuConfig = wysiwygToolbarConfigs.wCommandMenuConfig.concat(

wCommandMenuConfig.unshift(wysiwygToolbarConfigs.wGptItemData);

const mToolbarConfig = cloneDeep(markupToolbarConfigs.mToolbarConfig);

mToolbarConfig.push([
markupToolbarConfigs.mMermaidButton,
markupToolbarConfigs.mYfmHtmlBlockButton,
]);

mToolbarConfig.unshift([mGptToolbarItem]);

export const PlaygroundGPT = React.memo(() => {
const [yfmRaw, setYfmRaw] = React.useState<MarkupString>(initialMdContent);

const [showedAlertGpt, setShowedAlertGpt] = useState(true);

const gptExtensionProps = gptWidgetProps(setYfmRaw, {
showedGptAlert: Boolean(showedAlertGpt),
onCloseGptAlert: () => {
setShowedAlertGpt(false);
},
});

const markupExtension = mGptExtension(gptExtensionProps);
const wSelectionMenuConfig = [[wGptToolbarItem], ...wysiwygToolbarConfigs.wSelectionMenuConfig];

return (
<Playground
settingsVisible
initial={yfmRaw}
extraExtensions={(builder) =>
builder.use(
gptExtension,
gptWidgetProps(setYfmRaw, {
showedGptAlert: Boolean(showedAlertGpt),
onCloseGptAlert: () => {
setShowedAlertGpt(false);
},
}),
)
}
extraExtensions={(builder) => builder.use(gptExtension, gptExtensionProps)}
wysiwygCommandMenuConfig={wCommandMenuConfig}
extensionOptions={{selectionContext: {config: wSelectionMenuConfig}}}
wysiwygToolbarConfig={wToolbarConfig}
markupConfigExtensions={markupExtension}
markupToolbarConfig={mToolbarConfig}
/>
);
});
Expand Down
2 changes: 1 addition & 1 deletion demo/gptPlugin/gptWidgetOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import type {GptWidgetOptions} from '../../src/extensions/yfm/GPT/gptExtension/gptExtension';
import type {GptWidgetOptions} from '../../src';

const gptRequestHandler = async ({
markup,
Expand Down
16 changes: 16 additions & 0 deletions docs/how-to-connect-gpt-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@ import {
gptExtension,
MarkdownEditorView,
useMarkdownEditor,
markupToolbarConfigs,
mGptExtension,
} from '@gravity-ui/markdown-editor';

export const Editor: React.FC<EditorProps> = (props) => {
// add a plugin to the markup mode
const markupGptExtension = mGptExtension(gptWidgetProps);

const mdEditor = useMarkdownEditor({
// ...

markupConfig: {
extensions: markupGptExtension,
},

extraExtensions: (builder) =>
builder.use(
...
Expand All @@ -35,9 +45,15 @@ export const Editor: React.FC<EditorProps> = (props) => {
),
});

// add a plugin to the markup toolbar mode
const mToolbarConfig = markupToolbarConfigs.mToolbarConfig;

mToolbarConfig.push([mGptToolbarItem]);

return <MarkdownEditorView
...
editor={mdEditor}
markupToolbarConfig={mToolbarConfig}
/>
};
```
Expand Down
2 changes: 1 addition & 1 deletion src/bundle/config/wysiwyg.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {ActionStorage} from 'src/core';

import {headingType, pType} from '../../extensions';
import {gptHotKeys} from '../../extensions/additional/GPT/constants';
import type {
SelectionContextConfig,
SelectionContextItemData,
} from '../../extensions/behavior/SelectionContext';
// for typings from Math
import {gptHotKeys} from '../../extensions/yfm/GPT/constants';
import type {} from '../../extensions/yfm/Math';
import {i18n as i18nHint} from '../../i18n/hints';
import {i18n} from '../../i18n/menubar';
Expand Down
17 changes: 17 additions & 0 deletions src/extensions/additional/GPT/MarkupGpt/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type {Command, EditorView} from '../../../../cm/view';

import {HideMarkupGptEffect, ShowMarkupGptEffect} from './effects';

export const showMarkupGpt = (view: EditorView) => {
view.dispatch({effects: [ShowMarkupGptEffect.of(null)]});
};

export const hideMarkupGpt = (view: EditorView) => {
view.dispatch({effects: [HideMarkupGptEffect.of(null)]});
};

export const runMarkupGpt: Command = (view) => {
if (view) showMarkupGpt(view);

return true;
};
4 changes: 4 additions & 0 deletions src/extensions/additional/GPT/MarkupGpt/effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {StateEffect} from '../../../../cm/state';

export const ShowMarkupGptEffect = StateEffect.define();
export const HideMarkupGptEffect = StateEffect.define();
22 changes: 22 additions & 0 deletions src/extensions/additional/GPT/MarkupGpt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {keymap} from '@codemirror/view';

import {GptWidgetOptions} from '../../..';
import {gptHotKeys} from '../constants';

import {runMarkupGpt} from './commands';
import {mGptPlugin} from './plugin';

export {mGptToolbarItem} from './toolbar';
export {showMarkupGpt, hideMarkupGpt} from './commands';

export function mGptExtension(props: GptWidgetOptions) {
return [
mGptPlugin(props).extension,
keymap.of([
{
key: gptHotKeys.openGptKey,
run: runMarkupGpt,
},
]),
];
}
168 changes: 168 additions & 0 deletions src/extensions/additional/GPT/MarkupGpt/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {WidgetType} from '@codemirror/view';

import {GptWidgetOptions} from '../../..';
import {
Decoration,
type DecorationSet,
type EditorView,
type PluginValue,
ViewPlugin,
type ViewUpdate,
} from '../../../../cm/view';
import {ReactRendererFacet} from '../../../../markup';
import {WIDGET_DECO_CLASS_NAME} from '../constants';
import {isEmptyGptPrompts} from '../utils';

import {hideMarkupGpt} from './commands';
import {HideMarkupGptEffect, ShowMarkupGptEffect} from './effects';
import {renderPopup} from './popup';

class SpanWidget extends WidgetType {
private className = '';
private textContent = '';

constructor(className: string, textContent: string) {
super();
this.className = className;
this.textContent = textContent;
}

toDOM() {
const spanElem = document.createElement('span');
spanElem.className = this.className;
spanElem.textContent = this.textContent;
return spanElem;
}
}

export function mGptPlugin(gptProps: GptWidgetOptions) {
return ViewPlugin.fromClass(
class implements PluginValue {
readonly _view: EditorView;
readonly _renderItem;

_anchor: Element | null = null;

decos: DecorationSet = Decoration.none;
disablePromptPresets = true;
markup: string | null = null;

selectedPosition = {
from: 0,
to: 0,
};

constructor(view: EditorView) {
this._view = view;
this._renderItem = view.state
.facet(ReactRendererFacet)
.createItem('gpt-in-markup-mode', () => this.renderPopup());
}

update(update: ViewUpdate) {
if (update.docChanged || update.selectionSet) {
this.decos = Decoration.none;
return;
}

this.decos = this.decos.map(update.changes);

const {from, to} = update.state.selection.main;

this.selectedPosition.from = from;
this.selectedPosition.to = to;

for (const tr of update.transactions) {
for (const eff of tr.effects) {
if (eff.is(ShowMarkupGptEffect)) {
this._setSelectedText(this._getDecorationText(update, from, to));

if (from === to) {
this.disablePromptPresets = true;

if (isEmptyGptPrompts(gptProps, true)) return;

const decorationWidget = Decoration.widget({
widget: new SpanWidget(WIDGET_DECO_CLASS_NAME, ' '),
});

this.decos = Decoration.set([decorationWidget.range(from)]);

return;
}

this.disablePromptPresets = false;

if (isEmptyGptPrompts(gptProps, false)) return;

this.decos = Decoration.set([
{
from,
to,
value: Decoration.mark({class: WIDGET_DECO_CLASS_NAME}),
},
]);
}

if (eff.is(HideMarkupGptEffect)) {
this.decos = Decoration.none;
}
}
}
}

docViewUpdate() {
this._anchor = this._view.dom
.getElementsByClassName(WIDGET_DECO_CLASS_NAME)
.item(0);
this._renderItem.rerender();
}

destroy() {
this._clearSelectedText();
this._renderItem.remove();
}

renderPopup() {
if (!this._anchor || this.markup === null) {
return null;
}

return renderPopup(this._anchor as HTMLElement, {
...gptProps,
disablePromptPresets: this.disablePromptPresets,
onClose: () => hideMarkupGpt(this._view),
markup: this.markup,
onApplyResult: (changedMarkup) => this._onApplyResult(changedMarkup),
});
}

_getDecorationText(update: ViewUpdate, from: number, to: number): string {
return update.state.doc.sliceString(from, to);
}

_clearSelectedText() {
this.markup = null;
}

_setSelectedText(str: string) {
this.markup = str;
}

_onApplyResult(changedMarkup: string) {
const {from, to} = this.selectedPosition;
const changes = [{from: from, to: to, insert: changedMarkup}];

const transaction = this._view.state.update({
changes: changes,
effects: [HideMarkupGptEffect.of(null)],
});

this._view.dispatch(transaction);
}
},
{
decorations: (value) => value.decos,
},
);
}
Loading
Loading