Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ export default function ChatBotPageContent({
};


const hasSessions = chat.sessionsForDisplay.length > 0;

return (
<div className="flex h-full relative">
{compactMode ? (
<div className="absolute right-4 top-4 z-10 flex items-center gap-2">
{sessionSelector}
{hasSessions && sessionSelector}
<Button
variant="outline"
size="icon"
Expand All @@ -113,7 +115,7 @@ export default function ChatBotPageContent({
</Button>
</div>
) : (
sessionSelector
hasSessions && sessionSelector
)}

<main className="flex-1 min-w-0 flex relative mt-10">
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -64,10 +78,10 @@ export default function ChatWelcome({ onSend, disabled = false }: ChatWelcomePro
</div>

<Suggestions className="justify-center flex-wrap">
{SUGGESTION_KEYS.map((key) => (
{suggestions.map((text, i) => (
<Suggestion
key={key}
suggestion={t(`Welcome.Suggestions.${key}`)}
key={i}
suggestion={text}
onClick={handleSuggestionClick}
disabled={disabled}
/>
Expand All @@ -87,11 +101,12 @@ export default function ChatWelcome({ onSend, disabled = false }: ChatWelcomePro
/>
</div>
<div className="flex items-start gap-2 w-full">
<TableMentionTextarea value={input} onChange={setInput} tables={tables.map((t: any) => t.name ?? t)}>
<TableMentionTextarea value={input} onChange={setInput} tables={tables.map((t: any) => t.name ?? t)} autoFocus>
<PromptInputTextarea
placeholder={t('Input.GlobalPlaceholder')}
placeholder={t('Input.WelcomePlaceholder')}
value={input}
onChange={(e) => 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"
/>
</TableMentionTextarea>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>();

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ type TableMentionTextareaProps = {
onChange: (value: string) => void;
tables: string[];
children: any;
autoFocus?: boolean;
} & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange'>;

export function TableMentionTextarea({ value, onChange, tables, children }: TableMentionTextareaProps) {
export function TableMentionTextarea({ value, onChange, tables, children, autoFocus }: TableMentionTextareaProps) {
const textareaRef = React.useRef<HTMLTextAreaElement | null>(null);

React.useEffect(() => {
if (autoFocus && textareaRef.current) {
textareaRef.current.focus();
}
}, []);
const t = useTranslations('Chatbot');

const [open, setOpen] = React.useState(false);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading