diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7b6f11d61..496be3389 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,7 @@ "react-dropzone": "^14.3.5", "react-helmet": "^6.1.0", "react-i18next": "^15.0.2", + "react-icons": "^5.3.0", "react-markdown": "^9.0.1", "react-redux": "^8.0.5", "react-router-dom": "^6.8.1", @@ -7885,6 +7886,14 @@ } } }, + "node_modules/react-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 868a72ae5..f45d99272 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "react-helmet": "^6.1.0", "react-dropzone": "^14.3.5", "react-i18next": "^15.0.2", + "react-icons": "^5.3.0", "react-markdown": "^9.0.1", "react-redux": "^8.0.5", "react-router-dom": "^6.8.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ba0a4bd7c..9dd9ceb93 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import './locale/i18n'; import { Outlet } from 'react-router-dom'; import { SharedConversation } from './conversation/SharedConversation'; import { useDarkTheme } from './hooks'; +import Onboarding from './components/Onboarding'; function MainLayout() { const { isMobile } = useMediaQuery(); @@ -33,20 +34,26 @@ function MainLayout() { export default function App() { const [, , componentMounted] = useDarkTheme(); + const [isOnboarding] = useState(false); + if (!componentMounted) { return
; } return (
- - }> - } /> - } /> - } /> - - } /> - } /> - + {isOnboarding ? ( + + ) : ( + + }> + } /> + } /> + } /> + + } /> + } /> + + )}
); } diff --git a/frontend/src/assets/Back.svg b/frontend/src/assets/Back.svg new file mode 100644 index 000000000..779962abd --- /dev/null +++ b/frontend/src/assets/Back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/Logo.svg b/frontend/src/assets/Logo.svg new file mode 100644 index 000000000..4d6fb7bf7 --- /dev/null +++ b/frontend/src/assets/Logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/bg.svg b/frontend/src/assets/bg.svg new file mode 100644 index 000000000..6d530e156 --- /dev/null +++ b/frontend/src/assets/bg.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/assets/globe.svg b/frontend/src/assets/globe.svg new file mode 100644 index 000000000..28c54e667 --- /dev/null +++ b/frontend/src/assets/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/profie.png b/frontend/src/assets/profie.png new file mode 100644 index 000000000..20809b182 Binary files /dev/null and b/frontend/src/assets/profie.png differ diff --git a/frontend/src/components/CollectFromWebsiteForm.tsx b/frontend/src/components/CollectFromWebsiteForm.tsx new file mode 100644 index 000000000..c7a1427c4 --- /dev/null +++ b/frontend/src/components/CollectFromWebsiteForm.tsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import { FiLoader } from 'react-icons/fi'; + +const CollectFromWebsiteForm: React.FC = () => { + const [isFetching, setIsFetching] = useState(false); + const [formData, setFormData] = useState({ + name: '', + link: '', + }); + const [selectedOption, setSelectedOption] = useState('From URL'); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value, + }); + }; + + const handleLinkPaste = (e: React.ChangeEvent) => { + const { value } = e.target; + setFormData({ + ...formData, + link: value, + }); + + // Simulate fetching animation for link paste + setIsFetching(true); + setTimeout(() => { + setIsFetching(false); + // Handle link fetching logic here (API call, etc.) + }, 2000); + }; + + const handleSelectChange = (e: React.ChangeEvent) => { + setSelectedOption(e.target.value); + }; + + return ( +
+

+ Collect from a website +

+ +
+ {/* Custom styled dropdown */} +
+ + {/* Custom dropdown arrow */} +
+ + + +
+
+ + {/* Name input */} +
+ + +
+ + {/* Link input */} +
+ + +
+ + {/* Fetching status */} + {/* Fetching status */} +
+
+ {isFetching ? ( + <> + + Fetching + +
+ + ) : ( + <> + Fetching + + + )} +
+
+
+
+ ); +}; + +export default CollectFromWebsiteForm; diff --git a/frontend/src/components/Onboarding.tsx b/frontend/src/components/Onboarding.tsx new file mode 100644 index 000000000..e2de287b7 --- /dev/null +++ b/frontend/src/components/Onboarding.tsx @@ -0,0 +1,392 @@ +'use client'; + +import { useState } from 'react'; +import Globe from '../assets/globe.svg'; +import Profile from '../assets/profie.png'; +import Logo from '../assets/Logo.svg'; +import L2C1 from '../assets/file_upload.svg'; +import L2C2 from '../assets/website_collect.svg'; +import Back from '../assets/Back.svg'; +import UploadFromDeviceForm from './UploadFromDeviceForm'; +import CollectFromWebsiteForm from './CollectFromWebsiteForm'; +import { useDarkTheme } from '../hooks'; + +interface LevelIndicatorProps { + currentLevel: number; + indicatorLevel: number; +} + +const LevelIndicator: React.FC = ({ + currentLevel, + indicatorLevel, +}) => { + const isActive = currentLevel === indicatorLevel; + const isPast = currentLevel > indicatorLevel; + + return ( +
+
+
+ ); +}; + +export default function Onboarding() { + const [language] = useState('EN'); + const [level, setLevel] = useState(1); + const [selectedCard, setSelectedCard] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [progress, setProgress] = useState(0); + const [isFinalPage, setIsFinalPage] = useState(false); + const [isTrainingComplete, setIsTrainingComplete] = useState(false); + const [isDarkTheme, toggleTheme] = useDarkTheme(); + + const handleLevelUp = () => { + if (level < 3) { + setLevel((prevLevel) => prevLevel + 1); + } + }; + + const handleLevelDown = () => { + if (level > 1) { + setLevel((prevLevel) => prevLevel - 1); + } + setSelectedCard(null); + }; + + const handleCardSelect = (card: number) => { + setSelectedCard(card); + }; + + const handleFinalButtonClick = () => { + setIsLoading(true); + setIsFinalPage(true); + + const updateProgress = (current: number) => { + if (current >= 100) { + setProgress(100); + setIsLoading(false); + setIsTrainingComplete(true); + return; + } + setProgress(current); + setTimeout(() => updateProgress(current + 10), 400); + }; + + updateProgress(0); + }; + + const getGradientForLevel = (level: number): string => { + switch (level) { + case 1: + return `relative bg-gradient-to-br from-green-200 via-white to-white dark:from-[#222327] dark:to-black`; // Light greenish tone + case 2: + return 'bg-gradient-to-br from-pink-300 via-white to-white dark:from-[#222327] dark:to-black'; // Light pink tone + case 3: + return `bg-gradient-to-br ${isTrainingComplete == true ? 'from-green-200 ' : 'from-orange-100 '} via-white to-white dark:from-[#222327] dark:to-black`; // Light orange tone + default: + return 'bg-white dark:bg-gray-900'; // Default light and dark backgrounds + } + }; + + const renderContentForLevel = (level: number) => { + if (isFinalPage) { + return ( +
+ Logo + {isTrainingComplete == true ? ( +
+

+ Training Complete ! +

+
+ ) : ( + <> + {' '} + {/* Heading */} +

+ Training is in progress... +

+ {/* Subheading */} +

+ This may take several minutes +

+ + )} + +
+
+ {/* New Progress Bar */} +
+
+
+
+ + {progress}% + +
+
+ + {/* Keyframes for rotation animation */} + +
+
+ + {!isLoading && ( + + )} + {isLoading && ( + + )} +
+ ); + } + + switch (level) { + case 1: + return ( + <> +
+ Logo +

+ Welcome to DocsGPT +

+

+ Your technical documentation assistant. +

+
+ + ); + case 2: + return ( +
+ {/* Logo */} + Logo + + {/* Heading */} +

+ Upload from device or from web? +

+ + {/* Subheading */} +

+ You can choose how to add your first document to DocsGPT +

+ + {/* Card Container */} +
+ {/* Card 1 */} +
+
handleCardSelect(1)} + > +
+ L1C1 +
+
+

+ Upload from device +

+
+ + {/* Card 2 */} +
+
handleCardSelect(2)} + > +
+ L1C2 +
+
+

+ Collect from a website +

+
+
+
+ ); + case 3: + return ( +
+ Logo + + {/* Heading */} +

+ Upload new document +

+ {selectedCard === 1 ? ( + + ) : ( + + )} + {isLoading && ( +
+
+

{progress}%

+
+ )} +
+ ); + default: + return null; + } + }; + + return ( +
+
+ {/* alternate background */} + {/*
+
+
*/} + +
+ {/* Left Section */} +
+ {/* Left Section */} +
+ {language} + Globe +
+
+ + {/* Profile Section */} +
+
+ Profile +
+
+ + {/* Main content section */} +
+ {renderContentForLevel(level)} + + {!isFinalPage && ( + <> +
+
+ {[1, 2, 3].map((num) => ( + + ))} +
+
+ +
+ {level === 3 && !isLoading && ( + + )} + +
+ + )} +
+
+ ); +} diff --git a/frontend/src/components/UploadFromDeviceForm.tsx b/frontend/src/components/UploadFromDeviceForm.tsx new file mode 100644 index 000000000..da08328a1 --- /dev/null +++ b/frontend/src/components/UploadFromDeviceForm.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react'; + +interface UploadFromDeviceForm { + language: string; + level: number; + selectedCard: number | null; + isLoading: boolean; + progress: number; + isFinalPage: boolean; +} + +export default function UploadFromDeviceForm() { + const [uploadedFiles, setUploadedFiles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files ? e.target.files[0] : null; + if (file && file.size <= 25 * 1024 * 1024) { + setIsLoading(true); + await new Promise((resolve) => setTimeout(resolve, 2000)); + setUploadedFiles([...uploadedFiles, file.name]); + setIsLoading(false); + } else { + alert('File size exceeds 25MB or invalid file format'); + } + }; + + return ( +
+

