From 276928213c667ab0c5d402a3ecf31477e6adcfe0 Mon Sep 17 00:00:00 2001 From: Marcos Medina Date: Mon, 22 Jun 2026 17:09:14 +0200 Subject: [PATCH 1/2] feat(ai): welcome cards + suggestions above composer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bake the welcome-cards capability into F0AiChat as a data-driven `welcomeScreenCards` prop (symmetric with `welcomeScreenSuggestions`), rendered by an internal WelcomeScreenCardsRow — replacing the standalone F0AiChatWelcomeCards component, which the consumer had to hand-wire through the `footer` slot. Also always render the welcome suggestions row above the composer so its popover opens upward instead of covering the input. Combines and supersedes #4534 and #4537. Co-Authored-By: Claude Opus 4.8 --- .../react/src/sds/ai/F0AiChat/F0AiChat.tsx | 2 + .../_mock/MockConnectedChatInput.tsx | 10 +++ packages/react/src/sds/ai/F0AiChat/index.ts | 1 + .../src/sds/ai/F0AiChat/internal-types.ts | 6 ++ .../providers/AiChatStateProvider.tsx | 8 +++ packages/react/src/sds/ai/F0AiChat/types.ts | 28 ++++++++ .../ai/F0AiChatTextArea/F0AiChatTextArea.tsx | 31 ++++++-- .../__stories__/F0AiChatTextArea.stories.tsx | 35 +++++++++ .../F0AiChatTextArea.welcomeLayout.test.tsx | 51 +++++++++++-- .../__tests__/WelcomeScreenCardsRow.test.tsx | 72 +++++++++++++++++++ .../components/WelcomeScreenCardsRow.tsx | 45 ++++++++++++ .../WelcomeScreenSuggestionsRow.tsx | 7 +- .../src/sds/ai/F0AiChatTextArea/types.ts | 17 ++++- 13 files changed, 296 insertions(+), 17 deletions(-) create mode 100644 packages/react/src/sds/ai/F0AiChatTextArea/__tests__/WelcomeScreenCardsRow.test.tsx create mode 100644 packages/react/src/sds/ai/F0AiChatTextArea/components/WelcomeScreenCardsRow.tsx diff --git a/packages/react/src/sds/ai/F0AiChat/F0AiChat.tsx b/packages/react/src/sds/ai/F0AiChat/F0AiChat.tsx index 34fdedbf29..7a13fec349 100644 --- a/packages/react/src/sds/ai/F0AiChat/F0AiChat.tsx +++ b/packages/react/src/sds/ai/F0AiChat/F0AiChat.tsx @@ -40,6 +40,7 @@ const F0AiChatProviderComponent = ({ chatMessages, chatInput, welcomeScreenSuggestions, + welcomeScreenCards, disclaimer, resizable = false, defaultVisualizationMode, @@ -72,6 +73,7 @@ const F0AiChatProviderComponent = ({ chatMessages={chatMessages} chatInput={chatInput} welcomeScreenSuggestions={welcomeScreenSuggestions} + welcomeScreenCards={welcomeScreenCards} disclaimer={disclaimer} resizable={resizable} defaultVisualizationMode={defaultVisualizationMode} diff --git a/packages/react/src/sds/ai/F0AiChat/__stories__/_mock/MockConnectedChatInput.tsx b/packages/react/src/sds/ai/F0AiChat/__stories__/_mock/MockConnectedChatInput.tsx index a8f350f796..1e2bbcb46e 100644 --- a/packages/react/src/sds/ai/F0AiChat/__stories__/_mock/MockConnectedChatInput.tsx +++ b/packages/react/src/sds/ai/F0AiChat/__stories__/_mock/MockConnectedChatInput.tsx @@ -33,6 +33,7 @@ export const MockConnectedChatInput = () => { visualizationMode, creditWarning, welcomeScreenSuggestions, + welcomeScreenCards, tracking, openGame, } = useAiChat() @@ -70,6 +71,13 @@ export const MockConnectedChatInput = () => { [sendMessage, tracking] ) + const handleCardSelect = useCallback( + (message: string) => { + sendMessage(message) + }, + [sendMessage] + ) + return ( { fullscreen={fullscreen} welcomeScreenSuggestions={welcomeScreenSuggestions} onSuggestionClick={handleSuggestionClick} + welcomeScreenCards={welcomeScreenCards} + onCardSelect={handleCardSelect} /> ) } diff --git a/packages/react/src/sds/ai/F0AiChat/index.ts b/packages/react/src/sds/ai/F0AiChat/index.ts index ad981cf8d9..cae6dcb9ec 100644 --- a/packages/react/src/sds/ai/F0AiChat/index.ts +++ b/packages/react/src/sds/ai/F0AiChat/index.ts @@ -24,6 +24,7 @@ export type { DashboardCanvasContent, DataDownloadCanvasContent, F0AIMessage, + F0AiChatWelcomeCard, F0Message, F0ToolCall, FormCanvasContent, diff --git a/packages/react/src/sds/ai/F0AiChat/internal-types.ts b/packages/react/src/sds/ai/F0AiChat/internal-types.ts index 2047a0ae40..1b55b8b264 100644 --- a/packages/react/src/sds/ai/F0AiChat/internal-types.ts +++ b/packages/react/src/sds/ai/F0AiChat/internal-types.ts @@ -17,6 +17,7 @@ import { type PendingQuote, type TranscribeFn, type VisualizationMode, + F0AiChatWelcomeCard, WelcomeScreenSuggestion, } from "./types" @@ -33,6 +34,7 @@ export interface AiChatState { chatMessages?: React.ReactNode chatInput?: React.ReactNode welcomeScreenSuggestions?: WelcomeScreenSuggestion[] + welcomeScreenCards?: F0AiChatWelcomeCard[] disclaimer?: AiChatDisclaimer resizable?: boolean defaultVisualizationMode?: VisualizationMode @@ -84,6 +86,10 @@ export type AiChatProviderReturnValue = { setWelcomeScreenSuggestions: React.Dispatch< React.SetStateAction > + welcomeScreenCards: F0AiChatWelcomeCard[] + setWelcomeScreenCards: React.Dispatch< + React.SetStateAction + > onThumbsUp?: ( message: F0AIMessage, { threadId, feedback }: { threadId: string; feedback: string } diff --git a/packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx b/packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx index e9238ef4ea..296a97948e 100644 --- a/packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx +++ b/packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx @@ -22,6 +22,7 @@ import { type PendingContext, type PendingQuote, type VisualizationMode, + F0AiChatWelcomeCard, WelcomeScreenSuggestion, } from "../types" import { DEFAULT_CHAT_WIDTH } from "../utils/constants" @@ -57,6 +58,7 @@ export const AiChatStateProvider: FC> = ({ chatMessages, chatInput, welcomeScreenSuggestions: initialWelcomeScreenSuggestions = [], + welcomeScreenCards: initialWelcomeScreenCards = [], disclaimer, resizable = false, defaultVisualizationMode = "sidepanel", @@ -114,6 +116,9 @@ export const AiChatStateProvider: FC> = ({ const [welcomeScreenSuggestions, setWelcomeScreenSuggestions] = useState< WelcomeScreenSuggestion[] >(initialWelcomeScreenSuggestions) + const [welcomeScreenCards, setWelcomeScreenCards] = useState< + F0AiChatWelcomeCard[] + >(initialWelcomeScreenCards) const i18n = useI18n() const [placeholders, setPlaceholders] = useState([ i18n.t("ai.inputPlaceholder"), @@ -262,6 +267,8 @@ export const AiChatStateProvider: FC> = ({ chatInput, welcomeScreenSuggestions, setWelcomeScreenSuggestions, + welcomeScreenCards, + setWelcomeScreenCards, onThumbsUp, onThumbsDown, placeholders, @@ -358,6 +365,7 @@ const REAL_VALUES: Partial = { shouldPlayEntranceAnimation: true, placeholders: [], welcomeScreenSuggestions: [], + welcomeScreenCards: [], } const NO_PROVIDER_CONTEXT = new Proxy({} as AiChatProviderReturnValue, { diff --git a/packages/react/src/sds/ai/F0AiChat/types.ts b/packages/react/src/sds/ai/F0AiChat/types.ts index 53f6f26423..83ad84577a 100644 --- a/packages/react/src/sds/ai/F0AiChat/types.ts +++ b/packages/react/src/sds/ai/F0AiChat/types.ts @@ -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 @@ -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 */ diff --git a/packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx b/packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx index 3d9872036d..6d14d06366 100644 --- a/packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx +++ b/packages/react/src/sds/ai/F0AiChatTextArea/F0AiChatTextArea.tsx @@ -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, @@ -81,6 +82,8 @@ export const F0AiChatTextArea = ({ fullscreen = false, welcomeScreenSuggestions, onSuggestionClick, + welcomeScreenCards, + onCardSelect, ref, }: F0AiChatTextAreaProps) => { const translation = useI18n() @@ -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 && @@ -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 @@ -368,7 +380,7 @@ export const F0AiChatTextArea = ({ {...(fullscreen ? composerReveal : {})} >
- {suggestionsRow && !fullscreen &&
{suggestionsRow}
} + {suggestionsRow &&
{suggestionsRow}
}
- {suggestionsRow && fullscreen && ( -
{suggestionsRow}
+ {showWelcomeCards && ( +
+ +
)} {footer && isWelcomeScreen && fullscreen && ( diff --git a/packages/react/src/sds/ai/F0AiChatTextArea/__stories__/F0AiChatTextArea.stories.tsx b/packages/react/src/sds/ai/F0AiChatTextArea/__stories__/F0AiChatTextArea.stories.tsx index dbed13cc9e..7c8130e318 100644 --- a/packages/react/src/sds/ai/F0AiChatTextArea/__stories__/F0AiChatTextArea.stories.tsx +++ b/packages/react/src/sds/ai/F0AiChatTextArea/__stories__/F0AiChatTextArea.stories.tsx @@ -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" @@ -12,6 +13,7 @@ import type { AiChatCreditWarning, AiChatDisclaimer, AiChatFileAttachmentConfig, + F0AiChatWelcomeCard, PendingContext, PendingQuote, PersonProfile, @@ -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 = ( @@ -135,6 +152,7 @@ type WrapperProps = { creditWarning?: AiChatCreditWarning disclaimer?: AiChatDisclaimer footer?: React.ReactNode + welcomeScreenCards?: F0AiChatWelcomeCard[] isWelcomeScreen?: boolean fullscreen?: boolean inProgress?: boolean @@ -151,6 +169,7 @@ const Wrapper = ({ creditWarning, disclaimer, footer, + welcomeScreenCards, isWelcomeScreen, fullscreen, inProgress, @@ -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} /> @@ -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, diff --git a/packages/react/src/sds/ai/F0AiChatTextArea/__tests__/F0AiChatTextArea.welcomeLayout.test.tsx b/packages/react/src/sds/ai/F0AiChatTextArea/__tests__/F0AiChatTextArea.welcomeLayout.test.tsx index af42ef5f68..dc78845ee1 100644 --- a/packages/react/src/sds/ai/F0AiChatTextArea/__tests__/F0AiChatTextArea.welcomeLayout.test.tsx +++ b/packages/react/src/sds/ai/F0AiChatTextArea/__tests__/F0AiChatTextArea.welcomeLayout.test.tsx @@ -1,9 +1,12 @@ import { describe, expect, it, vi } from "vitest" -import { ChartVerticalBars } from "@/icons/app" +import { ChartVerticalBars, File } from "@/icons/app" import { zeroRender as render, screen } from "@/testing/test-utils" -import { type WelcomeScreenSuggestion } from "../../F0AiChat/types" +import { + type F0AiChatWelcomeCard, + type WelcomeScreenSuggestion, +} from "../../F0AiChat/types" vi.mock("../components/TextareaField", () => ({ TextareaField: () =>