Skip to content

Commit

Permalink
Merge branch 'main' into plate/view
Browse files Browse the repository at this point in the history
  • Loading branch information
zbeyens committed Dec 19, 2024
2 parents 1d2fba3 + 091beed commit 1c0f8fe
Show file tree
Hide file tree
Showing 14 changed files with 326 additions and 45 deletions.
10 changes: 10 additions & 0 deletions .changeset/ai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@udecode/plate-ai': minor
---

- `api.aiChat.replaceSelection()` – new option `format: 'none' | 'single' | 'all'`
- `'single'` (default):
- Single block: Applies block's formatting to inserted content
- Multiple blocks: Preserves source formatting
- `'all'`: Forces first block's formatting on all inserted blocks
- `'none'`: Preserves source formatting completely
5 changes: 5 additions & 0 deletions .changeset/friendly-cooks-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/slate-utils': patch
---

Add `getFirstNodeText`
5 changes: 5 additions & 0 deletions .changeset/happy-buses-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/slate': patch
---

export type NodeTextsOptions
5 changes: 5 additions & 0 deletions .changeset/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-indent-list': patch
---

Fix todo list: insert break should inherit format
32 changes: 20 additions & 12 deletions apps/www/content/docs/ai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,15 @@ Template for system messages. Supports same placeholders as `promptTemplate`.

## API

### editor.aiChat.accept()
### api.aiChat.accept()

Accepts the current AI suggestion:

- Removes AI marks from the content
- Hides the AI chat interface
- Focuses the editor

### editor.aiChat.insertBelow()
### api.aiChat.insertBelow()

Inserts AI content below the current block.

Expand All @@ -303,30 +303,38 @@ Handles both block selection and normal selection modes:
- In block selection: Inserts after the last selected block
- In normal selection: Inserts after the current block

### editor.aiChat.replaceSelection()
### api.aiChat.replaceSelection()

Replaces the current selection with AI content.

<APIParameters>
<APIItem name="sourceEditor" type="PlateEditor">
Editor containing the content to replace with.
</APIItem>
<APIItem name="options" type="object" optional>
<APISubList>
<APISubListItem parent="options" name="forceUniformFormatting" type="boolean" optional>
When true, applies the first block's formatting to all inserted blocks. Defaults to false.
</APISubListItem>
</APISubList>
</APIItem>
</APIParameters>

Handles both block selection and normal selection modes:
Handles different selection modes:

- In block selection: Replaces all selected blocks
- In normal selection: Replaces the current selection
- Single block selection: Replaces the selected block, applying its formatting to all inserted content
- Multiple block selection: Replaces all selected blocks, preserving the original formatting unless `forceUniformFormatting` is enabled
- Normal selection: Replaces the current selection while maintaining surrounding context

### editor.aiChat.reset()
### api.aiChat.reset()

Resets the chat state:

- Stops any ongoing generation
- Clears chat messages
- Removes all AI nodes from the editor

### editor.aiChat.submit()
### api.aiChat.submit()

Submits a prompt to generate AI content.

Expand Down Expand Up @@ -355,7 +363,7 @@ In insert mode, undoes previous AI changes before submitting.

## Transforms

### editor.ai.insertNodes()
### tf.ai.insertNodes()

Inserts AI-generated nodes with the AI mark.

Expand All @@ -372,7 +380,7 @@ Inserts AI-generated nodes with the AI mark.
</APIItem>
</APIParameters>

### editor.ai.removeMarks()
### tf.ai.removeMarks()

Removes AI marks from nodes in the specified location.

Expand All @@ -386,7 +394,7 @@ Removes AI marks from nodes in the specified location.
</APIItem>
</APIParameters>

### editor.ai.removeNodes()
### tf.ai.removeNodes()

Removes nodes that have the AI mark.

Expand All @@ -400,7 +408,7 @@ Removes nodes that have the AI mark.
</APIItem>
</APIParameters>

### editor.ai.undo()
### tf.ai.undo()

Special undo operation for AI changes:

Expand Down
16 changes: 16 additions & 0 deletions apps/www/content/docs/api/slate-utils.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ Retrieves a consistent property value from a fragment of nodes.
The consistent property value found in the fragment, or undefined if no consistent value is found.
</APIReturns>

### getFirstNodeText

Gets the first text node from a node.

<APIParameters>
<APIItem name="root" type="TNode">
The root node to search for text nodes.
</APIItem>
<APIItem name="options" type="NodeTextsOptions<N, R>" optional>
Options for getting node texts.
</APIItem>
</APIParameters>
<APIReturns>
Returns the first text node entry or `undefined` if no text nodes are found.
</APIReturns>

### getLastChild

