Files
shensheshou/app/ai-agent/chat/page.tsx

416 lines
17 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)
try {
// 调用真实的AI Chat API
const response = await fetch("/api/ai-chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userMessage.content })
})
const data = await response.json()
let aiResponse: any = {
id: (Date.now() + 1).toString(),
role: "assistant",
timestamp: new Date(),
}
if (data.success && data.response) {
aiResponse.content = data.response.content
// 如果有数据结果,添加表格展示
if (data.response.data && data.response.data.length > 0) {
aiResponse.taskType = "NLQ"
aiResponse.result = {
type: "table",
title: "查询结果",
data: data.response.data.slice(0, 5).map((item: any, idx: number) => ({
id: idx + 1,
name: item.name || item. || "-",
phone: item.phone_masked || item.phone || "-",
level: item.user_level || item.traffic_pool?.pool_name || "-",
score: item.user_evaluation_score || item.rfm_composite_score || "-",
})),
total: data.response.data.length,
}
}
} else {
aiResponse.content = `⚠️ ${data.error || "查询失败,请重试"}`
}
setMessages((prev) => [...prev, aiResponse])
} catch (error: any) {
setMessages((prev) => [...prev, {
id: (Date.now() + 1).toString(),
role: "assistant",
content: `⚠️ 网络错误: ${error.message}`,
timestamp: new Date(),
}])
} finally {
setIsLoading(false)
}
}
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>
)
}