From ca32fced6114f25d3a80e55072754bc4d133499a Mon Sep 17 00:00:00 2001 From: jiaxin Date: Wed, 1 Apr 2026 11:43:51 +0800 Subject: [PATCH 1/2] feat: enhance AI experience with welcome page and suggested prompts (#122) Add a rich welcome page to the chatbot with a heading, suggested prompts, and an embedded input so users can start chatting immediately without manually creating a session first. Also add an "Ask AI to write SQL" button to the SQL Console empty state to guide users toward AI features. --- .../chatbot/chatbot-page.client.tsx | 20 +- .../chatbot/components/empty.tsx | 104 ++++++++++ .../[connectionId]/chatbot/thread/chatbox.tsx | 11 +- .../sql-console/components/tabs/tab-empty.tsx | 27 ++- apps/web/public/locales/en.json | 14 +- apps/web/public/locales/zh.json | 14 +- tests/e2e/ai-experience.spec.ts | 181 ++++++++++++++++++ 7 files changed, 359 insertions(+), 12 deletions(-) create mode 100644 tests/e2e/ai-experience.spec.ts diff --git a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/chatbot-page.client.tsx b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/chatbot-page.client.tsx index 03f98909..5e77b6fc 100644 --- a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/chatbot-page.client.tsx +++ b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/chatbot-page.client.tsx @@ -1,11 +1,12 @@ 'use client'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslations } from 'next-intl'; import { Button } from '@/registry/new-york-v4/ui/button'; import { Loader2, X } from 'lucide-react'; import ChatBotComp from './thread/chatbox'; +import ChatWelcome from './components/empty'; import ChatSessionSidebar from './sessions/chat-session-sidebar'; import SessionDeleteDialog from './sessions/session-delete-dialog'; @@ -79,6 +80,15 @@ export default function ChatBotPageContent({ ], ); + const pendingPromptRef = useRef(null); + + const handleWelcomeSend = async (text: string) => { + pendingPromptRef.current = text; + await chat.handleCreateSession(); + // pendingPromptRef is read by ChatBotComp on mount, then cleared after a tick + requestAnimationFrame(() => { pendingPromptRef.current = null; }); + }; + const onExecuteAction: CopilotActionExecutor = async (action) => { if (action.type === 'sql.replace') { console.log('replace sql', action.sql); @@ -119,6 +129,7 @@ export default function ChatBotPageContent({ key={mode === 'copilot' ? (copilotEnvelope?.meta?.tabId ?? 'copilot') : chat.selectedSessionId} sessionId={chat.selectedSessionId} initialMessages={chat.initialMessages} + initialPrompt={pendingPromptRef.current} onConversationActivity={chat.handleConversationActivity} onExecuteAction={onExecuteAction} onSessionCreated={(sessionId) => chat.setSelectedSessionId(sessionId)} @@ -127,9 +138,10 @@ export default function ChatBotPageContent({ /> ) ) : ( -
- {t('EmptyState')} -
+ )} diff --git a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/components/empty.tsx b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/components/empty.tsx index e69de29b..4b2d3fde 100644 --- a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/components/empty.tsx +++ b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/components/empty.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { useState } from 'react'; +import { useAtom } from 'jotai'; +import { useTranslations } from 'next-intl'; +import { Sparkles } from 'lucide-react'; + +import { + PromptInput, + PromptInputBody, + PromptInputSubmit, + PromptInputTextarea, + PromptInputFooter, + type PromptInputMessage, +} from '@/components/ai-elements/prompt-input'; +import { Suggestions, Suggestion } from '@/components/ai-elements/suggestion'; +import { activeDatabaseAtom } from '@/shared/stores/app.store'; +import { useDatabases } from '@/hooks/use-databases'; +import { DatabaseSelect } from '../../../components/sql-console-sidebar/database-select'; + +type ChatWelcomeProps = { + onSend: (text: string) => void; + disabled?: boolean; +}; + +const SUGGESTION_KEYS = ['TopUsers', 'ErrorLogs', 'OrderTrends', 'TableSummary'] as const; + +export default function ChatWelcome({ onSend, disabled = false }: ChatWelcomeProps) { + const t = useTranslations('Chatbot'); + const [input, setInput] = useState(''); + const [activeDatabase, setActiveDatabase] = useAtom(activeDatabaseAtom); + const { databases } = useDatabases(); + + const handleSubmit = (message: PromptInputMessage) => { + const text = message.text?.trim(); + if (!text) return; + onSend(text); + }; + + const handleSuggestionClick = (suggestion: string) => { + onSend(suggestion); + }; + + const handleDatabaseChange = (db: string) => { + setActiveDatabase(db); + }; + + return ( +
+
+
+
+ +

+ {t('Welcome.Heading')} +

+
+

+ {t('Welcome.Subheading')} +

+
+ + + {SUGGESTION_KEYS.map((key) => ( + + ))} + + +
+ + +
+
+ +
+
+ setInput(e.target.value)} + className="min-h-18 w-full resize-none border-0 bg-transparent text-sm focus-visible:outline-none focus-visible:ring-0" + /> +
+
+
+ + + +
+
+
+
+ ); +} diff --git a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/thread/chatbox.tsx b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/thread/chatbox.tsx index b86fa19f..6ea43d87 100644 --- a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/thread/chatbox.tsx +++ b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/thread/chatbox.tsx @@ -35,13 +35,14 @@ type ChatBotCompProps = { initialMessages: UIMessage[]; onConversationActivity?: () => void; onSessionCreated?: (sessionId: string) => void; + initialPrompt?: string | null; mode?: 'global' | 'copilot'; copilotEnvelope?: CopilotEnvelopeV1 | null; onExecuteAction?: CopilotActionExecutor; }; -const ChatBotComp = ({ sessionId, initialMessages, onConversationActivity, onSessionCreated, mode = 'global', copilotEnvelope = null, onExecuteAction }: ChatBotCompProps) => { +const ChatBotComp = ({ sessionId, initialMessages, onConversationActivity, onSessionCreated, initialPrompt = null, mode = 'global', copilotEnvelope = null, onExecuteAction }: ChatBotCompProps) => { const router = useRouter(); const params = useParams<{ organization: string; connectionId: string }>(); const t = useTranslations('Chatbot'); @@ -157,6 +158,14 @@ const ChatBotComp = ({ sessionId, initialMessages, onConversationActivity, onSes return () => cancelAnimationFrame(raf); }, [chatStateId, messages, restoreScrollPosition]); + const initialPromptSubmittedRef = useRef(false); + useEffect(() => { + if (initialPrompt && !initialPromptSubmittedRef.current && status === 'ready') { + initialPromptSubmittedRef.current = true; + handleSubmit({ text: initialPrompt, files: [] }); + } + }, [initialPrompt, status]); + const handleCopySql = useCallback( async (sql: string) => { try { diff --git a/apps/web/app/(app)/[organization]/[connectionId]/sql-console/components/tabs/tab-empty.tsx b/apps/web/app/(app)/[organization]/[connectionId]/sql-console/components/tabs/tab-empty.tsx index 028e541f..61e7a1f7 100644 --- a/apps/web/app/(app)/[organization]/[connectionId]/sql-console/components/tabs/tab-empty.tsx +++ b/apps/web/app/(app)/[organization]/[connectionId]/sql-console/components/tabs/tab-empty.tsx @@ -1,10 +1,20 @@ import { Button } from "@/registry/new-york-v4/ui/button"; -import { Loader2, Plus } from "lucide-react"; +import { Plus, Sparkles } from "lucide-react"; import { useTranslations } from "next-intl"; +import { useParams, useRouter } from "next/navigation"; export default function SQLTabEmpty(props: { addTab: () => void; disabled?: boolean }) { const { addTab, disabled = false } = props; const t = useTranslations('SqlConsole'); + const router = useRouter(); + const params = useParams<{ organization: string; connectionId: string }>(); + + const handleAskAI = () => { + if (params?.organization && params?.connectionId) { + router.push(`/${params.organization}/${params.connectionId}/chatbot`); + } + }; + return (
@@ -17,10 +27,17 @@ export default function SQLTabEmpty(props: { addTab: () => void; disabled?: bool
{t('Empty.ShortcutToggleCopilot')}
{t('Empty.ShortcutFormat')}
- +
+ + {t('Empty.OrDivider')} + +
); } diff --git a/apps/web/public/locales/en.json b/apps/web/public/locales/en.json index c7a2ddbc..47f14696 100644 --- a/apps/web/public/locales/en.json +++ b/apps/web/public/locales/en.json @@ -749,6 +749,16 @@ "Chatbot": { "Close": "Close", "EmptyState": "Select a session or create a new one.", + "Welcome": { + "Heading": "Ask anything about your data", + "Subheading": "AI generates SQL, runs queries, and visualizes results instantly", + "Suggestions": { + "TopUsers": "Show top 10 users by order amount", + "ErrorLogs": "Error logs in the last 24 hours", + "OrderTrends": "Daily order trends this month", + "TableSummary": "Summary of all tables with row counts" + } + }, "Typing": "Generating", "Input": { "GlobalPlaceholder": "@ mention tables to ask about the database, Shift + Enter for a new line", @@ -958,7 +968,9 @@ "ShortcutNewTab": "New tab: Cmd/Ctrl+T", "ShortcutToggleCopilot": "Open Copilot: Cmd/Ctrl+I", "ShortcutFormat": "Format SQL: Shift+Cmd/Ctrl+F", - "NewConsole": "New console" + "NewConsole": "New console", + "OrDivider": "or", + "AskAI": "Ask AI to write SQL" }, "Editor": { "NoActiveTab": "No active tab", diff --git a/apps/web/public/locales/zh.json b/apps/web/public/locales/zh.json index bfa095fe..fa435af1 100644 --- a/apps/web/public/locales/zh.json +++ b/apps/web/public/locales/zh.json @@ -751,6 +751,16 @@ "Chatbot": { "Close": "关闭", "EmptyState": "请选择一个会话或创建新的会话。", + "Welcome": { + "Heading": "向 AI 提问关于你的数据", + "Subheading": "AI 自动生成 SQL、执行查询并即时可视化结果", + "Suggestions": { + "TopUsers": "按订单金额显示前 10 名用户", + "ErrorLogs": "最近 24 小时的错误日志", + "OrderTrends": "本月每日订单趋势", + "TableSummary": "所有表的行数汇总" + } + }, "Typing": "正在生成", "Input": { "GlobalPlaceholder": "@ 选中表进行数据库问答,Shift + Enter 换行", @@ -960,7 +970,9 @@ "ShortcutNewTab": "新建标签页:Cmd/Ctrl+T", "ShortcutToggleCopilot": "切换 Copilot:Cmd/Ctrl+K", "ShortcutFormat": "格式化 SQL:Shift+Cmd/Ctrl+F", - "NewConsole": "新建控制台" + "NewConsole": "新建控制台", + "OrDivider": "或", + "AskAI": "让 AI 帮你写 SQL" }, "Editor": { "NoActiveTab": "没有活动标签", diff --git a/tests/e2e/ai-experience.spec.ts b/tests/e2e/ai-experience.spec.ts new file mode 100644 index 00000000..e28fb7c6 --- /dev/null +++ b/tests/e2e/ai-experience.spec.ts @@ -0,0 +1,181 @@ +import { expect } from '@playwright/test'; + +import { expectAppHealthy, test } from './fixtures'; +import { createWorkbenchConnection, mockWorkbenchApis, openMockConnectionConsole } from './helpers/workbench'; + +const seededConnection = createWorkbenchConnection(); + +/** + * Mock chatbot-related APIs so the page renders without a real backend. + */ +async function mockChatApis(page: import('@playwright/test').Page) { + const sessions: Array<{ id: string; title: string | null; type: string; createdAt: string }> = []; + + await page.route('**/api/chat/sessions*', async route => { + const request = route.request(); + if (request.method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(sessions), + }); + return; + } + if (request.method() === 'POST') { + const now = new Date().toISOString(); + const session = { + id: `session-${sessions.length + 1}`, + title: null, + type: 'global', + createdAt: now, + updatedAt: now, + lastMessageAt: null, + archivedAt: null, + metadata: null, + }; + sessions.push(session); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(session), + }); + return; + } + await route.fallback(); + }); + + await page.route('**/api/chat/session/*', async route => { + const request = route.request(); + if (request.method() === 'GET') { + const url = new URL(request.url()); + const sessionId = url.pathname.split('/').pop(); + const session = sessions.find(s => s.id === sessionId) ?? sessions[0]; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + detail: session ?? { id: sessionId, title: null, type: 'global' }, + messages: [], + }), + }); + return; + } + await route.fallback(); + }); + + // Mock the main chat POST — return a simple streamed text response + await page.route('**/api/chat', async route => { + if (route.request().method() !== 'POST') { + await route.fallback(); + return; + } + + const body = route.request().postDataJSON() as any; + const chatId = body?.chatId ?? sessions[0]?.id ?? 'session-1'; + + // Return a minimal AI SDK-compatible data stream + const responseText = 'Here is the result for your query.'; + const streamParts = [ + `0:${JSON.stringify(responseText)}\n`, + ]; + + await route.fulfill({ + status: 200, + contentType: 'text/plain; charset=utf-8', + headers: { + 'x-chat-id': chatId, + }, + body: streamParts.join(''), + }); + }); +} + +// --------------------------------------------------------------------------- +// Chatbot welcome page tests +// --------------------------------------------------------------------------- + +test.describe('Chatbot welcome page', () => { + test('shows welcome heading and suggested prompts when no session is selected', async ({ page, appErrors }) => { + await mockWorkbenchApis(page, { initialConnections: [seededConnection] }); + await mockChatApis(page); + + await page.goto('/'); + await page.waitForURL(/\/[^/]+\/connections$/); + + const match = page.url().match(/\/([^/]+)\/connections$/); + const orgId = match?.[1]; + + await page.goto(`/${orgId}/${seededConnection.connection.id}/chatbot`); + + // Welcome heading should be visible + await expect(page.getByText('Ask anything about your data')).toBeVisible(); + + // Subheading should be visible + await expect(page.getByText(/AI generates SQL/i)).toBeVisible(); + + // Suggested prompt buttons should be visible + await expect(page.getByRole('button', { name: /top 10 users/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /error logs/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /order trends/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /table.*row counts/i })).toBeVisible(); + + await expectAppHealthy(appErrors); + }); + + test('clicking a suggested prompt creates a session and sends a message', async ({ page, appErrors }) => { + await mockWorkbenchApis(page, { initialConnections: [seededConnection] }); + await mockChatApis(page); + + await page.goto('/'); + await page.waitForURL(/\/[^/]+\/connections$/); + + const match = page.url().match(/\/([^/]+)\/connections$/); + const orgId = match?.[1]; + + await page.goto(`/${orgId}/${seededConnection.connection.id}/chatbot`); + + // Click a suggestion + await page.getByRole('button', { name: /top 10 users/i }).click(); + + // Welcome page should disappear and chat view should appear + await expect(page.getByText('Ask anything about your data')).toBeHidden({ timeout: 10000 }); + + // The chat input area should now be visible (the PromptInput textarea within ChatBotComp) + await expect(page.locator('textarea[name="message"]')).toBeVisible(); + + await expectAppHealthy(appErrors); + }); +}); + +// --------------------------------------------------------------------------- +// SQL Console empty state AI button tests +// --------------------------------------------------------------------------- + +test.describe('SQL Console AI entry', () => { + test('shows "Ask AI" button in empty state', async ({ page, appErrors }) => { + await mockWorkbenchApis(page, { initialConnections: [seededConnection] }); + await openMockConnectionConsole(page, seededConnection); + + // The "Ask AI to write SQL" button should be visible + await expect(page.getByRole('button', { name: /ask ai/i })).toBeVisible(); + + await expectAppHealthy(appErrors); + }); + + test('"Ask AI" button navigates to chatbot page', async ({ page, appErrors }) => { + await mockWorkbenchApis(page, { initialConnections: [seededConnection] }); + await mockChatApis(page); + await openMockConnectionConsole(page, seededConnection); + + // Click the AI button + await page.getByRole('button', { name: /ask ai/i }).click(); + + // Should navigate to chatbot + await expect(page).toHaveURL(/\/chatbot$/); + + // Welcome page should be visible on the chatbot page + await expect(page.getByText('Ask anything about your data')).toBeVisible(); + + await expectAppHealthy(appErrors); + }); +}); From 124033e7acdc789802a3b46adedb6b238d617ea0 Mon Sep 17 00:00:00 2001 From: jiaxin Date: Wed, 1 Apr 2026 11:57:26 +0800 Subject: [PATCH 2/2] fix: correct E2E test mocks and filter PostHog noise - Fix chat API mock to use ApiEnvelope format ({ code: 0, data: {...} }) - Add openMockConnectionChatbot helper with localStorage setup - Filter PostHog console errors in test assertions (no API key in test env) --- tests/e2e/ai-experience.spec.ts | 102 +++++++++++++++++--------------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/tests/e2e/ai-experience.spec.ts b/tests/e2e/ai-experience.spec.ts index e28fb7c6..3fc63104 100644 --- a/tests/e2e/ai-experience.spec.ts +++ b/tests/e2e/ai-experience.spec.ts @@ -1,24 +1,38 @@ -import { expect } from '@playwright/test'; +import { expect, type Page, type Route } from '@playwright/test'; -import { expectAppHealthy, test } from './fixtures'; +import { test } from './fixtures'; import { createWorkbenchConnection, mockWorkbenchApis, openMockConnectionConsole } from './helpers/workbench'; +/** + * expectAppHealthy variant that ignores PostHog console errors + * (PostHog has no valid API key in the test environment). + */ +function expectAppHealthy(appErrors: string[]) { + const relevant = appErrors.filter(e => !/posthog/i.test(e)); + expect(relevant, relevant.join('\n')).toEqual([]); +} + const seededConnection = createWorkbenchConnection(); +const json = async (route: Route, body: unknown, status = 200) => { + await route.fulfill({ + status, + contentType: 'application/json', + body: JSON.stringify(body), + }); +}; + /** * Mock chatbot-related APIs so the page renders without a real backend. + * Response format must match ApiEnvelope: { code: 0, data: { ... } } */ -async function mockChatApis(page: import('@playwright/test').Page) { +async function mockChatApis(page: Page) { const sessions: Array<{ id: string; title: string | null; type: string; createdAt: string }> = []; await page.route('**/api/chat/sessions*', async route => { const request = route.request(); if (request.method() === 'GET') { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(sessions), - }); + await json(route, { code: 0, message: 'success', data: { sessions } }); return; } if (request.method() === 'POST') { @@ -34,11 +48,7 @@ async function mockChatApis(page: import('@playwright/test').Page) { metadata: null, }; sessions.push(session); - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(session), - }); + await json(route, { code: 0, message: 'success', data: { session } }); return; } await route.fallback(); @@ -50,13 +60,13 @@ async function mockChatApis(page: import('@playwright/test').Page) { const url = new URL(request.url()); const sessionId = url.pathname.split('/').pop(); const session = sessions.find(s => s.id === sessionId) ?? sessions[0]; - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - detail: session ?? { id: sessionId, title: null, type: 'global' }, + await json(route, { + code: 0, + message: 'success', + data: { + session: session ?? { id: sessionId, title: null, type: 'global' }, messages: [], - }), + }, }); return; } @@ -73,23 +83,37 @@ async function mockChatApis(page: import('@playwright/test').Page) { const body = route.request().postDataJSON() as any; const chatId = body?.chatId ?? sessions[0]?.id ?? 'session-1'; - // Return a minimal AI SDK-compatible data stream const responseText = 'Here is the result for your query.'; - const streamParts = [ - `0:${JSON.stringify(responseText)}\n`, - ]; + const streamParts = [`0:${JSON.stringify(responseText)}\n`]; await route.fulfill({ status: 200, contentType: 'text/plain; charset=utf-8', - headers: { - 'x-chat-id': chatId, - }, + headers: { 'x-chat-id': chatId }, body: streamParts.join(''), }); }); } +/** + * Navigate to chatbot page with mocked connection in localStorage. + */ +async function openMockConnectionChatbot(page: Page, connection = seededConnection) { + await page.goto('/'); + await page.waitForURL(/\/[^/]+\/connections$/); + + const match = page.url().match(/\/([^/]+)\/connections$/); + const orgId = match?.[1]; + if (!orgId) throw new Error(`Failed to resolve org id from URL: ${page.url()}`); + + await page.evaluate( + value => window.localStorage.setItem('currentConnection', JSON.stringify(value)), + connection, + ); + + await page.goto(`/${orgId}/${connection.connection.id}/chatbot`); +} + // --------------------------------------------------------------------------- // Chatbot welcome page tests // --------------------------------------------------------------------------- @@ -98,14 +122,7 @@ test.describe('Chatbot welcome page', () => { test('shows welcome heading and suggested prompts when no session is selected', async ({ page, appErrors }) => { await mockWorkbenchApis(page, { initialConnections: [seededConnection] }); await mockChatApis(page); - - await page.goto('/'); - await page.waitForURL(/\/[^/]+\/connections$/); - - const match = page.url().match(/\/([^/]+)\/connections$/); - const orgId = match?.[1]; - - await page.goto(`/${orgId}/${seededConnection.connection.id}/chatbot`); + await openMockConnectionChatbot(page); // Welcome heading should be visible await expect(page.getByText('Ask anything about your data')).toBeVisible(); @@ -125,20 +142,16 @@ test.describe('Chatbot welcome page', () => { test('clicking a suggested prompt creates a session and sends a message', async ({ page, appErrors }) => { await mockWorkbenchApis(page, { initialConnections: [seededConnection] }); await mockChatApis(page); + await openMockConnectionChatbot(page); - await page.goto('/'); - await page.waitForURL(/\/[^/]+\/connections$/); - - const match = page.url().match(/\/([^/]+)\/connections$/); - const orgId = match?.[1]; - - await page.goto(`/${orgId}/${seededConnection.connection.id}/chatbot`); + // Wait for welcome state to be ready + await expect(page.getByRole('button', { name: /top 10 users/i })).toBeVisible(); // Click a suggestion await page.getByRole('button', { name: /top 10 users/i }).click(); // Welcome page should disappear and chat view should appear - await expect(page.getByText('Ask anything about your data')).toBeHidden({ timeout: 10000 }); + await expect(page.getByText('Ask anything about your data')).toBeHidden({ timeout: 15000 }); // The chat input area should now be visible (the PromptInput textarea within ChatBotComp) await expect(page.locator('textarea[name="message"]')).toBeVisible(); @@ -156,7 +169,6 @@ test.describe('SQL Console AI entry', () => { await mockWorkbenchApis(page, { initialConnections: [seededConnection] }); await openMockConnectionConsole(page, seededConnection); - // The "Ask AI to write SQL" button should be visible await expect(page.getByRole('button', { name: /ask ai/i })).toBeVisible(); await expectAppHealthy(appErrors); @@ -167,13 +179,9 @@ test.describe('SQL Console AI entry', () => { await mockChatApis(page); await openMockConnectionConsole(page, seededConnection); - // Click the AI button await page.getByRole('button', { name: /ask ai/i }).click(); - // Should navigate to chatbot await expect(page).toHaveURL(/\/chatbot$/); - - // Welcome page should be visible on the chatbot page await expect(page.getByText('Ask anything about your data')).toBeVisible(); await expectAppHealthy(appErrors);