complete_stream: no storing for api key req, fe:update shared conv

This commit is contained in:
ManishMadan2882
2024-07-24 02:01:32 +05:30
parent 0cf86d3bbc
commit 052669a0b0
7 changed files with 264 additions and 31 deletions

View File

@@ -189,13 +189,14 @@ def complete_stream(question, retriever, conversation_id, user_api_key):
llm = LLMCreator.create_llm( llm = LLMCreator.create_llm(
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
) )
conversation_id = save_conversation( if(user_api_key is None):
conversation_id, question, response_full, source_log_docs, llm 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 # send data.type = "end" to indicate that the stream has ended as json
data = json.dumps({"type": "id", "id": str(conversation_id)}) data = json.dumps({"type": "id", "id": str(conversation_id)})
yield f"data: {data}\n\n" yield f"data: {data}\n\n"
data = json.dumps({"type": "end"}) data = json.dumps({"type": "end"})
yield f"data: {data}\n\n" yield f"data: {data}\n\n"
except Exception as e: except Exception as e:

View File

@@ -77,7 +77,7 @@ function SourceDropdown({
/> />
</button> </button>
{isDocsListOpen && ( {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 ? (
options.map((option: any, index: number) => { options.map((option: any, index: number) => {
if (option.model === embeddingsName) { if (option.model === embeddingsName) {

View File

@@ -193,7 +193,7 @@ export default function Conversation() {
const handlePaste = (e: React.ClipboardEvent) => { const handlePaste = (e: React.ClipboardEvent) => {
e.preventDefault(); e.preventDefault();
const text = e.clipboardData.getData('text/plain'); const text = e.clipboardData.getData('text/plain');
document.execCommand('insertText', false, text); inputRef.current && (inputRef.current.innerText = text);
}; };
return ( return (

View File

@@ -1,9 +1,12 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Query } from './conversationModels'; import { Query } from './conversationModels';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ConversationBubble from './ConversationBubble'; import ConversationBubble from './ConversationBubble';
import Send from '../assets/send.svg';
import Spinner from '../assets/spinner.svg';
import { Fragment } from 'react'; import { Fragment } from 'react';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const SharedConversation = () => { const SharedConversation = () => {
@@ -13,6 +16,8 @@ const SharedConversation = () => {
const [queries, setQueries] = useState<Query[]>([]); const [queries, setQueries] = useState<Query[]>([]);
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [date, setDate] = useState(''); const [date, setDate] = useState('');
const [apiKey, setAPIKey] = useState<string | null>(null);
const inputRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
function formatISODate(isoDateStr: string) { function formatISODate(isoDateStr: string) {
const date = new Date(isoDateStr); const date = new Date(isoDateStr);
@@ -57,10 +62,15 @@ const SharedConversation = () => {
setQueries(data.queries); setQueries(data.queries);
setTitle(data.title); setTitle(data.title);
setDate(formatISODate(data.timestamp)); 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) => { const prepResponseView = (query: Query, index: number) => {
let responseView; let responseView;
if (query.response) { if (query.response) {
@@ -126,17 +136,51 @@ const SharedConversation = () => {
</div> </div>
</div> </div>
<div className=" flex flex-col items-center gap-4 pb-2"> <div className=" flex w-11/12 flex-col items-center gap-4 pb-2 md:w-10/12 lg:w-6/12">
<button {apiKey ? (
onClick={() => navigate('/')} <div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe" <div
> id="inputbox"
{t('sharedConv.button')} ref={inputRef}
</button> tabIndex={1}
<span className="hidden text-xs text-dark-charcoal dark:text-silver sm:inline"> onPaste={handlePaste}
{t('sharedConv.meta')} placeholder={t('inputPlaceholder')}
</span> 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">
<img
className="ml-[4px] h-6 w-6 text-white filter dark:invert"
//onClick={handleQuestionSubmission}
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> </div>
<span className="mb-2 hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
{t('sharedConv.meta')}
</span>
</div> </div>
); );
}; };

View File

@@ -233,3 +233,76 @@ export function sendFeedback(
} }
}); });
} }
export function fetchSharedAnswerSteaming( //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 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<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);
});
});
}

View File

@@ -1,8 +1,22 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; 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 Spinner from '../assets/spinner.svg';
import Exit from '../assets/exit.svg'; import Exit from '../assets/exit.svg';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; 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 = ({ export const ShareConversationModal = ({
close, close,
@@ -11,26 +25,87 @@ export const ShareConversationModal = ({
close: () => void; close: () => void;
conversationId: string; conversationId: string;
}) => { }) => {
const { t } = useTranslation();
const domain = window.location.origin;
const [identifier, setIdentifier] = useState<null | string>(null); const [identifier, setIdentifier] = useState<null | string>(null);
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
type StatusType = 'loading' | 'idle' | 'fetched' | 'failed';
const [status, setStatus] = useState<StatusType>('idle'); const [status, setStatus] = useState<StatusType>('idle');
const { t } = useTranslation(); const [allowPrompt, setAllowPrompt] = useState<boolean>(false);
const domain = window.location.origin;
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) => { const handleCopyKey = (url: string) => {
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url);
setIsCopied(true); setIsCopied(true);
}; };
const togglePromptPermission = () => {
setAllowPrompt(!allowPrompt);
setStatus('idle');
setIdentifier(null);
};
const shareCoversationPublicly: (isPromptable: boolean) => void = ( const shareCoversationPublicly: (isPromptable: boolean) => void = (
isPromptable = false, isPromptable = false,
) => { ) => {
setStatus('loading'); 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}`, { fetch(`${apiHost}/api/share?isPromptable=${isPromptable}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ conversation_id: conversationId }), body: JSON.stringify(payload),
}) })
.then((res) => { .then((res) => {
console.log(res.status); console.log(res.status);
@@ -44,6 +119,7 @@ export const ShareConversationModal = ({
}) })
.catch((err) => setStatus('failed')); .catch((err) => setStatus('failed'));
}; };
return ( 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-30 flex h-screen w-screen items-center justify-center bg-gray-alpha bg-opacity-50 text-chinese-black dark:text-silver">
<div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]"> <div className="relative w-11/12 rounded-2xl bg-white p-10 dark:bg-outer-space sm:w-[512px]">
@@ -53,13 +129,52 @@ export const ShareConversationModal = ({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h2 className="text-xl font-medium">{t('modals.shareConv.label')}</h2> <h2 className="text-xl font-medium">{t('modals.shareConv.label')}</h2>
<p className="text-sm">{t('modals.shareConv.note')}</p> <p className="text-sm">{t('modals.shareConv.note')}</p>
<div className="flex items-center justify-between">
<span className="text-lg">Allow users to prompt further</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"> <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/${ <span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 py-3 px-4">
identifier ?? '....' {`${domain}/share/${identifier ?? '....'}`}
}`}</span> </span>
{status === 'fetched' ? ( {status === 'fetched' ? (
<button <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 border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)} onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
> >
{isCopied {isCopied
@@ -68,9 +183,9 @@ export const ShareConversationModal = ({
</button> </button>
) : ( ) : (
<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 border-purple-30 p-2 text-center text-sm font-normal text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={() => { onClick={() => {
shareCoversationPublicly(false); shareCoversationPublicly(allowPrompt);
}} }}
> >
{t('modals.shareConv.create')} {t('modals.shareConv.create')}