mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge pull request #1034 from ManishMadan2882/main
Feat: sharing endpoints
This commit is contained in:
@@ -6,8 +6,9 @@ from urllib.parse import urlparse
|
||||
import requests
|
||||
from pymongo import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
from bson.binary import Binary, UuidRepresentation
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from bson.dbref import DBRef
|
||||
from application.api.user.tasks import ingest, ingest_remote
|
||||
|
||||
from application.core.settings import settings
|
||||
@@ -20,6 +21,8 @@ vectors_collection = db["vectors"]
|
||||
prompts_collection = db["prompts"]
|
||||
feedback_collection = db["feedback"]
|
||||
api_key_collection = db["api_keys"]
|
||||
shared_conversations_collections = db["shared_conversations"]
|
||||
|
||||
user = Blueprint("user", __name__)
|
||||
|
||||
current_dir = os.path.dirname(
|
||||
@@ -491,3 +494,73 @@ def delete_api_key():
|
||||
}
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
#route to share conversation
|
||||
##isPromptable should be passed through queries
|
||||
@user.route("/api/share",methods=["POST"])
|
||||
def share_conversation():
|
||||
try:
|
||||
data = request.get_json()
|
||||
user = "local"
|
||||
if(hasattr(data,"user")):
|
||||
user = data["user"]
|
||||
conversation_id = data["conversation_id"]
|
||||
isPromptable = request.args.get("isPromptable").lower() == "true"
|
||||
conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)})
|
||||
current_n_queries = len(conversation["queries"])
|
||||
pre_existing = shared_conversations_collections.find_one({
|
||||
"conversation_id":DBRef("conversations",ObjectId(conversation_id)),
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries
|
||||
})
|
||||
print("pre_existing",pre_existing)
|
||||
if(pre_existing is not None):
|
||||
explicit_binary = pre_existing["uuid"]
|
||||
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),200
|
||||
else:
|
||||
explicit_binary = Binary.from_uuid(uuid.uuid4(), UuidRepresentation.STANDARD)
|
||||
shared_conversations_collections.insert_one({
|
||||
"uuid":explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref":"conversations",
|
||||
"$id":ObjectId(conversation_id)
|
||||
} ,
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user
|
||||
})
|
||||
## Identifier as route parameter in frontend
|
||||
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201
|
||||
except Exception as err:
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
|
||||
#route to get publicly shared conversations
|
||||
@user.route("/api/shared_conversation/<string:identifier>",methods=["GET"])
|
||||
def get_publicly_shared_conversations(identifier : str):
|
||||
try:
|
||||
query_uuid = Binary.from_uuid(uuid.UUID(identifier), UuidRepresentation.STANDARD)
|
||||
shared = shared_conversations_collections.find_one({"uuid":query_uuid})
|
||||
conversation_queries=[]
|
||||
if shared and 'conversation_id' in shared and isinstance(shared['conversation_id'], DBRef):
|
||||
# Resolve the DBRef
|
||||
conversation_ref = shared['conversation_id']
|
||||
conversation = db.dereference(conversation_ref)
|
||||
if(conversation is None):
|
||||
return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404
|
||||
conversation_queries = conversation['queries'][:(shared["first_n_queries"])]
|
||||
for query in conversation_queries:
|
||||
query.pop("sources") ## avoid exposing sources
|
||||
else:
|
||||
return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404
|
||||
date = conversation["_id"].generation_time.isoformat()
|
||||
return jsonify({
|
||||
"success":True,
|
||||
"queries":conversation_queries,
|
||||
"title":conversation["name"],
|
||||
"timestamp":date
|
||||
}), 200
|
||||
except Exception as err:
|
||||
print (err)
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
|
||||
@@ -14,7 +14,9 @@ class EmbeddingsSingleton:
|
||||
@staticmethod
|
||||
def get_instance(embeddings_name, *args, **kwargs):
|
||||
if embeddings_name not in EmbeddingsSingleton._instances:
|
||||
EmbeddingsSingleton._instances[embeddings_name] = EmbeddingsSingleton._create_instance(embeddings_name, *args, **kwargs)
|
||||
EmbeddingsSingleton._instances[embeddings_name] = EmbeddingsSingleton._create_instance(
|
||||
embeddings_name, *args, **kwargs
|
||||
)
|
||||
return EmbeddingsSingleton._instances[embeddings_name]
|
||||
|
||||
@staticmethod
|
||||
|
||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"@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",
|
||||
@@ -4194,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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 = () => {
|
||||
@@ -192,6 +198,31 @@ export default function Conversation() {
|
||||
|
||||
return (
|
||||
<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}
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
@@ -215,9 +215,11 @@ const ConversationBubble = forwardRef<
|
||||
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`}
|
||||
>
|
||||
<div>
|
||||
<CoppyButton text={message} />
|
||||
<CopyButton text={message} />
|
||||
</div>
|
||||
</div>
|
||||
{handleFeedback && (
|
||||
<>
|
||||
<div
|
||||
className={`relative mr-5 flex items-center justify-center ${
|
||||
!isLikeClicked ? 'lg:invisible' : ''
|
||||
@@ -287,6 +289,8 @@ const ConversationBubble = forwardRef<
|
||||
</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,35 +112,107 @@ export default function ConversationTile({
|
||||
)}
|
||||
</div>
|
||||
{conversationId === conversation.id && (
|
||||
<div className="flex text-white dark:text-[#949494]">
|
||||
<div className="flex text-white dark:text-[#949494]" ref={menuRef}>
|
||||
{isEdit ? (
|
||||
<div className="flex gap-1">
|
||||
<img
|
||||
src={isEdit ? CheckMark2 : Edit}
|
||||
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();
|
||||
isEdit
|
||||
? handleSaveConversation({
|
||||
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`}
|
||||
className={`mr-4 mt-px h-3 w-3 cursor-pointer hover:opacity-50`}
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
isEdit ? onClear() : onDeleteConversation(conversation.id);
|
||||
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
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -48,6 +48,7 @@ module.exports = {
|
||||
'soap':'#D8CCF1',
|
||||
'independence':'#54546D',
|
||||
'philippine-yellow':'#FFC700',
|
||||
'bright-gray':'#EBEBEB'
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user