Files
users/app/page.tsx

479 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, 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>
)
}