mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge branch 'main' of https://github.com/utin-francis-peter/DocsGPT into fix/issue#1023
This commit is contained in:
14
frontend/package-lock.json
generated
14
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
3
frontend/src/assets/red-trash.svg
Normal file
3
frontend/src/assets/red-trash.svg
Normal 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 |
3
frontend/src/assets/share.svg
Normal file
3
frontend/src/assets/share.svg
Normal 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 |
3
frontend/src/assets/three-dots.svg
Normal file
3
frontend/src/assets/three-dots.svg
Normal 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 |
43
frontend/src/components/Input.tsx
Normal file
43
frontend/src/components/Input.tsx
Normal 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;
|
||||
21
frontend/src/components/types/index.ts
Normal file
21
frontend/src/components/types/index.ts
Normal 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;
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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] && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
144
frontend/src/conversation/SharedConversation.tsx
Normal file
144
frontend/src/conversation/SharedConversation.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -103,6 +103,22 @@
|
||||
"deleteConv": {
|
||||
"confirm": "すべての会話を削除してもよろしいですか?",
|
||||
"delete": "削除"
|
||||
},
|
||||
"shareConv": {
|
||||
"label": "共有ページを作成して共有する",
|
||||
"note": "ソースドキュメント、個人情報、および以降の会話は非公開のままになります",
|
||||
"create": "作成"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
"subtitle": "作成者",
|
||||
"button": "DocsGPT を始める",
|
||||
"meta": "DocsGPT は GenAI を使用しています、情報源を使用して重要情報を確認してください。"
|
||||
},
|
||||
"convTile": {
|
||||
"share": "共有",
|
||||
"delete": "削除",
|
||||
"rename": "名前変更",
|
||||
"deleteWarning": "この会話を削除してもよろしいですか?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,22 @@
|
||||
"deleteConv": {
|
||||
"confirm": "您确定要删除所有对话吗?",
|
||||
"delete": "删除"
|
||||
},
|
||||
"shareConv": {
|
||||
"label": "创建用于分享的公共页面",
|
||||
"note": "源文档、个人信息和后续对话将保持私密",
|
||||
"create": "创建"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
"subtitle": "使用创建",
|
||||
"button": "开始使用 DocsGPT",
|
||||
"meta": "DocsGPT 使用 GenAI,请使用资源查看关键信息。"
|
||||
},
|
||||
"convTile": {
|
||||
"share": "分享",
|
||||
"delete": "删除",
|
||||
"rename": "重命名",
|
||||
"deleteWarning": "您确定要删除此对话吗?"
|
||||
}
|
||||
}
|
||||
|
||||
90
frontend/src/modals/ShareConversationModal.tsx
Normal file
90
frontend/src/modals/ShareConversationModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -48,6 +48,7 @@ module.exports = {
|
||||
'soap':'#D8CCF1',
|
||||
'independence':'#54546D',
|
||||
'philippine-yellow':'#FFC700',
|
||||
'bright-gray':'#EBEBEB'
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user