Skip to content

Commit 7b07358

Browse files
committed
feat: preview and create steps from chat
1 parent 9688679 commit 7b07358

File tree

7 files changed

+210
-15
lines changed

7 files changed

+210
-15
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
function parseWorkflow(markdownText: string) {
2+
const result = {
3+
trigger: '',
4+
actions: '',
5+
}
6+
7+
// Extract trigger section (everything between "#### Start the workflow" and "#### Actions")
8+
const triggerMatch = markdownText.match(
9+
/#### Start the workflow\s+([\s\S]*?)#### Actions/,
10+
)
11+
if (triggerMatch) {
12+
result.trigger = triggerMatch[1].trim()
13+
}
14+
15+
// Extract actions section (everything after "#### Actions")
16+
const actionsMatch = markdownText.match(/#### Actions\s+([\s\S]*)/)
17+
if (actionsMatch) {
18+
result.actions = actionsMatch[1].trim()
19+
}
20+
21+
return result
22+
}
23+
24+
export { parseWorkflow }
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { FaTimes } from 'react-icons/fa'
2+
import { Box, Flex, Icon, IconButton, Text } from '@chakra-ui/react'
3+
import { useIsMobile } from '@opengovsg/design-system-react'
4+
5+
import StepsPreview from '../StepsPreview'
6+
7+
interface SideDrawerProps {
8+
isOpen: boolean
9+
onClose: () => void
10+
}
11+
12+
export default function SideDrawer({ isOpen, onClose }: SideDrawerProps) {
13+
const isMobile = useIsMobile()
14+
15+
return (
16+
<Box
17+
position="absolute"
18+
right={0}
19+
top={0}
20+
w={isMobile ? '100%' : '50%'}
21+
h="100%"
22+
bg="white"
23+
borderLeft={isMobile ? 'none' : '1px'}
24+
borderColor="gray.200"
25+
transform={isOpen ? 'translateX(0)' : 'translateX(100%)'}
26+
transition="transform 0.3s ease-in-out"
27+
zIndex={10}
28+
pointerEvents={isOpen ? 'auto' : 'none'}
29+
visibility={isOpen ? 'visible' : 'hidden'}
30+
>
31+
<Flex h="100%" flexDir="column" px={6} w="full">
32+
{/* Header */}
33+
<Flex justify="space-between" align="center" py={4}>
34+
<Text fontSize="xl" fontWeight="bold">
35+
Workflow preview
36+
</Text>
37+
<IconButton
38+
aria-label="Close drawer"
39+
icon={<Icon as={FaTimes} />}
40+
onClick={onClose}
41+
variant="clear"
42+
size="sm"
43+
/>
44+
</Flex>
45+
46+
{/* Content */}
47+
<Box flex={1} overflowY="auto" pb={4}>
48+
<StepsPreview />
49+
</Box>
50+
</Flex>
51+
</Box>
52+
)
53+
}

packages/frontend/src/pages/AiBuilder/components/ChatInterface/index.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
import { useEffect, useRef, useState } from 'react'
22
import { IoChevronDown } from 'react-icons/io5'
3+
import { useNavigate } from 'react-router-dom'
34
import { Box, Flex, IconButton, Text } from '@chakra-ui/react'
5+
import { useIsMobile } from '@opengovsg/design-system-react'
46

7+
import { parseWorkflow } from '@/components/AiBuilder/helpers/parseMarkdown'
8+
import * as URLS from '@/config/urls'
59
import { useChatStream } from '@/hooks/useChatStream'
10+
import { useAiBuilderContext } from '@/pages/AiBuilder/AiBuilderContext'
611
import ChatMessages from '@/pages/AiBuilder/components/ChatMessages'
712

813
import PromptInput from './PromptInput'
14+
import SideDrawer from './SideDrawer'
915

1016
export default function ChatInterface() {
17+
const navigate = useNavigate()
18+
const isMobile = useIsMobile()
19+
const { flowName, formInput } = useAiBuilderContext()
20+
1121
const { messages, currentResponse, isStreaming, sendMessage, cancelStream } =
1222
useChatStream()
1323
const messagesEndRef = useRef<HTMLDivElement>(null)
1424
const messagesContainerRef = useRef<HTMLDivElement>(null)
25+
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
1526
const [showScrollButton, setShowScrollButton] = useState(false)
1627

1728
// Check if user is near bottom of scroll
@@ -56,7 +67,27 @@ export default function ChatInterface() {
5667
const hasMessages = messages.length > 0 || isStreaming
5768

5869
const handleOpenPreview = () => {
59-
// TODO: implement steps preview
70+
const { trigger, actions } = parseWorkflow(
71+
messages[messages.length - 1].text,
72+
)
73+
74+
// NOTE: only need to update the location state if there has been changes
75+
// if the user just closed and open the side drawer, we don't need to update
76+
// as we don't want to generate the ai steps again
77+
if (formInput?.trigger !== trigger || formInput?.actions !== actions) {
78+
navigate(`${URLS.EDITOR}/ai`, {
79+
state: {
80+
flowName,
81+
isFormMode: false,
82+
formInput: {
83+
trigger,
84+
actions,
85+
},
86+
},
87+
replace: true,
88+
})
89+
}
90+
setIsDrawerOpen(true)
6091
}
6192

6293
// Initial empty state - centered layout
@@ -89,14 +120,15 @@ export default function ChatInterface() {
89120

90121
// Chat layout - messages with input at bottom
91122
return (
92-
<Flex h="100%" w="full" position="relative">
123+
<Flex h="100%" w="full" position="relative" overflow="hidden">
93124
{/* Main Chat Area */}
94125
<Flex
95126
h="100%"
96-
w="100%"
127+
w="full"
97128
flexDir="column"
98-
transition="width 0.3s ease-in-out"
99129
position="relative"
130+
pr={isDrawerOpen && !isMobile ? '50%' : '0'}
131+
transition="padding-right 0.3s ease-in-out"
100132
>
101133
{/* Messages Area */}
102134
<ChatMessages
@@ -149,7 +181,10 @@ export default function ChatInterface() {
149181
</Box>
150182
</Flex>
151183

152-
{/* TODO: Add Side Drawer */}
184+
<SideDrawer
185+
isOpen={isDrawerOpen}
186+
onClose={() => setIsDrawerOpen(false)}
187+
/>
153188
</Flex>
154189
)
155190
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { MdOutlineRemoveRedEye } from 'react-icons/md'
2+
import { useLocation, useNavigate } from 'react-router-dom'
3+
import { Box, Flex } from '@chakra-ui/react'
4+
import { Button } from '@opengovsg/design-system-react'
5+
6+
import { Message } from '@/hooks/useChatStream'
7+
import { ChakraStreamdown } from '@/theme/components/Streamdown'
8+
9+
import PlumberAvatar from './PlumberAvatar'
10+
11+
const PreviewStepsButton = ({
12+
messages,
13+
onOpenDrawer,
14+
}: {
15+
messages: Message[]
16+
onOpenDrawer: () => void
17+
}) => {
18+
const location = useLocation()
19+
const navigate = useNavigate()
20+
21+
return (
22+
<Flex gap={3} w="full" align="start">
23+
<PlumberAvatar />
24+
<Box flex={1} color="gray.900">
25+
<ChakraStreamdown isAnimating={false}>
26+
Satisfied with your workflow?
27+
</ChakraStreamdown>
28+
<Button
29+
variant="outline"
30+
size="sm"
31+
onClick={() => {
32+
navigate(location.pathname, {
33+
state: {
34+
...location.state,
35+
chatInput: messages[messages.length - 1].text,
36+
chatMessages: messages,
37+
},
38+
replace: true,
39+
})
40+
onOpenDrawer()
41+
}}
42+
mt={2}
43+
leftIcon={<MdOutlineRemoveRedEye />}
44+
bg="white"
45+
>
46+
Preview steps
47+
</Button>
48+
</Box>
49+
</Flex>
50+
)
51+
}
52+
53+
export default PreviewStepsButton

packages/frontend/src/pages/AiBuilder/components/ChatMessages/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Box, Flex, VStack } from '@chakra-ui/react'
33
import { Message } from '@/hooks/useChatStream'
44

55
import ChatMessage from './ChatMessage'
6+
import PreviewStepsButton from './PreviewStepsButton'
67
import StreamingMessage from './StreamingMessage'
78

89
interface ChatMessagesProps {
@@ -21,7 +22,8 @@ export default function ChatMessages({
2122
isStreaming,
2223
messagesEndRef,
2324
messagesContainerRef,
24-
onOpenDrawer: _onOpenDrawer, // TODO: Implement preview drawer
25+
hasMessages,
26+
onOpenDrawer: onOpenDrawer,
2527
}: ChatMessagesProps) {
2628
return (
2729
<Flex
@@ -37,6 +39,13 @@ export default function ChatMessages({
3739
<ChatMessage key={index} message={message} />
3840
))}
3941

42+
{hasMessages && !isStreaming && (
43+
<PreviewStepsButton
44+
messages={messages}
45+
onOpenDrawer={onOpenDrawer}
46+
/>
47+
)}
48+
4049
{/* Streaming response */}
4150
{isStreaming && (
4251
<StreamingMessage currentResponse={currentResponse} />

packages/frontend/src/pages/AiBuilder/components/StepsPreview/index.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IStepConfig } from '@plumber/types'
22

3-
import { Fragment, useCallback, useEffect, useMemo } from 'react'
3+
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
44
import { useLocation, useNavigate } from 'react-router-dom'
55
import { useMutation } from '@apollo/client'
66
import {
@@ -54,6 +54,7 @@ export default function StepsPreview() {
5454
ddSessionId,
5555
} = useAiBuilderContext()
5656

57+
const [error, setError] = useState<boolean>(false)
5758
const { isOpen, onClose, onOpen } = useDisclosure()
5859
const isMobile = useIsMobile()
5960
const navigate = useNavigate()
@@ -187,7 +188,19 @@ export default function StepsPreview() {
187188
if (isGeneratingSteps || !output) {
188189
return (
189190
<Center h="100%">
190-
{output && !isGeneratingSteps ? (
191+
{error ? (
192+
<Flex
193+
flexDir="column"
194+
alignItems="center"
195+
justifyContent="center"
196+
gap={4}
197+
w="100%"
198+
maxW="400px"
199+
>
200+
<Text>Something went wrong</Text>
201+
<Button onClick={() => setError(false)}>Try again</Button>
202+
</Flex>
203+
) : output && !isGeneratingSteps ? (
191204
<PrimarySpinner fontSize="4xl" />
192205
) : (
193206
<MultiStepLoader
@@ -269,12 +282,21 @@ export default function StepsPreview() {
269282
</GroupedStepContainer>
270283
)}
271284
<VStack mt={10} gap={2}>
272-
<Text textStyle="subhead-2">How does this workflow look?</Text>
285+
{isFormMode ? (
286+
<Text textStyle="subhead-2">How does this workflow look?</Text>
287+
) : (
288+
<Text textStyle="body-1">Looks good?</Text>
289+
)}
273290
<HStack alignItems="center" justifyContent="center" gap={2}>
274-
<Button variant="outline" onClick={onOpen}>
275-
Make changes
276-
</Button>
277-
<Button variant="outline" onClick={onCreateFlow}>
291+
{isFormMode && (
292+
<Button variant="outline" onClick={onOpen}>
293+
Make changes
294+
</Button>
295+
)}
296+
<Button
297+
variant={isFormMode ? 'outline' : 'solid'}
298+
onClick={onCreateFlow}
299+
>
278300
Create this workflow
279301
</Button>
280302
</HStack>

packages/frontend/src/pages/AiBuilder/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { BiChevronLeft } from 'react-icons/bi'
33
import { Link, useLocation } from 'react-router-dom'
44
import { Box, Container, Flex, HStack, Icon, Text } from '@chakra-ui/react'
55

6-
import { EDITOR_MARGIN_TOP } from '@/components/Editor/constants'
76
import * as URLS from '@/config/urls'
87
import InvalidEditorPage from '@/pages/Editor/components/InvalidEditorPage'
98

@@ -61,7 +60,7 @@ function AiBuilderContent() {
6160
maxW="full"
6261
px={0}
6362
py={isFormMode ? 10 : 0}
64-
mt={EDITOR_MARGIN_TOP}
63+
mt="51.5px"
6564
flex={1}
6665
overflowY="auto"
6766
sx={{

0 commit comments

Comments
 (0)