Skip to content

Commit ba9a6ee

Browse files
feat(lib): add new ai prompt action (#448)
* feat: add promptAI action * feat: fix ts * feat: add promptAI tests * feat: rename variable * feat: fix tests * feat: change test name * feat: adjust promptAI tests * feat: allow all types of prompt * feat: improve tests * feat: add todo to missing storyId payload property * feat: add missing tests * feat: add support to the new action in the test lib-helper * feat: add error message when promptAI action is used in the Sandbox * feat(SHAPE-8085): apply review feedback * feat(SHAPE-8085): fix tests * feat(SHAPE-8085): improve prompt ai response message * feat(SHAPE-8085): apply review feedback
1 parent 240f5e3 commit ba9a6ee

File tree

20 files changed

+769
-19
lines changed

20 files changed

+769
-19
lines changed

packages/demo/src/components/NonModalView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ContextRequester } from './ContextRequester'
77
import { UserContextRequester } from './UserContextRequester'
88
import { PluginComponent } from './FieldPluginDemo'
99
import { LanguageView } from './LanguageView'
10+
import { PromptAI } from './PromptAI'
1011

1112
export const NonModalView: PluginComponent = (props) => (
1213
<Paper>
@@ -18,6 +19,7 @@ export const NonModalView: PluginComponent = (props) => (
1819
<UserContextRequester {...props} />
1920
<HeightChangeDemo {...props} />
2021
<LanguageView {...props} />
22+
<PromptAI {...props} />
2123
</Stack>
2224
</Paper>
2325
)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
Button,
3+
Checkbox,
4+
FormControl,
5+
FormControlLabel,
6+
MenuItem,
7+
Stack,
8+
TextField,
9+
Typography,
10+
} from '@mui/material'
11+
import { useState } from 'react'
12+
import {
13+
isPromptAIPayloadValid,
14+
promptAIActionsList,
15+
PromptAIResponse,
16+
type PromptAIAction,
17+
} from '@storyblok/field-plugin'
18+
import type { PluginComponent } from './FieldPluginDemo'
19+
20+
export const PromptAI: PluginComponent = (props) => {
21+
const { actions } = props
22+
23+
const [promptQuestion, setPromptQuestion] = useState<string>('')
24+
const [promptAction, setPromptAction] = useState<PromptAIAction>('prompt')
25+
const [promptLanguage, setPromptLanguage] = useState<string>()
26+
const [promptTone, setPromptTone] = useState<string>()
27+
const [promptAIResponse, setPromptAIResponse] = useState<PromptAIResponse>()
28+
const [promptBasedOnCurrentStory, setPromptBasedOnCurrentStory] =
29+
useState<boolean>(false)
30+
31+
const onSubmit = async () => {
32+
const payload = {
33+
action: promptAction,
34+
text: promptQuestion,
35+
language: promptLanguage,
36+
tone: promptTone,
37+
basedOnCurrentStory: promptBasedOnCurrentStory,
38+
}
39+
40+
if (!isPromptAIPayloadValid(payload)) {
41+
console.error('Invalid Prompt AI payload')
42+
return
43+
}
44+
45+
const promptAIResponse = await actions.promptAI(payload)
46+
47+
setPromptAIResponse(promptAIResponse)
48+
}
49+
50+
return (
51+
<Stack
52+
gap={2}
53+
direction={'column'}
54+
>
55+
<Typography variant="subtitle1">Prompt AI</Typography>
56+
<Stack gap={2}>
57+
<TextField
58+
label="Ask a question"
59+
onChange={(e) => setPromptQuestion(e.target.value)}
60+
required
61+
/>
62+
<TextField
63+
label="Action"
64+
value={promptAction}
65+
select
66+
required
67+
onChange={(e) => setPromptAction(e.target.value as PromptAIAction)}
68+
>
69+
{promptAIActionsList.map((promptAIAction) => (
70+
<MenuItem
71+
key={promptAIAction}
72+
value={promptAIAction}
73+
>
74+
{promptAIAction}
75+
</MenuItem>
76+
))}
77+
</TextField>
78+
<TextField
79+
label="Language (optional)"
80+
onChange={(e) => setPromptLanguage(e.target.value)}
81+
/>
82+
<TextField
83+
label="Tone (optional)"
84+
onChange={(e) => setPromptTone(e.target.value)}
85+
/>
86+
<FormControl>
87+
<FormControlLabel
88+
label="Based on the current story"
89+
control={
90+
<Checkbox
91+
value={promptBasedOnCurrentStory}
92+
onChange={(e) => setPromptBasedOnCurrentStory(e.target.checked)}
93+
/>
94+
}
95+
sx={{ ml: '-9px' }}
96+
/>
97+
</FormControl>
98+
<Typography>
99+
{promptAIResponse?.ok === true &&
100+
`AI Generated Text: ${promptAIResponse.answer}`}
101+
{promptAIResponse?.ok === false &&
102+
`AI Error: ${promptAIResponse.error}`}
103+
</Typography>
104+
<Button
105+
variant="outlined"
106+
color="secondary"
107+
onClick={onSubmit}
108+
>
109+
Prompt
110+
</Button>
111+
</Stack>
112+
</Stack>
113+
)
114+
}

