diff --git a/frontend/components/gemini/GeminiChat.tsx b/frontend/components/gemini/GeminiChat.tsx index 5a09156..668d5a1 100644 --- a/frontend/components/gemini/GeminiChat.tsx +++ b/frontend/components/gemini/GeminiChat.tsx @@ -16,6 +16,7 @@ import { fetchTicketSummary, streamChat, type TicketSummaryData, + type AttachmentData, } from "@/lib/api"; // ── Types ──────────────────────────────────────────────────────────────────── @@ -162,7 +163,7 @@ export function GeminiChat() { [] ); - const handleSend = useCallback(async () => { + const handleSend = useCallback(async (attachments?: AttachmentData[]) => { const text = inputValue.trim(); if (!text || isLoading) return; setInputValue(""); @@ -173,6 +174,7 @@ export function GeminiChat() { role: "user", content: text, timestamp: new Date(), + ...(attachments && attachments.length > 0 ? { attachments } : {}), }; let convId = activeConvId; diff --git a/frontend/components/gemini/GeminiInput.tsx b/frontend/components/gemini/GeminiInput.tsx index 96fbb5b..5f74849 100644 --- a/frontend/components/gemini/GeminiInput.tsx +++ b/frontend/components/gemini/GeminiInput.tsx @@ -10,8 +10,13 @@ import { Database, FileText, Code2, + X, + Loader2, + Paperclip, + RotateCcw, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { uploadAttachment, type AttachmentData } from "@/lib/api"; interface Tool { id: string; @@ -19,10 +24,18 @@ interface Tool { label: string; } +interface UploadingFile { + id: string; + file: File; + status: "uploading" | "done" | "error"; + attachment?: AttachmentData; + error?: string; +} + interface GeminiInputProps { value: string; onChange: (val: string) => void; - onSubmit: () => void; + onSubmit: (attachments?: AttachmentData[]) => void; isLoading?: boolean; activeTools: Set; onActiveToolsChange: (tools: Set) => void; @@ -50,6 +63,7 @@ export function GeminiInput({ const textareaRef = useRef(null); const fileInputRef = useRef(null); const [showTools, setShowTools] = useState(false); + const [uploadingFiles, setUploadingFiles] = useState([]); // Auto-resize textarea useEffect(() => { @@ -62,7 +76,7 @@ export function GeminiInput({ const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); - if (value.trim() && !isLoading) onSubmit(); + if (canSend) handleSubmit(); } }; @@ -76,14 +90,72 @@ export function GeminiInput({ onActiveToolsChange(next); }; - const canSend = value.trim().length > 0 && !isLoading; + const hasUploading = uploadingFiles.some((f) => f.status === "uploading"); + const canSend = value.trim().length > 0 && !isLoading && !hasUploading; + + const handleSubmit = () => { + const attachments = uploadingFiles + .filter((f) => f.status === "done" && f.attachment) + .map((f) => f.attachment!); + onSubmit(attachments.length > 0 ? attachments : undefined); + setUploadingFiles([]); + }; + + const doUpload = async (entry: UploadingFile) => { + try { + const attachment = await uploadAttachment(entry.file); + setUploadingFiles((prev) => + prev.map((f) => + f.id === entry.id ? { ...f, status: "done", attachment } : f + ) + ); + } catch (err) { + setUploadingFiles((prev) => + prev.map((f) => + f.id === entry.id + ? { ...f, status: "error", error: err instanceof Error ? err.message : "Upload failed" } + : f + ) + ); + } + }; const handleFileUpload = (e: React.ChangeEvent) => { const files = e.target.files; - if (files && files.length > 0) { - // Handle file upload - console.log("上传文件:", files[0].name); - } + if (!files || files.length === 0) return; + + const newEntries: UploadingFile[] = Array.from(files).map((file) => ({ + id: `upload-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + file, + status: "uploading" as const, + })); + + setUploadingFiles((prev) => [...prev, ...newEntries]); + + // Start uploads + newEntries.forEach((entry) => doUpload(entry)); + + // Reset file input so the same file can be selected again + e.target.value = ""; + }; + + const handleRetryUpload = (entry: UploadingFile) => { + setUploadingFiles((prev) => + prev.map((f) => + f.id === entry.id ? { ...f, status: "uploading", error: undefined } : f + ) + ); + doUpload({ ...entry, status: "uploading" }); + }; + + const handleRemoveFile = (id: string) => { + setUploadingFiles((prev) => prev.filter((f) => f.id !== id)); + }; + + const formatSize = (bytes: number): string => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; return ( @@ -91,6 +163,58 @@ export function GeminiInput({
{/* Input container */}
+ {/* Uploaded files preview */} + {uploadingFiles.length > 0 && ( +
+ {uploadingFiles.map((entry) => ( +
+ {entry.status === "uploading" && ( + + )} + {entry.status === "done" && ( + + )} + {entry.status === "error" && ( + + )} + + {entry.file.name} + {entry.status === "done" && entry.attachment && ( + + {formatSize(entry.attachment.size_bytes)} + + )} + + {entry.status === "error" && ( + + )} + +
+ ))} +
+ )} + {/* Main input row */}
{/* Left: File upload button */} @@ -152,7 +276,7 @@ export function GeminiInput({