+ Upload from device +

+
+ {/* Name Input with better styling */} +
+ + +
+ + {/* File Upload Button */} +
+ +
+ + {/* File type information */} +
+

+ Please upload .pdf, .txt, .rst, .docx, .md, .zip limited to 25mb +

+
+
+ + {/* Uploaded Files Section */} +
+
+

+ Uploaded Files +

+
+ {isLoading ? ( + <> + + Fetching + +
+ + ) : ( + <> + Fetching +
+ + )} +
+
+ + {!isLoading && uploadedFiles.length === 0 ? ( +

None

+ ) : ( +
    + {uploadedFiles.map((file, index) => ( +
  • + {file} +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/a.tsx b/frontend/src/components/a.tsx new file mode 100644 index 000000000..e2de287b7 --- /dev/null +++ b/frontend/src/components/a.tsx @@ -0,0 +1,392 @@ +'use client'; + +import { useState } from 'react'; +import Globe from '../assets/globe.svg'; +import Profile from '../assets/profie.png'; +import Logo from '../assets/Logo.svg'; +import L2C1 from '../assets/file_upload.svg'; +import L2C2 from '../assets/website_collect.svg'; +import Back from '../assets/Back.svg'; +import UploadFromDeviceForm from './UploadFromDeviceForm'; +import CollectFromWebsiteForm from './CollectFromWebsiteForm'; +import { useDarkTheme } from '../hooks'; + +interface LevelIndicatorProps { + currentLevel: number; + indicatorLevel: number; +} + +const LevelIndicator: React.FC = ({ + currentLevel, + indicatorLevel, +}) => { + const isActive = currentLevel === indicatorLevel; + const isPast = currentLevel > indicatorLevel; + + return ( +
+
+
+ ); +}; + +export default function Onboarding() { + const [language] = useState('EN'); + const [level, setLevel] = useState(1); + const [selectedCard, setSelectedCard] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [progress, setProgress] = useState(0); + const [isFinalPage, setIsFinalPage] = useState(false); + const [isTrainingComplete, setIsTrainingComplete] = useState(false); + const [isDarkTheme, toggleTheme] = useDarkTheme(); + + const handleLevelUp = () => { + if (level < 3) { + setLevel((prevLevel) => prevLevel + 1); + } + }; + + const handleLevelDown = () => { + if (level > 1) { + setLevel((prevLevel) => prevLevel - 1); + } + setSelectedCard(null); + }; + + const handleCardSelect = (card: number) => { + setSelectedCard(card); + }; + + const handleFinalButtonClick = () => { + setIsLoading(true); + setIsFinalPage(true); + + const updateProgress = (current: number) => { + if (current >= 100) { + setProgress(100); + setIsLoading(false); + setIsTrainingComplete(true); + return; + } + setProgress(current); + setTimeout(() => updateProgress(current + 10), 400); + }; + + updateProgress(0); + }; + + const getGradientForLevel = (level: number): string => { + switch (level) { + case 1: + return `relative bg-gradient-to-br from-green-200 via-white to-white dark:from-[#222327] dark:to-black`; // Light greenish tone + case 2: + return 'bg-gradient-to-br from-pink-300 via-white to-white dark:from-[#222327] dark:to-black'; // Light pink tone + case 3: + return `bg-gradient-to-br ${isTrainingComplete == true ? 'from-green-200 ' : 'from-orange-100 '} via-white to-white dark:from-[#222327] dark:to-black`; // Light orange tone + default: + return 'bg-white dark:bg-gray-900'; // Default light and dark backgrounds + } + }; + + const renderContentForLevel = (level: number) => { + if (isFinalPage) { + return ( +
+ Logo + {isTrainingComplete == true ? ( +
+

+ Training Complete ! +

+
+ ) : ( + <> + {' '} + {/* Heading */} +

+ Training is in progress... +

+ {/* Subheading */} +

+ This may take several minutes +

+ + )} + +
+
+ {/* New Progress Bar */} +
+
+
+
+ + {progress}% + +
+
+ + {/* Keyframes for rotation animation */} + +
+
+ + {!isLoading && ( + + )} + {isLoading && ( + + )} +
+ ); + } + + switch (level) { + case 1: + return ( + <> +
+ Logo +

+ Welcome to DocsGPT +

+

+ Your technical documentation assistant. +

+
+ + ); + case 2: + return ( +
+ {/* Logo */} + Logo + + {/* Heading */} +

+ Upload from device or from web? +

+ + {/* Subheading */} +

+ You can choose how to add your first document to DocsGPT +

+ + {/* Card Container */} +
+ {/* Card 1 */} +
+
handleCardSelect(1)} + > +
+ L1C1 +
+
+

+ Upload from device +

+
+ + {/* Card 2 */} +
+
handleCardSelect(2)} + > +
+ L1C2 +
+
+

+ Collect from a website +

+
+
+
+ ); + case 3: + return ( +
+ Logo + + {/* Heading */} +

+ Upload new document +

+ {selectedCard === 1 ? ( + + ) : ( + + )} + {isLoading && ( +
+
+

{progress}%

+
+ )} +
+ ); + default: + return null; + } + }; + + return ( +
+
+ {/* alternate background */} + {/*
+
+
*/} + +
+ {/* Left Section */} +
+ {/* Left Section */} +
+ {language} + Globe +
+
+ + {/* Profile Section */} +
+
+ Profile +
+
+ + {/* Main content section */} +
+ {renderContentForLevel(level)} + + {!isFinalPage && ( + <> +
+
+ {[1, 2, 3].map((num) => ( + + ))} +
+
+ +
+ {level === 3 && !isLoading && ( + + )} + +
+ + )} +
+
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 91f1403dd..c8d292903 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -544,3 +544,22 @@ input:-webkit-autofill:focus { transform: translateY(0); } } + +.irregular-circle { + clip-path: ellipse(50% 48% at 50% 50%); +} + + +@keyframes fadeInSlideUp { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} +.animate-fadeInSlideUp { + animation: fadeInSlideUp 1s forwards; +} \ No newline at end of file diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs index 938eaceee..6e33e6739 100644 --- a/frontend/tailwind.config.cjs +++ b/frontend/tailwind.config.cjs @@ -50,6 +50,12 @@ module.exports = { 'philippine-yellow':'#FFC700', 'bright-gray':'#EBEBEB' }, + boxShadow: { + 'all-sides': '0 4px 8px rgba(0, 0, 0, 0.05), 0 -4px 8px rgba(0, 0, 0, 0.05), 4px 0 8px rgba(0, 0, 0, 0.05), -4px 0 8px rgba(0, 0, 0, 0.05)', + 'all-sides-hover': '0 8px 16px rgba(0, 0, 0, 0.09), 0 -8px 16px rgba(0, 0, 0, 0.09), 8px 0 16px rgba(0, 0, 0, 0.09), -8px 0 16px rgba(0, 0, 0, 0.09)', + // 'all-sides-dark': '0 8px 16px rgba(255, 255, 255, 0.1), 0 -8px 16px rgba(255, 255, 255, 0.1), 8px 0 16px rgba(255, 255, 255, 0.1), -8px 0 16px rgba(255, 255, 255, 0.1)', + // 'all-sides-dark-hover': '0 8px 16px rgba(255, 255, 255, 0.2), 0 -8px 16px rgba(255, 255, 255, 0.2), 8px 0 16px rgba(255, 255, 255, 0.2), -8px 0 16px rgba(255, 255, 255, 0.2)', + }, }, }, plugins: [],