packages/field-plugin/src/createFieldPlugin/FieldPluginActions.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { Asset, StoryData, UserData, ModalSize } from '../messaging'
1+
import type {
2+
Asset,
3+
StoryData,
4+
UserData,
5+
ModalSize,
6+
PromptAIPayload,
7+
PromptAIResponse,
8+
} from '../messaging'
29
import type { FieldPluginData } from './FieldPluginData'
310

411
export type SetContent<Content> = (
@@ -9,6 +16,7 @@ export type SetModalOpen<Content> = (
916
modalSize?: ModalSize,
1017
) => Promise<FieldPluginData<Content>>
1118
export type RequestContext = () => Promise<StoryData>
19+
export type PromptAI = (payload: PromptAIPayload) => Promise<PromptAIResponse>
1220
export type RequestUserContext = () => Promise<UserData>
1321
export type SelectAsset = () => Promise<Asset>
1422
export type Initialize<Content> = () => Promise<FieldPluginData<Content>>
@@ -17,6 +25,7 @@ export type FieldPluginActions<Content> = {
1725
setContent: SetContent<Content>
1826
setModalOpen: SetModalOpen<Content>
1927
requestContext: RequestContext
28+
promptAI: PromptAI
2029
requestUserContext: RequestUserContext
2130
selectAsset: SelectAsset
2231
}

packages/field-plugin/src/createFieldPlugin/createPluginActions/callbackQueue.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {
1+
import type {
2+
PromptAIResponseMessage,
23
AssetSelectedMessage,
34
ContextRequestMessage,
45
UserContextRequestMessage,
@@ -16,6 +17,7 @@ type CallbackMap = {
1617
userContext: Record<CallbackId, OnMessage<UserContextRequestMessage>>
1718
stateChanged: Record<CallbackId, OnMessage<StateChangedMessage>>
1819
loaded: Record<CallbackId, OnMessage<LoadedMessage>>
20+
promptAI: Record<CallbackId, OnMessage<PromptAIResponseMessage>>
1921
}
2022
type CallbackType = keyof CallbackMap
2123

@@ -26,7 +28,9 @@ export const callbackQueue = () => {
2628
userContext: {},
2729
stateChanged: {},
2830
loaded: {},
31+
promptAI: {},
2932
}
33+
3034
const pushCallback = <T extends CallbackType>(
3135
callbackType: T,
3236
callback: CallbackMap[T][CallbackId],

packages/field-plugin/src/createFieldPlugin/createPluginActions/createPluginActions.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type {
55
GetUserContextMessage,
66
ModalChangeMessage,
77
ValueChangeMessage,
8+
PromptAIPayload,
9+
PluginPromptAIMessage,
810
} from '../../messaging'
911
import { emptyAsset } from '../../messaging/pluginMessage/containerToPluginMessage/Asset.test'
1012

@@ -236,6 +238,34 @@ describe('createPluginActions', () => {
236238
})
237239
})
238240

241+
describe('promptAI()', () => {
242+
it('send a message to the container to prompt the AI', () => {
243+
const { uid, postToContainer, onUpdateState } = mock()
244+
const {
245+
actions: { promptAI },
246+
} = createPluginActions({
247+
uid,
248+
postToContainer,
249+
onUpdateState,
250+
validateContent,
251+
})
252+
253+
const promptAIPayload: PromptAIPayload = {
254+
action: 'prompt',
255+
text: 'Some text to prompt',
256+
}
257+
258+
promptAI(promptAIPayload)
259+
260+
expect(postToContainer).toHaveBeenLastCalledWith(
261+
expect.objectContaining({
262+
event: 'promptAI',
263+
promptAIPayload,
264+
} satisfies Partial<PluginPromptAIMessage>),
265+
)
266+
})
267+
})
268+
239269
describe('requestUserContext()', () => {
240270
it('send a message to the container to request the user info', () => {
241271
const { uid, postToContainer, onUpdateState } = mock()

packages/field-plugin/src/createFieldPlugin/createPluginActions/createPluginActions.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ import {
44
assetFromAssetSelectedMessage,
55
assetModalChangeMessage,
66
getContextMessage,
7+
getResponseFromPromptAIMessage,
78
getUserContextMessage,
89
heightChangeMessage,
910
modalChangeMessage,
10-
OnAssetSelectMessage,
11-
OnContextRequestMessage,
12-
OnUserContextRequestMessage,
13-
OnLoadedMessage,
14-
OnStateChangeMessage,
15-
OnUnknownPluginMessage,
1611
pluginLoadedMessage,
12+
getPluginPromptAIMessage,
1713
valueChangeMessage,
14+
type OnAssetSelectMessage,
15+
type OnContextRequestMessage,
16+
type OnUserContextRequestMessage,
17+
type OnLoadedMessage,
18+
type OnStateChangeMessage,
19+
type OnUnknownPluginMessage,
20+
type OnPromptAIMessage,
21+
type PromptAIPayload,
1822
} from '../../messaging'
1923
import { FieldPluginActions, Initialize } from '../FieldPluginActions'
2024
import { pluginStateFromStateChangeMessage } from './partialPluginStateFromStateChangeMessage'
@@ -57,10 +61,12 @@ export const createPluginActions: CreatePluginActions = ({
5761
popCallback('stateChanged', data.callbackId)?.(data)
5862
onUpdateState(pluginStateFromStateChangeMessage(data, validateContent))
5963
}
64+
6065
const onLoaded: OnLoadedMessage = (data) => {
6166
popCallback('loaded', data.callbackId)?.(data)
6267
onUpdateState(pluginStateFromStateChangeMessage(data, validateContent))
6368
}
69+
6470
const onContextRequest: OnContextRequestMessage = (data) => {
6571
popCallback('context', data.callbackId)?.(data)
6672
}
@@ -70,6 +76,11 @@ export const createPluginActions: CreatePluginActions = ({
7076
const onAssetSelect: OnAssetSelectMessage = (data) => {
7177
popCallback('asset', data.callbackId)?.(data)
7278
}
79+
80+
const onPromptAI: OnPromptAIMessage = (data) => {
81+
popCallback('promptAI', data.callbackId)?.(data)
82+
}
83+
7384
const onUnknownMessage: OnUnknownPluginMessage = (data) => {
7485
// TODO remove side-effect, making functions in this file pure.
7586
// perhaps only show this message in development mode?
@@ -88,6 +99,7 @@ export const createPluginActions: CreatePluginActions = ({
8899
onContextRequest,
89100
onUserContextRequest,
90101
onAssetSelect,
102+
onPromptAI,
91103
onUnknownMessage,
92104
}
93105

@@ -146,6 +158,16 @@ export const createPluginActions: CreatePluginActions = ({
146158
postToContainer(getContextMessage({ uid, callbackId }))
147159
})
148160
},
161+
promptAI: (promptAIMessage: PromptAIPayload) => {
162+
return new Promise((resolve) => {
163+
const callbackId = pushCallback('promptAI', (message) =>
164+
resolve(getResponseFromPromptAIMessage(message)),
165+
)
166+
postToContainer(
167+
getPluginPromptAIMessage(promptAIMessage, { uid, callbackId }),
168+
)
169+
})
170+
},
149171
requestUserContext: () => {
150172
return new Promise((resolve) => {
151173
const callbackId = pushCallback('userContext', (message) =>

packages/field-plugin/src/createFieldPlugin/createPluginActions/createPluginMessageListener/createPluginMessageListener.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {
1+
import type {
22
OnAssetSelectMessage,
33
OnContextRequestMessage,
44
OnUserContextRequestMessage,
55
OnLoadedMessage,
6+
OnPromptAIMessage,
67
OnStateChangeMessage,
78
OnUnknownPluginMessage,
89
} from '../../../messaging'
@@ -14,6 +15,7 @@ export type PluginMessageCallbacks = {
1415
onContextRequest: OnContextRequestMessage
1516
onUserContextRequest: OnUserContextRequestMessage
1617
onAssetSelect: OnAssetSelectMessage
18+
onPromptAI: OnPromptAIMessage
1719
onUnknownMessage: OnUnknownPluginMessage
1820
}
1921

0 commit comments

Comments
 (0)