Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: sharing endpoints #1034

Merged
merged 14 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion application/api/user/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
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
Expand All @@ -20,6 +21,8 @@
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(
Expand Down Expand Up @@ -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

4 changes: 3 additions & 1 deletion application/vectorstore/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 35 additions & 10 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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>
</>
);
}
6 changes: 3 additions & 3 deletions frontend/src/PageNotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/assets/red-trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/share.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/three-dots.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions frontend/src/conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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}
Expand Down
Loading
Loading