Files
users/app/data-ingestion/sources/[id]/page.tsx

400 lines
15 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 { useParams, useRouter } from "next/navigation"
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
ArrowLeft,
Database,
Send,
Loader2,
Brain,
RefreshCw,
Settings,
Table,
BarChart3,
Zap,
Clock,
HardDrive,
FolderTree,
} from "lucide-react"
interface DataSource {
id: string
name: string
nameCn: string
description: string
type: string
status: string
database?: string
host?: string
recordCount: number
collections?: number
latency?: number
dataCategory?: string
}
interface Collection {
name: string
count: number
indexes: number
size: string
}
interface ChatMessage {
role: "user" | "assistant"
content: string
timestamp: string
}
export default function DataSourceDetailPage() {
const params = useParams()
const router = useRouter()
const sourceId = params.id as string
const [loading, setLoading] = useState(true)
const [source, setSource] = useState<DataSource | null>(null)
const [collections, setCollections] = useState<Collection[]>([])
const [activeTab, setActiveTab] = useState("overview")
// AI查询
const [query, setQuery] = useState("")
const [querying, setQuerying] = useState(false)
const [messages, setMessages] = useState<ChatMessage[]>([])
const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
loadDataSource()
}, [sourceId])
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages])
const loadDataSource = async () => {
setLoading(true)
try {
const res = await fetch('/api/data-sources')
const data = await res.json()
if (data.success) {
const found = data.sources.find((s: DataSource) => s.id === sourceId)
if (found) {
setSource(found)
// 加载集合信息
if (found.database) {
loadCollections(found.database)
}
}
}
} catch (error) {
console.error('加载失败:', error)
} finally {
setLoading(false)
}
}
const loadCollections = async (dbName: string) => {
try {
const res = await fetch(`/api/data-sources?action=collections&db=${dbName}`)
const data = await res.json()
if (data.success) {
setCollections(data.collections || [])
}
} catch (error) {
console.error('加载集合失败:', error)
}
}
// AI查询
const handleQuery = async () => {
if (!query.trim() || querying) return
const userMsg: ChatMessage = {
role: "user",
content: query,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
}
setMessages(prev => [...prev, userMsg])
setQuery("")
setQuerying(true)
try {
// 构造针对特定数据库的查询
const enhancedQuery = source?.database
? `${source.database} 数据库中查询: ${query}`
: query
const response = await fetch("/api/ai-chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: enhancedQuery })
})
const data = await response.json()
setMessages(prev => [...prev, {
role: "assistant",
content: data.success ? data.response.content : `查询失败: ${data.error}`,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
}])
} catch (error: any) {
setMessages(prev => [...prev, {
role: "assistant",
content: `错误: ${error.message}`,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
}])
} finally {
setQuerying(false)
}
}
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()
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
)
}
if (!source) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20 p-6">
<Button variant="ghost" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
<div className="mt-20 text-center text-gray-500"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
<div className="p-6 space-y-4">
{/* 顶部导航 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4 mr-1" />
</Button>
<div>
<h1 className="text-xl font-bold text-gray-900">{source.nameCn || source.name}</h1>
<p className="text-sm text-gray-500 font-mono">{source.database || source.name}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge className={source.status === 'connected' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}>
{source.status === 'connected' ? '已连接' : '待配置'}
</Badge>
<Button variant="outline" size="sm">
<Settings className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-100 flex items-center justify-center">
<HardDrive className="h-5 w-5 text-blue-600" />
</div>
<div>
<div className="text-xl font-bold">{formatNumber(source.recordCount)}</div>
<div className="text-xs text-gray-500"></div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-green-100 flex items-center justify-center">
<FolderTree className="h-5 w-5 text-green-600" />
</div>
<div>
<div className="text-xl font-bold">{source.collections || 0}</div>
<div className="text-xs text-gray-500"></div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-100 flex items-center justify-center">
<Zap className="h-5 w-5 text-purple-600" />
</div>
<div>
<div className="text-xl font-bold">{source.latency || 0}ms</div>
<div className="text-xs text-gray-500"></div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-orange-100 flex items-center justify-center">
<Clock className="h-5 w-5 text-orange-600" />
</div>
<div>
<div className="text-xl font-bold"></div>
<div className="text-xs text-gray-500"></div>
</div>
</CardContent>
</Card>
</div>
{/* 标签页 */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-white/80">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="query">AI查询</TabsTrigger>
<TabsTrigger value="collections"></TabsTrigger>
<TabsTrigger value="stats"></TabsTrigger>
</TabsList>
{/* 概览 */}
<TabsContent value="overview" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-6">
<h3 className="font-medium text-gray-900 mb-4"></h3>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 rounded-lg bg-gray-50">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="font-medium">{source.type.toUpperCase()}</div>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="font-medium">{source.dataCategory || '其他'}</div>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="font-mono text-sm">{source.host || 'localhost'}</div>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="font-mono text-sm">{source.database || '-'}</div>
</div>
</div>
<div className="mt-4 p-3 rounded-lg bg-blue-50">
<div className="text-xs text-blue-600 mb-1"></div>
<div className="text-sm text-gray-700">{source.description}</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* AI查询 */}
<TabsContent value="query" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-2 border-b">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Brain className="h-4 w-4 text-purple-500" />
AI - {source.nameCn || source.name}
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{/* 消息区域 */}
<div className="h-[300px] overflow-y-auto p-4 space-y-3">
{messages.length === 0 && (
<div className="text-center text-gray-400 py-10">
<Brain className="h-10 w-10 mx-auto mb-2 opacity-50" />
<p className="text-sm">AI将在此数据库中搜索</p>
<p className="text-xs mt-1">: "查询前10条数据" "统计用户分布"</p>
</div>
)}
{messages.map((msg, i) => (
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[80%] rounded-xl px-4 py-2 text-sm ${
msg.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white'
: 'bg-gray-100 text-gray-800'
}`}>
<div className="whitespace-pre-wrap">{msg.content}</div>
<div className={`text-xs mt-1 ${msg.role === 'user' ? 'text-blue-100' : 'text-gray-400'}`}>
{msg.timestamp}
</div>
</div>
</div>
))}
{querying && (
<div className="flex justify-start">
<div className="bg-gray-100 rounded-xl px-4 py-2 flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-sm text-gray-500">...</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* 输入框 */}
<div className="p-3 border-t flex gap-2">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleQuery()}
placeholder={`${source.database || source.name} 中查询...`}
className="flex-1 bg-gray-50 border-0"
/>
<Button onClick={handleQuery} disabled={querying || !query.trim()}>
{querying ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 集合列表 */}
<TabsContent value="collections" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="space-y-2">
{collections.length === 0 ? (
<div className="text-center text-gray-400 py-10">
<Table className="h-10 w-10 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
</div>
) : (
collections.map((coll, i) => (
<div key={i} className="flex items-center justify-between p-3 rounded-lg bg-gray-50 hover:bg-gray-100">
<div className="flex items-center gap-3">
<Table className="h-4 w-4 text-gray-400" />
<span className="font-mono text-sm">{coll.name}</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>{formatNumber(coll.count)} </span>
<span>{coll.indexes} </span>
<span>{coll.size}</span>
</div>
</div>
))
)}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 统计 */}
<TabsContent value="stats" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="text-center text-gray-400 py-10">
<BarChart3 className="h-10 w-10 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
)
}