Files
shensheshou/app/ai-agent/nlq/page.tsx
v0 b17b488f8e refactor: restructure navigation and module layout
Reorganize navigation and module structure based on new requirements.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2026-01-31 04:32:36 +00:00

337 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Search,
Play,
Download,
Copy,
History,
Star,
StarOff,
Clock,
Database,
Loader2,
ChevronRight,
Table,
BarChart3,
} from "lucide-react"
// 第四部分AI Agent - 自然语言查询 (NLQ)
export default function NLQPage() {
const [query, setQuery] = useState("")
const [isLoading, setIsLoading] = useState(false)
const [activeTab, setActiveTab] = useState("query")
const [showResult, setShowResult] = useState(false)
const [queryResult, setQueryResult] = useState<any>(null)
// 示例查询
const exampleQueries = [
{ label: "高价值用户", query: "查找最近30天消费超过1万元的用户" },
{ label: "流失用户分析", query: "统计最近7天未登录的活跃用户数量" },
{ label: "地域分布", query: "按省份统计用户数量,按数量降序排列" },
{ label: "消费趋势", query: "统计每天的交易金额最近30天" },
]
// 历史查询
const historyQueries = [
{ id: "1", query: "查找最近30天消费超过1万元的用户", time: "2分钟前", starred: true },
{ id: "2", query: "统计各渠道用户转化率", time: "1小时前", starred: false },
{ id: "3", query: "获取VIP用户的平均消费金额", time: "2小时前", starred: true },
{ id: "4", query: "按年龄段统计用户数量", time: "昨天", starred: false },
{ id: "5", query: "查询高活跃度但低消费的用户群体", time: "昨天", starred: false },
]
const handleExecuteQuery = () => {
if (!query.trim()) return
setIsLoading(true)
setShowResult(false)
// 模拟AI解析和查询
setTimeout(() => {
setQueryResult({
sql: `SELECT u.id, u.name, u.phone, SUM(t.amount) as total_spending
FROM users u
JOIN transactions t ON u.id = t.user_id
WHERE t.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.name, u.phone
HAVING total_spending > 10000
ORDER BY total_spending DESC
LIMIT 100`,
data: [
{ id: "U001", name: "张三", phone: "138****1234", total_spending: 25680 },
{ id: "U002", name: "李四", phone: "139****5678", total_spending: 18920 },
{ id: "U003", name: "王五", phone: "137****9012", total_spending: 15340 },
{ id: "U004", name: "赵六", phone: "136****3456", total_spending: 13280 },
{ id: "U005", name: "钱七", phone: "135****7890", total_spending: 12150 },
],
totalCount: 823,
executionTime: "0.23s",
explanation: "我将您的查询解析为从用户表和交易表中联合查询筛选最近30天内消费总额超过10000元的用户按消费金额降序排列。",
})
setShowResult(true)
setIsLoading(false)
}, 1500)
}
const handleExampleClick = (exampleQuery: string) => {
setQuery(exampleQuery)
}
const handleHistoryClick = (historyQuery: string) => {
setQuery(historyQuery)
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
<div className="p-6 space-y-6">
{/* 顶部标题 */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-500 mt-1">AI自动转换为SQL并执行查询</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* 左侧:历史记录 */}
<div className="lg:col-span-1">
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-semibold flex items-center gap-2">
<History className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{historyQueries.map((item) => (
<button
key={item.id}
onClick={() => handleHistoryClick(item.query)}
className="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors group"
>
<div className="flex items-start justify-between">
<p className="text-sm text-gray-700 line-clamp-2 group-hover:text-blue-600">
{item.query}
</p>
{item.starred ? (
<Star className="h-4 w-4 text-yellow-500 shrink-0" />
) : (
<StarOff className="h-4 w-4 text-gray-300 shrink-0 opacity-0 group-hover:opacity-100" />
)}
</div>
<div className="flex items-center gap-1 mt-1 text-xs text-gray-400">
<Clock className="h-3 w-3" />
{item.time}
</div>
</button>
))}
</div>
</CardContent>
</Card>
</div>
{/* 右侧:查询区域 */}
<div className="lg:col-span-3 space-y-6">
{/* 查询输入 */}
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardContent className="p-6">
<div className="space-y-4">
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleExecuteQuery()}
placeholder="输入您的查询例如查找最近30天消费超过1万元的用户"
className="pl-12 py-6 text-lg bg-white"
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500"></span>
{exampleQueries.map((example, index) => (
<button
key={index}
onClick={() => handleExampleClick(example.query)}
className="text-sm text-blue-600 hover:text-blue-700 hover:underline"
>
{example.label}
</button>
))}
</div>
<Button onClick={handleExecuteQuery} disabled={isLoading || !query.trim()}>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
</>
)}
</Button>
</div>
</div>
</CardContent>
</Card>
{/* 查询结果 */}
{showResult && queryResult && (
<div className="space-y-4">
{/* AI解释 */}
<Card className="bg-blue-50 border-blue-200">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-blue-100">
<Database className="h-4 w-4 text-blue-600" />
</div>
<div className="flex-1">
<p className="text-sm text-blue-800">{queryResult.explanation}</p>
<div className="flex items-center gap-4 mt-2 text-xs text-blue-600">
<span>{queryResult.executionTime}</span>
<span>{queryResult.totalCount} </span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* SQL和结果 */}
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-gray-100">
<TabsTrigger value="query">
<Table className="h-4 w-4 mr-1" />
</TabsTrigger>
<TabsTrigger value="sql">
<Database className="h-4 w-4 mr-1" />
SQL语句
</TabsTrigger>
<TabsTrigger value="chart">
<BarChart3 className="h-4 w-4 mr-1" />
</TabsTrigger>
</TabsList>
</Tabs>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Copy className="h-4 w-4 mr-1" />
</Button>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<TabsContent value="query" className="mt-0">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-gray-50">
<th className="text-left py-3 px-4 font-medium">ID</th>
<th className="text-left py-3 px-4 font-medium"></th>
<th className="text-left py-3 px-4 font-medium"></th>
<th className="text-right py-3 px-4 font-medium"></th>
</tr>
</thead>
<tbody>
{queryResult.data.map((row: any, index: number) => (
<tr key={index} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-3 px-4 font-mono text-gray-600">{row.id}</td>
<td className="py-3 px-4">{row.name}</td>
<td className="py-3 px-4 text-gray-500">{row.phone}</td>
<td className="py-3 px-4 text-right text-green-600 font-semibold">
¥{row.total_spending.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex items-center justify-between mt-4 pt-4 border-t">
<span className="text-sm text-gray-500">
1-{queryResult.data.length} {queryResult.totalCount}
</span>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" disabled>
</Button>
<Button variant="outline" size="sm">
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="sql" className="mt-0">
<div className="bg-gray-900 rounded-lg p-4 font-mono text-sm text-gray-100 overflow-x-auto">
<pre className="whitespace-pre-wrap">{queryResult.sql}</pre>
</div>
<div className="flex items-center gap-2 mt-4">
<Button variant="outline" size="sm">
<Copy className="h-4 w-4 mr-1" />
SQL
</Button>
<Button variant="outline" size="sm">
SQL编辑器中打开
</Button>
</div>
</TabsContent>
<TabsContent value="chart" className="mt-0">
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
<div className="text-center text-gray-500">
<BarChart3 className="h-12 w-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
</div>
</TabsContent>
</CardContent>
</Card>
</div>
)}
{/* 无结果时的提示 */}
{!showResult && !isLoading && (
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardContent className="p-12 text-center">
<Search className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-sm text-gray-500 mb-6">
AI将自动转换为SQL并执行
</p>
<div className="flex flex-wrap justify-center gap-2">
{exampleQueries.map((example, index) => (
<button
key={index}
onClick={() => handleExampleClick(example.query)}
className="px-4 py-2 rounded-full bg-gray-100 text-sm text-gray-600 hover:bg-gray-200 transition-colors"
>
{example.label}
</button>
))}
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>
</div>
)
}