Skip to content

Commit

Permalink
useAI
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfeng33 committed Sep 30, 2024
1 parent 3e8cd2a commit a0b6e25
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 222 deletions.
2 changes: 1 addition & 1 deletion apps/www/src/registry/default/example/playground-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export const usePlaygroundEditor = (id: any = '', scrollSelector?: string) => {
options: {
areaOptions: {
behaviour: {
startThreshold: 20,
startThreshold: 10,
},
boundaries: `#${scrollSelector}`,
container: `#${scrollSelector}`,
Expand Down
179 changes: 31 additions & 148 deletions apps/www/src/registry/default/plate-ui/ai-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
/* eslint-disable tailwindcss/no-custom-classname */
'use client';

import React, {
type KeyboardEvent,
memo,
startTransition,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';

import type { actionGroup } from '@udecode/plate-menu';
import React, { memo, useMemo } from 'react';

import { cn } from '@udecode/cn';
import {
AIPlugin,
getContent,
streamInsertText,
streamInsertTextSelection,
} from '@udecode/plate-ai/react';
import { useEditorPlugin } from '@udecode/plate-core/react';
import { Ariakit, filterAndBuildMenuTree } from '@udecode/plate-menu';
import { focusEditor } from '@udecode/slate-react';
import isHotkey from 'is-hotkey';
import { useAI } from '@udecode/plate-ai/react';

import { Icons } from '@/components/icons';

Expand All @@ -47,138 +28,46 @@ import { Menu, comboboxVariants, renderSearchMenuItems } from './menu';

// eslint-disable-next-line react/display-name
export const AIMenu = memo(({ children }: React.PropsWithChildren) => {
const { api, editor, setOption, setOptions, useOption } =
useEditorPlugin(AIPlugin);

const isOpen = useOption('isOpen', editor.id);
const action = useOption('action');
const aiState = useOption('aiState');
const menuType = useOption('menuType');
const setAction = (action: actionGroup) => setOption('action', action);

const { aiEditor } = editor.useOptions(AIPlugin);

// init
const menu = Ariakit.useMenuStore();
useEffect(() => {
setOptions({
store: menu,
});
// eslint-disable-next`-line react-hooks/exhaustive-deps
}, [isOpen, menu, setOptions]);

const [values, setValues] = useState(defaultValues);
const [searchValue, setSearchValue] = useState('');

const streamInsert = useCallback(async () => {
if (!aiEditor) return;
if (menuType === 'selection') {
const content = getContent(editor, aiEditor);

await streamInsertTextSelection(editor, aiEditor, {
prompt: `user prompt is ${searchValue} the content is ${content}`,
});
} else if (menuType === 'space') {
await streamInsertText(editor, {
prompt: searchValue,
});
}
}, [aiEditor, editor, menuType, searchValue]);

const onInputKeyDown = async (e: KeyboardEvent<HTMLInputElement>) => {
if (isHotkey('backspace')(e) && searchValue.length === 0) {
e.preventDefault();
api.ai.hide();
focusEditor(editor);
}
if (isHotkey('enter')(e)) await streamInsert();
};

const onCloseMenu = useCallback(() => {
// close menu if ai is not generating
if (aiState === 'idle' || aiState === 'done') {
api.ai.hide();
focusEditor(editor);
}
// abort if ai is generating
if (aiState === 'generating' || aiState === 'requesting') {
api.ai.abort();
}
}, [aiState, api.ai, editor]);

// close on escape
useEffect(() => {
const keydown = (e: any) => {
if (!isOpen || !isHotkey('escape')(e)) return;

onCloseMenu();
};

document.addEventListener('keydown', keydown);

return () => {
document.removeEventListener('keydown', keydown);
};
}, [aiState, api.ai, editor, isOpen, onCloseMenu]);

// block editor while generating
// const setReadOnly = usePlateStore().set.readOnly();
useEffect(() => {
if (aiState === 'generating') {
// setReadOnly(true);
}
if (aiState === 'done') {
// setReadOnly(false);
setSearchValue('');
}
}, [aiState, setSearchValue]);
const {
CurrentItems,
action,
aiEditor,
aiState,
comboboxProps,
menuProps,
menuType,
searchItems,
submitButtonProps,
onCloseMenu,
} = useAI({
aiActions: {
CursorCommandsActions,
CursorSuggestionActions,
SelectionCommandsActions,
SelectionSuggestionActions,
},
aiCommands: {
CursorCommands,
CursorSuggestions,
SelectionCommands,
SelectionSuggestions,
},
defaultValues,
});

useActionHandler(action, aiEditor!);

const [CurrentItems, CurrentActions] = React.useMemo(() => {
if (aiState === 'done') {
if (menuType === 'selection')
return [SelectionSuggestions, SelectionSuggestionActions];

return [CursorSuggestions, CursorSuggestionActions];
}
if (menuType === 'selection')
return [SelectionCommands, SelectionCommandsActions];

return [CursorCommands, CursorCommandsActions];
}, [aiState, menuType]);

/** IME */
const [isComposing, setIsComposing] = useState(false);

const searchItems = useMemo(() => {
return isComposing
? []
: filterAndBuildMenuTree(Object.values(CurrentActions), searchValue);
}, [CurrentActions, isComposing, searchValue]);

return (
<>
<Menu
variant="ai"
loading={aiState === 'generating' || aiState === 'requesting'}
open={isOpen}
onClickOutside={() => {
return editor.getApi(AIPlugin).ai.hide();
}}
onValueChange={(value) => startTransition(() => setSearchValue(value))}
onValuesChange={(values: typeof defaultValues) => {
setValues(values);
}}
{...menuProps}
combobox={
<input
id="__potion_ai_menu_searchRef"
className="flex-1 px-1"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onCompositionEnd={() => setIsComposing(false)}
onCompositionStart={() => setIsComposing(true)}
onKeyDown={onInputKeyDown}
{...comboboxProps}
placeholder={
aiState === 'done'
? 'Tell AI what todo next'
Expand All @@ -199,10 +88,7 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => {
size="icon"
variant="ghost"
className="ml-2"
disabled={searchValue.trim().length === 0}
onClick={async () => {
await streamInsert();
}}
{...submitButtonProps}
>
<Icons.submit></Icons.submit>
</Button>
Expand Down Expand Up @@ -233,9 +119,6 @@ export const AIMenu = memo(({ children }: React.PropsWithChildren) => {
></Icons.stop>
</div>
}
setAction={setAction}
store={menu}
values={values}
>
{renderSearchMenuItems(searchItems, { hiddenOnEmpty: true }) ?? (
<CurrentItems />
Expand Down
3 changes: 2 additions & 1 deletion apps/www/src/registry/default/plate-ui/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const comboboxListVariants = cva('rounded-sm', {
type variant = 'ai' | 'default';

type StyledMenuProps = MenuProps & {
variant: variant;
variant?: variant;
};

export const Menu = React.forwardRef<HTMLDivElement, StyledMenuProps>(
Expand Down Expand Up @@ -326,6 +326,7 @@ export function renderSearchMenuItems(
) {
if (!matches) return null;
if (matches.length === 0) {
// eslint-disable-next-line react/jsx-no-useless-fragment
if (options?.hiddenOnEmpty) return <></>;

return <div className={cn(menuItemVariants())}>No results</div>;
Expand Down
1 change: 1 addition & 0 deletions packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"dependencies": {
"@udecode/plate-combobox": "38.0.1",
"@udecode/plate-markdown": "38.0.1",
"@udecode/plate-menu": "38.0.1",
"@udecode/plate-selection": "38.0.11",
"lodash": "^4.17.21"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/ai/src/react/ai/AIPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ExtendConfig } from '@udecode/plate-core';
import type { AriakitTypes } from '@udecode/plate-menu';
import type { NodeEntry, Path } from 'slate';

import {
Expand Down Expand Up @@ -53,7 +54,7 @@ export type AIPluginConfig = ExtendConfig<
lastWorkPath: Path | null;
menuType: 'selection' | 'space' | null;
openEditorId: string | null;
store: any | null;
store: AriakitTypes.MenuStore | null;
} & ExposeOptions &
AIApi &
AISelectors,
Expand Down
5 changes: 5 additions & 0 deletions packages/ai/src/react/ai/hook/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* @file Automatically generated by barrelsby.
*/

export * from './useAI';
Loading

0 comments on commit a0b6e25

Please sign in to comment.