diff --git a/packages/frontend/src/assets/plumber-logo.svg b/packages/frontend/src/assets/plumber-logo.svg
new file mode 100644
index 0000000000..23a7aab094
--- /dev/null
+++ b/packages/frontend/src/assets/plumber-logo.svg
@@ -0,0 +1,5 @@
+
diff --git a/packages/frontend/src/hooks/useChatStream.ts b/packages/frontend/src/hooks/useChatStream.ts
new file mode 100644
index 0000000000..3daa2cbac5
--- /dev/null
+++ b/packages/frontend/src/hooks/useChatStream.ts
@@ -0,0 +1,127 @@
+import { useCallback, useMemo } from 'react'
+import type { UIMessage } from '@ai-sdk/react'
+import { useChat } from '@ai-sdk/react'
+import { useToast } from '@opengovsg/design-system-react'
+import { DefaultChatTransport } from 'ai'
+
+export interface Message {
+ text: string
+ traceId?: string
+ generationId?: string
+ isUser: boolean
+}
+
+// Custom message type with metadata
+type CustomUIMessage = UIMessage<{
+ traceId?: string
+}>
+
+export function useChatStream() {
+ const toast = useToast()
+
+ const {
+ messages: aiMessages,
+ sendMessage,
+ status,
+ error: aiError,
+ stop,
+ } = useChat({
+ transport: new DefaultChatTransport({
+ api: '/api/chat',
+ credentials: 'include',
+ prepareSendMessagesRequest: ({ messages }) => {
+ // Send all messages to maintain conversation context
+ const body = {
+ messages: messages,
+ sessionId: '',
+ }
+ return { body }
+ },
+ }),
+ onError: (error: Error) => {
+ toast({
+ title: 'Error: ' + error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ position: 'top',
+ })
+ },
+ })
+
+ // Helper function to extract text content from UIMessage
+ const extractTextContent = useCallback((msg: CustomUIMessage): string => {
+ return msg.parts
+ .filter((part) => part.type === 'text')
+ .map((part) => (part as any).text)
+ .join('')
+ }, [])
+
+ // Transform AI SDK messages to our Message format
+ const messages = useMemo(() => {
+ const isActivelyStreaming = status === 'streaming' || status === 'submitted'
+
+ // Filter user and assistant messages
+ let messagesToTransform = aiMessages.filter(
+ (msg) => msg.role === 'user' || msg.role === 'assistant',
+ )
+
+ // If streaming, exclude the last assistant message (shown via currentResponse)
+ if (isActivelyStreaming && messagesToTransform.length > 0) {
+ const lastMsg = messagesToTransform[messagesToTransform.length - 1]
+ if (lastMsg.role === 'assistant') {
+ messagesToTransform = messagesToTransform.slice(0, -1)
+ }
+ }
+
+ return messagesToTransform.map((msg) => {
+ // Extract traceId from message metadata
+ const traceId = msg.metadata?.traceId
+
+ return {
+ text: extractTextContent(msg),
+ isUser: msg.role === 'user',
+ traceId: traceId,
+ }
+ })
+ }, [aiMessages, extractTextContent, status])
+
+ // Get the current streaming response (last assistant message that's still being streamed)
+ const currentResponse = useMemo(() => {
+ const isActivelyStreaming = status === 'streaming' || status === 'submitted'
+
+ if (!isActivelyStreaming) {
+ return ''
+ }
+
+ const lastMessage = aiMessages[aiMessages.length - 1]
+ if (lastMessage && lastMessage.role === 'assistant') {
+ return extractTextContent(lastMessage)
+ }
+ return ''
+ }, [aiMessages, status, extractTextContent])
+
+ // Wrapper for sendMessage that matches the expected signature
+ const sendMessageWrapper = useCallback(
+ (userPrompt: string) => {
+ sendMessage({
+ role: 'user',
+ parts: [{ type: 'text', text: userPrompt }],
+ })
+ },
+ [sendMessage],
+ )
+
+ const cancelStream = useCallback(() => {
+ stop()
+ }, [stop])
+
+ return {
+ messages,
+ currentResponse,
+ isStreaming: status === 'submitted' || status === 'streaming',
+ error: aiError?.message || null,
+ sendMessage: sendMessageWrapper,
+ cancelStream,
+ }
+}
diff --git a/packages/frontend/src/pages/AiBuilder/AiBuilderContext.tsx b/packages/frontend/src/pages/AiBuilder/AiBuilderContext.tsx
index 910d9882c4..74c4d2a8a1 100644
--- a/packages/frontend/src/pages/AiBuilder/AiBuilderContext.tsx
+++ b/packages/frontend/src/pages/AiBuilder/AiBuilderContext.tsx
@@ -7,6 +7,7 @@ import { datadogRum } from '@datadog/browser-rum'
import { useIsMobile } from '@opengovsg/design-system-react'
import PrimarySpinner from '@/components/PrimarySpinner'
+import { LaunchDarklyContext } from '@/contexts/LaunchDarkly'
import { GET_APPS } from '@/graphql/queries/get-apps'
import { getStepGroupTypeAndCaption, getStepStructure } from '@/helpers/toolbox'
@@ -30,9 +31,9 @@ interface AiBuilderStep extends IStep {
interface AIBuilderContextValue extends AIBuilderSharedProps {
allApps: IApp[]
- isMobile: boolean
triggerStep: IStep | null
steps: AiBuilderStep[]
+ isMobile: boolean
actionSteps: IStep[]
stepsBeforeGroup: IStep[]
groupedSteps: IStep[][]
@@ -40,6 +41,8 @@ interface AIBuilderContextValue extends AIBuilderSharedProps {
stepGroupCaption: string | null
// DataDog RUM Session ID so we can associate the trace with the RUM
ddSessionId: string
+ // TODO(kevinkim-ogp): remove this once A/B test is complete
+ aiBuilderType: string
}
const AiBuilderContext = createContext(
@@ -69,6 +72,9 @@ export const AiBuilderContextProvider = ({
}: AiBuilderContextProviderProps) => {
const isMobile = useIsMobile()
const ddSessionId = datadogRum.getInternalContext()?.session_id ?? ''
+ // TODO(kevinkim-ogp): remove this once A/B test is complete
+ const { getFlagValue } = useContext(LaunchDarklyContext)
+ const aiBuilderType = getFlagValue('ai-builder-type', 'none')
const { data: getAppsData, loading: isLoadingAllApps } = useQuery(GET_APPS)
@@ -126,6 +132,8 @@ export const AiBuilderContextProvider = ({
stepGroupType,
stepGroupCaption,
ddSessionId,
+ // TODO(kevinkim-ogp): remove this once A/B test is complete
+ aiBuilderType,
}}
>
{children}
diff --git a/packages/frontend/src/pages/AiBuilder/components/ChatInterface/PromptInput.tsx b/packages/frontend/src/pages/AiBuilder/components/ChatInterface/PromptInput.tsx
new file mode 100644
index 0000000000..0e3da32d22
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/ChatInterface/PromptInput.tsx
@@ -0,0 +1,168 @@
+import {
+ type FormEvent,
+ type KeyboardEvent,
+ type SyntheticEvent,
+ useRef,
+ useState,
+} from 'react'
+import { FaArrowCircleRight } from 'react-icons/fa'
+import { FaCircleStop } from 'react-icons/fa6'
+import { Box, Flex, Icon, Text, Textarea } from '@chakra-ui/react'
+
+import pairLogo from '@/assets/pair-logo.svg'
+import { ImageBox } from '@/components/FlowStepConfigurationModal/ChooseAndAddConnection/ConfigureExcelConnection'
+import { AI_CHAT_IDEAS, AiChatIdea, AiFormIdea } from '@/pages/Flows/constants'
+
+import IdeaButtons from '../IdeaButtons'
+
+interface PromptInputProps {
+ isStreaming: boolean
+ showIdeas?: boolean
+ placeholder?: string
+ sendMessage: (message: string) => void
+ cancelStream: () => void
+}
+
+export default function PromptInput({
+ isStreaming,
+ showIdeas = false,
+ placeholder = 'Send a message',
+ sendMessage,
+ cancelStream,
+}: PromptInputProps) {
+ const [input, setInput] = useState('')
+ const textareaRef = useRef(null)
+
+ const handleSubmit = async (e: SyntheticEvent) => {
+ e.preventDefault()
+ if (input?.trim()) {
+ sendMessage(input)
+ setInput('')
+ if (textareaRef.current) {
+ textareaRef.current.style.height = 'auto'
+ }
+ }
+ }
+
+ const handleKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault()
+ handleSubmit(e)
+ }
+ }
+
+ const handleResize = (e?: FormEvent) => {
+ const target = e?.currentTarget || textareaRef.current
+ if (!target) {
+ return
+ }
+ const maxHeight = window.innerHeight * 0.4 - 100 // 40vh minus padding/margins
+ target.style.height = 'auto'
+ target.style.height = Math.min(target.scrollHeight, maxHeight) + 'px'
+ }
+
+ return (
+
+
+
+
+ {showIdeas && (
+ {
+ setInput((idea as AiChatIdea).input)
+ // trigger resize after state update
+ setTimeout(() => handleResize(), 0)
+ }}
+ />
+ )}
+
+
+
+ Powered by{' '}
+
+
+
+
+ )
+}
diff --git a/packages/frontend/src/pages/AiBuilder/components/ChatInterface/index.tsx b/packages/frontend/src/pages/AiBuilder/components/ChatInterface/index.tsx
new file mode 100644
index 0000000000..45ea962081
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/ChatInterface/index.tsx
@@ -0,0 +1,155 @@
+import { useEffect, useRef, useState } from 'react'
+import { IoChevronDown } from 'react-icons/io5'
+import { Box, Flex, IconButton, Text } from '@chakra-ui/react'
+
+import { useChatStream } from '@/hooks/useChatStream'
+import ChatMessages from '@/pages/AiBuilder/components/ChatMessages'
+
+import PromptInput from './PromptInput'
+
+export default function ChatInterface() {
+ const { messages, currentResponse, isStreaming, sendMessage, cancelStream } =
+ useChatStream()
+ const messagesEndRef = useRef(null)
+ const messagesContainerRef = useRef(null)
+ const [showScrollButton, setShowScrollButton] = useState(false)
+
+ // Check if user is near bottom of scroll
+ useEffect(() => {
+ const container = messagesContainerRef.current
+ if (!container) {
+ return
+ }
+
+ const handleScroll = () => {
+ const { scrollTop, scrollHeight, clientHeight } = container
+ const isNearBottom = scrollHeight - scrollTop - clientHeight < 100
+ setShowScrollButton(!isNearBottom && scrollHeight > clientHeight)
+ }
+
+ container.addEventListener('scroll', handleScroll)
+ // Check initially and on content changes
+ handleScroll()
+
+ return () => container.removeEventListener('scroll', handleScroll)
+ }, [messages, currentResponse])
+
+ // Auto-scroll to bottom when new messages arrive (only if already at bottom)
+ useEffect(() => {
+ const container = messagesContainerRef.current
+ if (!container) {
+ return
+ }
+
+ const { scrollTop, scrollHeight, clientHeight } = container
+ const isNearBottom = scrollHeight - scrollTop - clientHeight < 100
+
+ if (isNearBottom) {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
+ }
+ }, [messages, currentResponse])
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
+ }
+
+ const hasMessages = messages.length > 0 || isStreaming
+
+ const handleOpenPreview = () => {
+ // TODO: implement steps preview
+ }
+
+ // Initial empty state - centered layout
+ if (!hasMessages) {
+ return (
+
+ {messages.length === 0 && (
+ What happens in your workflow?
+ )}
+
+
+
+
+ )
+ }
+
+ // Chat layout - messages with input at bottom
+ return (
+
+ {/* Main Chat Area */}
+
+ {/* Messages Area */}
+
+
+ {/* Input Area - Fixed at bottom */}
+
+ {/* Scroll to Bottom Button */}
+ {showScrollButton && (
+ }
+ onClick={scrollToBottom}
+ size="xs"
+ borderRadius="full"
+ border="1px"
+ bg="white"
+ _hover={{
+ bg: 'interaction.muted.neutral.hover',
+ }}
+ position="absolute"
+ top="-56px"
+ left="50%"
+ transform="translateX(-50%)"
+ zIndex={10}
+ boxShadow="md"
+ />
+ )}
+
+
+
+
+
+
+ {/* TODO: Add Side Drawer */}
+
+ )
+}
diff --git a/packages/frontend/src/pages/AiBuilder/components/ChatMessages/ChatMessage.tsx b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/ChatMessage.tsx
new file mode 100644
index 0000000000..b9c6439b0b
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/ChatMessage.tsx
@@ -0,0 +1,53 @@
+import { Box, Flex, Text } from '@chakra-ui/react'
+
+import { Message } from '@/hooks/useChatStream'
+import { ChakraStreamdown } from '@/theme/components/Streamdown'
+
+import ChatMessageToolbar from './ChatMessageToolbar'
+import PlumberAvatar from './PlumberAvatar'
+
+interface ChatMessageProps {
+ message: Message
+}
+
+const AiMessage = ({ message }: ChatMessageProps) => {
+ return (
+
+
+
+
+ {message.text || ''}
+
+
+
+
+ )
+}
+
+const UserMessage = ({ message }: ChatMessageProps) => {
+ return (
+
+
+
+ {message.text}
+
+
+
+ )
+}
+
+const ChatMessage = ({ message }: { message: Message }) => {
+ if (message.isUser) {
+ return
+ }
+ return
+}
+
+export default ChatMessage
diff --git a/packages/frontend/src/pages/AiBuilder/components/ChatMessages/ChatMessageToolbar.tsx b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/ChatMessageToolbar.tsx
new file mode 100644
index 0000000000..6549315f95
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/ChatMessageToolbar.tsx
@@ -0,0 +1,28 @@
+import { FaRegThumbsDown } from 'react-icons/fa'
+import { Flex, Icon, IconButton, Tooltip } from '@chakra-ui/react'
+
+const DEFAULT_BUTTON_PROPS = {
+ size: 'xs',
+ variant: 'clear',
+ color: 'gray.500',
+ _hover: { color: 'gray.700', bg: 'gray.100' },
+}
+
+export default function ChatMessageToolbar() {
+ const handleThumbsDown = () => {
+ // TODO: Implement feedback functionality
+ }
+
+ return (
+
+
+ }
+ onClick={handleThumbsDown}
+ />
+
+
+ )
+}
diff --git a/packages/frontend/src/pages/AiBuilder/components/ChatMessages/Loader.tsx b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/Loader.tsx
new file mode 100644
index 0000000000..16378de8ff
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/Loader.tsx
@@ -0,0 +1,30 @@
+import { Box, Flex, keyframes } from '@chakra-ui/react'
+
+// Animated dots keyframes
+const dotFlashing = keyframes`
+ 0%, 80%, 100% { opacity: 0.3; }
+ 40% { opacity: 1; }
+`
+
+function Dot({ animation }: { animation: string }) {
+ return (
+
+ )
+}
+
+export default function Loader() {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/packages/frontend/src/pages/AiBuilder/components/ChatMessages/PlumberAvatar.tsx b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/PlumberAvatar.tsx
new file mode 100644
index 0000000000..af02d14e08
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/PlumberAvatar.tsx
@@ -0,0 +1,18 @@
+import { Image, ImageProps } from '@chakra-ui/react'
+
+import plumberLogo from '@/assets/plumber-logo.svg'
+
+const PlumberAvatar = (props: ImageProps) => {
+ return (
+
+ )
+}
+
+export default PlumberAvatar
diff --git a/packages/frontend/src/pages/AiBuilder/components/ChatMessages/StreamingMessage.tsx b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/StreamingMessage.tsx
new file mode 100644
index 0000000000..c4a4876a16
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/StreamingMessage.tsx
@@ -0,0 +1,34 @@
+import { Box, Flex } from '@chakra-ui/react'
+
+import Loader from '@/pages/AiBuilder/components/ChatMessages/Loader'
+import { ChakraStreamdown } from '@/theme/components/Streamdown'
+
+import PlumberAvatar from './PlumberAvatar'
+
+interface StreamingMessageProps {
+ currentResponse: string
+}
+
+const StreamingMessage = ({ currentResponse }: StreamingMessageProps) => {
+ if (currentResponse) {
+ return (
+
+
+
+
+ {currentResponse}
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+export default StreamingMessage
diff --git a/packages/frontend/src/pages/AiBuilder/components/ChatMessages/index.tsx b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/index.tsx
new file mode 100644
index 0000000000..27083bb035
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/ChatMessages/index.tsx
@@ -0,0 +1,50 @@
+import { Box, Flex, VStack } from '@chakra-ui/react'
+
+import { Message } from '@/hooks/useChatStream'
+
+import ChatMessage from './ChatMessage'
+import StreamingMessage from './StreamingMessage'
+
+interface ChatMessagesProps {
+ messages: Message[]
+ currentResponse: string
+ isStreaming: boolean
+ messagesEndRef: React.RefObject
+ messagesContainerRef: React.RefObject
+ hasMessages: boolean
+ onOpenDrawer: () => void
+}
+
+export default function ChatMessages({
+ messages,
+ currentResponse,
+ isStreaming,
+ messagesEndRef,
+ messagesContainerRef,
+ onOpenDrawer: _onOpenDrawer, // TODO: Implement preview drawer
+}: ChatMessagesProps) {
+ return (
+
+
+
+ {messages.map((message, index) => (
+
+ ))}
+
+ {/* Streaming response */}
+ {isStreaming && (
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/packages/frontend/src/pages/AiBuilder/components/IdeaButtons.tsx b/packages/frontend/src/pages/AiBuilder/components/IdeaButtons.tsx
new file mode 100644
index 0000000000..146f7c2141
--- /dev/null
+++ b/packages/frontend/src/pages/AiBuilder/components/IdeaButtons.tsx
@@ -0,0 +1,52 @@
+import { Flex, Text } from '@chakra-ui/react'
+import { Button, FormLabel, useIsMobile } from '@opengovsg/design-system-react'
+
+import { TemplateIcon } from '@/helpers/flow-templates'
+import { AiChatIdea, AiFormIdea } from '@/pages/Flows/constants'
+
+interface IdeaButtonsProps {
+ ideas: AiChatIdea[] | AiFormIdea[]
+ onClick: (idea: AiChatIdea | AiFormIdea) => void
+}
+
+export default function IdeaButtons({ ideas, onClick }: IdeaButtonsProps) {
+ const isMobile = useIsMobile()
+
+ return (
+
+
+ {/* arbitrary isRequired to hide optional text */}
+ Try:
+
+
+ {ideas.map((idea) => (
+
+ ))}
+
+
+ )
+}
diff --git a/packages/frontend/src/pages/AiBuilder/index.tsx b/packages/frontend/src/pages/AiBuilder/index.tsx
index 2f2f35ad02..5647823c44 100644
--- a/packages/frontend/src/pages/AiBuilder/index.tsx
+++ b/packages/frontend/src/pages/AiBuilder/index.tsx
@@ -2,12 +2,12 @@ import { Helmet } from 'react-helmet'
import { BiChevronLeft } from 'react-icons/bi'
import { Link, useLocation } from 'react-router-dom'
import { Box, Container, Flex, HStack, Icon, Text } from '@chakra-ui/react'
-import { Tag } from '@opengovsg/design-system-react'
import { EDITOR_MARGIN_TOP } from '@/components/Editor/constants'
import * as URLS from '@/config/urls'
import InvalidEditorPage from '@/pages/Editor/components/InvalidEditorPage'
+import ChatInterface from './components/ChatInterface'
import StepsPreview from './components/StepsPreview'
import {
AiBuilderContextProvider,
@@ -15,7 +15,12 @@ import {
} from './AiBuilderContext'
function AiBuilderContent() {
- const { flowName, isFormMode } = useAiBuilderContext()
+ const { aiBuilderType, flowName, isFormMode } = useAiBuilderContext()
+
+ // TODO(kevinkim-ogp): remove this once A/B test is complete
+ if (aiBuilderType === 'ai-form') {
+ return
+ }
return (
<>
@@ -55,7 +60,7 @@ function AiBuilderContent() {
- {isFormMode ? (
-
- ) : (
-
- Build with AI
- What happens in your workflow?
- {/* TODO: Add chat interface */}
-
- )}
+ {isFormMode ? : }
>
diff --git a/packages/frontend/src/pages/Flows/components/CreateFlowModal/index.tsx b/packages/frontend/src/pages/Flows/components/CreateFlowModal/index.tsx
index da501398be..743537ecd5 100644
--- a/packages/frontend/src/pages/Flows/components/CreateFlowModal/index.tsx
+++ b/packages/frontend/src/pages/Flows/components/CreateFlowModal/index.tsx
@@ -22,7 +22,7 @@ interface CreateFlowModalProps {
export default function CreateFlowModal(props: CreateFlowModalProps) {
const { onClose } = props
- const { createMode } = useCreateFlowContext()
+ const { createMode, aiBuilderType } = useCreateFlowContext()
const navigate = useNavigate()
const [createFlow, { loading }] = useMutation(CREATE_FLOW)
@@ -63,26 +63,32 @@ export default function CreateFlowModal(props: CreateFlowModalProps) {
[createFlow, navigate],
)
- const handleSubmit = (_event: FormEvent) => {
+ const handleSubmit = (event: FormEvent) => {
const trimmedFlowName = inputRef.current?.value.trim()
if (!trimmedFlowName) {
return
}
- if (createMode === 'ai-form') {
+ // TODO(kevinkim-ogp): remove aiBuilderType once A/B test is complete
+ if (
+ createMode === 'ai-form' &&
+ (aiBuilderType === 'ai-form' || aiBuilderType === 'all')
+ ) {
// Store the flow name before switching to AI modal content
setFlowName(trimmedFlowName)
setShowAiModalContent(true)
return
}
- // TODO: Add AI chat mode
- // if (createMode === 'ai-chat') {
- // onClose()
- // navigate(`${URLS.EDITOR}/ai`, {
- // state: { flowName: trimmedFlowName },
- // })
- // }
+ if (createMode === 'ai-chat' && aiBuilderType === 'ai-chat') {
+ event.preventDefault()
+ onClose()
+ navigate(`${URLS.EDITOR}/ai`, {
+ state: { flowName: trimmedFlowName, isFormMode: false },
+ replace: true,
+ })
+ return
+ }
// default to new flow
onCreateFlow(trimmedFlowName)
diff --git a/packages/frontend/src/pages/Flows/components/EmptyFlows.tsx b/packages/frontend/src/pages/Flows/components/EmptyFlows.tsx
index 741f26ac74..54311b8b4f 100644
--- a/packages/frontend/src/pages/Flows/components/EmptyFlows.tsx
+++ b/packages/frontend/src/pages/Flows/components/EmptyFlows.tsx
@@ -9,8 +9,12 @@ import CreatePipeTile, { TileProps } from './CreatePipeTile'
export default function EmptyFlows({ onCreate }: { onCreate: () => void }) {
const navigate = useNavigate()
- const { canUseAiBuilder, setCreateMode, setSkipModeSelection } =
- useCreateFlowContext()
+ const {
+ aiBuilderType,
+ canUseAiBuilder,
+ setCreateMode,
+ setSkipModeSelection,
+ } = useCreateFlowContext()
const TILES = [
canUseAiBuilder && {
@@ -19,9 +23,13 @@ export default function EmptyFlows({ onCreate }: { onCreate: () => void }) {
'Describe your workflow and we'll create the steps for you',
iconName: 'BiSolidMagicWand',
onClick: () => {
- setCreateMode('ai-form')
- setSkipModeSelection(true)
- onCreate()
+ if (aiBuilderType === 'ai-form' || aiBuilderType === 'all') {
+ setCreateMode('ai-form')
+ setSkipModeSelection(true)
+ onCreate()
+ } else {
+ navigate(`${URLS.EDITOR}/ai`)
+ }
},
},
{
diff --git a/packages/frontend/src/pages/Flows/constants.ts b/packages/frontend/src/pages/Flows/constants.ts
index e00fd8e7a2..f682f4010c 100644
--- a/packages/frontend/src/pages/Flows/constants.ts
+++ b/packages/frontend/src/pages/Flows/constants.ts
@@ -43,3 +43,43 @@ export const AI_FORM_IDEAS = [
"When a new event attendance is received, find the attendee in Tiles.\nIf the attendee is found, update the Attended? column to Yes.\nIf the attendee is not found, create a new row in Tiles with the attendee's details.",
},
]
+
+export type AiFormIdea = {
+ label: string
+ icon: string
+ trigger: string
+ actions: string
+}
+
+export const AI_CHAT_IDEAS = [
+ {
+ label: 'Route support enquiries',
+ icon: 'BiDirections',
+ input:
+ 'Route FormSG support requests to different teams based on the issue type selected - technical issues go to IT, billing questions to Finance, and general enquiries to Customer Service.',
+ },
+ {
+ label: 'Schedule reminders',
+ icon: 'BiCalendar',
+ input:
+ 'I need to remind supervisors who have pending cases to act on them 3 days before the due date. The pending cases are recorded in a table.',
+ },
+ {
+ label: 'Attendance taking',
+ icon: 'BiCheckDouble',
+ input:
+ 'I have a FormSG that I am using to track attendance. I need this workflow to track who has signed up and is present. It should also note down if this person is a new registrant.',
+ },
+ // {
+ // label: 'Send follow ups',
+ // icon: 'BiEnvelope',
+ // input:
+ // 'When a new form submission is received, send a follow up email to the customer.',
+ // },
+]
+
+export type AiChatIdea = {
+ label: string
+ icon: string
+ input: string
+}
diff --git a/packages/frontend/src/theme/components/Streamdown.tsx b/packages/frontend/src/theme/components/Streamdown.tsx
new file mode 100644
index 0000000000..83c9a064bd
--- /dev/null
+++ b/packages/frontend/src/theme/components/Streamdown.tsx
@@ -0,0 +1,36 @@
+import { Code, Link, List, ListItem, Text } from '@chakra-ui/react'
+import { Streamdown } from 'streamdown'
+
+interface ChakraStreamdownProps {
+ children: string
+ isAnimating?: boolean
+}
+
+export function ChakraStreamdown({
+ children,
+ isAnimating = false,
+}: ChakraStreamdownProps) {
+ return (
+ ,
+ h2: (props) => ,
+ h3: (props) => ,
+ h4: (props) => ,
+ h5: (props) => ,
+ h6: (props) => ,
+ p: (props) => ,
+ code: (props) => ,
+ a: (props) => ,
+ ul: (props) =>
,
+ ol: (props) => (
+
+ ),
+ li: (props) => ,
+ }}
+ >
+ {children}
+
+ )
+}