diff --git a/.gitignore b/.gitignore
index ac5ff190..f5480882 100644
--- a/.gitignore
+++ b/.gitignore
@@ -172,5 +172,5 @@ application/vectors/
node_modules/
.vscode/settings.json
-models/
+/models/
model/
diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py
index 9559133c..7eed8434 100644
--- a/application/api/answer/routes.py
+++ b/application/api/answer/routes.py
@@ -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:
diff --git a/application/api/user/routes.py b/application/api/user/routes.py
index e67eb992..a26ddc51 100644
--- a/application/api/user/routes.py
+++ b/application/api/user/routes.py
@@ -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
-
\ No newline at end of file
+ return jsonify({"success":False,"error":str(err)}),400
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 05792187..9bad8724 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -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();
diff --git a/frontend/src/components/SourceDropdown.tsx b/frontend/src/components/SourceDropdown.tsx
index 6983fe76..ce130b4d 100644
--- a/frontend/src/components/SourceDropdown.tsx
+++ b/frontend/src/components/SourceDropdown.tsx
@@ -77,7 +77,7 @@ function SourceDropdown({
/>
{isDocsListOpen && (
-
+
{options ? (
options.map((option: any, index: number) => {
if (option.model === embeddingsName) {
diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx
index e6406dd0..1f57fee3 100644
--- a/frontend/src/conversation/Conversation.tsx
+++ b/frontend/src/conversation/Conversation.tsx
@@ -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]"
>
![]()
) : (
-
+
![]()
(false);
const [isShareModalOpen, setShareModalState] = useState
(false);
+ const [isHovered, setIsHovered] = useState(false);
const [deleteModalState, setDeleteModalState] =
useState('INACTIVE');
const menuRef = useRef(null);
@@ -77,128 +79,139 @@ export default function ConversationTile({
setIsEdit(false);
}
return (
- {
- 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]'
- : ''
- }`}
- >
+ <>
{
+ 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]'
+ : ''
+ }`}
>
-

- {isEdit ? (
-
setConversationsName(e.target.value)}
+
+

- ) : (
-
- {conversationName}
-
- )}
-
- {conversationId === conversation.id && (
-
{isEdit ? (
-
-

{
- event.stopPropagation();
- handleSaveConversation({
- id: conversationId,
- name: conversationName,
- });
- }}
- />
-

{
- event.stopPropagation();
- onClear();
- }}
- />
-
+
setConversationsName(e.target.value)}
+ />
) : (
-
- )}
- {isOpen && (
-
-
-
-
-
+
+ {conversationName}
+
)}
- )}
+ {(conversationId === conversation.id || isHovered || isOpen) && (
+
+ {isEdit ? (
+
+

{
+ event.stopPropagation();
+ handleSaveConversation({
+ id: conversation.id,
+ name: conversationName,
+ });
+ }}
+ />
+

{
+ event.stopPropagation();
+ onClear();
+ }}
+ />
+
+ ) : (
+
+ )}
+ {isOpen && (
+
+
+
+
+
+ )}
+
+ )}
+
onDeleteConversation(conversation.id)}
submitLabel={t('convTile.delete')}
/>
- {isShareModalOpen && conversationId && (
+ {isShareModalOpen && (
{
setShareModalState(false);
+ isHovered && setIsHovered(false);
}}
- conversationId={conversationId}
+ conversationId={conversation.id}
/>
)}
-
+ >
);
}
diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx
index 557ef7f8..cbe80717 100644
--- a/frontend/src/conversation/SharedConversation.tsx
+++ b/frontend/src/conversation/SharedConversation.tsx
@@ -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([]);
- 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(null);
const { t } = useTranslation();
+ const dispatch = useDispatch();
+
+ const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
+ const [eventInterrupt, setEventInterrupt] = useState(false);
+ const endMessageRef = useRef(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 = (
{
+ 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 (
-
-
-
- {t('sharedConv.meta')}
-
+
+ {apiKey ? (
+
+
{
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleQuestionSubmission();
+ }
+ }}
+ >
+ {status === 'loading' ? (
+

+ ) : (
+
+

+
+ )}
+
+ ) : (
+
+ )}
+
+ {t('sharedConv.meta')}
+
);
-}
+};
diff --git a/frontend/src/conversation/conversationHandlers.ts b/frontend/src/conversation/conversationHandlers.ts
index 52f29f82..90bbc0a9 100644
--- a/frontend/src/conversation/conversationHandlers.ts
+++ b/frontend/src/conversation/conversationHandlers.ts
@@ -211,3 +211,115 @@ export function handleSendFeedback(
}
});
}
+
+export function handleFetchSharedAnswerStreaming( //for shared conversations
+ question: string,
+ signal: AbortSignal,
+ apiKey: string,
+ history: Array
= [],
+ onEvent: (event: MessageEvent) => void,
+): Promise {
+ history = history.map((item) => {
+ return { prompt: item.prompt, response: item.response };
+ });
+
+ return new Promise((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) => {
+ 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,
+ };
+ });
+}
diff --git a/frontend/src/conversation/sharedConversationSlice.ts b/frontend/src/conversation/sharedConversationSlice.ts
new file mode 100644
index 00000000..6d9ec936
--- /dev/null
+++ b/frontend/src/conversation/sharedConversationSlice.ts
@@ -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(
+ '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) {
+ state.status = action.payload;
+ },
+ setIdentifier(state, action: PayloadAction) {
+ 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) {
+ state.apiKey = action.payload;
+ },
+ addQuery(state, action: PayloadAction) {
+ state.queries.push(action.payload);
+ },
+ updateStreamingQuery(
+ state,
+ action: PayloadAction<{ index: number; query: Partial }>,
+ ) {
+ 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 }>,
+ ) {
+ 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;
+
+sharedConversationSlice;
+export default sharedConversationSlice.reducer;
diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json
index 0f5aa708..c7651f8b 100644
--- a/frontend/src/locale/en.json
+++ b/frontend/src/locale/en.json
@@ -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": {
diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json
index b91b67e9..d368749f 100644
--- a/frontend/src/locale/es.json
+++ b/frontend/src/locale/es.json
@@ -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": {
diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json
index 75e62589..350faf09 100644
--- a/frontend/src/locale/jp.json
+++ b/frontend/src/locale/jp.json
@@ -107,7 +107,8 @@
"shareConv": {
"label": "共有ページを作成して共有する",
"note": "ソースドキュメント、個人情報、および以降の会話は非公開のままになります",
- "create": "作成"
+ "create": "作成",
+ "option": "ユーザーがより多くのクエリを実行できるようにします。"
}
},
"sharedConv": {
diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json
index e46fe254..db93dcfc 100644
--- a/frontend/src/locale/zh.json
+++ b/frontend/src/locale/zh.json
@@ -107,7 +107,8 @@
"shareConv": {
"label": "创建用于分享的公共页面",
"note": "源文档、个人信息和后续对话将保持私密",
- "create": "创建"
+ "create": "创建",
+ "option": "允许用户进行更多查询。"
}
},
"sharedConv": {
diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx
index a43e4326..c7ef0ad6 100644
--- a/frontend/src/modals/ShareConversationModal.tsx
+++ b/frontend/src/modals/ShareConversationModal.tsx
@@ -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);
const [isCopied, setIsCopied] = useState(false);
- type StatusType = 'loading' | 'idle' | 'fetched' | 'failed';
const [status, setStatus] = useState('idle');
- const { t } = useTranslation();
- const domain = window.location.origin;
+ const [allowPrompt, setAllowPrompt] = useState(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 (
-
+