Merge branch 'refactor/llm-handler' of https://github.com/siiddhantt/DocsGPT into refactor/llm-handler

This commit is contained in:
Siddhant Rai
2025-06-11 19:28:47 +05:30
23 changed files with 429 additions and 193 deletions

View File

@@ -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(() => {

View File

@@ -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';
@@ -60,6 +60,7 @@ const ConversationBubble = forwardRef<
updated?: boolean,
index?: number,
) => void;
filesAttached?: { id: string; fileName: string }[];
}
>(function ConversationBubble(
{
@@ -75,6 +76,7 @@ const ConversationBubble = forwardRef<
questionNumber,
isStreaming,
handleUpdatedQuestionSubmission,
filesAttached,
},
ref,
) {
@@ -106,41 +108,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}

View File

@@ -223,6 +223,7 @@ export default function ConversationMessages({
handleUpdatedQuestionSubmission={handleQuestionSubmission}
questionNumber={index}
sources={query.sources}
filesAttached={query.attachments}
/>
{renderResponseView(query, index)}
</Fragment>

View File

@@ -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(() => {

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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;
@@ -320,39 +328,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();
},
},
@@ -378,11 +358,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,
@@ -393,10 +368,8 @@ export const {
updateStreamingSource,
updateToolCall,
setConversation,
setAttachments,
addAttachment,
updateAttachment,
removeAttachment,
setStatus,
raiseError,
resetConversation,
} = conversationSlice.actions;
export default conversationSlice.reducer;

View File

@@ -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 = [];