Files
AOUN/src/pages/aitools/chat/index.tsx
2025-11-28 22:44:54 +08:00

267 lines
12 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.
import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '@/hooks/useAuth';
import AIToolsLayout from '@/components/aitools/AIToolsLayout';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Bot, Send, Loader2, Eraser } from 'lucide-react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import ReactMarkdown from 'react-markdown';
interface Message {
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: number;
}
export default function AIChat() {
const { user } = useAuth();
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [selectedAssistant, setSelectedAssistant] = useState<string>('system');
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// Load initial system message based on selected assistant
useEffect(() => {
if (selectedAssistant === 'system') {
// System default
} else {
// Find custom assistant
const assistant = user?.AI配置?.?.find((a: any) => a. === selectedAssistant);
if (assistant && assistant.) {
// Optionally add a system message to context (not visible)
}
}
}, [selectedAssistant, user]);
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
role: 'user',
content: input,
timestamp: Date.now()
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [...messages, userMessage].map(m => ({ role: m.role, content: m.content })),
assistantId: selectedAssistant === 'system' ? 'system' : selectedAssistant,
mode: selectedAssistant === 'system' ? 'system' : 'custom',
// Pass custom config if needed, but backend should fetch from user profile for security
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to send message');
}
// Handle streaming response
const reader = response.body?.getReader();
const decoder = new TextDecoder();
const assistantMessage: Message = {
role: 'assistant',
content: '',
timestamp: Date.now()
};
setMessages(prev => [...prev, assistantMessage]);
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
assistantMessage.content += chunk;
setMessages(prev => {
const newMessages = [...prev];
newMessages[newMessages.length - 1] = { ...assistantMessage };
return newMessages;
});
}
}
} catch (error: any) {
toast.error(error.message || '发送失败,请重试');
// Remove the failed user message or show error state?
// For now just show toast
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const clearHistory = () => {
setMessages([]);
toast.success('对话历史已清空');
};
// Get available assistants
const assistants = [
{ value: 'system', label: '系统默认助手 (System)' },
...(user?.AI配置?.?.map((a: any) => ({
value: a.名称,
label: `${a.} (Custom)`
})) || [])
];
return (
<AIToolsLayout title="AI 智能对话" description="多模型智能问答助手,支持自定义模型配置">
<div className="h-[calc(100vh-140px)] flex flex-col bg-card rounded-2xl border border-border overflow-hidden shadow-sm">
{/* Header */}
<div className="p-4 border-b border-border flex items-center justify-between bg-muted/50/50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center text-green-600">
<Bot className="w-6 h-6" />
</div>
<div>
<h2 className="font-bold text-foreground">AI </h2>
<p className="text-xs text-muted-foreground">
: {selectedAssistant === 'system' ? '系统托管 (消耗积分)' : '自定义 (免费)'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Select value={selectedAssistant} onValueChange={setSelectedAssistant}>
<SelectTrigger className="w-[200px] bg-background">
<SelectValue placeholder="选择助手" />
</SelectTrigger>
<SelectContent>
{assistants.map(a => (
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="ghost" size="icon" onClick={clearHistory} title="清空对话">
<Eraser className="w-4 h-4 text-muted-foreground" />
</Button>
</div>
</div>
{/* Chat Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-6 bg-muted/50/30">
{messages.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground space-y-4">
<Bot className="w-16 h-16 opacity-20" />
<p>...</p>
<div className="grid grid-cols-2 gap-2 max-w-md w-full">
{['帮我写一篇关于AI的文章', '解释一下量子纠缠', '如何优化React性能', '翻译这段代码'].map((q, i) => (
<Button
key={i}
variant="outline"
className="text-xs justify-start h-auto py-3 px-4 bg-background hover:bg-green-50 hover:text-green-600 hover:border-green-200"
onClick={() => setInput(q)}
>
{q}
</Button>
))}
</div>
</div>
) : (
messages.map((msg, index) => (
<div
key={index}
className={cn(
"flex gap-3 max-w-3xl",
msg.role === 'user' ? "ml-auto flex-row-reverse" : "mr-auto"
)}
>
<Avatar className={cn("w-8 h-8", msg.role === 'user' ? "bg-primary" : "bg-green-600")}>
{msg.role === 'user' ? (
<AvatarImage src={user?.} />
) : (
<div className="w-full h-full flex items-center justify-center bg-green-100 text-green-600">
<Bot className="w-5 h-5" />
</div>
)}
<AvatarFallback>{msg.role === 'user' ? 'U' : 'AI'}</AvatarFallback>
</Avatar>
<div className={cn(
"rounded-2xl px-4 py-3 text-sm shadow-sm",
msg.role === 'user'
? "bg-primary text-white rounded-tr-none"
: "bg-card border border-border text-foreground rounded-tl-none"
)}>
{msg.role === 'assistant' ? (
<div className="prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown>{msg.content}</ReactMarkdown>
</div>
) : (
<div className="whitespace-pre-wrap">{msg.content}</div>
)}
</div>
</div>
))
)}
{isLoading && (
<div className="flex gap-3 mr-auto max-w-3xl">
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center text-green-600">
<Bot className="w-5 h-5" />
</div>
<div className="bg-card border border-border rounded-2xl rounded-tl-none px-4 py-3 flex items-center">
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
<span className="text-xs text-muted-foreground ml-2">...</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="p-4 bg-card border-t border-border">
<div className="relative max-w-4xl mx-auto">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入您的问题... (Shift+Enter 换行)"
className="pr-12 py-6 rounded-xl bg-muted/50 border-border focus:bg-background transition-colors"
disabled={isLoading}
/>
<Button
size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 rounded-lg"
onClick={handleSend}
disabled={!input.trim() || isLoading}
>
<Send className="w-4 h-4" />
</Button>
</div>
<div className="text-center mt-2">
<p className="text-[10px] text-muted-foreground">
AI
</p>
</div>
</div>
</div>
</AIToolsLayout>
);
}