Returns the last child of a node or `null` if no children.
Expand Down
17 changes: 15 additions & 2 deletions apps/www/src/registry/default/components/editor/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { TocPlugin } from '@udecode/plate-heading/react';
import { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';
import { IndentListPlugin } from '@udecode/plate-indent-list/react';
import { insertColumnGroup, toggleColumnGroup } from '@udecode/plate-layout';
import { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';
import { LinkPlugin, triggerFloatingLink } from '@udecode/plate-link/react';
import { insertEquation, insertInlineEquation } from '@udecode/plate-math';
import {
Expand All @@ -45,9 +46,21 @@ import {
VideoPlugin,
} from '@udecode/plate-media/react';
import { insertTable } from '@udecode/plate-table';
import { TablePlugin } from '@udecode/plate-table/react';
import {
TableCellPlugin,
TablePlugin,
TableRowPlugin,
} from '@udecode/plate-table/react';
import { Path } from 'slate';

export const STRUCTURAL_TYPES = [
ColumnPlugin.key,
ColumnItemPlugin.key,
TablePlugin.key,
TableRowPlugin.key,
TableCellPlugin.key,
];

const ACTION_THREE_COLUMNS = 'action_three_columns';

const insertList = (editor: PlateEditor, type: string) => {
Expand Down Expand Up @@ -186,7 +199,7 @@ export const setBlockType = (
}
}

const entries = getBlocks(editor);
const entries = getBlocks(editor, { mode: 'lowest' });

entries.forEach((entry) => setEntry(entry));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from 'lucide-react';

import {
STRUCTURAL_TYPES,
getBlockType,
setBlockType,
} from '@/registry/default/components/editor/transforms';
Expand Down Expand Up @@ -119,6 +120,7 @@ export function TurnIntoDropdownMenu(props: DropdownMenuProps) {
const value = useSelectionFragmentProp({
defaultValue: ParagraphPlugin.key,
getProp: (node) => getBlockType(node as any),
structuralTypes: STRUCTURAL_TYPES,
});
const selectedItem = React.useMemo(
() =>
Expand Down
90 changes: 78 additions & 12 deletions packages/ai/src/react/ai-chat/transforms/replaceSelectionAIChat.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { PlateEditor } from '@udecode/plate-common/react';

import { isEditorEmpty, withNewBatch } from '@udecode/plate-common';
import { focusEditor } from '@udecode/plate-common/react';
import {
type TElement,
getFirstNodeText,
getNodeProps,
isEditorEmpty,
isText,
withNewBatch,
} from '@udecode/plate-common';
import { type PlateEditor, focusEditor } from '@udecode/plate-common/react';
import {
BlockSelectionPlugin,
removeBlockSelectionNodes,
Expand All @@ -12,7 +17,8 @@ import type { AIChatPluginConfig } from '../AIChatPlugin';

export const replaceSelectionAIChat = (
editor: PlateEditor,
sourceEditor: PlateEditor
sourceEditor: PlateEditor,
{ format = 'single' }: { format?: 'all' | 'none' | 'single' } = {}
) => {
if (!sourceEditor || isEditorEmpty(sourceEditor)) return;

Expand All @@ -23,11 +29,21 @@ export const replaceSelectionAIChat = (

editor.getApi<AIChatPluginConfig>({ key: 'ai' }).aiChat.hide();

if (isBlockSelecting) {
const firstBlockPath = editor
.getApi(BlockSelectionPlugin)
.blockSelection.getNodes()[0][1];
// If no blocks selected, treat it like a normal selection replacement
if (!isBlockSelecting) {
editor.insertFragment(sourceEditor.children);
focusEditor(editor);

return;
}

const blockSelectionApi = editor.getApi(BlockSelectionPlugin).blockSelection;
const selectedBlocks = blockSelectionApi.getNodes();

if (selectedBlocks.length === 0) return;
// If format is 'none' or multiple blocks with 'single',
// just insert the content as is
if (format === 'none' || (format === 'single' && selectedBlocks.length > 1)) {
editor.withoutNormalizing(() => {
removeBlockSelectionNodes(editor);

Expand All @@ -37,13 +53,63 @@ export const replaceSelectionAIChat = (
.blockSelection.insertBlocksAndSelect(
cloneDeep(sourceEditor.children),
{
at: firstBlockPath,
at: selectedBlocks[0][1],
}
);
});
});
} else {
editor.insertFragment(sourceEditor.children);

focusEditor(editor);

return;
}

// Apply formatting from first block when:
// - formatting is 'all', or
// - only one block is selected
const [firstBlockNode, firstBlockPath] = selectedBlocks[0];
const firstBlockProps = getNodeProps(firstBlockNode);

// Get formatting from first text node
const firstTextEntry = getFirstNodeText(firstBlockNode as TElement);

if (!firstTextEntry) return;

const textProps = getNodeProps(firstTextEntry[0]);

// Apply text props recursively to text nodes
const applyTextProps = (node: any): any => {
if (isText(node)) {
return { ...textProps, ...node };
}
if (node.children) {
return {
...node,
children: node.children.map(applyTextProps),
};
}

return node;
};

editor.withoutNormalizing(() => {
removeBlockSelectionNodes(editor);

withNewBatch(editor, () => {
// Create new blocks with first block's formatting
const newBlocks = cloneDeep(sourceEditor.children).map((block) => ({
...block,
...firstBlockProps,
children: block.children.map(applyTextProps),
}));

editor
.getTransforms(BlockSelectionPlugin)
.blockSelection.insertBlocksAndSelect(newBlocks, {
at: firstBlockPath,
});
});
});

focusEditor(editor);
};
Loading

0 comments on commit 1c0f8fe

Please sign in to comment.