479 lines
18 KiB
TypeScript
479 lines
18 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect, useRef } from "react"
|
||
import {
|
||
Users,
|
||
Database,
|
||
RefreshCw,
|
||
Activity,
|
||
Brain,
|
||
Tags,
|
||
Package,
|
||
ArrowUpRight,
|
||
Zap,
|
||
Send,
|
||
Loader2,
|
||
Search,
|
||
Phone,
|
||
User,
|
||
TrendingUp,
|
||
Settings,
|
||
ChevronRight,
|
||
Sparkles,
|
||
Target,
|
||
} from "lucide-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 Link from "next/link"
|
||
|
||
interface ChatMessage {
|
||
role: "user" | "assistant" | "thinking"
|
||
content: string
|
||
timestamp: string
|
||
data?: any
|
||
thinking?: string // AI思考过程
|
||
portrait?: UserPortrait // 用户画像
|
||
}
|
||
|
||
// 标准用户画像模板
|
||
interface UserPortrait {
|
||
phone?: string
|
||
qq?: string
|
||
name?: string
|
||
gender?: string
|
||
province?: string
|
||
city?: string
|
||
level?: string
|
||
rfmScore?: number
|
||
tags?: string[]
|
||
behavior?: {
|
||
lastActive?: string
|
||
frequency?: string
|
||
}
|
||
dataCompleteness?: number
|
||
}
|
||
|
||
interface AIStatus {
|
||
status: string
|
||
database?: {
|
||
connected: boolean
|
||
totalUsers: number
|
||
latency: number
|
||
}
|
||
}
|
||
|
||
export default function HomePage() {
|
||
// AI对话状态
|
||
const [query, setQuery] = useState("")
|
||
const [loading, setLoading] = useState(false)
|
||
const [aiStatus, setAiStatus] = useState<AIStatus | null>(null)
|
||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||
{
|
||
role: "assistant",
|
||
content: "你好!我是神射手AI助手。\n\n你可以:\n• 输入手机号查询用户画像\n• 输入QQ号查询关联信息\n• 问我任何关于用户数据的问题",
|
||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||
}
|
||
])
|
||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||
|
||
// 统计数据
|
||
const [stats, setStats] = useState({
|
||
totalUsers: 0,
|
||
totalDataSources: 0,
|
||
latency: 0,
|
||
})
|
||
|
||
// 获取AI状态
|
||
useEffect(() => {
|
||
fetch("/api/ai-chat")
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
setAiStatus(data)
|
||
if (data.database) {
|
||
setStats({
|
||
totalUsers: data.database.totalUsers || 0,
|
||
totalDataSources: 26,
|
||
latency: data.database.latency || 0,
|
||
})
|
||
}
|
||
})
|
||
.catch(console.error)
|
||
}, [])
|
||
|
||
// 自动滚动到底部
|
||
useEffect(() => {
|
||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||
}, [messages])
|
||
|
||
// 解析用户画像数据
|
||
const parsePortrait = (data: any): UserPortrait | undefined => {
|
||
if (!data) return undefined
|
||
return {
|
||
phone: data.phone || data.手机号,
|
||
qq: data.qq || data.QQ,
|
||
name: data.name || data.姓名,
|
||
gender: data.gender || data.性别,
|
||
province: data.province || data.省份,
|
||
city: data.city || data.城市,
|
||
level: data.level || data.user_level || data.等级,
|
||
rfmScore: data.rfm_score || data.rfmScore || data.RFM评分,
|
||
tags: data.tags || [],
|
||
behavior: {
|
||
lastActive: data.last_active || data.最近活跃,
|
||
frequency: data.frequency || data.活跃频率,
|
||
},
|
||
dataCompleteness: data.data_completeness || data.数据完整度,
|
||
}
|
||
}
|
||
|
||
// 生成思考过程
|
||
const generateThinking = (query: string): string => {
|
||
const isPhone = /^1[3-9]\d{9}$/.test(query.replace(/\s/g, ''))
|
||
const isQQ = /^\d{5,11}$/.test(query.replace(/\s|qq/gi, ''))
|
||
|
||
if (isPhone) {
|
||
return `🔍 识别到手机号查询\n→ 拆解意图: 查询用户画像\n→ 数据源: KR.用户估值, KR_腾讯\n→ 执行: 手机号精确匹配\n→ 输出: 标准用户画像模板`
|
||
} else if (isQQ) {
|
||
return `🔍 识别到QQ号查询\n→ 拆解意图: QQ关联查询\n→ 数据源: KR_腾讯.qq_phone\n→ 执行: QQ号精确匹配\n→ 输出: 关联手机号 + 用户画像`
|
||
} else {
|
||
return `🔍 自然语言理解中...\n→ 拆解意图: ${query.slice(0, 20)}...\n→ 思考策略: 语义分析\n→ 执行: Skill查询`
|
||
}
|
||
}
|
||
|
||
// 发送消息
|
||
const handleSend = async () => {
|
||
if (!query.trim() || loading) return
|
||
|
||
const userMessage: ChatMessage = {
|
||
role: "user",
|
||
content: query,
|
||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||
}
|
||
|
||
// 添加思考过程消息
|
||
const thinkingMessage: ChatMessage = {
|
||
role: "thinking",
|
||
content: generateThinking(query),
|
||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||
}
|
||
|
||
setMessages(prev => [...prev, userMessage, thinkingMessage])
|
||
setQuery("")
|
||
setLoading(true)
|
||
|
||
try {
|
||
const response = await fetch("/api/ai-chat", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ message: query })
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
// 解析用户画像
|
||
const portrait = parsePortrait(data.response?.data)
|
||
|
||
setMessages(prev => {
|
||
// 移除思考消息,添加结果
|
||
const filtered = prev.filter(m => m.role !== "thinking")
|
||
return [...filtered, {
|
||
role: "assistant",
|
||
content: data.success ? data.response.content : `查询失败: ${data.error || "未知错误"}`,
|
||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||
data: data.response?.data,
|
||
portrait,
|
||
thinking: generateThinking(query),
|
||
}]
|
||
})
|
||
} catch (error: any) {
|
||
setMessages(prev => {
|
||
const filtered = prev.filter(m => m.role !== "thinking")
|
||
return [...filtered, {
|
||
role: "assistant",
|
||
content: `网络错误: ${error.message}`,
|
||
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
|
||
}]
|
||
})
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 快捷查询
|
||
const quickQuery = (q: string) => {
|
||
setQuery(q)
|
||
setTimeout(() => handleSend(), 100)
|
||
}
|
||
|
||
const formatNumber = (num: number): string => {
|
||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||
return num.toLocaleString()
|
||
}
|
||
|
||
// 核心功能模块
|
||
const modules = [
|
||
{
|
||
title: "数据接入",
|
||
icon: Database,
|
||
color: "from-blue-500 to-cyan-500",
|
||
href: "/data-ingestion/sources",
|
||
desc: "数据源 · 清洗 · 调度",
|
||
},
|
||
{
|
||
title: "标签画像",
|
||
icon: Tags,
|
||
color: "from-green-500 to-emerald-500",
|
||
href: "/tag-portrait",
|
||
desc: "标签 · 画像 · 流量池",
|
||
},
|
||
{
|
||
title: "数据市场",
|
||
icon: Package,
|
||
color: "from-orange-500 to-red-500",
|
||
href: "/data-market",
|
||
desc: "API · 订阅 · 变现",
|
||
},
|
||
]
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20 p-4 md:p-6">
|
||
{/* 顶部状态栏 */}
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<h1 className="text-xl font-bold text-gray-900">神射手</h1>
|
||
<p className="text-xs text-gray-500">用户资产数字化中台</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Badge className={`${aiStatus?.status === 'online' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'} text-xs`}>
|
||
<div className={`w-1.5 h-1.5 rounded-full ${aiStatus?.status === 'online' ? 'bg-green-500' : 'bg-yellow-500'} mr-1`}></div>
|
||
{stats.latency}ms
|
||
</Badge>
|
||
<Link href="/monitoring/health">
|
||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||
<Activity className="h-4 w-4" />
|
||
</Button>
|
||
</Link>
|
||
<Link href="/settings">
|
||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||
<Settings className="h-4 w-4" />
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||
<Card className="border-0 shadow-sm bg-white/70">
|
||
<CardContent className="p-3">
|
||
<div className="flex items-center gap-2">
|
||
<Users className="h-4 w-4 text-blue-500" />
|
||
<div>
|
||
<div className="text-lg font-bold">{formatNumber(stats.totalUsers)}</div>
|
||
<div className="text-xs text-gray-500">用户</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-0 shadow-sm bg-white/70">
|
||
<CardContent className="p-3">
|
||
<div className="flex items-center gap-2">
|
||
<Database className="h-4 w-4 text-green-500" />
|
||
<div>
|
||
<div className="text-lg font-bold">{stats.totalDataSources}</div>
|
||
<div className="text-xs text-gray-500">数据源</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-0 shadow-sm bg-white/70">
|
||
<CardContent className="p-3">
|
||
<div className="flex items-center gap-2">
|
||
<Zap className="h-4 w-4 text-orange-500" />
|
||
<div>
|
||
<div className="text-lg font-bold">{stats.latency}ms</div>
|
||
<div className="text-xs text-gray-500">延迟</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* AI对话区域 - 核心功能 */}
|
||
<Card className="border-0 shadow-lg bg-white/90 backdrop-blur mb-4 flex flex-col" style={{ height: 'calc(100vh - 340px)', minHeight: '300px' }}>
|
||
<CardHeader className="pb-2 border-b flex-shrink-0">
|
||
<CardTitle className="text-sm font-medium flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Brain className="h-4 w-4 text-purple-500" />
|
||
AI 智能对话
|
||
</div>
|
||
<Badge variant="outline" className="text-xs">
|
||
{formatNumber(stats.totalUsers)} 可查询
|
||
</Badge>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
|
||
{/* 消息列表 */}
|
||
<CardContent className="flex-1 overflow-y-auto p-4 space-y-3">
|
||
{messages.map((msg, i) => (
|
||
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||
{/* 思考过程 */}
|
||
{msg.role === 'thinking' && (
|
||
<div className="max-w-[85%] rounded-2xl px-4 py-2.5 bg-yellow-50 border border-yellow-200">
|
||
<div className="flex items-center gap-2 text-yellow-700 text-xs mb-1">
|
||
<Sparkles className="h-3 w-3 animate-pulse" />
|
||
AI思考中...
|
||
</div>
|
||
<div className="text-xs text-yellow-800 whitespace-pre-wrap font-mono">{msg.content}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 用户消息 */}
|
||
{msg.role === 'user' && (
|
||
<div className="max-w-[85%] rounded-2xl px-4 py-2.5 bg-gradient-to-r from-blue-500 to-purple-500 text-white">
|
||
<div className="text-sm whitespace-pre-wrap">{msg.content}</div>
|
||
<div className="text-xs mt-1 text-blue-100">{msg.timestamp}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* AI回复 */}
|
||
{msg.role === 'assistant' && (
|
||
<div className="max-w-[85%] space-y-2">
|
||
{/* 思考过程折叠显示 */}
|
||
{msg.thinking && (
|
||
<div className="rounded-xl px-3 py-2 bg-yellow-50 border border-yellow-100 text-xs">
|
||
<div className="text-yellow-600 font-medium mb-1">💭 思考过程</div>
|
||
<div className="text-yellow-800 whitespace-pre-wrap font-mono">{msg.thinking}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 用户画像卡片 */}
|
||
{msg.portrait && (msg.portrait.phone || msg.portrait.qq) && (
|
||
<div className="rounded-xl p-3 bg-gradient-to-r from-purple-50 to-blue-50 border border-purple-100">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center text-white font-bold">
|
||
{(msg.portrait.name || '?')[0]}
|
||
</div>
|
||
<div>
|
||
<div className="font-semibold text-gray-900">{msg.portrait.name || '未知用户'}</div>
|
||
<div className="text-xs text-gray-500">
|
||
{msg.portrait.phone && <span className="mr-2">📱 {msg.portrait.phone}</span>}
|
||
{msg.portrait.qq && <span>QQ: {msg.portrait.qq}</span>}
|
||
</div>
|
||
</div>
|
||
{msg.portrait.level && (
|
||
<Badge className="ml-auto bg-purple-100 text-purple-700">{msg.portrait.level}级</Badge>
|
||
)}
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||
{msg.portrait.rfmScore !== undefined && (
|
||
<div className="p-2 rounded bg-white/80">
|
||
<div className="text-gray-500">RFM</div>
|
||
<div className="font-bold text-purple-600">{msg.portrait.rfmScore}</div>
|
||
</div>
|
||
)}
|
||
{msg.portrait.province && (
|
||
<div className="p-2 rounded bg-white/80">
|
||
<div className="text-gray-500">地区</div>
|
||
<div className="font-medium">{msg.portrait.province} {msg.portrait.city}</div>
|
||
</div>
|
||
)}
|
||
{msg.portrait.dataCompleteness !== undefined && (
|
||
<div className="p-2 rounded bg-white/80">
|
||
<div className="text-gray-500">完整度</div>
|
||
<div className="font-medium text-green-600">{msg.portrait.dataCompleteness}%</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{msg.portrait.tags && msg.portrait.tags.length > 0 && (
|
||
<div className="flex flex-wrap gap-1 mt-2">
|
||
{msg.portrait.tags.slice(0, 5).map((tag, ti) => (
|
||
<Badge key={ti} variant="secondary" className="text-xs">{tag}</Badge>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 文本回复 */}
|
||
<div className="rounded-2xl px-4 py-2.5 bg-gray-100 text-gray-800">
|
||
<div className="text-sm whitespace-pre-wrap">{msg.content}</div>
|
||
<div className="text-xs mt-1 text-gray-400">{msg.timestamp}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
{loading && (
|
||
<div className="flex justify-start">
|
||
<div className="bg-gray-100 rounded-2xl px-4 py-2.5 flex items-center gap-2 text-gray-500">
|
||
<Loader2 className="w-4 h-4 animate-spin" />
|
||
<span className="text-sm">查询中...</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div ref={messagesEndRef} />
|
||
</CardContent>
|
||
|
||
{/* 快捷查询 */}
|
||
<div className="px-4 py-2 border-t flex-shrink-0">
|
||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("13407000001")}>
|
||
<Phone className="w-3 h-3 mr-1" /> 查手机
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("28533368 qq")}>
|
||
<User className="w-3 h-3 mr-1" /> 查QQ
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("系统状态")}>
|
||
<Activity className="w-3 h-3 mr-1" /> 系统状态
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="text-xs shrink-0 h-7" onClick={() => quickQuery("高价值用户")}>
|
||
<TrendingUp className="w-3 h-3 mr-1" /> 高价值
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 输入框 */}
|
||
<div className="p-3 border-t flex-shrink-0">
|
||
<div className="flex gap-2">
|
||
<Input
|
||
value={query}
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
onKeyPress={(e) => e.key === "Enter" && handleSend()}
|
||
placeholder="输入手机号、QQ号或问题..."
|
||
className="flex-1 h-10 bg-gray-50 border-0"
|
||
/>
|
||
<Button
|
||
onClick={handleSend}
|
||
disabled={loading || !query.trim()}
|
||
className="h-10 px-4 bg-gradient-to-r from-blue-500 to-purple-500"
|
||
>
|
||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 功能模块入口 */}
|
||
<div className="grid grid-cols-3 gap-3">
|
||
{modules.map((module, i) => (
|
||
<Link key={i} href={module.href}>
|
||
<Card className="border-0 shadow-sm bg-white/70 cursor-pointer hover:shadow-md transition-all group">
|
||
<CardContent className="p-3">
|
||
<div className={`w-10 h-10 rounded-xl bg-gradient-to-r ${module.color} flex items-center justify-center mb-2 group-hover:scale-110 transition-transform`}>
|
||
<module.icon className="w-5 h-5 text-white" />
|
||
</div>
|
||
<h3 className="font-medium text-sm text-gray-900">{module.title}</h3>
|
||
<p className="text-xs text-gray-500 mt-0.5">{module.desc}</p>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|