About
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +25,7 @@ function HomePage() {
|
|||||||
const [view, setView] = useState(null);
|
const [view, setView] = useState(null);
|
||||||
const [fileContent, setFileContent] = useState("");
|
const [fileContent, setFileContent] = useState("");
|
||||||
const [uploadingDefault, setUploadingDefault] = useState(false);
|
const [uploadingDefault, setUploadingDefault] = useState(false);
|
||||||
|
const autoUploadedRef = useRef(false);
|
||||||
|
|
||||||
const token = useSelector((state) => state.userDetails.token);
|
const token = useSelector((state) => state.userDetails.token);
|
||||||
const userId = useSelector((state) => state.userDetails.userId);
|
const userId = useSelector((state) => state.userDetails.userId);
|
||||||
@@ -49,6 +50,22 @@ function HomePage() {
|
|||||||
const showUploadDefault = isGuest && !hasDefaultFile;
|
const showUploadDefault = isGuest && !hasDefaultFile;
|
||||||
const showViewExamples = isGuest && hasDefaultFile;
|
const showViewExamples = isGuest && hasDefaultFile;
|
||||||
|
|
||||||
|
// Auto-upload default context for guest with no files
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isGuest || loading || hasDefaultFile || autoUploadedRef.current) return;
|
||||||
|
autoUploadedRef.current = true;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(DEFAULT_CONTEXT_FILE);
|
||||||
|
const text = await res.text();
|
||||||
|
const file = new File([text], "guest-example.txt", { type: "text/plain" });
|
||||||
|
dispatch(uploadFilesWithProgress([file]));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Auto-upload failed:", err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [isGuest, loading, hasDefaultFile, dispatch]);
|
||||||
|
|
||||||
const handleGuestLogin = () => {
|
const handleGuestLogin = () => {
|
||||||
dispatch(fetchLoginUser({ email: GUEST_EMAIL, password: GUEST_PASSWORD }));
|
dispatch(fetchLoginUser({ email: GUEST_EMAIL, password: GUEST_PASSWORD }));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,426 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import fetchGetChatList from "../features/fetch-async/fetchGetChatList";
|
|
||||||
import {
|
|
||||||
switchSearchMode,
|
|
||||||
setTopK,
|
|
||||||
setTopP,
|
|
||||||
} from "../features/slices/rag-slice";
|
|
||||||
import fetchCreateNewChat from "../features/fetch-async/fetchCreateNewChat";
|
|
||||||
import {
|
|
||||||
TOP_K_MIN_VALUE,
|
|
||||||
TOP_K_DEFAULT_VALUE,
|
|
||||||
TOP_K_MAX_VALUE,
|
|
||||||
TOP_P_FAST_VALUE,
|
|
||||||
TOP_P_DEFAULT_VALUE,
|
|
||||||
TOP_P_SLOW_VALUE,
|
|
||||||
} from "../features/constants";
|
|
||||||
|
|
||||||
// Helper function to format date
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
const now = new Date();
|
|
||||||
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
if (diffDays === 0) return "Today";
|
|
||||||
if (diffDays === 1) return "Yesterday";
|
|
||||||
|
|
||||||
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const RagPage = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
||||||
const [isCreatingChat, setIsCreatingChat] = useState(false);
|
|
||||||
const [newChatTitle, setNewChatTitle] = useState("");
|
|
||||||
const chatList = useSelector((state) => state.chats.chatList);
|
|
||||||
|
|
||||||
// Sort chats: newest first (by createdAt descending)
|
|
||||||
const sortedChats = [...chatList].sort(
|
|
||||||
(a, b) => new Date(b.createdAt) - new Date(a.createdAt),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (chatList.length === 0) {
|
|
||||||
dispatch(fetchGetChatList());
|
|
||||||
}
|
|
||||||
}, [dispatch, chatList.length]);
|
|
||||||
|
|
||||||
// User details selectors
|
|
||||||
const { userName, userEmail, loadedFiles, loading } = useSelector(
|
|
||||||
(state) => state.userDetails,
|
|
||||||
);
|
|
||||||
|
|
||||||
// RAG config selectors
|
|
||||||
const { isUseOnlyContextSearch, topK, topP } = useSelector(
|
|
||||||
(state) => state.ragConfig,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSearchModeChange = () => {
|
|
||||||
dispatch(switchSearchMode());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTopKChange = (value) => {
|
|
||||||
dispatch(setTopK(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTopPChange = (value) => {
|
|
||||||
dispatch(setTopP(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNewChatClick = () => {
|
|
||||||
setIsCreatingChat(true);
|
|
||||||
setNewChatTitle("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateChat = () => {
|
|
||||||
if (newChatTitle.trim()) {
|
|
||||||
dispatch(fetchCreateNewChat(newChatTitle.trim()));
|
|
||||||
setIsCreatingChat(false);
|
|
||||||
setNewChatTitle("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelCreate = () => {
|
|
||||||
setIsCreatingChat(false);
|
|
||||||
setNewChatTitle("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
|
||||||
if (e.key === "Enter" && newChatTitle.trim()) {
|
|
||||||
handleCreateChat();
|
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
handleCancelCreate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full bg-linear-to-br from-slate-950 via-slate-900 to-indigo-950 flex">
|
|
||||||
{/* Left Sidebar */}
|
|
||||||
<aside className="w-64 bg-slate-900/80 border-r border-slate-700/50 flex flex-col backdrop-blur-sm">
|
|
||||||
{/* New Chat Button / Input */}
|
|
||||||
<div className="p-3">
|
|
||||||
{isCreatingChat ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newChatTitle}
|
|
||||||
onChange={(e) => setNewChatTitle(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onBlur={handleCancelCreate}
|
|
||||||
placeholder="Title"
|
|
||||||
autoFocus
|
|
||||||
className="flex-1 px-3 py-2.5 rounded-lg bg-slate-800 border border-slate-600 text-white placeholder-slate-400 text-sm focus:outline-none focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
{newChatTitle.trim() && (
|
|
||||||
<button
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
onClick={handleCreateChat}
|
|
||||||
className="px-3 py-2.5 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium transition-colors"
|
|
||||||
>
|
|
||||||
Ok
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={handleNewChatClick}
|
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M12 4v16m8-8H4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
New chat
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chats Section */}
|
|
||||||
<div className="flex-1 overflow-y-auto px-3">
|
|
||||||
<div className="flex items-center gap-2 px-2 py-2 text-slate-400">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm font-medium">Chats</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chat List */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
{sortedChats.map((chat) => (
|
|
||||||
<button
|
|
||||||
key={chat.id}
|
|
||||||
className="w-full text-left px-3 py-2.5 rounded-lg text-slate-300 hover:bg-slate-800/60 hover:text-white transition-colors group"
|
|
||||||
>
|
|
||||||
<p className="text-sm truncate">{chat.title}</p>
|
|
||||||
<p className="text-xs text-slate-500 group-hover:text-slate-400">
|
|
||||||
{formatDate(chat.createdAt)}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings Dropdown at Bottom */}
|
|
||||||
<div className="p-3 pb-4 border-t border-slate-700/50 shrink-0 relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
|
||||||
className="w-full flex items-center justify-between px-3 py-2.5 rounded-lg text-slate-300 hover:bg-slate-800/60 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5 text-slate-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm font-medium">Settings</span>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
className={`w-4 h-4 text-slate-400 transition-transform ${isSettingsOpen ? "rotate-180" : ""}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5 15l7-7 7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Settings Dropdown Content */}
|
|
||||||
{isSettingsOpen && (
|
|
||||||
<div className="mt-2 bg-slate-800/80 rounded-lg border border-slate-700/50 p-3 space-y-3">
|
|
||||||
{/* Search Mode */}
|
|
||||||
<div>
|
|
||||||
<h4 className="text-xs font-semibold text-slate-400 mb-2 uppercase tracking-wider">
|
|
||||||
Search Mode
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="searchMode"
|
|
||||||
checked={isUseOnlyContextSearch}
|
|
||||||
onChange={handleSearchModeChange}
|
|
||||||
className="w-3 h-3 text-indigo-500 bg-slate-700 border-slate-600"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300">Only context</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="searchMode"
|
|
||||||
checked={!isUseOnlyContextSearch}
|
|
||||||
onChange={handleSearchModeChange}
|
|
||||||
className="w-3 h-3 text-indigo-500 bg-slate-700 border-slate-600"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300">Allow external</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Top K */}
|
|
||||||
<div>
|
|
||||||
<h4 className="text-xs font-semibold text-slate-400 mb-2 uppercase tracking-wider">
|
|
||||||
Top K
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="topK"
|
|
||||||
checked={topK === TOP_K_MIN_VALUE}
|
|
||||||
onChange={() => handleTopKChange(TOP_K_MIN_VALUE)}
|
|
||||||
className="w-3 h-3 text-indigo-500 bg-slate-700 border-slate-600"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300">Precise</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="topK"
|
|
||||||
checked={topK === TOP_K_DEFAULT_VALUE}
|
|
||||||
onChange={() => handleTopKChange(TOP_K_DEFAULT_VALUE)}
|
|
||||||
className="w-3 h-3 text-indigo-500 bg-slate-700 border-slate-600"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300">Default</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="topK"
|
|
||||||
checked={topK === TOP_K_MAX_VALUE}
|
|
||||||
onChange={() => handleTopKChange(TOP_K_MAX_VALUE)}
|
|
||||||
className="w-3 h-3 text-indigo-500 bg-slate-700 border-slate-600"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300">Extended</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Top P */}
|
|
||||||
<div>
|
|
||||||
<h4 className="text-xs font-semibold text-slate-400 mb-2 uppercase tracking-wider">
|
|
||||||
Top P
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="topP"
|
|
||||||
checked={topP === TOP_P_FAST_VALUE}
|
|
||||||
onChange={() => handleTopPChange(TOP_P_FAST_VALUE)}
|
|
||||||
className="w-3 h-3 text-indigo-500 bg-slate-700 border-slate-600"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300">Fast</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="topP"
|
|
||||||
checked={topP === TOP_P_DEFAULT_VALUE}
|
|
||||||
onChange={() => handleTopPChange(TOP_P_DEFAULT_VALUE)}
|
|
||||||
className="w-3 h-3 text-indigo-500 bg-slate-700 border-slate-600"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300">Default</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="topP"
|
|
||||||
checked={topP === TOP_P_SLOW_VALUE}
|
|
||||||
onChange={() => handleTopPChange(TOP_P_SLOW_VALUE)}
|
|
||||||
className="w-3 h-3 text-indigo-500 bg-slate-700 border-slate-600"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300">Large scan</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<main className="flex-1 p-6 overflow-y-auto">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold bg-linear-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
|
|
||||||
RAG Query
|
|
||||||
</h1>
|
|
||||||
<div className="h-1 w-24 bg-linear-to-r from-indigo-500 to-purple-500 rounded-full mt-2" />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* User Info Section */}
|
|
||||||
<div className="bg-slate-800/60 rounded-xl p-4 border border-slate-700/40 mb-6 backdrop-blur-sm">
|
|
||||||
<h2 className="text-sm font-semibold text-slate-200 mb-3 flex items-center gap-2">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-indigo-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
User Profile
|
|
||||||
</h2>
|
|
||||||
{loading ? (
|
|
||||||
<p className="text-slate-400 text-xs">Loading...</p>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
|
||||||
<div>
|
|
||||||
<span className="text-slate-500 block mb-0.5">Username</span>
|
|
||||||
<span className="text-slate-200 font-medium">
|
|
||||||
{userName || "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-slate-500 block mb-0.5">Email</span>
|
|
||||||
<span className="text-slate-200 truncate block font-medium">
|
|
||||||
{userEmail || "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Warning if no files */}
|
|
||||||
{(!loadedFiles || loadedFiles.length === 0) && (
|
|
||||||
<div className="bg-amber-500/10 border border-amber-500/30 rounded-xl p-6 text-center">
|
|
||||||
<svg
|
|
||||||
className="w-12 h-12 text-amber-400 mx-auto mb-3"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<p className="text-amber-200 font-medium">
|
|
||||||
Unable to make RAG query
|
|
||||||
</p>
|
|
||||||
<p className="text-amber-300/70 text-sm mt-1">
|
|
||||||
Please upload at least one context file first
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Chat area placeholder */}
|
|
||||||
{loadedFiles && loadedFiles.length > 0 && (
|
|
||||||
<div className="bg-slate-800/40 rounded-xl p-8 border border-slate-700/40 text-center">
|
|
||||||
<p className="text-slate-400">Start a conversation...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RagPage;
|
|
||||||
@@ -1,683 +0,0 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
uploadFilesWithProgress,
|
|
||||||
resetUploadState,
|
|
||||||
setAreFilesValid,
|
|
||||||
setQuotaError,
|
|
||||||
clearQuotaError,
|
|
||||||
cancelUpload,
|
|
||||||
} from "../features/uploadFilesSlice";
|
|
||||||
import {
|
|
||||||
MAX_FILE_SIZE_KB,
|
|
||||||
MAX_FILES_TO_UPLOAD,
|
|
||||||
PROGRESS_STATUS_PROCESSING,
|
|
||||||
PROGRESS_STATUS_COMPLETED,
|
|
||||||
PROGRESS_STATUS_ERROR,
|
|
||||||
PROGRESS_STATUS_SKIPPED,
|
|
||||||
} from "../features/constants";
|
|
||||||
|
|
||||||
export default function UploadPage() {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
uploading,
|
|
||||||
error,
|
|
||||||
success,
|
|
||||||
areFilesValid,
|
|
||||||
quotaError,
|
|
||||||
progress,
|
|
||||||
uploadedFiles,
|
|
||||||
} = useSelector((state) => state.uploadFiles);
|
|
||||||
|
|
||||||
const loadedFiles = useSelector((state) => state.userDetails.loadedFiles);
|
|
||||||
const maxFilesToLoad = useSelector(
|
|
||||||
(state) => state.userDetails.maxFilesToLoad
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadedCount = loadedFiles?.length || 0;
|
|
||||||
const remainingSlots = Math.max(0, maxFilesToLoad - loadedCount);
|
|
||||||
|
|
||||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
||||||
const fileInputRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(resetUploadState());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const handleUploadClick = () => {
|
|
||||||
if (remainingSlots === 0) {
|
|
||||||
dispatch(setQuotaError("You have reached the maximum number of files"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = null;
|
|
||||||
fileInputRef.current.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilesChange = (event) => {
|
|
||||||
const files = Array.from(event.target.files || []);
|
|
||||||
dispatch(clearQuotaError());
|
|
||||||
|
|
||||||
// Check max files per upload
|
|
||||||
if (files.length > MAX_FILES_TO_UPLOAD) {
|
|
||||||
dispatch(resetUploadState());
|
|
||||||
dispatch(setAreFilesValid(false));
|
|
||||||
dispatch(
|
|
||||||
setQuotaError(
|
|
||||||
`You can upload maximum ${MAX_FILES_TO_UPLOAD} files at once`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setSelectedFiles([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check remaining slots
|
|
||||||
if (files.length > remainingSlots) {
|
|
||||||
dispatch(resetUploadState());
|
|
||||||
dispatch(setAreFilesValid(false));
|
|
||||||
dispatch(
|
|
||||||
setQuotaError(
|
|
||||||
`You can only upload ${remainingSlots} more file(s). Already loaded: ${loadedCount}/${maxFilesToLoad}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setSelectedFiles([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check file size
|
|
||||||
const isSizeCorrect = files.every(
|
|
||||||
(file) => file.size <= MAX_FILE_SIZE_KB * 1024
|
|
||||||
);
|
|
||||||
if (!isSizeCorrect) {
|
|
||||||
dispatch(resetUploadState());
|
|
||||||
dispatch(setAreFilesValid(false));
|
|
||||||
dispatch(
|
|
||||||
setQuotaError(`Each file must be no more than ${MAX_FILE_SIZE_KB} KB`)
|
|
||||||
);
|
|
||||||
setSelectedFiles([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(setAreFilesValid(true));
|
|
||||||
setSelectedFiles(files);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSend = () => {
|
|
||||||
if (!selectedFiles.length || uploading) return;
|
|
||||||
dispatch(uploadFilesWithProgress(selectedFiles));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClear = () => {
|
|
||||||
setSelectedFiles([]);
|
|
||||||
dispatch(clearQuotaError());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
dispatch(cancelUpload());
|
|
||||||
setSelectedFiles([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTryAgain = () => {
|
|
||||||
dispatch(resetUploadState());
|
|
||||||
setSelectedFiles([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoHome = () => {
|
|
||||||
dispatch(resetUploadState());
|
|
||||||
navigate("/");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUploadAgain = () => {
|
|
||||||
dispatch(resetUploadState());
|
|
||||||
setSelectedFiles([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case PROGRESS_STATUS_PROCESSING:
|
|
||||||
return "⏳";
|
|
||||||
case PROGRESS_STATUS_COMPLETED:
|
|
||||||
return "✅";
|
|
||||||
case PROGRESS_STATUS_ERROR:
|
|
||||||
return "❌";
|
|
||||||
case PROGRESS_STATUS_SKIPPED:
|
|
||||||
return "⏭️";
|
|
||||||
default:
|
|
||||||
return "📄";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case PROGRESS_STATUS_PROCESSING:
|
|
||||||
return "Processing...";
|
|
||||||
case PROGRESS_STATUS_COMPLETED:
|
|
||||||
return "Completed";
|
|
||||||
case PROGRESS_STATUS_ERROR:
|
|
||||||
return "Error";
|
|
||||||
case PROGRESS_STATUS_SKIPPED:
|
|
||||||
return "Skipped (duplicate)";
|
|
||||||
default:
|
|
||||||
return "Waiting...";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Loading state with progress
|
|
||||||
if (uploading) {
|
|
||||||
return (
|
|
||||||
<div style={styles.container}>
|
|
||||||
<h1 style={styles.title}>
|
|
||||||
Upload text files for context searching in LLM
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div style={styles.progressContainer}>
|
|
||||||
<div style={styles.progressHeader}>
|
|
||||||
<span style={styles.progressIcon}>📤</span>
|
|
||||||
<span>Uploading files...</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={styles.progressBarContainer}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...styles.progressBarFill,
|
|
||||||
width: `${progress.percent}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={styles.progressStats}>
|
|
||||||
<div style={styles.statItem}>
|
|
||||||
<span style={styles.statValue}>{progress.percent}%</span>
|
|
||||||
<span style={styles.statLabel}>Complete</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{progress.currentFile && (
|
|
||||||
<div style={styles.currentFile}>
|
|
||||||
<span>{getStatusIcon(progress.status)}</span>
|
|
||||||
<span style={styles.currentFileName}>{progress.currentFile}</span>
|
|
||||||
<span style={styles.currentFileStatus}>
|
|
||||||
{getStatusText(progress.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={styles.cancelButtonContainer}>
|
|
||||||
<button style={styles.cancelButton} onClick={handleCancel}>
|
|
||||||
Cancel Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error state
|
|
||||||
if (error.status !== undefined && error.status !== null) {
|
|
||||||
return (
|
|
||||||
<div style={styles.container}>
|
|
||||||
<h1 style={styles.title}>
|
|
||||||
Upload text files for context searching in LLM
|
|
||||||
</h1>
|
|
||||||
<div style={styles.errorBox}>
|
|
||||||
<span style={styles.errorIcon}>⚠️</span>
|
|
||||||
<div>
|
|
||||||
<div style={styles.errorMessage}>{error.message}</div>
|
|
||||||
<div style={styles.errorStatus}>Status: {error.status}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={styles.buttonGroup}>
|
|
||||||
<button style={styles.button} onClick={handleTryAgain}>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
<button style={styles.buttonSecondary} onClick={handleGoHome}>
|
|
||||||
Home
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success state
|
|
||||||
if (success) {
|
|
||||||
const filesCount = uploadedFiles.length;
|
|
||||||
return (
|
|
||||||
<div style={styles.container}>
|
|
||||||
<h1 style={styles.title}>
|
|
||||||
Upload text files for context searching in LLM
|
|
||||||
</h1>
|
|
||||||
<div style={styles.successBox}>
|
|
||||||
<span style={styles.successIcon}>✅</span>
|
|
||||||
<div>
|
|
||||||
<div style={styles.successMessage}>Upload completed!</div>
|
|
||||||
<div style={styles.successStats}>
|
|
||||||
{filesCount} of {filesCount} files processed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={styles.buttonGroup}>
|
|
||||||
<button style={styles.button} onClick={handleUploadAgain}>
|
|
||||||
Upload more
|
|
||||||
</button>
|
|
||||||
<button style={styles.buttonSecondary} onClick={handleGoHome}>
|
|
||||||
Home
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default state - file selection
|
|
||||||
return (
|
|
||||||
<div style={styles.container}>
|
|
||||||
<h3 style={styles.title}>
|
|
||||||
Upload text files for context searching in LLM
|
|
||||||
</h3>
|
|
||||||
<p style={styles.subtitle}>
|
|
||||||
Each file must be no more than {MAX_FILE_SIZE_KB} KB. You can upload up
|
|
||||||
to {MAX_FILES_TO_UPLOAD} files at once.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* File quota info */}
|
|
||||||
<div style={styles.quotaContainer}>
|
|
||||||
<div style={styles.quotaRow}>
|
|
||||||
<span style={styles.quotaLabel}>Files loaded:</span>
|
|
||||||
<span style={styles.quotaValue}>
|
|
||||||
{loadedCount} / {maxFilesToLoad}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style={styles.quotaRow}>
|
|
||||||
<span style={styles.quotaLabel}>Remaining slots:</span>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
...styles.quotaValue,
|
|
||||||
color: remainingSlots > 0 ? "#38a169" : "#e53e3e",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{remainingSlots}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style={styles.quotaProgressContainer}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...styles.quotaProgressFill,
|
|
||||||
width: `${
|
|
||||||
maxFilesToLoad > 0 ? (loadedCount / maxFilesToLoad) * 100 : 0
|
|
||||||
}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!areFilesValid && !quotaError && (
|
|
||||||
<div style={styles.warningBox}>
|
|
||||||
<span>⚠️</span>
|
|
||||||
<span>File requirements not met. Please adjust your selection.</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{quotaError && (
|
|
||||||
<div style={styles.warningBox}>
|
|
||||||
<span>⚠️</span>
|
|
||||||
<span>{quotaError}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{remainingSlots === 0 && !quotaError && (
|
|
||||||
<div style={styles.errorBox}>
|
|
||||||
<span style={styles.errorIcon}>🚫</span>
|
|
||||||
<div>
|
|
||||||
<div style={styles.errorMessage}>
|
|
||||||
You have reached the maximum number of files
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...styles.uploadButton,
|
|
||||||
...(remainingSlots === 0 && styles.buttonDisabled),
|
|
||||||
}}
|
|
||||||
onClick={handleUploadClick}
|
|
||||||
disabled={remainingSlots === 0}
|
|
||||||
>
|
|
||||||
<span style={styles.uploadIcon}>📁</span>
|
|
||||||
Select files
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept=".txt"
|
|
||||||
multiple
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={handleFilesChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedFiles.length > 0 && (
|
|
||||||
<div style={styles.selectedFilesContainer}>
|
|
||||||
<h4 style={styles.selectedFilesTitle}>
|
|
||||||
Selected files ({selectedFiles.length}):
|
|
||||||
</h4>
|
|
||||||
<ul style={styles.fileList}>
|
|
||||||
{selectedFiles.map((file, index) => (
|
|
||||||
<li key={index} style={styles.fileItem}>
|
|
||||||
<span style={styles.fileIcon}>📄</span>
|
|
||||||
<span style={styles.fileName}>{file.name}</span>
|
|
||||||
<span style={styles.fileSize}>
|
|
||||||
{(file.size / 1024).toFixed(1)} KB
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={styles.buttonGroup}>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...styles.button,
|
|
||||||
...((!selectedFiles.length || uploading) && styles.buttonDisabled),
|
|
||||||
}}
|
|
||||||
onClick={handleSend}
|
|
||||||
disabled={!selectedFiles.length || uploading}
|
|
||||||
>
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...styles.buttonSecondary,
|
|
||||||
...((!selectedFiles.length || uploading) && styles.buttonDisabled),
|
|
||||||
}}
|
|
||||||
onClick={handleClear}
|
|
||||||
disabled={!selectedFiles.length || uploading}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
container: {
|
|
||||||
padding: "2rem",
|
|
||||||
maxWidth: "600px",
|
|
||||||
margin: "0 auto",
|
|
||||||
fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#1a1a2e",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
color: "#666",
|
|
||||||
marginBottom: "1.5rem",
|
|
||||||
lineHeight: 1.5,
|
|
||||||
},
|
|
||||||
quotaContainer: {
|
|
||||||
backgroundColor: "#f8f9fa",
|
|
||||||
borderRadius: "8px",
|
|
||||||
padding: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
border: "1px solid #e0e0e0",
|
|
||||||
},
|
|
||||||
quotaRow: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
},
|
|
||||||
quotaLabel: {
|
|
||||||
color: "#666",
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
},
|
|
||||||
quotaValue: {
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "#1a1a2e",
|
|
||||||
},
|
|
||||||
quotaProgressContainer: {
|
|
||||||
width: "100%",
|
|
||||||
height: "6px",
|
|
||||||
backgroundColor: "#e0e0e0",
|
|
||||||
borderRadius: "3px",
|
|
||||||
overflow: "hidden",
|
|
||||||
marginTop: "0.5rem",
|
|
||||||
},
|
|
||||||
quotaProgressFill: {
|
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "#667eea",
|
|
||||||
borderRadius: "3px",
|
|
||||||
transition: "width 0.3s ease",
|
|
||||||
},
|
|
||||||
progressContainer: {
|
|
||||||
backgroundColor: "#f8f9fa",
|
|
||||||
borderRadius: "12px",
|
|
||||||
padding: "1.5rem",
|
|
||||||
marginTop: "1.5rem",
|
|
||||||
},
|
|
||||||
progressHeader: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
fontSize: "1.1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
progressIcon: {
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
},
|
|
||||||
progressBarContainer: {
|
|
||||||
width: "100%",
|
|
||||||
height: "8px",
|
|
||||||
backgroundColor: "#e0e0e0",
|
|
||||||
borderRadius: "4px",
|
|
||||||
overflow: "hidden",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
progressBarFill: {
|
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "#4caf50",
|
|
||||||
borderRadius: "4px",
|
|
||||||
transition: "width 0.3s ease",
|
|
||||||
},
|
|
||||||
progressStats: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
statItem: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
statValue: {
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#1a1a2e",
|
|
||||||
},
|
|
||||||
statLabel: {
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
color: "#666",
|
|
||||||
},
|
|
||||||
currentFile: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.75rem",
|
|
||||||
padding: "0.75rem",
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: "1px solid #e0e0e0",
|
|
||||||
},
|
|
||||||
currentFileName: {
|
|
||||||
flex: 1,
|
|
||||||
fontWeight: 500,
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
},
|
|
||||||
currentFileStatus: {
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
color: "#666",
|
|
||||||
},
|
|
||||||
cancelButtonContainer: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
marginTop: "1.5rem",
|
|
||||||
},
|
|
||||||
cancelButton: {
|
|
||||||
padding: "0.75rem 2rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "#fff",
|
|
||||||
backgroundColor: "#e53e3e",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "6px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "background-color 0.2s",
|
|
||||||
},
|
|
||||||
errorBox: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "1rem",
|
|
||||||
padding: "1rem",
|
|
||||||
backgroundColor: "#fff5f5",
|
|
||||||
border: "1px solid #feb2b2",
|
|
||||||
borderRadius: "8px",
|
|
||||||
marginTop: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
errorIcon: {
|
|
||||||
fontSize: "2rem",
|
|
||||||
},
|
|
||||||
errorMessage: {
|
|
||||||
color: "#c53030",
|
|
||||||
fontWeight: 500,
|
|
||||||
},
|
|
||||||
errorStatus: {
|
|
||||||
color: "#666",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
},
|
|
||||||
successBox: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "1rem",
|
|
||||||
padding: "1rem",
|
|
||||||
backgroundColor: "#f0fff4",
|
|
||||||
border: "1px solid #9ae6b4",
|
|
||||||
borderRadius: "8px",
|
|
||||||
marginTop: "1rem",
|
|
||||||
},
|
|
||||||
successIcon: {
|
|
||||||
fontSize: "2rem",
|
|
||||||
},
|
|
||||||
successMessage: {
|
|
||||||
color: "#276749",
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: "1.1rem",
|
|
||||||
},
|
|
||||||
successStats: {
|
|
||||||
color: "#666",
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
},
|
|
||||||
warningBox: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
padding: "0.75rem 1rem",
|
|
||||||
backgroundColor: "#fffaf0",
|
|
||||||
border: "1px solid #fbd38d",
|
|
||||||
borderRadius: "8px",
|
|
||||||
color: "#c05621",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
uploadButton: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
width: "100%",
|
|
||||||
padding: "1rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
backgroundColor: "#f8f9fa",
|
|
||||||
border: "2px dashed #ccc",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
},
|
|
||||||
uploadIcon: {
|
|
||||||
fontSize: "1.25rem",
|
|
||||||
},
|
|
||||||
selectedFilesContainer: {
|
|
||||||
marginTop: "1.5rem",
|
|
||||||
padding: "1rem",
|
|
||||||
backgroundColor: "#f8f9fa",
|
|
||||||
borderRadius: "8px",
|
|
||||||
},
|
|
||||||
selectedFilesTitle: {
|
|
||||||
margin: "0 0 0.75rem 0",
|
|
||||||
fontSize: "1rem",
|
|
||||||
color: "#333",
|
|
||||||
},
|
|
||||||
fileList: {
|
|
||||||
listStyle: "none",
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
fileItem: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
padding: "0.5rem 0",
|
|
||||||
borderBottom: "1px solid #e0e0e0",
|
|
||||||
},
|
|
||||||
fileIcon: {
|
|
||||||
fontSize: "1rem",
|
|
||||||
},
|
|
||||||
fileName: {
|
|
||||||
flex: 1,
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
},
|
|
||||||
fileSize: {
|
|
||||||
color: "#666",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
},
|
|
||||||
buttonGroup: {
|
|
||||||
display: "flex",
|
|
||||||
gap: "0.75rem",
|
|
||||||
marginTop: "1.5rem",
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
padding: "0.75rem 1.5rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "#fff",
|
|
||||||
backgroundColor: "#4a5568",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "6px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "background-color 0.2s",
|
|
||||||
},
|
|
||||||
buttonSecondary: {
|
|
||||||
padding: "0.75rem 1.5rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "#4a5568",
|
|
||||||
backgroundColor: "#e2e8f0",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "6px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "background-color 0.2s",
|
|
||||||
},
|
|
||||||
buttonDisabled: {
|
|
||||||
opacity: 0.5,
|
|
||||||
cursor: "not-allowed",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user