Skip to content

Commit 7143088

Browse files
committed
feat: prompt and generate steps
1 parent 13d363b commit 7143088

File tree

15 files changed

+1330
-50
lines changed

15 files changed

+1330
-50
lines changed
Lines changed: 12 additions & 0 deletions
Loading
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { useForm } from 'react-hook-form'
2+
import { FaWandMagicSparkles } from 'react-icons/fa6'
3+
import { Form } from 'react-router-dom'
4+
import {
5+
Flex,
6+
FormControl,
7+
FormErrorMessage,
8+
ModalBody,
9+
ModalFooter,
10+
ModalHeader,
11+
Text,
12+
Textarea,
13+
} from '@chakra-ui/react'
14+
import { zodResolver } from '@hookform/resolvers/zod'
15+
import { Button, FormLabel } from '@opengovsg/design-system-react'
16+
import { z } from 'zod'
17+
18+
import pairLogo from '@/assets/pair-logo.svg'
19+
import { ImageBox } from '@/components/FlowStepConfigurationModal/ChooseAndAddConnection/ConfigureExcelConnection'
20+
21+
const AI_FORM_SCHEMA = z.object({
22+
flowName: z.string().trim().min(1, 'Flow name is required'),
23+
trigger: z
24+
.string()
25+
.trim()
26+
.min(15, 'Trigger description must be at least 15 characters')
27+
.max(250, 'Trigger description must not exceed 250 characters'),
28+
actions: z
29+
.string()
30+
.trim()
31+
.min(30, 'Actions description must be at least 30 characters')
32+
.max(1000, 'Actions description must not exceed 1000 characters'),
33+
})
34+
35+
const AI_FORM_FIELDS = [
36+
{
37+
key: 'trigger' as const,
38+
label: 'How does your workflow start?',
39+
placeholder: 'When a new joiner submits a form with their details',
40+
resize: 'none' as const,
41+
},
42+
{
43+
key: 'actions' as const,
44+
label: 'Describe what happens next step-by-step',
45+
placeholder:
46+
'Add this new joiner to the database\nSend the new joiner a welcome email\nInform respective team that there is a new joiner',
47+
resize: 'vertical' as const,
48+
maxH: '200px',
49+
},
50+
]
51+
52+
export type AiFormData = z.infer<typeof AI_FORM_SCHEMA>
53+
54+
export const AIFormModalContent = ({
55+
flowName,
56+
trigger,
57+
actions,
58+
type,
59+
onBack,
60+
onSubmit,
61+
}: {
62+
flowName?: string
63+
trigger?: string
64+
actions?: string
65+
type: 'create' | 'update'
66+
onBack: () => void
67+
onSubmit: (data: AiFormData) => void
68+
}) => {
69+
const {
70+
register,
71+
handleSubmit,
72+
formState: { errors, isValid },
73+
} = useForm<AiFormData>({
74+
resolver: zodResolver(AI_FORM_SCHEMA),
75+
mode: 'onChange',
76+
defaultValues: {
77+
flowName: flowName || 'Name your Pipe',
78+
trigger: trigger || 'When a new joiner submits a form with their details',
79+
actions:
80+
actions ||
81+
'Add this new joiner to the database\nSend the new joiner a welcome email\nInform respective team that there is a new joiner',
82+
},
83+
})
84+
85+
return (
86+
<>
87+
<Form onSubmit={handleSubmit(onSubmit)}>
88+
<ModalHeader p="2.5rem 2rem 1.5rem">
89+
<Text textStyle="h4">Build with AI</Text>
90+
</ModalHeader>
91+
<ModalBody>
92+
<Flex gap={4} flexDir="column">
93+
{AI_FORM_FIELDS.map((field) => (
94+
<FormControl
95+
isRequired
96+
isInvalid={!!errors[field.key]}
97+
key={field.key}
98+
>
99+
<FormLabel>{field.label}</FormLabel>
100+
<Textarea
101+
{...register(field.key)}
102+
placeholder={field.placeholder}
103+
resize={field.resize}
104+
maxH={field.maxH}
105+
/>
106+
{errors[field.key] && (
107+
<FormErrorMessage>
108+
{errors[field.key]?.message}
109+
</FormErrorMessage>
110+
)}
111+
</FormControl>
112+
))}
113+
</Flex>
114+
</ModalBody>
115+
<ModalFooter>
116+
<Flex justifyContent="space-between" alignItems="center" w="100%">
117+
<Flex gap={1} alignItems="center">
118+
<Text textStyle="body-1">Powered by </Text>
119+
<ImageBox imageUrl={pairLogo} boxSize={10} />
120+
</Flex>
121+
<Flex gap={4}>
122+
<Button variant="clear" colorScheme="secondary" onClick={onBack}>
123+
Back
124+
</Button>
125+
<Button
126+
type="submit"
127+
leftIcon={<FaWandMagicSparkles />}
128+
isDisabled={!isValid}
129+
>
130+
{type === 'update' ? 'Update workflow' : 'Create'}
131+
</Button>
132+
</Flex>
133+
</Flex>
134+
</ModalFooter>
135+
</Form>
136+
</>
137+
)
138+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { IApp, IStep } from '@plumber/types'
2+
3+
import { createContext, useContext, useMemo } from 'react'
4+
import { useQuery } from '@apollo/client'
5+
import { Center } from '@chakra-ui/react'
6+
import { useIsMobile } from '@opengovsg/design-system-react'
7+
8+
import PrimarySpinner from '@/components/PrimarySpinner'
9+
import { GET_APPS } from '@/graphql/queries/get-apps'
10+
import { getStepGroupTypeAndCaption, getStepStructure } from '@/helpers/toolbox'
11+
12+
interface AIBuilderContextValue {
13+
allApps: IApp[]
14+
flowName: string
15+
inputPrompt: {
16+
trigger: string
17+
actions: string
18+
}
19+
isMobile: boolean
20+
isSteps: boolean
21+
output: {
22+
trigger: IStep
23+
actions: IStep[]
24+
}
25+
triggerStep: IStep | null
26+
steps: IStep[]
27+
actionSteps: IStep[]
28+
stepsBeforeGroup: IStep[]
29+
groupedSteps: IStep[][]
30+
stepGroupType: string | null
31+
stepGroupCaption: string | null
32+
}
33+
34+
const AiBuilderContext = createContext<AIBuilderContextValue | undefined>(
35+
undefined,
36+
)
37+
38+
export const useAiBuilderContext = () => {
39+
const context = useContext(AiBuilderContext)
40+
if (!context) {
41+
throw new Error(
42+
'useAiBuilderContext must be used within a AiBuilderContextProvider',
43+
)
44+
}
45+
return context
46+
}
47+
48+
interface AiBuilderContextProviderProps {
49+
children: React.ReactNode
50+
flowName: string
51+
inputPrompt: {
52+
trigger: string
53+
actions: string
54+
}
55+
isSteps: boolean
56+
output: {
57+
trigger: IStep
58+
actions: IStep[]
59+
}
60+
}
61+
62+
export const AiBuilderContextProvider = ({
63+
children,
64+
flowName,
65+
inputPrompt,
66+
isSteps,
67+
output,
68+
}: AiBuilderContextProviderProps) => {
69+
const isMobile = useIsMobile()
70+
71+
const { data: getAppsData, loading: isLoadingAllApps } = useQuery(GET_APPS)
72+
73+
const allApps = useMemo(
74+
() => getAppsData?.getApps ?? [],
75+
[getAppsData?.getApps],
76+
)
77+
const appsWithActions: IApp[] = allApps.filter(
78+
(app: IApp) => !!app.actions?.length,
79+
)
80+
81+
/**
82+
* NOTE: process the steps that have been returned by Pair
83+
* as if its in the Editor, but a lot simpler
84+
*/
85+
const steps = useMemo(
86+
() => [output?.trigger, ...(output?.actions || [])],
87+
[output?.trigger, output?.actions],
88+
)
89+
const [triggerStep, stepsBeforeGroup, groupedSteps] = useMemo(
90+
() => getStepStructure(appsWithActions, steps),
91+
[appsWithActions, steps],
92+
)
93+
94+
const { stepGroupType, stepGroupCaption } = useMemo(
95+
() => getStepGroupTypeAndCaption(groupedSteps),
96+
[groupedSteps],
97+
)
98+
99+
if (isLoadingAllApps) {
100+
return (
101+
<Center h="100vh">
102+
<PrimarySpinner fontSize="4xl" />
103+
</Center>
104+
)
105+
}
106+
107+
return (
108+
<AiBuilderContext.Provider
109+
value={{
110+
allApps,
111+
flowName,
112+
inputPrompt,
113+
isSteps,
114+
output,
115+
isMobile,
116+
steps,
117+
triggerStep,
118+
actionSteps: output?.actions || [],
119+
stepsBeforeGroup,
120+
groupedSteps,
121+
stepGroupType,
122+
stepGroupCaption,
123+
}}
124+
>
125+
{children}
126+
</AiBuilderContext.Provider>
127+
)
128+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { IApp, IStep } from '@plumber/types'
2+
3+
import { Box, Flex, Text } from '@chakra-ui/react'
4+
5+
import { branchStyles } from '@/components/FlowStepGroup/Content/IfThen/styles'
6+
7+
import Step from './Step'
8+
9+
interface BranchStepProps {
10+
allApps: IApp[]
11+
branchSteps: IStep[]
12+
isMobile: boolean
13+
}
14+
15+
export default function BranchStep(props: BranchStepProps) {
16+
const { allApps, branchSteps, isMobile } = props
17+
18+
const branchName = branchSteps[0].parameters?.branchName as string
19+
20+
return (
21+
<Flex key={String(branchSteps[0].position)} {...branchStyles.container}>
22+
<Box
23+
w={isMobile ? '0px' : '100%'}
24+
mb={2}
25+
h={6}
26+
overflow="hidden"
27+
role="group"
28+
>
29+
<Text textStyle="subhead-1" color="base.content.default" noOfLines={1}>
30+
{branchName}
31+
</Text>
32+
</Box>
33+
<Box w="100%">
34+
{branchSteps.map((step, index) => (
35+
<Step
36+
key={String(step.position)}
37+
allApps={allApps}
38+
step={step}
39+
isNested={true}
40+
isLastStep={index === branchSteps.length - 1}
41+
/>
42+
))}
43+
</Box>
44+
</Flex>
45+
)
46+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Flex } from '@chakra-ui/react'
2+
import { useIsMobile } from '@opengovsg/design-system-react'
3+
4+
import { MIN_FLOW_STEP_WIDTH } from '@/components/Editor/constants'
5+
import { flowStepGroupStyles } from '@/components/FlowStepGroup/styles'
6+
7+
import GroupedStepHeader from './GroupedStepHeader'
8+
9+
interface GroupedStepContainerProps {
10+
children: React.ReactNode
11+
isNested: boolean
12+
stepGroupType: string
13+
stepGroupCaption: string
14+
}
15+
16+
export default function GroupedStepContainer(props: GroupedStepContainerProps) {
17+
const { children, isNested, stepGroupType, stepGroupCaption } = props
18+
const isMobile = useIsMobile()
19+
return (
20+
<Flex justifyContent="center" w="100%">
21+
<Flex
22+
{...flowStepGroupStyles.container}
23+
display={isMobile ? 'block' : 'flex'}
24+
w="100%"
25+
minW={MIN_FLOW_STEP_WIDTH}
26+
pb={4}
27+
maxW="600px"
28+
>
29+
<GroupedStepHeader
30+
stepGroupType={stepGroupType}
31+
stepGroupCaption={stepGroupCaption}
32+
isNested={isNested}
33+
/>
34+
{children}
35+
</Flex>
36+
</Flex>
37+
)
38+
}

0 commit comments

Comments
 (0)