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/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 1b53ab2f..dddd945e 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -193,7 +193,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 ( diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index e365c6f0..9a2d8d16 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -1,9 +1,12 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } 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 Send from '../assets/send.svg'; +import Spinner from '../assets/spinner.svg'; + import { Fragment } from 'react'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const SharedConversation = () => { @@ -13,6 +16,8 @@ const SharedConversation = () => { const [queries, setQueries] = useState([]); const [title, setTitle] = useState(''); const [date, setDate] = useState(''); + const [apiKey, setAPIKey] = useState(null); + const inputRef = useRef(null); const { t } = useTranslation(); function formatISODate(isoDateStr: string) { const date = new Date(isoDateStr); @@ -57,10 +62,15 @@ const SharedConversation = () => { setQueries(data.queries); setTitle(data.title); setDate(formatISODate(data.timestamp)); + data.api_key && setAPIKey(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) { @@ -126,17 +136,51 @@ const SharedConversation = () => {
-
- - - {t('sharedConv.meta')} - +
+ {apiKey ? ( +
+
{ + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + //handleQuestionSubmission(); + } + }} + >
+ {status === 'loading' ? ( + + ) : ( +
+ +
+ )} +
+ ) : ( + + )}
+ + {t('sharedConv.meta')} +
); }; diff --git a/frontend/src/conversation/conversationApi.ts b/frontend/src/conversation/conversationApi.ts index e107abc8..3010a63d 100644 --- a/frontend/src/conversation/conversationApi.ts +++ b/frontend/src/conversation/conversationApi.ts @@ -233,3 +233,76 @@ export function sendFeedback( } }); } + +export function fetchSharedAnswerSteaming( //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 body = { + question: question, + history: JSON.stringify(history), + apiKey: apiKey, + }; + fetch(apiHost + '/stream', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + 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); + }); + }); +} diff --git a/frontend/src/conversation/sharedConversationSlice.ts b/frontend/src/conversation/sharedConversationSlice.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 37da934d..cffe477b 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -1,8 +1,22 @@ 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'; export const ShareConversationModal = ({ close, @@ -11,26 +25,87 @@ 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); + } fetch(`${apiHost}/api/share?isPromptable=${isPromptable}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ conversation_id: conversationId }), + body: JSON.stringify(payload), }) .then((res) => { console.log(res.status); @@ -44,6 +119,7 @@ export const ShareConversationModal = ({ }) .catch((err) => setStatus('failed')); }; + return (
@@ -53,13 +129,52 @@ export const ShareConversationModal = ({

{t('modals.shareConv.label')}

{t('modals.shareConv.note')}

+
+ Allow users to prompt further + +
+ {allowPrompt && ( +
+ + setSourcePath(selection) + } + options={extractDocPaths(sourceDocs ?? [])} + size="w-full" + rounded="xl" + /> +
+ )}
- {`${domain}/share/${ - identifier ?? '....' - }`} + + {`${domain}/share/${identifier ?? '....'}`} + {status === 'fetched' ? ( ) : (