Skip to content
Closed
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
2 changes: 2 additions & 0 deletions packages/react/src/sds/ai/F0AiChat/F0AiChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const F0AiChatProviderComponent = ({
chatMessages,
chatInput,
welcomeScreenSuggestions,
welcomeScreenCards,
disclaimer,
resizable = false,
defaultVisualizationMode,
Expand Down Expand Up @@ -72,6 +73,7 @@ const F0AiChatProviderComponent = ({
chatMessages={chatMessages}
chatInput={chatInput}
welcomeScreenSuggestions={welcomeScreenSuggestions}
welcomeScreenCards={welcomeScreenCards}
disclaimer={disclaimer}
resizable={resizable}
defaultVisualizationMode={defaultVisualizationMode}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const MockConnectedChatInput = () => {
visualizationMode,
creditWarning,
welcomeScreenSuggestions,
welcomeScreenCards,
tracking,
openGame,
} = useAiChat()
Expand Down Expand Up @@ -70,6 +71,13 @@ export const MockConnectedChatInput = () => {
[sendMessage, tracking]
)

const handleCardSelect = useCallback(
(message: string) => {
sendMessage(message)
},
[sendMessage]
)

return (
<F0AiChatTextArea
ref={containerRef}
Expand All @@ -91,6 +99,8 @@ export const MockConnectedChatInput = () => {
fullscreen={fullscreen}
welcomeScreenSuggestions={welcomeScreenSuggestions}
onSuggestionClick={handleSuggestionClick}
welcomeScreenCards={welcomeScreenCards}
onCardSelect={handleCardSelect}
/>
)
}
1 change: 1 addition & 0 deletions packages/react/src/sds/ai/F0AiChat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type {
DashboardCanvasContent,
DataDownloadCanvasContent,
F0AIMessage,
F0AiChatWelcomeCard,
F0Message,
F0ToolCall,
FormCanvasContent,
Expand Down
8 changes: 7 additions & 1 deletion packages/react/src/sds/ai/F0AiChat/internal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
type PendingQuote,
type TranscribeFn,
type VisualizationMode,
WelcomeScreenSuggestion,
type F0AiChatWelcomeCard,
type WelcomeScreenSuggestion,
} from "./types"

/**
Expand All @@ -33,6 +34,7 @@ export interface AiChatState {
chatMessages?: React.ReactNode
chatInput?: React.ReactNode
welcomeScreenSuggestions?: WelcomeScreenSuggestion[]
welcomeScreenCards?: F0AiChatWelcomeCard[]
disclaimer?: AiChatDisclaimer
resizable?: boolean
defaultVisualizationMode?: VisualizationMode
Expand Down Expand Up @@ -84,6 +86,10 @@ export type AiChatProviderReturnValue = {
setWelcomeScreenSuggestions: React.Dispatch<
React.SetStateAction<WelcomeScreenSuggestion[]>
>
welcomeScreenCards: F0AiChatWelcomeCard[]
setWelcomeScreenCards: React.Dispatch<
React.SetStateAction<F0AiChatWelcomeCard[]>
>
onThumbsUp?: (
message: F0AIMessage,
{ threadId, feedback }: { threadId: string; feedback: string }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import {

import { useI18n } from "@/lib/providers/i18n"

import { AiChatProviderReturnValue, AiChatState } from "../internal-types"
import type { AiChatProviderReturnValue, AiChatState } from "../internal-types"
import {
type AiChatMode,
type CanvasContent,
type PendingContext,
type PendingQuote,
type VisualizationMode,
WelcomeScreenSuggestion,
type F0AiChatWelcomeCard,
type WelcomeScreenSuggestion,
} from "../types"
import { DEFAULT_CHAT_WIDTH } from "../utils/constants"

Expand Down Expand Up @@ -57,6 +58,7 @@ export const AiChatStateProvider: FC<PropsWithChildren<AiChatState>> = ({
chatMessages,
chatInput,
welcomeScreenSuggestions: initialWelcomeScreenSuggestions = [],
welcomeScreenCards: initialWelcomeScreenCards = [],
disclaimer,
resizable = false,
defaultVisualizationMode = "sidepanel",
Expand Down Expand Up @@ -114,6 +116,9 @@ export const AiChatStateProvider: FC<PropsWithChildren<AiChatState>> = ({
const [welcomeScreenSuggestions, setWelcomeScreenSuggestions] = useState<
WelcomeScreenSuggestion[]
>(initialWelcomeScreenSuggestions)
const [welcomeScreenCards, setWelcomeScreenCards] = useState<
F0AiChatWelcomeCard[]
>(initialWelcomeScreenCards)
const i18n = useI18n()
const [placeholders, setPlaceholders] = useState<string[]>([
i18n.t("ai.inputPlaceholder"),
Expand Down Expand Up @@ -262,6 +267,8 @@ export const AiChatStateProvider: FC<PropsWithChildren<AiChatState>> = ({
chatInput,
welcomeScreenSuggestions,
setWelcomeScreenSuggestions,
welcomeScreenCards,
setWelcomeScreenCards,
onThumbsUp,
onThumbsDown,
placeholders,
Expand Down Expand Up @@ -358,6 +365,7 @@ const REAL_VALUES: Partial<AiChatProviderReturnValue> = {
shouldPlayEntranceAnimation: true,
placeholders: [],
welcomeScreenSuggestions: [],
welcomeScreenCards: [],
}

const NO_PROVIDER_CONTEXT = new Proxy({} as AiChatProviderReturnValue, {
Expand Down
28 changes: 28 additions & 0 deletions packages/react/src/sds/ai/F0AiChat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ export type AiChatProviderProps = {
*/
initialMessage?: string | string[]
welcomeScreenSuggestions?: WelcomeScreenSuggestion[]
/**
* Action/prompt cards rendered below the composer on the fullscreen welcome
* screen. The chat owns layout and, for prompt cards, the send.
*/
welcomeScreenCards?: F0AiChatWelcomeCard[]
disclaimer?: AiChatDisclaimer
/**
* Enable resizable chat window
Expand Down Expand Up @@ -425,6 +430,29 @@ export type WelcomeScreenSuggestion = {
items: WelcomeScreenSuggestionItem[]
}

/**
* A card shown below the composer on the fullscreen welcome screen, rendered
* as an `F0CardHorizontal`. Two kinds:
* - **Prompt cards** carry a `message` the chat sends when the card is clicked.
* - **Action cards** carry an `onClick` (e.g. open a dialog) which takes
* precedence over `message`.
*
* Data-driven and runtime-agnostic — the chat owns the layout and, for prompt
* cards, the send.
*/
export type F0AiChatWelcomeCard = {
icon: IconType
title: string
description?: string
/** Prompt cards: the message the chat sends when the card is clicked. */
message?: string
/**
* Action cards: custom click handler (e.g. open a dialog). Takes precedence
* over `message` when both are present.
*/
onClick?: () => void
}

/**
* Disclaimer configuration for the chat input
*/
Expand Down
31 changes: 24 additions & 7 deletions packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CreditWarningWrapper } from "./components/CreditWarningWrapper"
import { MentionPopover } from "./components/MentionPopover"
import { PendingQuoteChip } from "./components/PendingQuoteChip"
import { TextareaField } from "./components/TextareaField"
import { WelcomeScreenCardsRow } from "./components/WelcomeScreenCardsRow"
import { WelcomeScreenSuggestionsRow } from "./components/WelcomeScreenSuggestionsRow"
import type {
WelcomeScreenSuggestion,
Expand Down Expand Up @@ -81,6 +82,8 @@ export const F0AiChatTextArea = ({
fullscreen = false,
welcomeScreenSuggestions,
onSuggestionClick,
welcomeScreenCards,
onCardSelect,
ref,
}: F0AiChatTextAreaProps) => {
const translation = useI18n()
Expand Down Expand Up @@ -326,9 +329,9 @@ export const F0AiChatTextArea = ({
const hasOverlay =
mentions.mentions.length > 0 || mentions.inlineCompletion !== null

// Welcome suggestions row. On the welcome screen it sits above the textarea
// in sidepanel and below it in fullscreen (so the popover can open downward
// without covering the composer).
// Welcome suggestions row. On the welcome screen it always sits above the
// textarea (both sidepanel and fullscreen); the popover opens upward so it
// doesn't cover the composer.
const showSuggestions =
isWelcomeScreen &&
!!welcomeScreenSuggestions &&
Expand All @@ -340,10 +343,19 @@ export const F0AiChatTextArea = ({
suggestions={welcomeScreenSuggestions}
onItemClick={handleSuggestionClick}
onItemHover={setHoveredSuggestion}
side={fullscreen ? "bottom" : "top"}
side="top"
/>
) : null

// Welcome cards sit below the composer on the fullscreen welcome screen
// (same gate the footer slot uses). Prompt cards send their message through
// `onCardSelect`; action cards run their own `onClick`.
const showWelcomeCards =
isWelcomeScreen &&
fullscreen &&
!!welcomeScreenCards &&
welcomeScreenCards.length > 0

const isFullscreenWelcome = fullscreen && isWelcomeScreen

// Reveal the composer when the welcome screen gives way to the conversation
Expand All @@ -368,7 +380,7 @@ export const F0AiChatTextArea = ({
{...(fullscreen ? composerReveal : {})}
>
<div className="flex w-full max-w-content flex-col gap-2">
{suggestionsRow && !fullscreen && <div>{suggestionsRow}</div>}
{suggestionsRow && <div>{suggestionsRow}</div>}
<CreditWarningWrapper creditWarning={creditWarning}>
<motion.form
aria-busy={inProgress}
Expand Down Expand Up @@ -550,8 +562,13 @@ export const F0AiChatTextArea = ({
</CreditWarningWrapper>
</div>

{suggestionsRow && fullscreen && (
<div className="w-full max-w-content">{suggestionsRow}</div>
{showWelcomeCards && (
<div className="w-full max-w-content pt-2">
<WelcomeScreenCardsRow
cards={welcomeScreenCards}
onCardSelect={onCardSelect}
/>
</div>
)}

{footer && isWelcomeScreen && fullscreen && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useRef, useState } from "react"
import { F0AiChatTextArea } from "../F0AiChatTextArea"
import type { F0AiChatTextAreaSubmitPayload } from "../types"

import { File, Marketplace } from "@/icons/app"
import { mockTranscribe } from "@/lib/storybook-utils/ai-mocks"

import { F0ClarifyingPanel } from "../../F0ClarifyingPanel"
Expand All @@ -12,6 +13,7 @@ import type {
AiChatCreditWarning,
AiChatDisclaimer,
AiChatFileAttachmentConfig,
F0AiChatWelcomeCard,
PendingContext,
PendingQuote,
PersonProfile,
Expand Down Expand Up @@ -91,6 +93,21 @@ const CREDIT_WARNING: AiChatCreditWarning = {
onDismiss: () => console.log("dismiss clicked"),
}

const WELCOME_CARDS: F0AiChatWelcomeCard[] = [
{
icon: File,
title: "Empty survey",
description: "Start from scratch",
message: "Create an empty survey.",
},
{
icon: Marketplace,
title: "Templates",
description: "Browse pre-made surveys",
onClick: () => console.log("open templates"),
},
]

const noop = () => {}

const buildClarifyingState = (
Expand Down Expand Up @@ -135,6 +152,7 @@ type WrapperProps = {
creditWarning?: AiChatCreditWarning
disclaimer?: AiChatDisclaimer
footer?: React.ReactNode
welcomeScreenCards?: F0AiChatWelcomeCard[]
isWelcomeScreen?: boolean
fullscreen?: boolean
inProgress?: boolean
Expand All @@ -151,6 +169,7 @@ const Wrapper = ({
creditWarning,
disclaimer,
footer,
welcomeScreenCards,
isWelcomeScreen,
fullscreen,
inProgress,
Expand Down Expand Up @@ -194,6 +213,13 @@ const Wrapper = ({
searchPersons={searchPersons}
disclaimer={disclaimer}
footer={footer}
welcomeScreenCards={welcomeScreenCards}
onCardSelect={(message) =>
setSubmissions((prev) => [
...prev,
{ text: message, files: [], context: null, quote: null },
])
}
isWelcomeScreen={isWelcomeScreen}
fullscreen={fullscreen}
/>
Expand Down Expand Up @@ -311,6 +337,15 @@ export const FullscreenWelcome: Story = {
},
}

export const WithWelcomeCards: Story = {
args: {
isWelcomeScreen: true,
fullscreen: true,
welcomeScreenCards: WELCOME_CARDS,
disclaimer: DISCLAIMER,
},
}

export const InProgress: Story = {
args: {
inProgress: true,
Expand Down
Loading
Loading