Files
shensheshou/app/ai-assistant/page.tsx

313 lines
13 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 Link from "next/link"
import { useState, useEffect, useRef } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { Cpu, BarChart3, TrendingUp, Users, Target, Brain, ArrowRight, Send, Sparkles, Loader2 } from "lucide-react"
interface ChatMessage {
role: "user" | "assistant"
content: string
timestamp: string
}
interface AIStatus {
status: string
model: string
database?: {
connected: boolean
totalUsers: number
latency: number
}
}
export default function AIAssistant() {
const [message, setMessage] = useState("")
const [isLoading, setIsLoading] = useState(false)
const [aiStatus, setAIStatus] = useState<AIStatus | null>(null)
const chatEndRef = useRef<HTMLDivElement>(null)
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([
{
role: "assistant",
content: "你好我是神射手AI助手可以帮助你查询用户数据、分析RFM估值。\n\n💡 试试:\n- 查 13407000001\n- 28533368 qq\n- 系统状态\n- 帮助",
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
},
])
// 获取 AI 状态
useEffect(() => {
fetch("/api/ai-chat")
.then(res => res.json())
.then(data => setAIStatus(data))
.catch(console.error)
}, [])
// 自动滚动到底部
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [chatHistory])
const aiFeatures = [
{
icon: <BarChart3 className="w-8 h-8" />,
title: "数据分析",
description: "自动分析用户行为,生成洞察报告",
color: "from-blue-500 to-cyan-500",
href: "/ai-assistant/data-analysis",
stats: { reports: 1258, insights: 856 },
},
{
icon: <TrendingUp className="w-8 h-8" />,
title: "趋势预测",
description: "预测用户行为和价值变化趋势",
color: "from-purple-500 to-pink-500",
href: "/ai-assistant/trend-prediction",
stats: { accuracy: 94.2, predictions: 2450 },
},
{
icon: <Users className="w-8 h-8" />,
title: "用户画像",
description: "智能生成用户群体画像",
color: "from-green-500 to-emerald-500",
href: "/ai-assistant/user-profiling",
stats: { profiles: 4285, segments: 128 },
},
{
icon: <Target className="w-8 h-8" />,
title: "精准推荐",
description: "推荐最佳营销策略和用户群",
color: "from-orange-500 to-red-500",
href: "/ai-assistant/recommendation",
stats: { campaigns: 586, conversion: 18.5 },
},
]
const quickQuestions = ["查 13407000001", "系统状态", "RFM分析", "高价值用户 TOP10", "帮助"]
const handleSend = async () => {
if (!message.trim() || isLoading) return
const userMessage: ChatMessage = {
role: "user",
content: message,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
}
setChatHistory(prev => [...prev, userMessage])
setMessage("")
setIsLoading(true)
try {
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()
if (data.success && data.response) {
setChatHistory(prev => [...prev, {
role: "assistant",
content: data.response.content,
timestamp: data.response.timestamp || new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
}])
} else {
setChatHistory(prev => [...prev, {
role: "assistant",
content: `⚠️ ${data.error || "请求失败,请稍后重试"}`,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
}])
}
} catch (error: any) {
setChatHistory(prev => [...prev, {
role: "assistant",
content: `⚠️ 网络错误: ${error.message}`,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
}])
} finally {
setIsLoading(false)
}
}
return (
<div className="space-y-8">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center shadow-lg">
<Brain className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900">AI智能助手</h1>
<p className="text-gray-500"></p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm">
<CardHeader>
<CardTitle className="text-gray-900 flex items-center gap-2">
<Cpu className="w-5 h-5 text-green-600" />
AI运行状态
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<Badge variant="outline" className={`${aiStatus?.status === 'online' ? 'border-green-200 text-green-600 bg-green-50' : 'border-yellow-200 text-yellow-600 bg-yellow-50'}`}>
<div className={`w-2 h-2 rounded-full ${aiStatus?.status === 'online' ? 'bg-green-500' : 'bg-yellow-500'} animate-pulse mr-2`}></div>
{aiStatus?.status === 'online' ? '运行中' : '加载中'}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="text-gray-900 font-semibold">{aiStatus?.model || '神射手 AI'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="text-gray-900 font-semibold">{aiStatus?.database?.latency || '--'}ms</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="text-green-600 font-semibold">
{aiStatus?.database?.totalUsers ? `${(aiStatus.database.totalUsers / 1e8).toFixed(1)}亿` : '--'}
</span>
</div>
</div>
</CardContent>
</Card>
<div className="lg:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6">
{aiFeatures.map((feature, index) => (
<Link key={index} href={feature.href}>
<Card className="hover:shadow-lg transition-all cursor-pointer h-full bg-white/60 backdrop-blur-md border-gray-200 shadow-sm group">
<CardContent className="p-6">
<div className="flex items-start gap-4 mb-4">
<div
className={`w-16 h-16 rounded-xl bg-gradient-to-br ${feature.color} flex items-center justify-center text-white shadow-md group-hover:scale-105 transition-transform`}
>
{feature.icon}
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-gray-900">{feature.title}</h3>
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-blue-500 transition-colors" />
</div>
<p className="text-gray-500 text-sm">{feature.description}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-100">
{Object.entries(feature.stats).map(([key, value], i) => (
<div key={i}>
<div className="text-xs text-gray-500 mb-1">
{key === "reports"
? "报告数"
: key === "insights"
? "洞察数"
: key === "accuracy"
? "准确率"
: key === "predictions"
? "预测数"
: key === "profiles"
? "画像数"
: key === "segments"
? "分组数"
: key === "campaigns"
? "活动数"
: "转化率"}
</div>
<div className="text-lg font-bold text-gray-900">
{typeof value === "number" && value % 1 !== 0 ? `${value}%` : value}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
<div className="lg:col-span-3">
<Card className="bg-white/60 backdrop-blur-md border-gray-200 shadow-sm h-[600px] flex flex-col">
<CardHeader className="border-b border-gray-100">
<CardTitle className="text-gray-900 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-blue-600" />
AI对话
</CardTitle>
</CardHeader>
<CardContent className="p-6 flex flex-col flex-1 overflow-hidden">
<div className="flex-1 overflow-y-auto space-y-4 mb-6 pr-2">
{chatHistory.map((chat, index) => (
<div key={index} className={`flex ${chat.role === "user" ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[80%] rounded-2xl p-4 shadow-sm ${chat.role === "user" ? "bg-gradient-to-br from-blue-500 to-purple-500 text-white" : "bg-white border border-gray-100 text-gray-800"}`}
>
<div className="mb-1 whitespace-pre-wrap">{chat.content}</div>
<div className={`text-xs ${chat.role === "user" ? "text-blue-100" : "text-gray-400"}`}>
{chat.timestamp}
</div>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white border border-gray-100 rounded-2xl p-4 shadow-sm">
<div className="flex items-center gap-2 text-gray-500">
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</div>
</div>
</div>
)}
<div ref={chatEndRef} />
</div>
<div className="mb-4 flex flex-wrap gap-2">
{quickQuestions.map((question, index) => (
<Button
key={index}
variant="outline"
size="sm"
onClick={() => setMessage(question)}
className="bg-white hover:bg-gray-50 text-xs border-gray-200 text-gray-600"
>
<Sparkles className="w-3 h-3 mr-1 text-yellow-500" />
{question}
</Button>
))}
</div>
<div className="flex items-end gap-3">
<Textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}}
placeholder="输入您的问题按Enter发送..."
className="bg-white border-gray-200 text-gray-900 resize-none focus:ring-blue-500"
rows={3}
/>
<Button
onClick={handleSend}
disabled={isLoading || !message.trim()}
className="bg-gradient-to-r from-blue-500 to-purple-500 h-[88px] w-20 shadow-md hover:shadow-lg transition-shadow disabled:opacity-50"
>
{isLoading ? <Loader2 className="w-6 h-6 animate-spin" /> : <Send className="w-6 h-6" />}
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}