Files
users/app/ai-agent/chat/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

433 lines
18 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, useRef, useEffect } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
Bot,
Send,
User,
Sparkles,
Database,
Tags,
FileText,
Download,
Copy,
ThumbsUp,
ThumbsDown,
Loader2,
Zap,
Search,
BarChart3,
Table,
RefreshCw,
} from "lucide-react"
// 第四部分AI Agent智能系统 - 智能对话
export default function AIAgentChatPage() {
const [messages, setMessages] = useState<any[]>([
{
id: "1",
role: "assistant",
content: "您好我是神射手AI Agent可以帮您完成以下任务\n\n- **自然语言查询**:用自然语言查询用户数据\n- **智能打标**AI自动为用户打标签\n- **数据清洗**AI识别并清洗异常数据\n- **智能报告**:自动生成数据分析报告\n\n请问有什么可以帮您",
timestamp: new Date(),
},
])
const [inputValue, setInputValue] = useState("")
const [isLoading, setIsLoading] = useState(false)
const scrollRef = useRef<HTMLDivElement>(null)
// 快捷指令
const quickCommands = [
{ icon: Search, label: "查询高价值用户", command: "帮我找出最近30天内消费超过1万元的高价值用户" },
{ icon: Tags, label: "AI打标签", command: "为最近7天未登录的用户打上流失风险标签" },
{ icon: Zap, label: "数据清洗", command: "检查用户表中的手机号格式,清洗异常数据" },
{ icon: FileText, label: "生成报告", command: "生成本月用户增长分析报告" },
]
// 支持的指令类型
const supportedTasks = [
{ type: "NLQ", label: "自然语言查询", desc: "用自然语言查询数据库", icon: Search },
{ type: "TAG", label: "AI打标", desc: "智能为用户打标签", icon: Tags },
{ type: "CLEAN", label: "数据清洗", desc: "AI识别清洗异常数据", icon: Zap },
{ type: "REPORT", label: "智能报告", desc: "自动生成分析报告", icon: FileText },
]
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [messages])
const handleSend = async () => {
if (!inputValue.trim() || isLoading) return
const userMessage = {
id: Date.now().toString(),
role: "user",
content: inputValue,
timestamp: new Date(),
}
setMessages((prev) => [...prev, userMessage])
setInputValue("")
setIsLoading(true)
// 模拟AI响应
setTimeout(() => {
let response: any = {
id: (Date.now() + 1).toString(),
role: "assistant",
timestamp: new Date(),
}
// 根据输入内容生成不同类型的响应
if (inputValue.includes("高价值") || inputValue.includes("消费") || inputValue.includes("查询")) {
response.content = "我已理解您的需求。正在为您查询符合条件的用户..."
response.taskType = "NLQ"
response.result = {
type: "table",
title: "高价值用户查询结果",
sql: "SELECT * FROM users WHERE total_spending > 10000 AND last_active_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
data: [
{ id: "U001", name: "张三", phone: "138****1234", spending: "¥15,680", lastActive: "2天前" },
{ id: "U002", name: "李四", phone: "139****5678", spending: "¥12,340", lastActive: "1天前" },
{ id: "U003", name: "王五", phone: "137****9012", spending: "¥11,890", lastActive: "3天前" },
],
total: 823,
}
} else if (inputValue.includes("打标") || inputValue.includes("标签")) {
response.content = "我将为符合条件的用户打上「流失风险」标签。这是一个AI打标任务需要您确认后执行。"
response.taskType = "TAG"
response.result = {
type: "confirmation",
title: "AI打标任务确认",
description: "为最近7天未登录的用户打上「流失风险」标签",
affectedCount: 12456,
confidence: 0.87,
needReview: true,
}
} else if (inputValue.includes("清洗") || inputValue.includes("异常")) {
response.content = "我已扫描用户表,发现以下数据质量问题:"
response.taskType = "CLEAN"
response.result = {
type: "cleaning",
title: "数据清洗建议",
issues: [
{ field: "phone", issue: "格式不规范", count: 234, example: "1381234567 → 138****4567" },
{ field: "email", issue: "缺失值", count: 1256, suggestion: "填充默认值或标记" },
{ field: "birth_date", issue: "超出范围", count: 45, example: "2099-01-01" },
],
needReview: true,
}
} else if (inputValue.includes("报告") || inputValue.includes("分析")) {
response.content = "正在为您生成用户增长分析报告..."
response.taskType = "REPORT"
response.result = {
type: "report",
title: "用户增长分析报告2026年1月",
summary: "本月新增用户23,456人同比增长15.2%活跃用户856,234人DAU/MAU比值0.32。",
highlights: [
"新用户转化率提升至18.5%环比增长3.2%",
"高价值用户占比8.7%贡献65%的交易额",
"用户流失率下降至2.1%,创历史新低",
],
}
} else {
response.content = "我理解您的需求。请问您想要:\n1. 查询用户数据\n2. AI打标签\n3. 数据清洗\n4. 生成报告\n\n请选择或详细描述您的需求。"
}
setMessages((prev) => [...prev, response])
setIsLoading(false)
}, 1500)
}
const handleQuickCommand = (command: string) => {
setInputValue(command)
}
const renderMessageContent = (message: any) => {
return (
<div className="space-y-3">
<p className="whitespace-pre-wrap">{message.content}</p>
{message.result?.type === "table" && (
<Card className="bg-gray-50 border-0">
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-900">{message.result.title}</h4>
<Badge variant="secondary"> {message.result.total} </Badge>
</div>
<div className="bg-gray-100 rounded p-2 mb-3 font-mono text-xs text-gray-600 overflow-x-auto">
{message.result.sql}
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-3">ID</th>
<th className="text-left py-2 px-3"></th>
<th className="text-left py-2 px-3"></th>
<th className="text-left py-2 px-3"></th>
<th className="text-left py-2 px-3"></th>
</tr>
</thead>
<tbody>
{message.result.data.map((row: any, index: number) => (
<tr key={index} className="border-b border-gray-100">
<td className="py-2 px-3">{row.id}</td>
<td className="py-2 px-3">{row.name}</td>
<td className="py-2 px-3">{row.phone}</td>
<td className="py-2 px-3 text-green-600 font-medium">{row.spending}</td>
<td className="py-2 px-3">{row.lastActive}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex items-center gap-2 mt-3">
<Button size="sm" variant="outline">
<Download className="h-4 w-4 mr-1" />
Excel
</Button>
<Button size="sm" variant="outline">
<Copy className="h-4 w-4 mr-1" />
SQL
</Button>
</div>
</CardContent>
</Card>
)}
{message.result?.type === "confirmation" && (
<Card className="bg-blue-50 border-blue-200">
<CardContent className="p-4">
<h4 className="font-medium text-gray-900 mb-2">{message.result.title}</h4>
<p className="text-sm text-gray-600 mb-3">{message.result.description}</p>
<div className="flex items-center gap-4 text-sm mb-3">
<span><strong>{message.result.affectedCount.toLocaleString()}</strong></span>
<span><strong>{(message.result.confidence * 100).toFixed(0)}%</strong></span>
</div>
{message.result.needReview && (
<div className="flex items-center gap-2">
<Button size="sm"></Button>
<Button size="sm" variant="outline"></Button>
<Button size="sm" variant="ghost"></Button>
</div>
)}
</CardContent>
</Card>
)}
{message.result?.type === "cleaning" && (
<Card className="bg-yellow-50 border-yellow-200">
<CardContent className="p-4">
<h4 className="font-medium text-gray-900 mb-3">{message.result.title}</h4>
<div className="space-y-3">
{message.result.issues.map((issue: any, index: number) => (
<div key={index} className="flex items-start justify-between p-3 bg-white rounded-lg">
<div>
<div className="flex items-center gap-2">
<Badge variant="secondary">{issue.field}</Badge>
<span className="text-sm font-medium">{issue.issue}</span>
</div>
{issue.example && (
<p className="text-xs text-gray-500 mt-1">{issue.example}</p>
)}
{issue.suggestion && (
<p className="text-xs text-gray-500 mt-1">{issue.suggestion}</p>
)}
</div>
<Badge className="bg-red-100 text-red-700">{issue.count} </Badge>
</div>
))}
</div>
<div className="flex items-center gap-2 mt-3">
<Button size="sm"></Button>
<Button size="sm" variant="outline"></Button>
</div>
</CardContent>
</Card>
)}
{message.result?.type === "report" && (
<Card className="bg-green-50 border-green-200">
<CardContent className="p-4">
<h4 className="font-medium text-gray-900 mb-2">{message.result.title}</h4>
<p className="text-sm text-gray-600 mb-3">{message.result.summary}</p>
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700"></p>
{message.result.highlights.map((highlight: string, index: number) => (
<div key={index} className="flex items-start gap-2 text-sm text-gray-600">
<span className="text-green-500"></span>
{highlight}
</div>
))}
</div>
<div className="flex items-center gap-2 mt-3">
<Button size="sm">
<Download className="h-4 w-4 mr-1" />
</Button>
<Button size="sm" variant="outline">
<BarChart3 className="h-4 w-4 mr-1" />
</Button>
</div>
</CardContent>
</Card>
)}
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
<div className="flex h-[calc(100vh-64px)]">
{/* 左侧:功能面板 */}
<div className="w-64 border-r bg-white/80 backdrop-blur p-4 hidden lg:block">
<h3 className="font-semibold text-gray-900 mb-4">AI能力</h3>
<div className="space-y-2">
{supportedTasks.map((task) => (
<div
key={task.type}
className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"
>
<div className="p-2 rounded-lg bg-blue-50">
<task.icon className="h-4 w-4 text-blue-500" />
</div>
<div>
<p className="text-sm font-medium text-gray-900">{task.label}</p>
<p className="text-xs text-gray-500">{task.desc}</p>
</div>
</div>
))}
</div>
<div className="mt-6">
<h3 className="font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-2">
{quickCommands.map((cmd, index) => (
<button
key={index}
onClick={() => handleQuickCommand(cmd.command)}
className="w-full flex items-center gap-2 p-2 rounded-lg text-left text-sm text-gray-600 hover:bg-gray-50 transition-colors"
>
<cmd.icon className="h-4 w-4 text-gray-400" />
{cmd.label}
</button>
))}
</div>
</div>
</div>
{/* 中间:对话区域 */}
<div className="flex-1 flex flex-col">
{/* 消息列表 */}
<ScrollArea className="flex-1 p-6" ref={scrollRef}>
<div className="max-w-3xl mx-auto space-y-6">
{messages.map((message) => (
<div
key={message.id}
className={`flex gap-4 ${message.role === "user" ? "flex-row-reverse" : ""}`}
>
<div
className={`w-10 h-10 rounded-full flex items-center justify-center shrink-0 ${
message.role === "user"
? "bg-blue-500"
: "bg-gradient-to-br from-purple-500 to-blue-500"
}`}
>
{message.role === "user" ? (
<User className="h-5 w-5 text-white" />
) : (
<Bot className="h-5 w-5 text-white" />
)}
</div>
<div
className={`flex-1 ${message.role === "user" ? "text-right" : ""}`}
>
<div
className={`inline-block max-w-full text-left rounded-2xl px-4 py-3 ${
message.role === "user"
? "bg-blue-500 text-white"
: "bg-white shadow-sm"
}`}
>
{message.role === "user" ? (
<p>{message.content}</p>
) : (
renderMessageContent(message)
)}
</div>
{message.role === "assistant" && (
<div className="flex items-center gap-2 mt-2">
<Button variant="ghost" size="sm" className="h-7 px-2">
<ThumbsUp className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-7 px-2">
<ThumbsDown className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-7 px-2">
<Copy className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" className="h-7 px-2">
<RefreshCw className="h-3 w-3" />
</Button>
</div>
)}
</div>
</div>
))}
{isLoading && (
<div className="flex gap-4">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center">
<Bot className="h-5 w-5 text-white" />
</div>
<div className="bg-white rounded-2xl px-4 py-3 shadow-sm">
<div className="flex items-center gap-2 text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
<span>AI正在思考...</span>
</div>
</div>
</div>
)}
</div>
</ScrollArea>
{/* 输入区域 */}
<div className="border-t bg-white/80 backdrop-blur p-4">
<div className="max-w-3xl mx-auto">
<div className="flex items-center gap-3">
<div className="flex-1 relative">
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
placeholder="输入您的问题例如帮我找出最近30天的高价值用户..."
className="pr-12 py-6 bg-white"
/>
<Button
size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2"
onClick={handleSend}
disabled={isLoading || !inputValue.trim()}
>
<Send className="h-4 w-4" />
</Button>
</div>
</div>
<p className="text-xs text-gray-400 mt-2 text-center">
AI生成的内容可能存在误差
</p>
</div>
</div>
</div>
</div>
</div>
)
}