mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge pull request #1039 from ManishMadan2882/main
Feature(Sharing): Introducing Promptable conversations
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -172,5 +172,5 @@ application/vectors/
|
||||
|
||||
node_modules/
|
||||
.vscode/settings.json
|
||||
models/
|
||||
/models/
|
||||
model/
|
||||
|
||||
@@ -189,13 +189,14 @@ def complete_stream(question, retriever, conversation_id, user_api_key):
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
|
||||
)
|
||||
conversation_id = save_conversation(
|
||||
conversation_id, question, response_full, source_log_docs, llm
|
||||
)
|
||||
|
||||
# send data.type = "end" to indicate that the stream has ended as json
|
||||
data = json.dumps({"type": "id", "id": str(conversation_id)})
|
||||
yield f"data: {data}\n\n"
|
||||
if(user_api_key is None):
|
||||
conversation_id = save_conversation(
|
||||
conversation_id, question, response_full, source_log_docs, llm
|
||||
)
|
||||
# send data.type = "end" to indicate that the stream has ended as json
|
||||
data = json.dumps({"type": "id", "id": str(conversation_id)})
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
data = json.dumps({"type": "end"})
|
||||
yield f"data: {data}\n\n"
|
||||
except Exception as e:
|
||||
|
||||
@@ -502,24 +502,88 @@ def delete_api_key():
|
||||
def share_conversation():
|
||||
try:
|
||||
data = request.get_json()
|
||||
user = "local"
|
||||
if(hasattr(data,"user")):
|
||||
user = data["user"]
|
||||
user = "local" if "user" not in data else 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"])
|
||||
|
||||
##generate binary representation of uuid
|
||||
explicit_binary = Binary.from_uuid(uuid.uuid4(), UuidRepresentation.STANDARD)
|
||||
|
||||
if(isPromptable):
|
||||
source = "default" if "source" not in data else data["source"]
|
||||
prompt_id = "default" if "prompt_id" not in data else data["prompt_id"]
|
||||
chunks = "2" if "chunks" not in data else data["chunks"]
|
||||
|
||||
name = conversation["name"]+"(shared)"
|
||||
pre_existing_api_document = api_key_collection.find_one({
|
||||
"prompt_id":prompt_id,
|
||||
"chunks":chunks,
|
||||
"source":source,
|
||||
"user":user
|
||||
})
|
||||
api_uuid = str(uuid.uuid4())
|
||||
if(pre_existing_api_document):
|
||||
api_uuid = pre_existing_api_document["key"]
|
||||
pre_existing = shared_conversations_collections.find_one({
|
||||
"conversation_id":DBRef("conversations",ObjectId(conversation_id)),
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user,
|
||||
"api_key":api_uuid
|
||||
})
|
||||
if(pre_existing is not None):
|
||||
return jsonify({"success":True, "identifier":str(pre_existing["uuid"].as_uuid())}),200
|
||||
else:
|
||||
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,
|
||||
"api_key":api_uuid
|
||||
})
|
||||
return jsonify({"success":True,"identifier":str(explicit_binary.as_uuid())})
|
||||
else:
|
||||
api_key_collection.insert_one(
|
||||
{
|
||||
"name": name,
|
||||
"key": api_uuid,
|
||||
"source": source,
|
||||
"user": user,
|
||||
"prompt_id": prompt_id,
|
||||
"chunks": chunks,
|
||||
}
|
||||
)
|
||||
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,
|
||||
"api_key":api_uuid
|
||||
})
|
||||
## Identifier as route parameter in frontend
|
||||
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201
|
||||
|
||||
##isPromptable = False
|
||||
pre_existing = shared_conversations_collections.find_one({
|
||||
"conversation_id":DBRef("conversations",ObjectId(conversation_id)),
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user
|
||||
})
|
||||
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)
|
||||
return jsonify({"success":True, "identifier":str(pre_existing["uuid"].as_uuid())}),200
|
||||
else:
|
||||
shared_conversations_collections.insert_one({
|
||||
"uuid":explicit_binary,
|
||||
"conversation_id": {
|
||||
@@ -532,7 +596,8 @@ def share_conversation():
|
||||
})
|
||||
## Identifier as route parameter in frontend
|
||||
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201
|
||||
except Exception as err:
|
||||
except Exception as err:
|
||||
print (err)
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
|
||||
#route to get publicly shared conversations
|
||||
@@ -554,13 +619,15 @@ def get_publicly_shared_conversations(identifier : str):
|
||||
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({
|
||||
res = {
|
||||
"success":True,
|
||||
"queries":conversation_queries,
|
||||
"title":conversation["name"],
|
||||
"timestamp":date
|
||||
}), 200
|
||||
}
|
||||
if(shared["isPromptable"] and "api_key" in shared):
|
||||
res["api_key"] = shared["api_key"]
|
||||
return jsonify(res), 200
|
||||
except Exception as err:
|
||||
print (err)
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
@@ -10,7 +10,7 @@ import { useState } from 'react';
|
||||
import Setting from './settings';
|
||||
import './locale/i18n';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import SharedConversation from './conversation/SharedConversation';
|
||||
import { SharedConversation } from './conversation/SharedConversation';
|
||||
import { useDarkTheme } from './hooks';
|
||||
inject();
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ function SourceDropdown({
|
||||
/>
|
||||
</button>
|
||||
{isDocsListOpen && (
|
||||
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-40 overflow-y-auto rounded-b-xl border border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal">
|
||||
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-28 overflow-y-auto rounded-b-xl border border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal">
|
||||
{options ? (
|
||||
options.map((option: any, index: number) => {
|
||||
if (option.model === embeddingsName) {
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function Conversation() {
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData('text/plain');
|
||||
document.execCommand('insertText', false, text);
|
||||
inputRef.current && (inputRef.current.innerText = text);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -206,7 +206,7 @@ export default function Conversation() {
|
||||
onClick={() => {
|
||||
setShareModalState(true);
|
||||
}}
|
||||
className="fixed top-4 right-20 z-30 rounded-full hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||
className="fixed top-4 right-20 z-0 rounded-full hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img
|
||||
className="m-2 h-5 w-5 filter dark:invert"
|
||||
@@ -289,7 +289,7 @@ export default function Conversation() {
|
||||
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
|
||||
></img>
|
||||
) : (
|
||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000">
|
||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
|
||||
<img
|
||||
className="ml-[4px] h-6 w-6 text-white "
|
||||
onClick={handleQuestionSubmission}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import Exit from '../assets/exit.svg';
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
@@ -38,6 +39,7 @@ export default function ConversationTile({
|
||||
const [conversationName, setConversationsName] = useState('');
|
||||
const [isOpen, setOpen] = useState<boolean>(false);
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [deleteModalState, setDeleteModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -77,128 +79,139 @@ export default function ConversationTile({
|
||||
setIsEdit(false);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={tileRef}
|
||||
onClick={() => {
|
||||
selectConversation(conversation.id);
|
||||
}}
|
||||
className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
||||
conversationId === conversation.id
|
||||
? 'bg-gray-100 dark:bg-[#28292E]'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={`flex ${
|
||||
conversationId === conversation.id ? 'w-[75%]' : 'w-[95%]'
|
||||
} gap-4`}
|
||||
ref={tileRef}
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false);
|
||||
}}
|
||||
onClick={() => {
|
||||
conversationId !== conversation.id &&
|
||||
selectConversation(conversation.id);
|
||||
}}
|
||||
className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
||||
conversationId === conversation.id || isOpen || isHovered
|
||||
? 'bg-gray-100 dark:bg-[#28292E]'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={isDarkTheme ? MessageDark : Message}
|
||||
className="ml-4 w-5 dark:text-white"
|
||||
/>
|
||||
{isEdit ? (
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
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)}
|
||||
<div className={`flex w-10/12 gap-4`}>
|
||||
<img
|
||||
src={isDarkTheme ? MessageDark : Message}
|
||||
className="ml-4 w-5 dark:text-white"
|
||||
/>
|
||||
) : (
|
||||
<p className="my-auto overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-normal leading-6 text-eerie-black dark:text-white">
|
||||
{conversationName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{conversationId === conversation.id && (
|
||||
<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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
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)}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
<p className="my-auto overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-normal leading-6 text-eerie-black dark:text-white">
|
||||
{conversationName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(conversationId === conversation.id || isHovered || isOpen) && (
|
||||
<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: SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
handleSaveConversation({
|
||||
id: conversation.id,
|
||||
name: conversationName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={Exit}
|
||||
alt="Exit"
|
||||
className={`mr-4 mt-px h-3 w-3 cursor-pointer filter hover:opacity-50 dark:invert`}
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event: SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={(event: SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
setOpen(true);
|
||||
}}
|
||||
className="mr-2 flex w-4 justify-center"
|
||||
>
|
||||
<img src={threeDots} width={8} />
|
||||
</button>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div className="flex-start absolute z-30 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={(event: SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
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={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: SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
message={t('convTile.deleteWarning')}
|
||||
modalState={deleteModalState}
|
||||
@@ -206,14 +219,15 @@ export default function ConversationTile({
|
||||
handleSubmit={() => onDeleteConversation(conversation.id)}
|
||||
submitLabel={t('convTile.delete')}
|
||||
/>
|
||||
{isShareModalOpen && conversationId && (
|
||||
{isShareModalOpen && (
|
||||
<ShareConversationModal
|
||||
close={() => {
|
||||
setShareModalState(false);
|
||||
isHovered && setIsHovered(false);
|
||||
}}
|
||||
conversationId={conversationId}
|
||||
conversationId={conversation.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,65 @@
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { Query } from './conversationModels';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import conversationService from '../api/services/conversationService';
|
||||
import ConversationBubble from './ConversationBubble';
|
||||
import { Query } from './conversationModels';
|
||||
import Send from '../assets/send.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import {
|
||||
selectClientAPIKey,
|
||||
setClientApiKey,
|
||||
updateQuery,
|
||||
addQuery,
|
||||
fetchSharedAnswer,
|
||||
selectStatus,
|
||||
} from './sharedConversationSlice';
|
||||
import { setIdentifier, setFetchedData } from './sharedConversationSlice';
|
||||
|
||||
export default function SharedConversation() {
|
||||
const params = useParams();
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AppDispatch } from '../store';
|
||||
|
||||
import {
|
||||
selectDate,
|
||||
selectTitle,
|
||||
selectQueries,
|
||||
} from './sharedConversationSlice';
|
||||
import { useSelector } from 'react-redux';
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
|
||||
export const SharedConversation = () => {
|
||||
const navigate = useNavigate();
|
||||
const { identifier } = params;
|
||||
const [queries, setQueries] = useState<Query[]>([]);
|
||||
const [title, setTitle] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
const { identifier } = useParams(); //identifier is a uuid, not conversationId
|
||||
|
||||
const queries = useSelector(selectQueries);
|
||||
const title = useSelector(selectTitle);
|
||||
const date = useSelector(selectDate);
|
||||
const apiKey = useSelector(selectClientAPIKey);
|
||||
const status = useSelector(selectStatus);
|
||||
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||
const [eventInterrupt, setEventInterrupt] = useState(false);
|
||||
const endMessageRef = useRef<HTMLDivElement>(null);
|
||||
const handleUserInterruption = () => {
|
||||
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
!eventInterrupt && scrollIntoView();
|
||||
}, [queries.length, queries[queries.length - 1]]);
|
||||
|
||||
useEffect(() => {
|
||||
identifier && dispatch(setIdentifier(identifier));
|
||||
const element = document.getElementById('inputbox') as HTMLInputElement;
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
function formatISODate(isoDateStr: string) {
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
@@ -46,27 +92,54 @@ export default function SharedConversation() {
|
||||
const formattedDate = `Published ${month} ${day}, ${year} at ${hours}:${minutesStr} ${ampm}`;
|
||||
return formattedDate;
|
||||
}
|
||||
const fetchQueris = () => {
|
||||
conversationService
|
||||
.getSharedConversation(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));
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
if (queries.length) {
|
||||
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
||||
queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry
|
||||
}
|
||||
}, [queries[queries.length - 1]]);
|
||||
|
||||
const scrollIntoView = () => {
|
||||
endMessageRef?.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
};
|
||||
|
||||
const fetchQueries = () => {
|
||||
identifier &&
|
||||
conversationService
|
||||
.getSharedConversation(identifier || '')
|
||||
.then((res) => {
|
||||
if (res.status === 404 || res.status === 400)
|
||||
navigate('/pagenotfound');
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
dispatch(
|
||||
setFetchedData({
|
||||
queries: data.queries,
|
||||
title: data.title,
|
||||
date: data.date,
|
||||
identifier,
|
||||
}),
|
||||
);
|
||||
data.api_key && dispatch(setClientApiKey(data.api_key));
|
||||
}
|
||||
});
|
||||
};
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData('text/plain');
|
||||
inputRef.current && (inputRef.current.innerText = text);
|
||||
};
|
||||
const prepResponseView = (query: Query, index: number) => {
|
||||
let responseView;
|
||||
if (query.response) {
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
|
||||
key={`${index}ANSWER`}
|
||||
message={query.response}
|
||||
@@ -76,6 +149,7 @@ export default function SharedConversation() {
|
||||
} else if (query.error) {
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
|
||||
key={`${index}ERROR`}
|
||||
message={query.error}
|
||||
@@ -85,8 +159,44 @@ export default function SharedConversation() {
|
||||
}
|
||||
return responseView;
|
||||
};
|
||||
const handleQuestionSubmission = () => {
|
||||
if (inputRef.current?.textContent && status !== 'loading') {
|
||||
if (lastQueryReturnedErr) {
|
||||
// update last failed query with new prompt
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: queries.length - 1,
|
||||
query: {
|
||||
prompt: inputRef.current.textContent,
|
||||
},
|
||||
}),
|
||||
);
|
||||
handleQuestion({
|
||||
question: queries[queries.length - 1].prompt,
|
||||
isRetry: true,
|
||||
});
|
||||
} else {
|
||||
handleQuestion({ question: inputRef.current.textContent });
|
||||
}
|
||||
inputRef.current.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuestion = ({
|
||||
question,
|
||||
isRetry = false,
|
||||
}: {
|
||||
question: string;
|
||||
isRetry?: boolean;
|
||||
}) => {
|
||||
question = question.trim();
|
||||
if (question === '') return;
|
||||
setEventInterrupt(false);
|
||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
||||
dispatch(fetchSharedAnswer({ question }));
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchQueris();
|
||||
fetchQueries();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -112,6 +222,7 @@ export default function SharedConversation() {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={'mb-1 last:mb-28 md:mb-7'}
|
||||
key={`${index}QUESTION`}
|
||||
message={query.prompt}
|
||||
@@ -127,17 +238,51 @@ export default function SharedConversation() {
|
||||
</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 className=" flex w-11/12 flex-col items-center gap-4 pb-2 md:w-10/12 lg:w-6/12">
|
||||
{apiKey ? (
|
||||
<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}
|
||||
tabIndex={1}
|
||||
onPaste={handlePaste}
|
||||
placeholder={t('inputPlaceholder')}
|
||||
contentEditable
|
||||
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleQuestionSubmission();
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
{status === 'loading' ? (
|
||||
<img
|
||||
src={Spinner}
|
||||
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent filter dark:invert"
|
||||
></img>
|
||||
) : (
|
||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
|
||||
<img
|
||||
onClick={handleQuestionSubmission}
|
||||
className="ml-[4px] h-6 w-6 text-white filter dark:invert"
|
||||
src={Send}
|
||||
></img>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
<span className="mb-2 hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
|
||||
{t('sharedConv.meta')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -211,3 +211,115 @@ export function handleSendFeedback(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function handleFetchSharedAnswerStreaming( //for shared conversations
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
apiKey: string,
|
||||
history: Array<any> = [],
|
||||
onEvent: (event: MessageEvent) => void,
|
||||
): Promise<Answer> {
|
||||
history = history.map((item) => {
|
||||
return { prompt: item.prompt, response: item.response };
|
||||
});
|
||||
|
||||
return new Promise<Answer>((resolve, reject) => {
|
||||
const payload = {
|
||||
question: question,
|
||||
history: JSON.stringify(history),
|
||||
api_key: apiKey,
|
||||
};
|
||||
conversationService
|
||||
.answerStream(payload, signal)
|
||||
.then((response) => {
|
||||
if (!response.body) throw Error('No response body');
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let counterrr = 0;
|
||||
const processStream = ({
|
||||
done,
|
||||
value,
|
||||
}: ReadableStreamReadResult<Uint8Array>) => {
|
||||
if (done) {
|
||||
console.log(counterrr);
|
||||
return;
|
||||
}
|
||||
|
||||
counterrr += 1;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.trim() == '') {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('data:')) {
|
||||
line = line.substring(5);
|
||||
}
|
||||
|
||||
const messageEvent: MessageEvent = new MessageEvent('message', {
|
||||
data: line,
|
||||
});
|
||||
|
||||
onEvent(messageEvent); // handle each message
|
||||
}
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
};
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Connection failed:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function handleFetchSharedAnswer(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
apiKey: string,
|
||||
): Promise<
|
||||
| {
|
||||
result: any;
|
||||
answer: any;
|
||||
sources: any;
|
||||
query: string;
|
||||
}
|
||||
| {
|
||||
result: any;
|
||||
answer: any;
|
||||
sources: any;
|
||||
query: string;
|
||||
title: any;
|
||||
}
|
||||
> {
|
||||
return conversationService
|
||||
.answer(
|
||||
{
|
||||
question: question,
|
||||
api_key: apiKey,
|
||||
},
|
||||
signal,
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
const result = data.answer;
|
||||
return {
|
||||
answer: result,
|
||||
query: question,
|
||||
result,
|
||||
sources: data.sources,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
229
frontend/src/conversation/sharedConversationSlice.ts
Normal file
229
frontend/src/conversation/sharedConversationSlice.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import store from '../store';
|
||||
import { Query, Status, Answer } from '../conversation/conversationModels';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import {
|
||||
handleFetchSharedAnswer,
|
||||
handleFetchSharedAnswerStreaming,
|
||||
} from './conversationHandlers';
|
||||
|
||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||
interface SharedConversationsType {
|
||||
queries: Query[];
|
||||
apiKey?: string;
|
||||
identifier: string;
|
||||
status: Status;
|
||||
date?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const initialState: SharedConversationsType = {
|
||||
queries: [],
|
||||
identifier: '',
|
||||
status: 'idle',
|
||||
};
|
||||
|
||||
export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
'shared/fetchAnswer',
|
||||
async ({ question }, { dispatch, getState, signal }) => {
|
||||
const state = getState() as RootState;
|
||||
|
||||
if (state.preference && state.sharedConversation.apiKey) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchSharedAnswerStreaming(
|
||||
question,
|
||||
signal,
|
||||
state.sharedConversation.apiKey,
|
||||
state.sharedConversation.queries,
|
||||
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
// check if the 'end' event has been received
|
||||
if (data.type === 'end') {
|
||||
// set status to 'idle'
|
||||
dispatch(sharedConversationSlice.actions.setStatus('idle'));
|
||||
dispatch(saveToLocalStorage());
|
||||
} else if (data.type === 'error') {
|
||||
// set status to 'failed'
|
||||
dispatch(sharedConversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
sharedConversationSlice.actions.raiseError({
|
||||
index: state.conversation.queries.length - 1,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const result = data.answer;
|
||||
dispatch(
|
||||
updateStreamingQuery({
|
||||
index: state.sharedConversation.queries.length - 1,
|
||||
query: { response: result },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const answer = await handleFetchSharedAnswer(
|
||||
question,
|
||||
signal,
|
||||
state.sharedConversation.apiKey,
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
sourcesPrepped = answer.sources.map((source: { title: string }) => {
|
||||
if (source && source.title) {
|
||||
const titleParts = source.title.split('/');
|
||||
return {
|
||||
...source,
|
||||
title: titleParts[titleParts.length - 1],
|
||||
};
|
||||
}
|
||||
return source;
|
||||
});
|
||||
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: state.sharedConversation.queries.length - 1,
|
||||
query: { response: answer.answer, sources: sourcesPrepped },
|
||||
}),
|
||||
);
|
||||
dispatch(sharedConversationSlice.actions.setStatus('idle'));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
conversationId: null,
|
||||
title: null,
|
||||
answer: '',
|
||||
query: question,
|
||||
result: '',
|
||||
sources: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const sharedConversationSlice = createSlice({
|
||||
name: 'sharedConversation',
|
||||
initialState,
|
||||
reducers: {
|
||||
setStatus(state, action: PayloadAction<Status>) {
|
||||
state.status = action.payload;
|
||||
},
|
||||
setIdentifier(state, action: PayloadAction<string>) {
|
||||
state.identifier = action.payload;
|
||||
},
|
||||
setFetchedData(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
queries: Query[];
|
||||
title: string;
|
||||
date: string;
|
||||
identifier: string;
|
||||
}>,
|
||||
) {
|
||||
const { queries, title, identifier, date } = action.payload;
|
||||
const previousQueriesStr = localStorage.getItem(identifier);
|
||||
const localySavedQueries: Query[] = previousQueriesStr
|
||||
? JSON.parse(previousQueriesStr)
|
||||
: [];
|
||||
state.queries = [...queries, ...localySavedQueries];
|
||||
state.title = title;
|
||||
state.date = date;
|
||||
state.identifier = identifier;
|
||||
},
|
||||
setClientApiKey(state, action: PayloadAction<string>) {
|
||||
state.apiKey = action.payload;
|
||||
},
|
||||
addQuery(state, action: PayloadAction<Query>) {
|
||||
state.queries.push(action.payload);
|
||||
},
|
||||
updateStreamingQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
if (query.response != undefined) {
|
||||
state.queries[index].response =
|
||||
(state.queries[index].response || '') + query.response;
|
||||
} else {
|
||||
state.queries[index] = {
|
||||
...state.queries[index],
|
||||
...query,
|
||||
};
|
||||
}
|
||||
},
|
||||
updateQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
state.queries[index] = {
|
||||
...state.queries[index],
|
||||
...query,
|
||||
};
|
||||
},
|
||||
raiseError(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; message: string }>,
|
||||
) {
|
||||
const { index, message } = action.payload;
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
saveToLocalStorage(state) {
|
||||
const previousQueriesStr = localStorage.getItem(state.identifier);
|
||||
previousQueriesStr
|
||||
? localStorage.setItem(
|
||||
state.identifier,
|
||||
JSON.stringify([
|
||||
...JSON.parse(previousQueriesStr),
|
||||
state.queries[state.queries.length - 1],
|
||||
]),
|
||||
)
|
||||
: localStorage.setItem(
|
||||
state.identifier,
|
||||
JSON.stringify([state.queries[state.queries.length - 1]]),
|
||||
);
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(fetchSharedAnswer.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
})
|
||||
.addCase(fetchSharedAnswer.rejected, (state, action) => {
|
||||
if (action.meta.aborted) {
|
||||
state.status = 'idle';
|
||||
return state;
|
||||
}
|
||||
state.status = 'failed';
|
||||
state.queries[state.queries.length - 1].error =
|
||||
'Something went wrong. Please check your internet connection.';
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setStatus,
|
||||
setIdentifier,
|
||||
setFetchedData,
|
||||
setClientApiKey,
|
||||
updateQuery,
|
||||
updateStreamingQuery,
|
||||
addQuery,
|
||||
saveToLocalStorage,
|
||||
} = sharedConversationSlice.actions;
|
||||
|
||||
export const selectStatus = (state: RootState) => state.conversation.status;
|
||||
export const selectClientAPIKey = (state: RootState) =>
|
||||
state.sharedConversation.apiKey;
|
||||
export const selectQueries = (state: RootState) =>
|
||||
state.sharedConversation.queries;
|
||||
export const selectTitle = (state: RootState) => state.sharedConversation.title;
|
||||
export const selectDate = (state: RootState) => state.sharedConversation.date;
|
||||
|
||||
type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
sharedConversationSlice;
|
||||
export default sharedConversationSlice.reducer;
|
||||
@@ -107,7 +107,8 @@
|
||||
"shareConv": {
|
||||
"label": "Create a public page to share",
|
||||
"note": "Source document, personal information and further conversation will remain private",
|
||||
"create": "Create"
|
||||
"create": "Create",
|
||||
"option": "Allow users to prompt further"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
"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"
|
||||
"create": "Crear",
|
||||
"option": "Permitir a los usuarios realizar más consultas."
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
"shareConv": {
|
||||
"label": "共有ページを作成して共有する",
|
||||
"note": "ソースドキュメント、個人情報、および以降の会話は非公開のままになります",
|
||||
"create": "作成"
|
||||
"create": "作成",
|
||||
"option": "ユーザーがより多くのクエリを実行できるようにします。"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
"shareConv": {
|
||||
"label": "创建用于分享的公共页面",
|
||||
"note": "源文档、个人信息和后续对话将保持私密",
|
||||
"create": "创建"
|
||||
"create": "创建",
|
||||
"option": "允许用户进行更多查询。"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
selectSourceDocs,
|
||||
selectSelectedDocs,
|
||||
selectChunks,
|
||||
selectPrompt,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import { Doc } from '../models/misc';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import Exit from '../assets/exit.svg';
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
const embeddingsName =
|
||||
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
||||
'huggingface_sentence-transformers/all-mpnet-base-v2';
|
||||
|
||||
type StatusType = 'loading' | 'idle' | 'fetched' | 'failed';
|
||||
|
||||
import conversationService from '../api/services/conversationService';
|
||||
import Exit from '../assets/exit.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
|
||||
export const ShareConversationModal = ({
|
||||
close,
|
||||
@@ -12,24 +27,84 @@ export const ShareConversationModal = ({
|
||||
close: () => void;
|
||||
conversationId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const domain = window.location.origin;
|
||||
|
||||
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 [allowPrompt, setAllowPrompt] = useState<boolean>(false);
|
||||
|
||||
const sourceDocs = useSelector(selectSourceDocs);
|
||||
const preSelectedDoc = useSelector(selectSelectedDocs);
|
||||
const selectedPrompt = useSelector(selectPrompt);
|
||||
const selectedChunk = useSelector(selectChunks);
|
||||
|
||||
const extractDocPaths = (docs: Doc[]) =>
|
||||
docs
|
||||
? docs
|
||||
.filter((doc) => doc.model === embeddingsName)
|
||||
.map((doc: Doc) => {
|
||||
let namePath = doc.name;
|
||||
if (doc.language === namePath) {
|
||||
namePath = '.project';
|
||||
}
|
||||
let docPath = 'default';
|
||||
if (doc.location === 'local') {
|
||||
docPath = 'local' + '/' + doc.name + '/';
|
||||
} else if (doc.location === 'remote') {
|
||||
docPath =
|
||||
doc.language +
|
||||
'/' +
|
||||
namePath +
|
||||
'/' +
|
||||
doc.version +
|
||||
'/' +
|
||||
doc.model +
|
||||
'/';
|
||||
}
|
||||
return {
|
||||
label: doc.name,
|
||||
value: docPath,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const [sourcePath, setSourcePath] = useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
} | null>(preSelectedDoc ? extractDocPaths([preSelectedDoc])[0] : null);
|
||||
|
||||
const handleCopyKey = (url: string) => {
|
||||
navigator.clipboard.writeText(url);
|
||||
setIsCopied(true);
|
||||
};
|
||||
|
||||
const togglePromptPermission = () => {
|
||||
setAllowPrompt(!allowPrompt);
|
||||
setStatus('idle');
|
||||
setIdentifier(null);
|
||||
};
|
||||
|
||||
const shareCoversationPublicly: (isPromptable: boolean) => void = (
|
||||
isPromptable = false,
|
||||
) => {
|
||||
setStatus('loading');
|
||||
const payload: {
|
||||
conversation_id: string;
|
||||
chunks?: string;
|
||||
prompt_id?: string;
|
||||
source?: string;
|
||||
} = { conversation_id: conversationId };
|
||||
if (isPromptable) {
|
||||
payload.chunks = selectedChunk;
|
||||
payload.prompt_id = selectedPrompt.id;
|
||||
sourcePath && (payload.source = sourcePath.value);
|
||||
}
|
||||
conversationService
|
||||
.shareConversation(isPromptable, { conversation_id: conversationId })
|
||||
.shareConversation(isPromptable, payload)
|
||||
.then((res) => {
|
||||
console.log(res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
@@ -40,8 +115,9 @@ export const ShareConversationModal = ({
|
||||
})
|
||||
.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="fixed top-0 left-0 z-40 flex h-screen w-screen cursor-default 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} />
|
||||
@@ -49,13 +125,52 @@ export const ShareConversationModal = ({
|
||||
<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-center justify-between">
|
||||
<span className="text-lg">{t('modals.shareConv.option')}</span>
|
||||
<label className=" cursor-pointer select-none items-center">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allowPrompt}
|
||||
onChange={togglePromptPermission}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div
|
||||
className={`box block h-8 w-14 rounded-full border border-purple-30 ${
|
||||
allowPrompt
|
||||
? 'bg-purple-30 dark:bg-purple-30'
|
||||
: 'dark:bg-transparent'
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
className={`absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full transition ${
|
||||
allowPrompt ? 'translate-x-full bg-silver' : 'bg-purple-30'
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{allowPrompt && (
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
placeholder={t('modals.createAPIKey.sourceDoc')}
|
||||
selectedValue={sourcePath}
|
||||
onSelect={(selection: { label: string; value: string }) =>
|
||||
setSourcePath(selection)
|
||||
}
|
||||
options={extractDocPaths(sourceDocs ?? [])}
|
||||
size="w-full"
|
||||
rounded="xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 py-3 px-4">
|
||||
{`${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"
|
||||
className="my-1 h-10 w-28 rounded-full border border-solid bg-purple-30 p-2 text-sm text-white hover:bg-[#6F3FD1]"
|
||||
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
|
||||
>
|
||||
{isCopied
|
||||
@@ -64,9 +179,9 @@ export const ShareConversationModal = ({
|
||||
</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"
|
||||
className="my-1 flex h-10 w-28 items-center justify-evenly rounded-full border border-solid bg-purple-30 p-2 text-center text-sm font-normal text-white hover:bg-[#6F3FD1]"
|
||||
onClick={() => {
|
||||
shareCoversationPublicly(false);
|
||||
shareCoversationPublicly(allowPrompt);
|
||||
}}
|
||||
>
|
||||
{t('modals.shareConv.create')}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { conversationSlice } from './conversation/conversationSlice';
|
||||
import { sharedConversationSlice } from './conversation/sharedConversationSlice';
|
||||
import {
|
||||
prefListenerMiddleware,
|
||||
prefSlice,
|
||||
@@ -42,6 +43,7 @@ const store = configureStore({
|
||||
reducer: {
|
||||
preference: prefSlice.reducer,
|
||||
conversation: conversationSlice.reducer,
|
||||
sharedConversation: sharedConversationSlice.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(prefListenerMiddleware.middleware),
|
||||
|
||||
Reference in New Issue
Block a user