remove minio
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check } from "lucide-react";
|
||||
import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check, X, File as FileIcon } from "lucide-react";
|
||||
import { api } from "@/lib/api";
|
||||
import { useVisualizationStore } from "@/store/visualizationStore";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -56,6 +56,11 @@ export function ChatInterface() {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const activeSessionKey = queryParams.get("session") || "api:default";
|
||||
|
||||
// File upload state
|
||||
const [attachedFile, setAttachedFile] = useState<{ filename: string; url: string; columns?: string[]; summary?: string } | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels();
|
||||
}, []);
|
||||
@@ -110,6 +115,45 @@ export function ChatInterface() {
|
||||
{ icon: Search, label: "深度问数", color: "text-blue-500", bg: "bg-blue-50" },
|
||||
];
|
||||
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/v1/upload/file", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
...(localStorage.getItem("token") ? { Authorization: `Bearer ${localStorage.getItem("token")}` } : {}),
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Upload failed");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setAttachedFile({
|
||||
filename: file.name,
|
||||
url: data.url,
|
||||
columns: data.columns,
|
||||
summary: data.summary,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("File upload error:", error);
|
||||
// Could show a toast notification here
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
@@ -122,6 +166,13 @@ export function ChatInterface() {
|
||||
const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input };
|
||||
setMessages(prev => [...prev, newMessage]);
|
||||
setInput("");
|
||||
|
||||
let messagePayload = newMessage.content;
|
||||
if (attachedFile) {
|
||||
messagePayload = `[用户上传了文件: ${attachedFile.filename}]\n[文件内容摘要: ${attachedFile.summary || "无"}]\n[数据列: ${attachedFile.columns?.join(", ") || "无"}]\n[文件下载链接: ${attachedFile.url}]\n\n${newMessage.content}`;
|
||||
setAttachedFile(null);
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setVizLoading(true);
|
||||
setVizError(null);
|
||||
@@ -145,7 +196,7 @@ export function ChatInterface() {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: newMessage.content,
|
||||
message: messagePayload,
|
||||
session_id: activeSessionKey,
|
||||
model_id: effectiveModelId,
|
||||
}),
|
||||
@@ -203,7 +254,7 @@ export function ChatInterface() {
|
||||
|
||||
if (!streamedText) {
|
||||
const fallback = await api.post<{ response: string }>("/nanobot/chat", {
|
||||
message: newMessage.content,
|
||||
message: messagePayload,
|
||||
session_id: activeSessionKey,
|
||||
model_id: effectiveModelId,
|
||||
});
|
||||
@@ -217,7 +268,7 @@ export function ChatInterface() {
|
||||
// Fallback to existing NL2SQL or other skills (e.g. for "表格问答" or "深度问数")
|
||||
const source = selectedDataSource.split('-')[0]; // postgres-main -> postgres
|
||||
const response = await api.post<{sql?: string, result?: unknown, error?: string}>('/api/v1/agent/nl2sql', {
|
||||
query: newMessage.content,
|
||||
query: messagePayload,
|
||||
source: source,
|
||||
session_id: activeSessionKey,
|
||||
model_id: selectedModelId
|
||||
@@ -300,7 +351,14 @@ export function ChatInterface() {
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 h-[calc(100vh-100px)]">
|
||||
|
||||
{/* Hidden file input available in all states */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
accept=".csv,.xls,.xlsx"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<div className="min-h-full">
|
||||
{messages.length <= 1 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center pt-[20vh] px-4 pb-32">
|
||||
@@ -317,6 +375,19 @@ export function ChatInterface() {
|
||||
{/* Input Area */}
|
||||
<div className="w-full max-w-3xl relative">
|
||||
<div className="bg-white rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.06)] border border-zinc-100 p-4 transition-shadow hover:shadow-[0_8px_40px_rgb(0,0,0,0.08)]">
|
||||
{attachedFile && (
|
||||
<div className="mx-2 mb-3 p-2.5 bg-blue-50/50 border border-blue-100/50 rounded-xl flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5 text-sm text-blue-900">
|
||||
<div className="p-1.5 bg-blue-100 rounded-md">
|
||||
<FileIcon className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<span className="font-medium truncate max-w-[300px]">{attachedFile.filename}</span>
|
||||
</div>
|
||||
<button onClick={() => setAttachedFile(null)} className="p-1 text-blue-400 hover:text-blue-600 hover:bg-blue-100/50 rounded-md transition-colors">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
className="w-full min-h-[60px] max-h-[200px] resize-none border-none focus:ring-0 text-lg text-zinc-700 placeholder:text-zinc-300 bg-transparent p-2"
|
||||
placeholder="先思考后回答,解决更有难度的问题"
|
||||
@@ -349,8 +420,14 @@ export function ChatInterface() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9 text-zinc-400 hover:text-zinc-600 rounded-full">
|
||||
<Paperclip className="h-5 w-5" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-zinc-400 hover:text-zinc-600 rounded-full"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading ? <Loader2 className="h-5 w-5 animate-spin" /> : <Paperclip className="h-5 w-5" />}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
@@ -422,27 +499,49 @@ export function ChatInterface() {
|
||||
{messages.length > 1 && (
|
||||
<div className="absolute bottom-6 left-0 right-0 px-4">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-white rounded-2xl shadow-xl border border-zinc-200/60 p-2 flex items-center gap-2 ring-1 ring-zinc-100">
|
||||
<Input
|
||||
className="flex-1 border-none shadow-none focus-visible:ring-0 text-base text-zinc-700 placeholder:text-zinc-400 h-11 bg-transparent"
|
||||
placeholder="Send a message..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
size="icon"
|
||||
disabled={!input.trim() || isLoading}
|
||||
className={`h-9 w-9 rounded-lg transition-all ${
|
||||
input.trim()
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-md'
|
||||
: 'bg-zinc-100 text-zinc-300 hover:bg-zinc-100 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-5 w-5" />}
|
||||
</Button>
|
||||
<div className="bg-white rounded-2xl shadow-xl border border-zinc-200/60 p-2 flex flex-col gap-2 ring-1 ring-zinc-100">
|
||||
{attachedFile && (
|
||||
<div className="mx-2 mt-1 p-2 bg-blue-50/50 border border-blue-100/50 rounded-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-blue-900">
|
||||
<FileIcon className="h-3.5 w-3.5 text-blue-600" />
|
||||
<span className="font-medium truncate max-w-[200px]">{attachedFile.filename}</span>
|
||||
</div>
|
||||
<button onClick={() => setAttachedFile(null)} className="text-blue-400 hover:text-blue-600">
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-zinc-400 hover:text-zinc-600 rounded-full shrink-0"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading || isLoading}
|
||||
>
|
||||
{isUploading ? <Loader2 className="h-5 w-5 animate-spin" /> : <Paperclip className="h-5 w-5" />}
|
||||
</Button>
|
||||
<Input
|
||||
className="flex-1 border-none shadow-none focus-visible:ring-0 text-base text-zinc-700 placeholder:text-zinc-400 h-11 bg-transparent"
|
||||
placeholder="Send a message..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
size="icon"
|
||||
disabled={!input.trim() || isLoading}
|
||||
className={`h-9 w-9 rounded-lg shrink-0 transition-all ${
|
||||
input.trim()
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-md'
|
||||
: 'bg-zinc-100 text-zinc-300 hover:bg-zinc-100 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-5 w-5" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user