mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-12-01 09:33:14 +00:00
Merge branch 'main' into refactor/llm-handler
This commit is contained in:
@@ -28,6 +28,10 @@ import {
|
||||
updateConversationId,
|
||||
updateQuery,
|
||||
} from './conversationSlice';
|
||||
import {
|
||||
selectCompletedAttachments,
|
||||
clearAttachments,
|
||||
} from '../upload/uploadSlice';
|
||||
|
||||
export default function Conversation() {
|
||||
const { t } = useTranslation();
|
||||
@@ -39,6 +43,7 @@ export default function Conversation() {
|
||||
const status = useSelector(selectStatus);
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
const completedAttachments = useSelector(selectCompletedAttachments);
|
||||
|
||||
const [uploadModalState, setUploadModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
@@ -107,15 +112,25 @@ export default function Conversation() {
|
||||
const trimmedQuestion = question.trim();
|
||||
if (trimmedQuestion === '') return;
|
||||
|
||||
const filesAttached = completedAttachments
|
||||
.filter((a) => a.id)
|
||||
.map((a) => ({ id: a.id as string, fileName: a.fileName }));
|
||||
|
||||
if (index !== undefined) {
|
||||
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
|
||||
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||
} else {
|
||||
if (!isRetry) dispatch(addQuery({ prompt: trimmedQuestion }));
|
||||
if (!isRetry)
|
||||
dispatch(
|
||||
addQuery({
|
||||
prompt: trimmedQuestion,
|
||||
attachments: filesAttached,
|
||||
}),
|
||||
);
|
||||
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||
}
|
||||
},
|
||||
[dispatch, handleFetchAnswer],
|
||||
[dispatch, handleFetchAnswer, completedAttachments],
|
||||
);
|
||||
|
||||
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
|
||||
@@ -178,6 +193,7 @@ export default function Conversation() {
|
||||
query: { conversationId: null },
|
||||
}),
|
||||
);
|
||||
dispatch(clearAttachments());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
import DocumentationDark from '../assets/documentation-dark.svg';
|
||||
import ChevronDown from '../assets/chevron-down.svg';
|
||||
import Cloud from '../assets/cloud.svg';
|
||||
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
|
||||
@@ -59,6 +59,7 @@ const ConversationBubble = forwardRef<
|
||||
updated?: boolean,
|
||||
index?: number,
|
||||
) => void;
|
||||
filesAttached?: { id: string; fileName: string }[];
|
||||
}
|
||||
>(function ConversationBubble(
|
||||
{
|
||||
@@ -74,6 +75,7 @@ const ConversationBubble = forwardRef<
|
||||
questionNumber,
|
||||
isStreaming,
|
||||
handleUpdatedQuestionSubmission,
|
||||
filesAttached,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
@@ -105,41 +107,66 @@ const ConversationBubble = forwardRef<
|
||||
<div
|
||||
onMouseEnter={() => setIsQuestionHovered(true)}
|
||||
onMouseLeave={() => setIsQuestionHovered(false)}
|
||||
className={className}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`flex flex-row-reverse justify-items-start ${className}`}
|
||||
>
|
||||
<Avatar
|
||||
size="SMALL"
|
||||
className="mt-2 flex-shrink-0 text-2xl"
|
||||
avatar={
|
||||
<img className="mr-1 rounded-full" width={30} src={UserIcon} />
|
||||
}
|
||||
/>
|
||||
{!isEditClicked && (
|
||||
<>
|
||||
<div className="mr-2 flex flex-col">
|
||||
<div className="flex flex-col items-end">
|
||||
{filesAttached && filesAttached.length > 0 && (
|
||||
<div className="mb-4 mr-12 flex flex-wrap justify-end gap-2">
|
||||
{filesAttached.map((file, index) => (
|
||||
<div
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
className="ml-2 mr-2 flex max-w-full items-center whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-[19px] py-[14px] text-sm leading-normal text-white sm:text-base"
|
||||
key={index}
|
||||
title={file.fileName}
|
||||
className="flex items-center rounded-xl bg-[#EFF3F4] p-2 text-[14px] text-[#5D5D5D] dark:bg-[#393B3D] dark:text-bright-gray"
|
||||
>
|
||||
{message}
|
||||
<div className="mr-2 items-center justify-center rounded-lg bg-purple-30 p-[5.5px]">
|
||||
<img
|
||||
src={DocumentationDark}
|
||||
alt="Attachment"
|
||||
className="h-[15px] w-[15px] object-fill"
|
||||
/>
|
||||
</div>
|
||||
<span className="max-w-[150px] truncate font-normal">
|
||||
{file.fileName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditClicked(true);
|
||||
setEditInputBox(message ?? '');
|
||||
}}
|
||||
className={`mt-3 flex h-fit flex-shrink-0 cursor-pointer items-center rounded-full p-2 hover:bg-light-silver dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
|
||||
>
|
||||
<img src={Edit} alt="Edit" className="cursor-pointer" />
|
||||
</button>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={ref}
|
||||
className={`flex flex-row-reverse justify-items-start`}
|
||||
>
|
||||
<Avatar
|
||||
size="SMALL"
|
||||
className="mt-2 flex-shrink-0 text-2xl"
|
||||
avatar={
|
||||
<img className="mr-1 rounded-full" width={30} src={UserIcon} />
|
||||
}
|
||||
/>
|
||||
{!isEditClicked && (
|
||||
<>
|
||||
<div className="mr-2 flex flex-col">
|
||||
<div
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
className="ml-2 mr-2 flex max-w-full items-center whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-[19px] py-[14px] text-sm leading-normal text-white sm:text-base"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditClicked(true);
|
||||
setEditInputBox(message ?? '');
|
||||
}}
|
||||
className={`mt-3 flex h-fit flex-shrink-0 cursor-pointer items-center rounded-full p-2 hover:bg-light-silver dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
|
||||
>
|
||||
<img src={Edit} alt="Edit" className="cursor-pointer" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isEditClicked && (
|
||||
<div
|
||||
ref={editableQueryRef}
|
||||
|
||||
@@ -223,6 +223,7 @@ export default function ConversationMessages({
|
||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||
questionNumber={index}
|
||||
sources={query.sources}
|
||||
filesAttached={query.attachments}
|
||||
/>
|
||||
{renderResponseView(query, index)}
|
||||
</Fragment>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
setIdentifier,
|
||||
updateQuery,
|
||||
} from './sharedConversationSlice';
|
||||
import { selectCompletedAttachments } from '../upload/uploadSlice';
|
||||
|
||||
export const SharedConversation = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -34,6 +35,7 @@ export const SharedConversation = () => {
|
||||
const date = useSelector(selectDate);
|
||||
const apiKey = useSelector(selectClientAPIKey);
|
||||
const status = useSelector(selectStatus);
|
||||
const completedAttachments = useSelector(selectCompletedAttachments);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
@@ -106,7 +108,19 @@ export const SharedConversation = () => {
|
||||
}) => {
|
||||
question = question.trim();
|
||||
if (question === '') return;
|
||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
||||
|
||||
const filesAttached = completedAttachments
|
||||
.filter((a) => a.id)
|
||||
.map((a) => ({ id: a.id as string, fileName: a.fileName }));
|
||||
|
||||
!isRetry &&
|
||||
dispatch(
|
||||
addQuery({
|
||||
prompt: question,
|
||||
attachments: filesAttached,
|
||||
}),
|
||||
); //dispatch only new queries
|
||||
|
||||
dispatch(fetchSharedAnswer({ question }));
|
||||
};
|
||||
useEffect(() => {
|
||||
|
||||
@@ -279,11 +279,12 @@ export function handleSendFeedback(
|
||||
});
|
||||
}
|
||||
|
||||
export function handleFetchSharedAnswerStreaming( //for shared conversations
|
||||
export function handleFetchSharedAnswerStreaming(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
apiKey: string,
|
||||
history: Array<any> = [],
|
||||
attachments: string[] = [],
|
||||
onEvent: (event: MessageEvent) => void,
|
||||
): Promise<Answer> {
|
||||
history = history.map((item) => {
|
||||
@@ -300,6 +301,7 @@ export function handleFetchSharedAnswerStreaming( //for shared conversations
|
||||
history: JSON.stringify(history),
|
||||
api_key: apiKey,
|
||||
save_conversation: false,
|
||||
attachments: attachments.length > 0 ? attachments : undefined,
|
||||
};
|
||||
conversationService
|
||||
.answerStream(payload, null, signal)
|
||||
@@ -355,6 +357,7 @@ export function handleFetchSharedAnswer(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
apiKey: string,
|
||||
attachments?: string[],
|
||||
): Promise<
|
||||
| {
|
||||
result: any;
|
||||
@@ -370,15 +373,15 @@ export function handleFetchSharedAnswer(
|
||||
title: any;
|
||||
}
|
||||
> {
|
||||
const payload = {
|
||||
question: question,
|
||||
api_key: apiKey,
|
||||
attachments:
|
||||
attachments && attachments.length > 0 ? attachments : undefined,
|
||||
};
|
||||
|
||||
return conversationService
|
||||
.answer(
|
||||
{
|
||||
question: question,
|
||||
api_key: apiKey,
|
||||
},
|
||||
null,
|
||||
signal,
|
||||
)
|
||||
.answer(payload, null, signal)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface ConversationState {
|
||||
queries: Query[];
|
||||
status: Status;
|
||||
conversationId: string | null;
|
||||
attachments: Attachment[];
|
||||
}
|
||||
|
||||
export interface Answer {
|
||||
@@ -46,7 +45,7 @@ export interface Query {
|
||||
sources?: { title: string; text: string; link: string }[];
|
||||
tool_calls?: ToolCallsType[];
|
||||
error?: string;
|
||||
attachments?: { fileName: string; id: string }[];
|
||||
attachments?: { id: string; fileName: string }[];
|
||||
}
|
||||
|
||||
export interface RetrievalPayload {
|
||||
|
||||
@@ -3,16 +3,20 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { getConversations } from '../preferences/preferenceApi';
|
||||
import { setConversations } from '../preferences/preferenceSlice';
|
||||
import store from '../store';
|
||||
import {
|
||||
clearAttachments,
|
||||
selectCompletedAttachments,
|
||||
} from '../upload/uploadSlice';
|
||||
import {
|
||||
handleFetchAnswer,
|
||||
handleFetchAnswerSteaming,
|
||||
} from './conversationHandlers';
|
||||
import {
|
||||
Answer,
|
||||
Attachment,
|
||||
ConversationState,
|
||||
Query,
|
||||
Status,
|
||||
ConversationState,
|
||||
Attachment,
|
||||
} from './conversationModels';
|
||||
import { ToolCallsType } from './types';
|
||||
|
||||
@@ -20,7 +24,6 @@ const initialState: ConversationState = {
|
||||
queries: [],
|
||||
status: 'idle',
|
||||
conversationId: null,
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||
@@ -45,9 +48,14 @@ export const fetchAnswer = createAsyncThunk<
|
||||
|
||||
let isSourceUpdated = false;
|
||||
const state = getState() as RootState;
|
||||
const attachmentIds = state.conversation.attachments
|
||||
.filter((a) => a.id && a.status === 'completed')
|
||||
const attachmentIds = selectCompletedAttachments(state)
|
||||
.filter((a) => a.id)
|
||||
.map((a) => a.id) as string[];
|
||||
|
||||
if (attachmentIds.length > 0) {
|
||||
dispatch(clearAttachments());
|
||||
}
|
||||
|
||||
const currentConversationId = state.conversation.conversationId;
|
||||
const conversationIdToSend = isPreview ? null : currentConversationId;
|
||||
const save_conversation = isPreview ? false : true;
|
||||
@@ -319,39 +327,11 @@ export const conversationSlice = createSlice({
|
||||
const { index, message } = action.payload;
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
setAttachments: (state, action: PayloadAction<Attachment[]>) => {
|
||||
state.attachments = action.payload;
|
||||
},
|
||||
addAttachment: (state, action: PayloadAction<Attachment>) => {
|
||||
state.attachments.push(action.payload);
|
||||
},
|
||||
updateAttachment: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
taskId: string;
|
||||
updates: Partial<Attachment>;
|
||||
}>,
|
||||
) => {
|
||||
const index = state.attachments.findIndex(
|
||||
(att) => att.taskId === action.payload.taskId,
|
||||
);
|
||||
if (index !== -1) {
|
||||
state.attachments[index] = {
|
||||
...state.attachments[index],
|
||||
...action.payload.updates,
|
||||
};
|
||||
}
|
||||
},
|
||||
removeAttachment: (state, action: PayloadAction<string>) => {
|
||||
state.attachments = state.attachments.filter(
|
||||
(att) => att.taskId !== action.payload && att.id !== action.payload,
|
||||
);
|
||||
},
|
||||
|
||||
resetConversation: (state) => {
|
||||
state.queries = initialState.queries;
|
||||
state.status = initialState.status;
|
||||
state.conversationId = initialState.conversationId;
|
||||
state.attachments = initialState.attachments;
|
||||
handleAbort();
|
||||
},
|
||||
},
|
||||
@@ -377,11 +357,6 @@ export const selectQueries = (state: RootState) => state.conversation.queries;
|
||||
|
||||
export const selectStatus = (state: RootState) => state.conversation.status;
|
||||
|
||||
export const selectAttachments = (state: RootState) =>
|
||||
state.conversation.attachments;
|
||||
export const selectCompletedAttachments = (state: RootState) =>
|
||||
state.conversation.attachments.filter((att) => att.status === 'completed');
|
||||
|
||||
export const {
|
||||
addQuery,
|
||||
updateQuery,
|
||||
@@ -392,10 +367,8 @@ export const {
|
||||
updateStreamingSource,
|
||||
updateToolCall,
|
||||
setConversation,
|
||||
setAttachments,
|
||||
addAttachment,
|
||||
updateAttachment,
|
||||
removeAttachment,
|
||||
setStatus,
|
||||
raiseError,
|
||||
resetConversation,
|
||||
} = conversationSlice.actions;
|
||||
export default conversationSlice.reducer;
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
handleFetchSharedAnswer,
|
||||
handleFetchSharedAnswerStreaming,
|
||||
} from './conversationHandlers';
|
||||
import {
|
||||
selectCompletedAttachments,
|
||||
clearAttachments,
|
||||
} from '../upload/uploadSlice';
|
||||
|
||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||
interface SharedConversationsType {
|
||||
@@ -29,6 +33,14 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
async ({ question }, { dispatch, getState, signal }) => {
|
||||
const state = getState() as RootState;
|
||||
|
||||
const attachmentIds = selectCompletedAttachments(state)
|
||||
.filter((a) => a.id)
|
||||
.map((a) => a.id) as string[];
|
||||
|
||||
if (attachmentIds.length > 0) {
|
||||
dispatch(clearAttachments());
|
||||
}
|
||||
|
||||
if (state.preference && state.sharedConversation.apiKey) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchSharedAnswerStreaming(
|
||||
@@ -36,7 +48,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
signal,
|
||||
state.sharedConversation.apiKey,
|
||||
state.sharedConversation.queries,
|
||||
|
||||
attachmentIds,
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
// check if the 'end' event has been received
|
||||
@@ -92,6 +104,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
question,
|
||||
signal,
|
||||
state.sharedConversation.apiKey,
|
||||
attachmentIds,
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
|
||||
Reference in New Issue
Block a user