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
@@ -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';

Expand Down Expand Up @@ -79,6 +80,15 @@ export default function ChatBotPageContent({
],
);

const pendingPromptRef = useRef<string | null>(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);
Expand Down Expand Up @@ -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)}
Expand All @@ -127,9 +138,10 @@ export default function ChatBotPageContent({
/>
)
) : (
<div className="flex h-full w-full items-center justify-center text-muted-foreground">
{t('EmptyState')}
</div>
<ChatWelcome
onSend={handleWelcomeSend}
disabled={chat.creatingSession}
/>
)}
</main>

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex h-full w-full flex-col items-center justify-center p-4">
<div className="flex w-full max-w-2xl flex-col items-center gap-8">
<div className="flex flex-col items-center gap-2 text-center">
<div className="flex items-center gap-2">
<Sparkles className="h-6 w-6 text-primary" />
<h1 className="text-2xl font-semibold tracking-tight">
{t('Welcome.Heading')}
</h1>
</div>
<p className="text-sm text-muted-foreground">
{t('Welcome.Subheading')}
</p>
</div>

<Suggestions className="justify-center flex-wrap">
{SUGGESTION_KEYS.map((key) => (
<Suggestion
key={key}
suggestion={t(`Welcome.Suggestions.${key}`)}
onClick={handleSuggestionClick}
disabled={disabled}
/>
))}
</Suggestions>

<div className="w-full">
<PromptInput onSubmit={handleSubmit} className="mt-1">
<PromptInputBody>
<div className="flex flex-col gap-2 w-full">
<div className="flex items-center gap-2">
<DatabaseSelect
className="w-auto max-w-80 border-0 shadow-none text-xs outline-0 focus-visible:ring-0"
value={activeDatabase}
databases={databases}
onChange={handleDatabaseChange}
/>
</div>
<div className="flex items-start gap-2 w-full">
<PromptInputTextarea
placeholder={t('Input.GlobalPlaceholder')}
value={input}
onChange={(e) => 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"
/>
</div>
</div>
</PromptInputBody>
<PromptInputFooter className="justify-end">
<PromptInputSubmit disabled={disabled || !input} />
</PromptInputFooter>
</PromptInput>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex-1 flex flex-col items-center justify-center text-muted-foreground">
<div className="mb-6 text-5xl font-semibold tracking-wide">
Expand All @@ -17,10 +27,17 @@ export default function SQLTabEmpty(props: { addTab: () => void; disabled?: bool
<div>{t('Empty.ShortcutToggleCopilot')}</div>
<div>{t('Empty.ShortcutFormat')}</div>
</div>
<Button onClick={addTab} disabled={disabled}>
<Plus className="mr-2 h-4 w-4" />
{t('Empty.NewConsole')}
</Button>
<div className="flex flex-col items-center gap-3">
<Button onClick={addTab} disabled={disabled}>
<Plus className="mr-2 h-4 w-4" />
{t('Empty.NewConsole')}
</Button>
<span className="text-xs text-muted-foreground">{t('Empty.OrDivider')}</span>
<Button variant="outline" onClick={handleAskAI} disabled={disabled}>
<Sparkles className="mr-2 h-4 w-4" />
{t('Empty.AskAI')}
</Button>
</div>
</div>
);
}
14 changes: 13 additions & 1 deletion apps/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion apps/web/public/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 换行",
Expand Down Expand Up @@ -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": "没有活动标签",
Expand Down
Loading
Loading