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

feat: launch content studio option #144

Merged
merged 8 commits into from
Jun 20, 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
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
16
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
![Amplience Dynamic Content Generative Rich Text Editor Extension](media/screenshot.png)
![Amplience Dynamic Content Generative Rich Text Editor Extension](media/rte-extension-screenshot.jpg)

# Generative Rich Text Editor

> Generative Rich text field for use in [Amplience Dynamic Content](https://amplience.com/dynamic-content)

This extension is designed to replace the built in rich text editor with additional features and customization options including generative content from [ChatGPT](https://openai.com/chatgpt).
This extension is designed to replace the built in rich text editor with additional features and customization options including generative content from [Amplience Content Studio](#content-studio) and [ChatGPT](https://openai.com/chatgpt)

## Features

- AI
- [Content Studio](#content-studio)
- [Generative AI assistant](#generative-ai-assistant)
- Markdown output
- Paragraphs
Expand Down Expand Up @@ -163,6 +164,27 @@ This will output an array of "blocks". Each block has a type and associated data

You can customize the rich text editor by providing "params" in your content type schema. The examples below should be added to the "params" object in your "ui:extension".

### Content Studio

[Content Studio](https://amplience.com/ai/studios/content-studio/) gives marketers and merchants the power to generate personalized product content that’s on-brand, every time.

Use of Content Studio required you to have access to Content Studio and also Amplience Credits to generate content
Amplience credits provide an easy way to start using our AI features. See [Amplience credits](https://amplience.com/developers/docs/ai-services/credits/)

#### Disabling Content Studio

If for any reason you wish to disable Content Studio from this extension you can do so by adding the following in your installation parameters:

```json
{
"tools": {
"contentStudio": {
"disabled": true
}
}
}
```

### Generative AI Assistant

Powered by ChatGPT, the AI Assistant allows users to quickly generate and edit content using natural language prompts.
Expand Down
Binary file added media/rte-extension-screenshot.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/common/src/actions/RichTextActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface RichTextActions {
prompt: string,
keywords: string[]
): Promise<void>;
insertContentStudioContent(): Promise<void>;
}
18 changes: 6 additions & 12 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"homepage": "./",
"dependencies": {
"@amplience/content-studio-sdk": "^0.1.1",
"@datadog/browser-rum": "^4.43.0",
"@dc-extension-rich-text/common": "0.1.0",
"@dc-extension-rich-text/language-json": "0.1.0",
Expand Down Expand Up @@ -53,16 +54,9 @@
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
"browserslist": [
">0.2%",
"not dead",
"not op_mini all"
]
}
75 changes: 71 additions & 4 deletions packages/extension/src/ProseMirrorToolbar/ProseMirrorToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import {
Button,
Toolbar as MaterialToolbar,
Tooltip,
WithStyles,
createStyles,
withStyles,
Expand Down Expand Up @@ -32,7 +33,6 @@ const styles = createStyles({
height: 26,
alignSelf: "center",
textTransform: "none",
width: 104,
fontSize: 13,
textAlign: "center",
},
Expand All @@ -41,8 +41,25 @@ const styles = createStyles({
height: 13,
margin: "9px 4px",
},
tooltip: {
fontSize: 12,
backgroundColor: "#1A222D",
maxWidth: 340
},
arrow: {
color: "#1A222D",
},
});

const tooltips = {
ai: {
title: "Use ChatGPT to improve your copy"
},
contentStudio: {
title: "Generate on-brand content at scale with Content Studio"
}
}

export interface ToolbarButton {
type: "button";
toolName: string;
Expand Down Expand Up @@ -77,6 +94,7 @@ const ProseMirrorToolbar: React.SFC<ProseMirrorToolbarProps> = (
const group2 = layout.slice(3);
const isAiToolEnabled =
(params?.tools as { ai: { disabled: boolean } })?.ai?.disabled !== true;
const isContentStudioEnabled = (params?.tools as { contentStudio: { disabled: boolean } })?.contentStudio?.disabled !== true;

const renderToolbarElement = (idx: number, element: ToolbarElement) => {
switch (element.type) {
Expand Down Expand Up @@ -134,6 +152,10 @@ const ProseMirrorToolbar: React.SFC<ProseMirrorToolbarProps> = (
} catch {}
};

const launchContentStudio = async () => {
await richTextEditorContext.actions.insertContentStudioContent();
};

return (
<ProseMirrorToolbarContext.Provider
value={{
Expand Down Expand Up @@ -182,27 +204,72 @@ const ProseMirrorToolbar: React.SFC<ProseMirrorToolbarProps> = (
>
<MaterialToolbar className={classes.root} disableGutters={true}>
<div className={classes.group}>
{isAiToolEnabled ? (
{isContentStudioEnabled ? (
<>
<Tooltip
title={tooltips.contentStudio.title}
arrow
classes={{
arrow: props.classes.arrow,
tooltip: props.classes.tooltip,
}}
>
<Button
disabled={richTextEditorContext.isLocked}
onClick={showAIGenerateDialog}
onClick={launchContentStudio}
className={classes.button}
size="small"
startIcon={
!richTextEditorContext.isLocked && (
<SparklesIcon
style={{ width: 15, height: 15 }}
variant="content-studio"
></SparklesIcon>
)
}
>
{richTextEditorContext.isLocked ? (
<Loader></Loader>
) : (
"AI Assistant"
"Content Studio"
)}
</Button>
</Tooltip>
<div className={classes.divider}></div>
</>
) : (
""
)}
{isAiToolEnabled ? (
<>
<Tooltip
title={tooltips.ai.title}
arrow
classes={{
arrow: props.classes.arrow,
tooltip: props.classes.tooltip,
}}
>
<Button
disabled={richTextEditorContext.isLocked}
onClick={showAIGenerateDialog}
className={classes.button}
size="small"
startIcon={
!richTextEditorContext.isLocked && (
<SparklesIcon
style={{ width: 15, height: 15 }}
></SparklesIcon>
)
}
>
{richTextEditorContext.isLocked ? (
<Loader></Loader>
) : (
"AI Assistant"
)}
</Button>
</Tooltip>
<div className={classes.divider}></div>
</>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconButton, withStyles, WithStyles } from "@material-ui/core";
import { IconButton, Tooltip, withStyles, WithStyles } from "@material-ui/core";
import React, { PropsWithChildren } from "react";

import { ProseMirrorToolbarContext } from "../ProseMirrorToolbar";
Expand All @@ -14,7 +14,15 @@ export interface ProseMirrorToolbarButtonProps
const styles = {
root: {
margin: "1px"
}
},
tooltip: {
fontSize: 12,
backgroundColor: "#1A222D",
maxWidth: 240
},
arrow: {
color: "#1A222D",
},
};

const ProseMirrorToolbarIconButton: React.SFC<ProseMirrorToolbarButtonProps> = (
Expand Down Expand Up @@ -48,16 +56,25 @@ const ProseMirrorToolbarIconButton: React.SFC<ProseMirrorToolbarButtonProps> = (
}

return (
<IconButton
className={clsx(classes.root)}
onMouseDown={handleClick}
size="small"
disabled={!toolState.enabled || props.isLocked}
color={toolState.active ? "primary" : "default"}
title={toolState.label}
<Tooltip
title={toolState.label}
arrow
classes={{
arrow: clsx(classes.arrow),
tooltip: clsx(classes.tooltip),
}}
>
{toolState.displayIcon ? toolState.displayIcon : toolState.label}
</IconButton>
<IconButton
className={clsx(classes.root)}
onMouseDown={handleClick}
size="small"
disabled={!toolState.enabled || props.isLocked}
color={toolState.active ? "primary" : "default"}
title={toolState.label}
>
{toolState.displayIcon ? toolState.displayIcon : toolState.label}
</IconButton>
</Tooltip>
);
};

Expand Down
40 changes: 40 additions & 0 deletions packages/extension/src/RichTextActions/RichTextActionsImpl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { fetchEventSource } from "@microsoft/fetch-event-source";
import React from "react";
import { AIConfiguration } from "../AIPromptDialog";
import { track } from "../gainsight";
import { AmplienceContentStudio } from '@amplience/content-studio-sdk';

interface ChatModel {
name: string;
Expand Down Expand Up @@ -495,4 +496,43 @@ Do not converse with the user.

this.context?.setIsLocked(false);
}

public async insertContentStudioContent(): Promise<void> {
try {
const { proseMirrorEditorView, params, language } = this.context!;
const { dispatch } = proseMirrorEditorView;
let { state } = proseMirrorEditorView;

const studio = new AmplienceContentStudio({
baseUrl:
params?.tools?.contentStudio?.baseUrl ||
"https://app.amplience.net/content-studio",
});

const { content } = await studio.getContent();
const textFields = Object.values(content)
.filter((x) => typeof x === "string") as string[];

if (textFields.length > 0) {
let fragment = (language as MarkdownLanguage).parseMarkdown(
textFields[0].trim()
).content;

if (fragment.content.length === 1) {
fragment = fragment.content[0].content;
}

let startPosition = state.selection?.from ?? state.doc.content.size;
let endPosition = state.selection?.to || startPosition;

const transaction = state.tr.replaceWith(
startPosition,
endPosition,
fragment
);

dispatch(transaction);
}
} catch (err) {}
}
}
27 changes: 25 additions & 2 deletions packages/extension/src/SparklesIcon/SparklesIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
import React from "react";

export const SparklesIcon = (props: any) => {
const { readOnly, style } = props;
return (
const { readOnly, style, variant } = props;
return variant === "content-studio" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
fill="none"
viewBox="0 0 26 26"
>
<g
stroke={readOnly ? "#D9D9D9" : "#F88B8B"}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
clipPath="url(#clip0_2281_13381)"
>
<path d="M12.417 9.834h-11M1.417 4.5H17.5M7.417 15.166h-6M1.417 20.5h6M21.333 22.25a1.917 1.917 0 011.917 1.917 1.917 1.917 0 011.917-1.916 1.917 1.917 0 01-1.917-1.917 1.917 1.917 0 01-1.917 1.917zm0-11.5a1.917 1.917 0 011.917 1.917 1.917 1.917 0 011.917-1.916 1.917 1.917 0 01-1.917-1.917 1.917 1.917 0 01-1.917 1.917zm-5.708 11.5a5.75 5.75 0 015.75-5.75 5.75 5.75 0 01-5.75-5.75 5.75 5.75 0 01-5.75 5.75 5.75 5.75 0 015.75 5.75z"></path>
</g>
<defs>
<clipPath id="clip0_2281_13381">
<path fill="#fff" d="M0 0H26V26H0z"></path>
</clipPath>
</defs>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
Expand Down
1 change: 1 addition & 0 deletions packages/extension/src/content-studio-sdk.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '@amplience/content-studio-sdk'
Loading
Loading