Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix misc #3868

Merged
merged 2 commits into from
Dec 19, 2024
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
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
16 changes: 15 additions & 1 deletion 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 @@ -44,9 +45,22 @@ import {
MediaEmbedPlugin,
VideoPlugin,
} from '@udecode/plate-media/react';
import { TablePlugin, insertTable } from '@udecode/plate-table/react';
import {
TableCellPlugin,
TablePlugin,
TableRowPlugin,
insertTable,
} 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
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
Loading