This commit is contained in:
utin-francis-peter
2024-07-16 22:05:12 +01:00
31 changed files with 852 additions and 205 deletions

View File

@@ -11,6 +11,8 @@
"@reduxjs/toolkit": "^1.9.2",
"@vercel/analytics": "^0.1.10",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
@@ -1488,7 +1490,7 @@
"version": "18.0.10",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
"devOptional": true,
"dev": true,
"dependencies": {
"@types/react": "*"
}
@@ -4193,6 +4195,15 @@
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -6606,6 +6617,7 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",

View File

@@ -22,6 +22,8 @@
"@reduxjs/toolkit": "^1.9.2",
"@vercel/analytics": "^0.1.10",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",

View File

@@ -1,4 +1,5 @@
import { Routes, Route } from 'react-router-dom';
import { useEffect } from 'react';
import Navigation from './Navigation';
import Conversation from './conversation/Conversation';
import About from './About';
@@ -8,29 +9,53 @@ import { useMediaQuery } from './hooks';
import { useState } from 'react';
import Setting from './settings';
import './locale/i18n';
import { Outlet } from 'react-router-dom';
import SharedConversation from './conversation/SharedConversation';
import { useDarkTheme } from './hooks';
inject();
export default function App() {
function MainLayout() {
const { isMobile } = useMediaQuery();
const [navOpen, setNavOpen] = useState(!isMobile);
return (
<div className="min-h-full min-w-full dark:bg-raisin-black">
<div className="dark:bg-raisin-black">
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
<div
className={`transition-all duration-200 ${
className={`min-h-screen ${
!isMobile
? `ml-0 ${!navOpen ? 'md:mx-auto lg:mx-auto' : 'md:ml-72'}`
: 'ml-0 md:ml-16'
}`}
>
<Routes>
<Route path="/" element={<Conversation />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<PageNotFound />} />
<Route path="/settings" element={<Setting />} />
</Routes>
<Outlet />
</div>
</div>
);
}
export default function App() {
const [isDarkTheme] = useDarkTheme();
useEffect(() => {
localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light');
if (isDarkTheme) {
document
.getElementById('root')
?.classList.add('dark', 'dark:bg-raisin-black');
} else {
document.getElementById('root')?.classList.remove('dark');
}
}, [isDarkTheme]);
return (
<>
<Routes>
<Route element={<MainLayout />}>
<Route index element={<Conversation />} />
<Route path="/about" element={<About />} />
<Route path="/settings" element={<Setting />} />
</Route>
<Route path="/share/:identifier" element={<SharedConversation />} />
<Route path="/*" element={<PageNotFound />} />
</Routes>
</>
);
}

View File

@@ -19,7 +19,7 @@ export default function Hero({
}>;
return (
<div
className={`mt-14 mb-4 flex w-full flex-col justify-end text-black-1000 dark:text-bright-gray sm:w-full lg:mt-6`}
className={`mt-16 mb-4 flex w-full flex-col justify-end text-black-1000 dark:text-bright-gray sm:w-full lg:mt-6`}
>
<div className="flex h-full w-full flex-col items-center justify-center">
<div className="flex items-center">

View File

@@ -2,11 +2,11 @@ import { Link } from 'react-router-dom';
export default function PageNotFound() {
return (
<div className="mx-5 grid min-h-screen md:mx-36">
<p className="mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 text-jet lg:p-10 xl:p-16">
<div className="grid min-h-screen dark:bg-raisin-black">
<p className="mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 text-jet dark:bg-outer-space dark:text-gray-100 lg:p-10 xl:p-16">
<h1>404</h1>
<p>The page you are looking for does not exist.</p>
<button className="pointer-cursor mr-4 flex cursor-pointer items-center justify-center rounded-full bg-blue-1000 py-2 px-4 text-white hover:bg-blue-3000">
<button className="pointer-cursor mr-4 flex cursor-pointer items-center justify-center rounded-full bg-blue-1000 py-2 px-4 text-white transition-colors duration-100 hover:bg-blue-3000">
<Link to="/">Go Back Home</Link>
</button>
</p>

View File

@@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.66427 17.7747C6.66427 18.2167 6.84488 18.6406 7.16637 18.9532C7.48787 19.2658 7.9239 19.4413 8.37856 19.4413H15.2357C15.6904 19.4413 16.1264 19.2658 16.4479 18.9532C16.7694 18.6406 16.95 18.2167 16.95 17.7747V7.77468H6.66427V17.7747ZM8.37856 9.44135H15.2357V17.7747H8.37856V9.44135ZM14.8071 5.27468L13.95 4.44135H9.66427L8.80713 5.27468H5.80713V6.94135H17.8071V5.27468H14.8071Z" fill="#D30000"/>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View File

@@ -0,0 +1,3 @@
<svg width="14" height="17" viewBox="0 0 14 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.04167 7.2997C1.96431 7.2997 1.89013 7.32976 1.83543 7.38326C1.78073 7.43677 1.75 7.50934 1.75 7.585V15.0029C1.75 15.1604 1.88067 15.2882 2.04167 15.2882H11.9583C12.0357 15.2882 12.1099 15.2581 12.1646 15.2046C12.2193 15.1511 12.25 15.0785 12.25 15.0029V7.585C12.25 7.50934 12.2193 7.43677 12.1646 7.38326C12.1099 7.32976 12.0357 7.2997 11.9583 7.2997H10.7917C10.5596 7.2997 10.337 7.20952 10.1729 7.04901C10.0089 6.8885 9.91667 6.67079 9.91667 6.44379C9.91667 6.21679 10.0089 5.99909 10.1729 5.83857C10.337 5.67806 10.5596 5.58788 10.7917 5.58788H11.9583C13.0853 5.58788 14 6.48259 14 7.585V15.0029C14 15.5325 13.7849 16.0405 13.402 16.4151C13.0191 16.7896 12.4998 17 11.9583 17H2.04167C1.50018 17 0.980877 16.7896 0.59799 16.4151C0.215104 16.0405 0 15.5325 0 15.0029V7.585C0 6.48259 0.914667 5.58788 2.04167 5.58788H3.20833C3.4404 5.58788 3.66296 5.67806 3.82705 5.83857C3.99115 5.99909 4.08333 6.21679 4.08333 6.44379C4.08333 6.67079 3.99115 6.8885 3.82705 7.04901C3.66296 7.20952 3.4404 7.2997 3.20833 7.2997H2.04167ZM6.7935 0.0838185C6.82059 0.0572492 6.85278 0.0361694 6.88821 0.0217864C6.92365 0.0074035 6.96164 0 7 0C7.03836 0 7.07635 0.0074035 7.11179 0.0217864C7.14722 0.0361694 7.17941 0.0572492 7.2065 0.0838185L10.5852 3.38877C10.6261 3.42867 10.6539 3.47955 10.6652 3.53496C10.6765 3.59037 10.6707 3.64782 10.6486 3.70001C10.6265 3.75221 10.589 3.7968 10.541 3.82815C10.4929 3.85949 10.4364 3.87617 10.3787 3.87607H7.875V10.438C7.875 10.665 7.78281 10.8827 7.61872 11.0433C7.45462 11.2038 7.23206 11.2939 7 11.2939C6.76794 11.2939 6.54538 11.2038 6.38128 11.0433C6.21719 10.8827 6.125 10.665 6.125 10.438V3.87607H3.62133C3.56357 3.87617 3.50708 3.85949 3.45902 3.82815C3.41096 3.7968 3.37349 3.75221 3.35138 3.70001C3.32926 3.64782 3.32348 3.59037 3.33478 3.53496C3.34607 3.47955 3.37394 3.42867 3.41483 3.38877L6.7935 0.0838185Z" fill="#747474"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,3 @@
<svg width="10" height="25" viewBox="0 0 10 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 3.5C3.9 3.5 3 4.4 3 5.5C3 6.6 3.9 7.5 5 7.5C6.1 7.5 7 6.6 7 5.5C7 4.4 6.1 3.5 5 3.5ZM5 17.5C3.9 17.5 3 18.4 3 19.5C3 20.6 3.9 21.5 5 21.5C6.1 21.5 7 20.6 7 19.5C7 18.4 6.1 17.5 5 17.5ZM5 10.5C3.9 10.5 3 11.4 3 12.5C3 13.6 3.9 14.5 5 14.5C6.1 14.5 7 13.6 7 12.5C7 11.4 6.1 10.5 5 10.5Z" fill="#747474"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1,43 @@
import { InputProps } from './types';
const Input = ({
id,
name,
type,
value,
isAutoFocused = false,
placeholder,
maxLength,
className,
colorVariant = 'silver',
children,
onChange,
onPaste,
onKeyDown,
}: InputProps) => {
const colorStyles = {
silver: 'border-silver dark:border-silver/40',
jet: 'border-jet',
gray: 'border-gray-5000 dark:text-silver',
};
return (
<input
className={`h-[42px] w-full rounded-full border-2 px-3 outline-none dark:bg-transparent dark:text-white ${className} ${colorStyles[colorVariant]}`}
type={type}
id={id}
name={name}
autoFocus={isAutoFocused}
placeholder={placeholder}
maxLength={maxLength}
value={value}
onChange={onChange}
onPaste={onPaste}
onKeyDown={onKeyDown}
>
{children}
</input>
);
};
export default Input;

View File

@@ -0,0 +1,21 @@
export type InputProps = {
type: 'text' | 'number';
value: string | string[] | number;
colorVariant?: 'silver' | 'jet' | 'gray';
isAutoFocused?: boolean;
id?: string;
maxLength?: number;
name?: string;
placeholder?: string;
className?: string;
children?: React.ReactElement;
onChange: (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
) => void;
onPaste?: (
e: React.ClipboardEvent<HTMLTextAreaElement | HTMLInputElement>,
) => void;
onKeyDown?: (
e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>,
) => void;
};

View File

@@ -11,6 +11,7 @@ import {
selectStatus,
updateQuery,
} from './conversationSlice';
import { selectConversationId } from '../preferences/preferenceSlice';
import Send from './../assets/send.svg';
import SendDark from './../assets/send_dark.svg';
import Spinner from './../assets/spinner.svg';
@@ -20,9 +21,13 @@ import { sendFeedback } from './conversationApi';
import { useTranslation } from 'react-i18next';
import ArrowDown from './../assets/arrow-down.svg';
import RetryIcon from '../components/RetryIcon';
import ShareIcon from '../assets/share.svg';
import { ShareConversationModal } from '../modals/ShareConversationModal';
export default function Conversation() {
const queries = useSelector(selectQueries);
const status = useSelector(selectStatus);
const conversationId = useSelector(selectConversationId);
const dispatch = useDispatch<AppDispatch>();
const endMessageRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(null);
@@ -31,6 +36,7 @@ export default function Conversation() {
const fetchStream = useRef<any>(null);
const [eventInterrupt, setEventInterrupt] = useState(false);
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
const { t } = useTranslation();
const handleUserInterruption = () => {
@@ -191,11 +197,36 @@ export default function Conversation() {
};
return (
<div className="flex h-screen flex-col gap-1">
<div className="flex h-screen flex-col gap-7 pb-2">
{conversationId && (
<>
<button
title="Share"
onClick={() => {
setShareModalState(true);
}}
className="fixed top-4 right-20 z-30 rounded-full hover:bg-bright-gray dark:hover:bg-[#28292E]"
>
<img
className="m-2 h-5 w-5 filter dark:invert"
alt="share"
src={ShareIcon}
/>
</button>
{isShareModalOpen && (
<ShareConversationModal
close={() => {
setShareModalState(false);
}}
conversationId={conversationId}
/>
)}
</>
)}
<div
onWheel={handleUserInterruption}
onTouchMove={handleUserInterruption}
className="flex h-[90%] w-full justify-center overflow-y-auto p-4 md:h-[83vh]"
className="flex h-[90%] w-full flex-1 justify-center overflow-y-auto p-4 md:h-[83vh]"
>
{queries.length > 0 && !hasScrolledToLast && (
<button
@@ -234,8 +265,8 @@ export default function Conversation() {
{queries.length === 0 && <Hero handleQuestion={handleQuestion} />}
</div>
<div className="bottom-safe fixed flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 pb-1 sm:w-6/12">
<div className="flex h-full w-full items-center rounded-full border border-silver bg-white dark:bg-raisin-black">
<div className="flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 pb-1 sm:w-6/12">
<div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
<div
id="inputbox"
ref={inputRef}

View File

@@ -1,6 +1,6 @@
import { forwardRef, useState } from 'react';
import Avatar from '../components/Avatar';
import CoppyButton from '../components/CopyButton';
import CopyButton from '../components/CopyButton';
import remarkGfm from 'remark-gfm';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
import classes from './ConversationBubble.module.css';
@@ -42,7 +42,7 @@ const ConversationBubble = forwardRef<
bubble = (
<div ref={ref} className={`flex flex-row-reverse self-end ${className}`}>
<Avatar className="mt-2 text-2xl" avatar="🧑‍💻"></Avatar>
<div className="ml-10 mr-2 flex items-center rounded-3xl bg-purple-30 p-3.5 text-white">
<div className="ml-10 mr-2 flex items-center rounded-[28px] bg-purple-30 py-[14px] px-[19px] text-white">
<ReactMarkdown className="whitespace-pre-wrap break-normal leading-normal">
{message}
</ReactMarkdown>
@@ -53,7 +53,7 @@ const ConversationBubble = forwardRef<
bubble = (
<div
ref={ref}
className={`flex flex-wrap self-start ${className} group flex-col pr-20 dark:text-bright-gray`}
className={`flex flex-wrap self-start ${className} group flex-col dark:text-bright-gray`}
>
<div className="flex flex-wrap self-start lg:flex-nowrap">
<Avatar
@@ -68,7 +68,7 @@ const ConversationBubble = forwardRef<
/>
<div
className={`ml-2 mr-5 flex max-w-[90vw] rounded-3xl bg-gray-1000 p-3.5 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
className={`ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
type === 'ERROR'
? 'relative flex-row items-center rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal text-red-3000 dark:border-red-2000 dark:text-white'
: 'flex-col rounded-3xl'
@@ -103,7 +103,7 @@ const ConversationBubble = forwardRef<
className={`absolute right-3 top-3 lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''} `}
>
<CoppyButton
<CopyButton
text={String(children).replace(/\n$/, '')}
/>
</div>
@@ -208,85 +208,89 @@ const ConversationBubble = forwardRef<
</>
)}
</div>
<div className="flex justify-center">
<div
className={`relative mr-5 block items-center justify-center lg:invisible
</div>
<div className="my-2 flex justify-start lg:ml-12">
<div
className={`relative mr-5 block items-center justify-center lg:invisible
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`}
>
<div className="absolute left-2 top-4">
<CoppyButton text={message} />
</div>
>
<div>
<CopyButton text={message} />
</div>
<div
className={`relative mr-5 flex items-center justify-center ${
!isLikeClicked ? 'lg:invisible' : ''
} ${
feedback === 'LIKE' || type !== 'ERROR'
? 'group-hover:lg:visible'
: ''
}`}
>
<div className="absolute left-6 top-4">
<div
className={`flex items-center justify-center rounded-full p-2 dark:bg-transparent ${
isLikeHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
}`}
>
<Like
className={`cursor-pointer
</div>
{handleFeedback && (
<>
<div
className={`relative mr-5 flex items-center justify-center ${
!isLikeClicked ? 'lg:invisible' : ''
} ${
feedback === 'LIKE' || type !== 'ERROR'
? 'group-hover:lg:visible'
: ''
}`}
>
<div>
<div
className={`flex items-center justify-center rounded-full p-2 dark:bg-transparent ${
isLikeHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
}`}
>
<Like
className={`cursor-pointer
${
isLikeClicked || feedback === 'LIKE'
? 'fill-white-3000 stroke-purple-30 dark:fill-transparent'
: 'fill-none stroke-gray-4000'
}`}
onClick={() => {
handleFeedback?.('LIKE');
setIsLikeClicked(true);
setIsDislikeClicked(false);
}}
onMouseEnter={() => setIsLikeHovered(true)}
onMouseLeave={() => setIsLikeHovered(false)}
></Like>
onClick={() => {
handleFeedback?.('LIKE');
setIsLikeClicked(true);
setIsDislikeClicked(false);
}}
onMouseEnter={() => setIsLikeHovered(true)}
onMouseLeave={() => setIsLikeHovered(false)}
></Like>
</div>
</div>
</div>
</div>
<div
className={`mr-13 relative flex items-center justify-center ${
!isDislikeClicked ? 'lg:invisible' : ''
} ${
feedback === 'DISLIKE' || type !== 'ERROR'
? 'group-hover:lg:visible'
: ''
}`}
>
<div className="absolute left-10 top-4">
<div
className={`flex items-center justify-center rounded-full p-2 ${
isDislikeHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
}`}
>
<Dislike
className={`cursor-pointer ${
isDislikeClicked || feedback === 'DISLIKE'
? 'fill-white-3000 stroke-red-2000 dark:fill-transparent'
: 'fill-none stroke-gray-4000'
<div
className={`mr-13 relative flex items-center justify-center ${
!isDislikeClicked ? 'lg:invisible' : ''
} ${
feedback === 'DISLIKE' || type !== 'ERROR'
? 'group-hover:lg:visible'
: ''
}`}
>
<div>
<div
className={`flex items-center justify-center rounded-full p-2 ${
isDislikeHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
}`}
onClick={() => {
handleFeedback?.('DISLIKE');
setIsDislikeClicked(true);
setIsLikeClicked(false);
}}
onMouseEnter={() => setIsDislikeHovered(true)}
onMouseLeave={() => setIsDislikeHovered(false)}
></Dislike>
>
<Dislike
className={`cursor-pointer ${
isDislikeClicked || feedback === 'DISLIKE'
? 'fill-white-3000 stroke-red-2000 dark:fill-transparent'
: 'fill-none stroke-gray-4000'
}`}
onClick={() => {
handleFeedback?.('DISLIKE');
setIsDislikeClicked(true);
setIsLikeClicked(false);
}}
onMouseEnter={() => setIsDislikeHovered(true)}
onMouseLeave={() => setIsDislikeHovered(false)}
></Dislike>
</div>
</div>
</div>
</div>
</div>
</>
)}
</div>
{sources && openSource !== null && sources[openSource] && (

View File

@@ -5,11 +5,15 @@ import Exit from '../assets/exit.svg';
import Message from '../assets/message.svg';
import MessageDark from '../assets/message-dark.svg';
import { useDarkTheme } from '../hooks';
import ConfirmationModal from '../modals/ConfirmationModal';
import CheckMark2 from '../assets/checkMark2.svg';
import Trash from '../assets/trash.svg';
import Trash from '../assets/red-trash.svg';
import Share from '../assets/share.svg';
import threeDots from '../assets/three-dots.svg';
import { selectConversationId } from '../preferences/preferenceSlice';
import { ActiveState } from '../models/misc';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { useTranslation } from 'react-i18next';
interface ConversationProps {
name: string;
id: string;
@@ -32,13 +36,19 @@ export default function ConversationTile({
const [isDarkTheme] = useDarkTheme();
const [isEdit, setIsEdit] = useState(false);
const [conversationName, setConversationsName] = useState('');
const [isOpen, setOpen] = useState<boolean>(false);
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
const [deleteModalState, setDeleteModalState] =
useState<ActiveState>('INACTIVE');
const menuRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
useEffect(() => {
setConversationsName(conversation.name);
}, [conversation.name]);
function handleEditConversation() {
setIsEdit(true);
setOpen(false);
}
function handleSaveConversation(changedConversation: ConversationProps) {
@@ -50,6 +60,18 @@ export default function ConversationTile({
}
}
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
function onClear() {
setConversationsName(conversation.name);
setIsEdit(false);
@@ -79,7 +101,7 @@ export default function ConversationTile({
<input
autoFocus
type="text"
className="h-6 w-full px-1 text-sm font-normal leading-6 outline-[#0075FF] focus:outline-1"
className="h-6 w-full bg-transparent px-1 text-sm font-normal leading-6 focus:outline-[#0075FF]"
value={conversationName}
onChange={(e) => setConversationsName(e.target.value)}
/>
@@ -90,36 +112,108 @@ export default function ConversationTile({
)}
</div>
{conversationId === conversation.id && (
<div className="flex text-white dark:text-[#949494]">
<img
src={isEdit ? CheckMark2 : Edit}
alt="Edit"
className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50"
id={`img-${conversation.id}`}
onClick={(event) => {
event.stopPropagation();
isEdit
? handleSaveConversation({
<div className="flex text-white dark:text-[#949494]" ref={menuRef}>
{isEdit ? (
<div className="flex gap-1">
<img
src={CheckMark2}
alt="Edit"
className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50"
id={`img-${conversation.id}`}
onClick={(event) => {
event.stopPropagation();
handleSaveConversation({
id: conversationId,
name: conversationName,
})
: handleEditConversation();
}}
/>
<img
src={isEdit ? Exit : Trash}
alt="Exit"
className={`mr-4 ${
isEdit ? 'h-3 w-3' : 'h-4 w-4'
}mt-px cursor-pointer hover:opacity-50`}
id={`img-${conversation.id}`}
onClick={(event) => {
event.stopPropagation();
isEdit ? onClear() : onDeleteConversation(conversation.id);
}}
/>
});
}}
/>
<img
src={isEdit ? Exit : Trash}
alt="Exit"
className={`mr-4 mt-px h-3 w-3 cursor-pointer hover:opacity-50`}
id={`img-${conversation.id}`}
onClick={(event) => {
event.stopPropagation();
onClear();
}}
/>
</div>
) : (
<button onClick={() => setOpen(!isOpen)}>
<img src={threeDots} className="mr-4 w-2" />
</button>
)}
{isOpen && (
<div className="flex-start absolute flex w-32 translate-x-1 translate-y-5 flex-col rounded-xl bg-stone-100 text-sm text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver md:w-36">
<button
onClick={() => {
setShareModalState(true);
setOpen(false);
}}
className="flex-start flex items-center gap-4 rounded-t-xl p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
>
<img
src={Share}
alt="Share"
width={14}
height={14}
className="cursor-pointer hover:opacity-50"
id={`img-${conversation.id}`}
/>
<span>{t('convTile.share')}</span>
</button>
<button
onClick={(event) => {
handleEditConversation();
}}
className="flex-start flex items-center gap-4 p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
>
<img
src={Edit}
alt="Edit"
width={16}
height={16}
className="cursor-pointer hover:opacity-50"
id={`img-${conversation.id}`}
/>
<span>{t('convTile.rename')}</span>
</button>
<button
onClick={(event) => {
setDeleteModalState('ACTIVE');
setOpen(false);
}}
className="flex-start flex items-center gap-3 rounded-b-xl p-2 text-red-700 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
>
<img
src={Trash}
alt="Edit"
width={24}
height={24}
className="cursor-pointer hover:opacity-50"
/>
<span>{t('convTile.delete')}</span>
</button>
</div>
)}
</div>
)}
<ConfirmationModal
message={t('convTile.deleteWarning')}
modalState={deleteModalState}
setModalState={setDeleteModalState}
handleSubmit={() => onDeleteConversation(conversation.id)}
submitLabel={t('convTile.delete')}
/>
{isShareModalOpen && conversationId && (
<ShareConversationModal
close={() => {
setShareModalState(false);
}}
conversationId={conversationId}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,144 @@
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { Query } from './conversationModels';
import { useTranslation } from 'react-i18next';
import ConversationBubble from './ConversationBubble';
import { Fragment } from 'react';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const SharedConversation = () => {
const params = useParams();
const navigate = useNavigate();
const { identifier } = params; //identifier is a uuid, not conversationId
const [queries, setQueries] = useState<Query[]>([]);
const [title, setTitle] = useState('');
const [date, setDate] = useState('');
const { t } = useTranslation();
function formatISODate(isoDateStr: string) {
const date = new Date(isoDateStr);
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'June',
'July',
'Aug',
'Sept',
'Oct',
'Nov',
'Dec',
];
const month = monthNames[date.getMonth()];
const day = date.getDate();
const year = date.getFullYear();
let hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12;
const minutesStr = minutes < 10 ? '0' + minutes : minutes;
const formattedDate = `Published ${month} ${day}, ${year} at ${hours}:${minutesStr} ${ampm}`;
return formattedDate;
}
const fetchQueris = () => {
fetch(`${apiHost}/api/shared_conversation/${identifier}`)
.then((res) => {
if (res.status === 404 || res.status === 400) navigate('/pagenotfound');
return res.json();
})
.then((data) => {
if (data.success) {
setQueries(data.queries);
setTitle(data.title);
setDate(formatISODate(data.timestamp));
}
});
};
const prepResponseView = (query: Query, index: number) => {
let responseView;
if (query.response) {
responseView = (
<ConversationBubble
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
key={`${index}ANSWER`}
message={query.response}
type={'ANSWER'}
></ConversationBubble>
);
} else if (query.error) {
responseView = (
<ConversationBubble
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
key={`${index}ERROR`}
message={query.error}
type="ERROR"
></ConversationBubble>
);
}
return responseView;
};
useEffect(() => {
fetchQueris();
}, []);
return (
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
<div className="flex w-full justify-center overflow-auto">
<div className="mt-0 w-11/12 md:w-10/12 lg:w-6/12">
<div className="mb-2 w-full border-b pb-2">
<h1 className="font-semi-bold text-4xl text-chinese-black dark:text-chinese-silver">
{title}
</h1>
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
{t('sharedConv.subtitle')}{' '}
<a href="/" className="text-[#007DFF]">
DocsGPT
</a>
</h2>
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
{date}
</h2>
</div>
<div className="">
{queries?.map((query, index) => {
return (
<Fragment key={index}>
<ConversationBubble
className={'mb-1 last:mb-28 md:mb-7'}
key={`${index}QUESTION`}
message={query.prompt}
type="QUESTION"
sources={query.sources}
></ConversationBubble>
{prepResponseView(query, index)}
</Fragment>
);
})}
</div>
</div>
</div>
<div className=" flex flex-col items-center gap-4 pb-2">
<button
onClick={() => navigate('/')}
className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe"
>
{t('sharedConv.button')}
</button>
<span className="hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
{t('sharedConv.meta')}
</span>
</div>
</div>
);
};
export default SharedConversation;

View File

@@ -77,21 +77,23 @@ export function useDarkTheme() {
// Set dark mode based on local storage preference
if (savedMode === 'Dark') {
setIsDarkTheme(true);
document.documentElement.classList.add('dark');
document.documentElement.classList.add('dark:bg-raisin-black');
document
.getElementById('root')
?.classList.add('dark', 'dark:bg-raisin-black');
} else {
// If no preference found, set to default (light mode)
setIsDarkTheme(false);
document.documentElement.classList.remove('dark');
document.getElementById('root')?.classList.remove('dark');
}
}, []);
useEffect(() => {
localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light');
if (isDarkTheme) {
document.documentElement.classList.add('dark');
document.documentElement.classList.add('dark:bg-raisin-black');
document
.getElementById('root')
?.classList.add('dark', 'dark:bg-raisin-black');
} else {
document.documentElement.classList.remove('dark');
document.getElementById('root')?.classList.remove('dark');
}
}, [isDarkTheme]);
//method to toggle theme

View File

@@ -22,6 +22,18 @@
background: #b1afaf;
}
@layer utilities {
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
@@ -409,10 +421,6 @@ template {
width: 0;
}
.bottom-safe {
bottom: env(safe-area-inset-bottom, 0);
}
.inputbox-style[contenteditable] {
padding-left: 36px;
padding-right: 36px;

View File

@@ -103,6 +103,22 @@
"deleteConv": {
"confirm": "Are you sure you want to delete all the conversations?",
"delete": "Delete"
},
"shareConv": {
"label": "Create a public page to share",
"note": "Source document, personal information and further conversation will remain private",
"create": "Create"
}
},
"sharedConv": {
"subtitle": "Created with",
"button": "Get Started with DocsGPT",
"meta": "DocsGPT uses GenAI, please review critical information using sources."
},
"convTile": {
"share": "Share",
"delete": "Delete",
"rename": "Rename",
"deleteWarning": "Are you sure you want to delete this conversation?"
}
}

View File

@@ -103,6 +103,22 @@
"deleteConv": {
"confirm": "¿Está seguro de que desea eliminar todas las conversaciones?",
"delete": "Eliminar"
},
"shareConv": {
"label": "Crear una página pública para compartir",
"note": "El documento original, la información personal y las conversaciones posteriores permanecerán privadas",
"create": "Crear"
}
},
"sharedConv": {
"subtitle": "Creado con",
"button": "Comienza con DocsGPT",
"meta": "DocsGPT utiliza GenAI, por favor revise la información crítica utilizando fuentes."
},
"convTile": {
"share": "Compartir",
"delete": "Eliminar",
"rename": "Renombrar",
"deleteWarning": "¿Está seguro de que desea eliminar esta conversación?"
}
}

View File

@@ -1,29 +1,38 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from './en.json'; //English
import es from './es.json'; //Spanish
import jp from './jp.json'; //Japanese
import zh from './zh.json'; //Mandarin
i18n.use(initReactI18next).init({
resources: {
en: {
translation: en,
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: {
translation: en,
},
es: {
translation: es,
},
jp: {
translation: jp,
},
zh: {
translation: zh,
},
},
es: {
translation: es,
fallbackLng: 'en',
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
lookupLocalStorage: 'docsgpt-locale',
},
jp: {
translation: jp,
},
zh: {
translation: zh,
},
},
});
});
const locale = localStorage.getItem('docsgpt-locale') ?? 'en';
i18n.changeLanguage(locale);
i18n.changeLanguage(i18n.language);
export default i18n;

View File

@@ -103,6 +103,22 @@
"deleteConv": {
"confirm": "すべての会話を削除してもよろしいですか?",
"delete": "削除"
},
"shareConv": {
"label": "共有ページを作成して共有する",
"note": "ソースドキュメント、個人情報、および以降の会話は非公開のままになります",
"create": "作成"
}
},
"sharedConv": {
"subtitle": "作成者",
"button": "DocsGPT を始める",
"meta": "DocsGPT は GenAI を使用しています、情報源を使用して重要情報を確認してください。"
},
"convTile": {
"share": "共有",
"delete": "削除",
"rename": "名前変更",
"deleteWarning": "この会話を削除してもよろしいですか?"
}
}

View File

@@ -103,6 +103,22 @@
"deleteConv": {
"confirm": "您确定要删除所有对话吗?",
"delete": "删除"
},
"shareConv": {
"label": "创建用于分享的公共页面",
"note": "源文档、个人信息和后续对话将保持私密",
"create": "创建"
}
},
"sharedConv": {
"subtitle": "使用创建",
"button": "开始使用 DocsGPT",
"meta": "DocsGPT 使用 GenAI请使用资源查看关键信息。"
},
"convTile": {
"share": "分享",
"delete": "删除",
"rename": "重命名",
"deleteWarning": "您确定要删除此对话吗?"
}
}

View File

@@ -0,0 +1,90 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Spinner from '../assets/spinner.svg';
import Exit from '../assets/exit.svg';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
export const ShareConversationModal = ({
close,
conversationId,
}: {
close: () => void;
conversationId: string;
}) => {
const [identifier, setIdentifier] = useState<null | string>(null);
const [isCopied, setIsCopied] = useState(false);
type StatusType = 'loading' | 'idle' | 'fetched' | 'failed';
const [status, setStatus] = useState<StatusType>('idle');
const { t } = useTranslation();
const domain = window.location.origin;
const handleCopyKey = (url: string) => {
navigator.clipboard.writeText(url);
setIsCopied(true);
};
const shareCoversationPublicly: (isPromptable: boolean) => void = (
isPromptable = false,
) => {
setStatus('loading');
fetch(`${apiHost}/api/share?isPromptable=${isPromptable}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ conversation_id: conversationId }),
})
.then((res) => {
console.log(res.status);
return res.json();
})
.then((data) => {
if (data.success && data.identifier) {
setIdentifier(data.identifier);
setStatus('fetched');
} else setStatus('failed');
})
.catch((err) => setStatus('failed'));
};
return (
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50 text-chinese-black dark:text-silver">
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
<button className="absolute top-3 right-4 m-2 w-3" onClick={close}>
<img className="filter dark:invert" src={Exit} />
</button>
<div className="flex flex-col gap-2">
<h2 className="text-xl font-medium">{t('modals.shareConv.label')}</h2>
<p className="text-sm">{t('modals.shareConv.note')}</p>
<div className="flex items-baseline justify-between gap-2">
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 p-3 shadow-inner">{`${domain}/share/${
identifier ?? '....'
}`}</span>
{status === 'fetched' ? (
<button
className="my-1 h-10 w-36 rounded-full border border-solid border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
>
{isCopied
? t('modals.saveKey.copied')
: t('modals.saveKey.copy')}
</button>
) : (
<button
className="my-1 flex h-10 w-36 items-center justify-evenly rounded-full border border-solid border-purple-30 p-2 text-center text-sm font-normal text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={() => {
shareCoversationPublicly(false);
}}
>
{t('modals.shareConv.create')}
{status === 'loading' && (
<img
src={Spinner}
className="inline animate-spin cursor-pointer bg-transparent filter dark:invert"
></img>
)}
</button>
)}
</div>
</div>
</div>
</div>
);
};

View File

@@ -4,6 +4,7 @@ import { ActiveState } from '../models/misc';
import { selectApiKey, setApiKey } from './preferenceSlice';
import { useMediaQuery, useOutsideAlerter } from './../hooks';
import Modal from '../modals';
import Input from '../components/Input';
export default function APIKeyModal({
modalState,
@@ -66,14 +67,15 @@ export default function APIKeyModal({
key for llm. Currently, we support only OpenAI but soon many more.
You can find it here.
</p>
<input
<Input
type="text"
className="h-10 w-full border-b-2 border-jet focus:outline-none"
colorVariant="jet"
className="h-10 border-b-2 focus:outline-none"
value={key}
maxLength={100}
placeholder="API Key"
onChange={(e) => setKey(e.target.value)}
/>
></Input>
</article>
);
}}

View File

@@ -1,5 +1,6 @@
import { ActiveState } from '../models/misc';
import Exit from '../assets/exit.svg';
import Input from '../components/Input';
function AddPrompt({
setModalState,
@@ -34,13 +35,13 @@ function AddPrompt({
Add your custom prompt and save it to DocsGPT
</p>
<div>
<input
<Input
placeholder="Prompt Name"
type="text"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 rounded-lg"
value={newPromptName}
onChange={(e) => setNewPromptName(e.target.value)}
></input>
></Input>
<div className="relative bottom-12 left-3 mt-[-3.00px]">
<span className="bg-white px-1 text-xs text-silver dark:bg-outer-space dark:text-silver">
Prompt Name
@@ -105,13 +106,13 @@ function EditPrompt({
Edit your custom prompt and save it to DocsGPT
</p>
<div>
<input
<Input
placeholder="Prompt Name"
type="text"
className="h-10 w-full rounded-lg border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="h-10 rounded-lg"
value={editPromptName}
onChange={(e) => setEditPromptName(e.target.value)}
></input>
></Input>
<div className="relative bottom-12 left-3 mt-[-3.00px]">
<span className="bg-white px-1 text-xs text-silver dark:bg-outer-space dark:text-silver">
Prompt Name

View File

@@ -10,6 +10,7 @@ import { selectSourceDocs } from '../preferences/preferenceSlice';
import Exit from '../assets/exit.svg';
import Trash from '../assets/trash.svg';
import { useTranslation } from 'react-i18next';
import Input from '../components/Input';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
@@ -237,12 +238,12 @@ const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
<span className="absolute left-2 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.createAPIKey.apiKeyName')}
</span>
<input
<Input
type="text"
className="h-[42px] w-full rounded-md border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
className="rounded-md"
value={APIKeyName}
onChange={(e) => setAPIKeyName(e.target.value)}
/>
></Input>
</div>
<div className="my-4">
<Dropdown

View File

@@ -7,6 +7,7 @@ import { getDocs } from '../preferences/preferenceApi';
import { setSelectedDocs, setSourceDocs } from '../preferences/preferenceSlice';
import Dropdown from '../components/Dropdown';
import { useTranslation } from 'react-i18next';
import Input from '../components/Input';
function Upload({
modalState,
@@ -55,7 +56,7 @@ function Upload({
}
}, []);
function ProgressBar({ progress }: { progress: number }) {
function ProgressBar({ progressPercent }: { progressPercent: number }) {
return (
<div className="my-5 w-[50%]">
<div
@@ -65,9 +66,9 @@ function Upload({
className={`h-full border-none p-1 w-${
progress || 0
}% flex items-center justify-center bg-purple-30 outline-none transition-all`}
style={{ width: `${progress || 0}%` }}
style={{ width: `${progressPercent || 0}%` }}
>
{progress >= 5 && `${progress}%`}
{progressPercent >= 5 && `${progressPercent}%`}
</div>
</div>
</div>
@@ -93,7 +94,7 @@ function Upload({
{/* <p className="mt-10 text-2xl">{progress?.percentage || 0}%</p> */}
{/* progress bar */}
<ProgressBar progress={progress?.percentage as number} />
<ProgressBar progressPercent={progress?.percentage as number} />
<button
onClick={() => {
@@ -272,7 +273,9 @@ function Upload({
},
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
if (name === 'search_queries' && value.length > 0) {
setRedditData({
@@ -323,12 +326,12 @@ function Upload({
{activeTab === 'file' && (
<>
<input
<Input
type="text"
className="h-[42px] w-full rounded-full border-2 border-gray-5000 px-3 outline-none dark:bg-transparent dark:text-silver"
colorVariant="gray"
value={docName}
onChange={(e) => setDocName(e.target.value)}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.name')}
@@ -373,25 +376,23 @@ function Upload({
/>
{urlType.label !== 'Reddit' ? (
<>
<input
<Input
placeholder={`Enter ${t('modals.uploadDoc.name')}`}
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={urlName}
onChange={(e) => setUrlName(e.target.value)}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.name')}
</span>
</div>
<input
<Input
placeholder={t('modals.uploadDoc.urlLink')}
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
value={url}
onChange={(e) => setUrl(e.target.value)}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.link')}
@@ -400,66 +401,61 @@ function Upload({
</>
) : (
<>
<input
<Input
placeholder="Enter client ID"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="client_id"
value={redditData.client_id}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.id')}
</span>
</div>
<input
<Input
placeholder="Enter client secret"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="client_secret"
value={redditData.client_secret}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.secret')}
</span>
</div>
<input
<Input
placeholder="Enter user agent"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="user_agent"
value={redditData.user_agent}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.agent')}
</span>
</div>
<input
<Input
placeholder="Enter search queries"
type="text"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="search_queries"
value={redditData.search_queries}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.searchQueries')}
</span>
</div>
<input
<Input
placeholder="Enter number of posts"
type="number"
className="h-[42px] w-full rounded-full border-2 border-silver px-3 outline-none dark:border-silver/40 dark:bg-transparent dark:text-white"
name="number_posts"
value={redditData.number_posts}
onChange={handleChange}
></input>
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.numberOfPosts')}

View File

@@ -48,6 +48,7 @@ module.exports = {
'soap':'#D8CCF1',
'independence':'#54546D',
'philippine-yellow':'#FFC700',
'bright-gray':'#EBEBEB'
},
},
},