- {/* Sidebar */} - ({ id, title }))} - activeConversationId={activeConvId} - onNewChat={handleNewChat} - onSelectConversation={handleSelectConversation} - onRenameConversation={handleRenameConversation} - onDeleteConversation={handleDeleteConversation} - onOpenExtensions={handleOpenExtensions} - connectedExtensionsCount={extensions.filter((e) => e.connected).length} - /> - - {/* Main content area */} -
- {/* Top bar */} - setSidebarOpen((v) => !v)} - /> - - {/* Messages or welcome */} -
- {messages.length === 0 && !isLoading ? ( - <> - - {ticketSystemConnected && ticketSummary && } - - ) : ( -
- {ticketSystemConnected && ticketSummary && } - {messages.map((msg) => { - if (msg.role === "assistant" && msg.content === "" && isLoading) { - return null; - } - return ( - setActiveWorkspaceId(msg.id) : undefined} - /> - ); - })} - {isLoading && messages.length > 0 && messages[messages.length - 1].role !== "assistant" && ( - - )} -
-
- )} -
- - {/* Input */} - -
- - {/* Right: Agent Workspace */} -
- -
- - {/* Extensions Panel */} - setExtensionsPanelOpen(false)} - extensions={extensions} - onUpdateExtension={handleUpdateExtension} - /> -
- ); -} diff --git a/frontend/components/gemini/GeminiInput.tsx b/frontend/components/gemini/GeminiInput.tsx deleted file mode 100644 index 2d0d9bf..0000000 --- a/frontend/components/gemini/GeminiInput.tsx +++ /dev/null @@ -1,362 +0,0 @@ -"use client"; - -import { useRef, useEffect, KeyboardEvent, useState } from "react"; -import { - Plus, - Mic, - ArrowUp, - Box, - Search, - Database, - FileText, - Code2, - X, - Loader2, - Paperclip, - RotateCcw, -} from "lucide-react"; -import { cn } from "@/lib/utils"; -import { uploadAttachment, type AttachmentData } from "@/lib/api"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -interface Tool { - id: string; - icon: React.ElementType; - 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: (attachments?: AttachmentData[]) => void; - isLoading?: boolean; - activeTools: Set; - onActiveToolsChange: (tools: Set) => void; - selectedModel: "flash" | "auto" | "pro"; - onSelectedModelChange: (model: "flash" | "auto" | "pro") => void; -} - -const TOOLS: Tool[] = [ - { id: "search", icon: Search, label: "搜索" }, - { id: "knowledge", icon: Database, label: "内部知识库" }, - { id: "sandbox", icon: Box, label: "沙盒" }, - { id: "document", icon: FileText, label: "文档生成" }, -]; - -export function GeminiInput({ - value, - onChange, - onSubmit, - isLoading, - activeTools, - onActiveToolsChange, - selectedModel, - onSelectedModelChange, -}: GeminiInputProps) { - const textareaRef = useRef(null); - const fileInputRef = useRef(null); - const [showTools, setShowTools] = useState(false); - const [uploadingFiles, setUploadingFiles] = useState([]); - - // Auto-resize textarea - useEffect(() => { - const ta = textareaRef.current; - if (!ta) return; - ta.style.height = "auto"; - ta.style.height = Math.min(ta.scrollHeight, 200) + "px"; - }, [value]); - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - if (canSend) handleSubmit(); - } - }; - - const toggleTool = (id: string) => { - const next = new Set(activeTools); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - onActiveToolsChange(next); - }; - - 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) 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 ( -
-
- {/* 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 */} - - - - {/* Tools toggle */} - - - {/* Textarea */} -