存客宝 React

This commit is contained in:
柳清爽
2025-03-29 16:50:39 +08:00
parent caea0b4b99
commit 7e7c199996
388 changed files with 53282 additions and 2076 deletions

View File

@@ -0,0 +1,284 @@
"use client"
import type React from "react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ArrowLeft, FileText, Trash2, Upload } from "lucide-react"
import { useRouter } from "next/navigation"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
interface KnowledgeFile {
id: string
name: string
type: string
size: string
uploadDate: string
}
export default function KnowledgeBasePage() {
const router = useRouter()
const [activeTab, setActiveTab] = useState("private")
const [privateFiles, setPrivateFiles] = useState<KnowledgeFile[]>([
{
id: "1",
name: "私域运营手册.pdf",
type: "PDF",
size: "2.4 MB",
uploadDate: "2023-12-10",
},
{
id: "2",
name: "客户画像分析.docx",
type: "Word",
size: "1.8 MB",
uploadDate: "2023-12-08",
},
{
id: "3",
name: "获客策略指南.pdf",
type: "PDF",
size: "3.2 MB",
uploadDate: "2023-12-05",
},
])
const [storeFiles, setStoreFiles] = useState<KnowledgeFile[]>([
{
id: "1",
name: "门店管理规范.pdf",
type: "PDF",
size: "1.9 MB",
uploadDate: "2023-12-09",
},
{
id: "2",
name: "产品介绍手册.docx",
type: "Word",
size: "2.1 MB",
uploadDate: "2023-12-07",
},
])
const [isUploading, setIsUploading] = useState(false)
const [showPreview, setShowPreview] = useState(false)
const [previewFile, setPreviewFile] = useState<KnowledgeFile | null>(null)
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files || files.length === 0) return
setIsUploading(true)
// 模拟上传过程
setTimeout(() => {
const newFiles = Array.from(files).map((file) => ({
id: Date.now().toString(),
name: file.name,
type: file.name.split(".").pop()?.toUpperCase() || "未知",
size: `${(file.size / (1024 * 1024)).toFixed(1)} MB`,
uploadDate: new Date().toISOString().split("T")[0],
}))
if (activeTab === "private") {
setPrivateFiles((prev) => [...prev, ...newFiles])
} else {
setStoreFiles((prev) => [...prev, ...newFiles])
}
setIsUploading(false)
e.target.value = "" // 重置文件输入
}, 1500)
}
const handleDeleteFile = (id: string) => {
if (activeTab === "private") {
setPrivateFiles((prev) => prev.filter((file) => file.id !== id))
} else {
setStoreFiles((prev) => prev.filter((file) => file.id !== id))
}
}
const handlePreviewFile = (file: KnowledgeFile) => {
setPreviewFile(file)
setShowPreview(true)
}
return (
<div className="flex flex-col h-screen max-h-screen bg-gray-50">
{/* 头部 */}
<div className="bg-white p-4 border-b flex items-center">
<Button variant="ghost" size="icon" onClick={() => router.back()} className="mr-2">
<ArrowLeft className="h-5 w-5" />
</Button>
<h1 className="text-xl font-semibold"></h1>
</div>
{/* 主内容区域 */}
<div className="flex-1 p-4 overflow-hidden">
<Card className="max-w-4xl mx-auto">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="private" onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="private"></TabsTrigger>
<TabsTrigger value="store"></TabsTrigger>
</TabsList>
<TabsContent value="private" className="space-y-4">
<div className="flex justify-between items-center">
<p className="text-sm text-gray-500"> {privateFiles.length} </p>
<div className="relative">
<input
type="file"
id="private-upload"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
accept=".pdf,.doc,.docx"
multiple
onChange={handleFileUpload}
/>
<Button variant="outline" className="flex items-center gap-2">
<Upload className="h-4 w-4" />
</Button>
</div>
</div>
<ScrollArea className="h-[calc(100vh-300px)]">
<div className="space-y-2">
{privateFiles.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-3 bg-white border rounded-md hover:bg-gray-50"
>
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-blue-500" />
<div>
<p className="font-medium">{file.name}</p>
<p className="text-xs text-gray-500">
{file.type} {file.size} {file.uploadDate}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => handlePreviewFile(file)}>
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteFile(file.id)}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
{privateFiles.length === 0 && (
<div className="text-center py-8 text-gray-500"></div>
)}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="store" className="space-y-4">
<div className="flex justify-between items-center">
<p className="text-sm text-gray-500"> {storeFiles.length} </p>
<div className="relative">
<input
type="file"
id="store-upload"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
accept=".pdf,.doc,.docx"
multiple
onChange={handleFileUpload}
/>
<Button variant="outline" className="flex items-center gap-2">
<Upload className="h-4 w-4" />
</Button>
</div>
</div>
<ScrollArea className="h-[calc(100vh-300px)]">
<div className="space-y-2">
{storeFiles.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-3 bg-white border rounded-md hover:bg-gray-50"
>
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-green-500" />
<div>
<p className="font-medium">{file.name}</p>
<p className="text-xs text-gray-500">
{file.type} {file.size} {file.uploadDate}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => handlePreviewFile(file)}>
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteFile(file.id)}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
{storeFiles.length === 0 && (
<div className="text-center py-8 text-gray-500"></div>
)}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
{/* 文件预览对话框 */}
<Dialog open={showPreview} onOpenChange={setShowPreview}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>{previewFile?.name}</DialogTitle>
</DialogHeader>
<div className="p-4 bg-gray-50 rounded-md min-h-[300px] flex items-center justify-center">
<div className="text-center">
<FileText className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
<p className="text-sm text-gray-400 mt-2">
: {previewFile?.type} : {previewFile?.size}
</p>
</div>
</div>
</DialogContent>
</Dialog>
{/* 上传中提示 */}
{isUploading && (
<div className="fixed inset-0 bg-black/20 flex items-center justify-center z-50">
<Card className="w-80">
<CardContent className="p-6">
<div className="flex flex-col items-center">
<div className="w-12 h-12 rounded-full border-4 border-t-blue-500 border-blue-200 animate-spin mb-4"></div>
<p className="font-medium">...</p>
<p className="text-sm text-gray-500 mt-1"></p>
</div>
</CardContent>
</Card>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,273 @@
"use client"
import type React from "react"
import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ArrowLeft, Image, Mic, Send, FileText, MicOff } from "lucide-react"
import { useRouter } from "next/navigation"
import { VoiceRecognition } from "@/app/components/VoiceRecognition"
import { ScrollArea } from "@/components/ui/scroll-area"
interface Message {
id: string
content: string
sender: "user" | "ai"
timestamp: Date
attachments?: {
type: "image" | "document"
name: string
url: string
}[]
}
export default function AIAssistantPage() {
const router = useRouter()
const [messages, setMessages] = useState<Message[]>([
{
id: "1",
content: "你好我是你的AI助手有什么可以帮助你的吗",
sender: "ai",
timestamp: new Date(),
},
])
const [inputValue, setInputValue] = useState("")
const [isRecording, setIsRecording] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const documentInputRef = useRef<HTMLInputElement>(null)
// 自动滚动到最新消息
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages])
const handleSendMessage = async () => {
if (!inputValue.trim() && !isRecording) return
const newUserMessage: Message = {
id: Date.now().toString(),
content: inputValue,
sender: "user",
timestamp: new Date(),
}
setMessages((prev) => [...prev, newUserMessage])
setInputValue("")
setIsLoading(true)
// 模拟AI响应
setTimeout(() => {
const aiResponse: Message = {
id: (Date.now() + 1).toString(),
content: `我已收到你的消息:"${newUserMessage.content}"。这是一个模拟的AI回复。`,
sender: "ai",
timestamp: new Date(),
}
setMessages((prev) => [...prev, aiResponse])
setIsLoading(false)
}, 1000)
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
const toggleRecording = () => {
setIsRecording(!isRecording)
}
const handleVoiceInput = (text: string) => {
setInputValue((prev) => prev + text)
setIsRecording(false)
}
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>, type: "image" | "document") => {
const files = e.target.files
if (!files || files.length === 0) return
const file = files[0]
const reader = new FileReader()
reader.onload = () => {
const newUserMessage: Message = {
id: Date.now().toString(),
content: type === "image" ? "我发送了一张图片" : `我上传了文档:${file.name}`,
sender: "user",
timestamp: new Date(),
attachments: [
{
type,
name: file.name,
url: reader.result as string,
},
],
}
setMessages((prev) => [...prev, newUserMessage])
setIsLoading(true)
// 模拟AI响应
setTimeout(() => {
const aiResponse: Message = {
id: (Date.now() + 1).toString(),
content:
type === "image"
? "我已收到你的图片,正在分析内容..."
: `我已收到你上传的文档:${file.name},正在分析内容...`,
sender: "ai",
timestamp: new Date(),
}
setMessages((prev) => [...prev, aiResponse])
setIsLoading(false)
}, 1500)
}
reader.readAsDataURL(file)
e.target.value = "" // 重置文件输入
}
const triggerFileUpload = (type: "image" | "document") => {
if (type === "image") {
fileInputRef.current?.click()
} else {
documentInputRef.current?.click()
}
}
const navigateToKnowledgeBase = () => {
router.push("/workspace/ai-assistant/knowledge-base")
}
return (
<div className="flex flex-col h-screen max-h-screen bg-gray-50">
{/* 头部 */}
<div className="bg-white p-4 border-b flex items-center justify-between">
<div className="flex items-center">
<Button variant="ghost" size="icon" onClick={() => router.back()} className="mr-2">
<ArrowLeft className="h-5 w-5" />
</Button>
<h1 className="text-xl font-semibold">AI对话助手</h1>
</div>
<Button variant="outline" size="sm" onClick={() => router.push("/workspace/ai-assistant/knowledge-base")}>
</Button>
</div>
{/* 聊天区域 */}
<ScrollArea className="flex-1 p-4 overflow-y-auto">
<div className="max-w-3xl mx-auto space-y-4">
{messages.map((message) => (
<div key={message.id} className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[80%] rounded-lg p-3 ${
message.sender === "user" ? "bg-blue-500 text-white" : "bg-white border shadow-sm"
}`}
>
{message.attachments?.map((attachment, index) => (
<div key={index} className="mb-2">
{attachment.type === "image" ? (
<div className="rounded-md overflow-hidden">
<img src={attachment.url || "/placeholder.svg"} alt="Uploaded" className="max-w-full h-auto" />
</div>
) : (
<div className="flex items-center gap-2 p-2 bg-gray-100 rounded-md">
<FileText className="h-5 w-5 text-gray-500" />
<span className="text-sm truncate">{attachment.name}</span>
</div>
)}
</div>
))}
<div className="whitespace-pre-wrap">{message.content}</div>
<div className={`text-xs mt-1 ${message.sender === "user" ? "text-blue-200" : "text-gray-400"}`}>
{message.timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</div>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="max-w-[80%] rounded-lg p-3 bg-white border shadow-sm">
<div className="flex space-x-2">
<div
className="w-2 h-2 rounded-full bg-gray-300 animate-bounce"
style={{ animationDelay: "0ms" }}
></div>
<div
className="w-2 h-2 rounded-full bg-gray-300 animate-bounce"
style={{ animationDelay: "150ms" }}
></div>
<div
className="w-2 h-2 rounded-full bg-gray-300 animate-bounce"
style={{ animationDelay: "300ms" }}
></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
</ScrollArea>
{/* 输入区域 */}
<div className="bg-white border-t p-4">
<div className="max-w-3xl mx-auto">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={toggleRecording}
className={isRecording ? "bg-red-100 text-red-500" : ""}
>
{isRecording ? <MicOff className="h-5 w-5" /> : <Mic className="h-5 w-5" />}
</Button>
<Button variant="outline" size="icon" onClick={() => triggerFileUpload("image")}>
<Image className="h-5 w-5" />
</Button>
<Button variant="outline" size="icon" onClick={() => triggerFileUpload("document")}>
<FileText className="h-5 w-5" />
</Button>
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入消息..."
className="flex-1"
/>
<Button onClick={handleSendMessage} disabled={!inputValue.trim() && !isRecording}>
<Send className="h-5 w-5" />
</Button>
</div>
</div>
</div>
{/* 隐藏的文件上传输入 */}
<input
type="file"
ref={fileInputRef}
onChange={(e) => handleFileUpload(e, "image")}
accept="image/*"
className="hidden"
/>
<input
type="file"
ref={documentInputRef}
onChange={(e) => handleFileUpload(e, "document")}
accept=".pdf,.doc,.docx"
className="hidden"
/>
{/* 语音识别组件 */}
{isRecording && <VoiceRecognition onResult={handleVoiceInput} onStop={() => setIsRecording(false)} />}
</div>
)
}