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 8cc6b05..35f08bc 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 @@ -97,11 +97,13 @@ export default function ChatBotPageContent({ }; + const hasSessions = chat.sessionsForDisplay.length > 0; + return (
{compactMode ? (
- {sessionSelector} + {hasSessions && sessionSelector}
) : ( - sessionSelector + hasSessions && sessionSelector )}
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 60c48ba..d22e65a 100644 --- a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/components/empty.tsx +++ b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/components/empty.tsx @@ -1,8 +1,9 @@ 'use client'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useAtom } from 'jotai'; import { useTranslations } from 'next-intl'; +import { useParams } from 'next/navigation'; import { Sparkles } from 'lucide-react'; import { @@ -17,8 +18,10 @@ import { Suggestions, Suggestion } from '@/components/ai-elements/suggestion'; import { activeDatabaseAtom } from '@/shared/stores/app.store'; import { useDatabases } from '@/hooks/use-databases'; import { useTables } from '@/hooks/use-tables'; +import { useSchema } from '@/hooks/use-schema'; import { DatabaseSelect } from '../../../components/sql-console-sidebar/database-select'; import { TableMentionTextarea } from '../thread/table-mention-textarea'; +import { generateSuggestions } from './suggestion-rules'; type ChatWelcomeProps = { onSend: (text: string) => void; @@ -33,6 +36,17 @@ export default function ChatWelcome({ onSend, disabled = false }: ChatWelcomePro const [activeDatabase, setActiveDatabase] = useAtom(activeDatabaseAtom); const { databases } = useDatabases(); const { tables } = useTables(activeDatabase); + const params = useParams(); + const connectionId = params.connectionId as string | undefined; + const { schema } = useSchema(connectionId); + + const suggestions = useMemo(() => { + const fallbacks = SUGGESTION_KEYS.map((key) => t(`Welcome.Suggestions.${key}`)); + if (!schema?.databases || !activeDatabase) return fallbacks; + const db = schema.databases.find((d) => d.name === activeDatabase); + if (!db?.tables?.length) return fallbacks; + return generateSuggestions(db.tables, fallbacks, 4); + }, [schema, activeDatabase, t]); const handleSubmit = (message: PromptInputMessage) => { const text = message.text?.trim(); @@ -64,10 +78,10 @@ export default function ChatWelcome({ onSend, disabled = false }: ChatWelcomePro
- {SUGGESTION_KEYS.map((key) => ( + {suggestions.map((text, i) => ( @@ -87,11 +101,12 @@ export default function ChatWelcome({ onSend, disabled = false }: ChatWelcomePro />
- t.name ?? t)}> + t.name ?? t)} autoFocus> setInput(e.target.value)} + autoFocus 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/components/suggestion-rules.ts b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/components/suggestion-rules.ts new file mode 100644 index 0000000..daccb6d --- /dev/null +++ b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/components/suggestion-rules.ts @@ -0,0 +1,96 @@ +import type { TableSchema } from '@/shared/stores/schema.store'; + +type SuggestedQuestion = { + text: string; + priority: number; +}; + +type TableRule = { + pattern: RegExp; + template: (table: string) => string; +}; + +type ColumnRule = { + pattern: RegExp; + template: (table: string, column: string) => string; +}; + +const TABLE_RULES: TableRule[] = [ + { pattern: /order/i, template: (t) => `Show daily order trends from ${t}` }, + { pattern: /user|customer/i, template: (t) => `Top 10 users by activity from ${t}` }, + { pattern: /log|event/i, template: (t) => `Error logs in the last 24 hours from ${t}` }, + { pattern: /product|item/i, template: (t) => `Most popular products from ${t}` }, + { pattern: /payment|transaction/i, template: (t) => `Payment summary from ${t}` }, +]; + +const COLUMN_RULES: ColumnRule[] = [ + { + pattern: /^(created_at|updated_at|timestamp|date|time|datetime|created|updated)$/i, + template: (t) => `Trend of ${t} in the last 7 days`, + }, + { + pattern: /^(amount|price|revenue|total|cost|salary|balance)$/i, + template: (t, c) => `Top 10 records from ${t} by ${c}`, + }, + { + pattern: /^(status|state|type|category)$/i, + template: (t, c) => `Breakdown of ${t} by ${c}`, + }, +]; + +function getTableBaseName(name: string): string { + const parts = name.split('.'); + return parts[parts.length - 1]; +} + +export function generateSuggestions( + tables: TableSchema[], + fallbacks: string[], + limit = 4, +): string[] { + const suggestions: SuggestedQuestion[] = []; + const usedTables = new Set(); + + for (const table of tables) { + const baseName = getTableBaseName(table.name); + + for (const rule of TABLE_RULES) { + if (rule.pattern.test(baseName) && !usedTables.has(table.name)) { + suggestions.push({ text: rule.template(baseName), priority: 1 }); + usedTables.add(table.name); + break; + } + } + } + + for (const table of tables) { + if (usedTables.has(table.name)) continue; + const baseName = getTableBaseName(table.name); + + for (const column of table.columns) { + if (usedTables.has(table.name)) break; + + for (const rule of COLUMN_RULES) { + if (rule.pattern.test(column)) { + suggestions.push({ text: rule.template(baseName, column), priority: 2 }); + usedTables.add(table.name); + break; + } + } + } + } + + suggestions.sort((a, b) => a.priority - b.priority); + const result = suggestions.slice(0, limit).map((s) => s.text); + + if (result.length < limit) { + for (const fb of fallbacks) { + if (result.length >= limit) break; + if (!result.includes(fb)) { + result.push(fb); + } + } + } + + return result; +} diff --git a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/thread/table-mention-textarea.tsx b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/thread/table-mention-textarea.tsx index fcb3b56..fee6d0c 100644 --- a/apps/web/app/(app)/[organization]/[connectionId]/chatbot/thread/table-mention-textarea.tsx +++ b/apps/web/app/(app)/[organization]/[connectionId]/chatbot/thread/table-mention-textarea.tsx @@ -14,10 +14,17 @@ type TableMentionTextareaProps = { onChange: (value: string) => void; tables: string[]; children: any; + autoFocus?: boolean; } & Omit, 'value' | 'onChange'>; -export function TableMentionTextarea({ value, onChange, tables, children }: TableMentionTextareaProps) { +export function TableMentionTextarea({ value, onChange, tables, children, autoFocus }: TableMentionTextareaProps) { const textareaRef = React.useRef(null); + + React.useEffect(() => { + if (autoFocus && textareaRef.current) { + textareaRef.current.focus(); + } + }, []); const t = useTranslations('Chatbot'); const [open, setOpen] = React.useState(false); diff --git a/apps/web/public/locales/en.json b/apps/web/public/locales/en.json index e83d187..2468db0 100644 --- a/apps/web/public/locales/en.json +++ b/apps/web/public/locales/en.json @@ -763,7 +763,8 @@ "Input": { "GlobalPlaceholder": "@ mention tables to ask about the database, Shift + Enter for a new line", "CopilotPlaceholder": "Ask about this SQL / let AI fix or rewrite it, Shift + Enter for a new line", - "SentWithAttachments": "Sent with attachments" + "SentWithAttachments": "Sent with attachments", + "WelcomePlaceholder": "Ask about your data… Try: \"Top 10 users by revenue last week\"" }, "Sessions": { "Title": "Sessions",