mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge branch 'refactor/llm-handler' of https://github.com/siiddhantt/DocsGPT into refactor/llm-handler
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.16669 11.5H9.83335V13.1666H8.16669V11.5ZM8.16669 4.83329H9.83335V9.83329H8.16669V4.83329ZM8.99169 0.666626C4.39169 0.666626 0.666687 4.39996 0.666687 8.99996C0.666687 13.6 4.39169 17.3333 8.99169 17.3333C13.6 17.3333 17.3334 13.6 17.3334 8.99996C17.3334 4.39996 13.6 0.666626 8.99169 0.666626ZM9.00002 15.6666C5.31669 15.6666 2.33335 12.6833 2.33335 8.99996C2.33335 5.31663 5.31669 2.33329 9.00002 2.33329C12.6834 2.33329 15.6667 5.31663 15.6667 8.99996C15.6667 12.6833 12.6834 15.6666 9.00002 15.6666Z" fill="#F44336"/>
|
||||
<path d="M8.16669 11.5H9.83335V13.1666H8.16669V11.5ZM8.16669 4.83329H9.83335V9.83329H8.16669V4.83329ZM8.99169 0.666626C4.39169 0.666626 0.666687 4.39996 0.666687 8.99996C0.666687 13.6 4.39169 17.3333 8.99169 17.3333C13.6 17.3333 17.3334 13.6 17.3334 8.99996C17.3334 4.39996 13.6 0.666626 8.99169 0.666626ZM9.00002 15.6666C5.31669 15.6666 2.33335 12.6833 2.33335 8.99996C2.33335 5.31663 5.31669 2.33329 9.00002 2.33329C12.6834 2.33329 15.6667 5.31663 15.6667 8.99996C15.6667 12.6833 12.6834 15.6666 9.00002 15.6666Z" fill="#ECECF1"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 636 B After Width: | Height: | Size: 636 B |
@@ -9,6 +9,7 @@ import ClipIcon from '../assets/clip.svg';
|
||||
import ExitIcon from '../assets/exit.svg';
|
||||
import PaperPlane from '../assets/paper_plane.svg';
|
||||
import SourceIcon from '../assets/source.svg';
|
||||
import DocumentationDark from '../assets/documentation-dark.svg';
|
||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import ToolIcon from '../assets/tool.svg';
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
removeAttachment,
|
||||
selectAttachments,
|
||||
updateAttachment,
|
||||
} from '../conversation/conversationSlice';
|
||||
} from '../upload/uploadSlice';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import {
|
||||
@@ -262,71 +263,81 @@ export default function MessageInput({
|
||||
{attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`group relative flex items-center rounded-[32px] border border-[#AAAAAA] bg-white px-2 py-1 text-[12px] text-[#5D5D5D] dark:border-purple-taupe dark:bg-[#1F2028] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
|
||||
className={`group relative flex items-center rounded-xl bg-[#EFF3F4] px-2 py-1 text-[12px] text-[#5D5D5D] dark:bg-[#393B3D] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
|
||||
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
|
||||
}`}
|
||||
title={attachment.fileName}
|
||||
>
|
||||
<div className="mr-2 items-center justify-center rounded-lg bg-purple-30 p-[5.5px]">
|
||||
{attachment.status === 'completed' && (
|
||||
<img
|
||||
src={DocumentationDark}
|
||||
alt="Attachment"
|
||||
className="h-[15px] w-[15px] object-fill"
|
||||
/>
|
||||
)}
|
||||
|
||||
{attachment.status === 'failed' && (
|
||||
<img
|
||||
src={AlertIcon}
|
||||
alt="Failed"
|
||||
className="h-[15px] w-[15px] object-fill"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(attachment.status === 'uploading' ||
|
||||
attachment.status === 'processing') && (
|
||||
<div className="flex h-[15px] w-[15px] items-center justify-center">
|
||||
<svg className="h-[15px] w-[15px]" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-0"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="transparent"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
/>
|
||||
<circle
|
||||
className="text-[#ECECF1]"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
strokeDasharray="62.83"
|
||||
strokeDashoffset={
|
||||
62.83 * (1 - attachment.progress / 100)
|
||||
}
|
||||
transform="rotate(-90 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
|
||||
{attachment.fileName}
|
||||
</span>
|
||||
|
||||
{attachment.status === 'completed' && (
|
||||
<button
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-white p-1 opacity-0 transition-opacity hover:bg-white/95 focus:opacity-100 group-hover:opacity-100 dark:bg-[#1F2028] dark:hover:bg-[#1F2028]/95"
|
||||
onClick={() => {
|
||||
if (attachment.id) {
|
||||
dispatch(removeAttachment(attachment.id));
|
||||
}
|
||||
}}
|
||||
aria-label={t('conversation.attachments.remove')}
|
||||
>
|
||||
<img
|
||||
src={ExitIcon}
|
||||
alt={t('conversation.attachments.remove')}
|
||||
className="h-2.5 w-2.5 filter dark:invert"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{attachment.status === 'failed' && (
|
||||
<button
|
||||
className="ml-1.5 flex items-center justify-center rounded-full p-1"
|
||||
onClick={() => {
|
||||
if (attachment.id) {
|
||||
dispatch(removeAttachment(attachment.id));
|
||||
} else if (attachment.taskId) {
|
||||
dispatch(removeAttachment(attachment.taskId));
|
||||
}
|
||||
}}
|
||||
aria-label={t('conversation.attachments.remove')}
|
||||
>
|
||||
<img
|
||||
src={AlertIcon}
|
||||
alt="Upload failed"
|
||||
className="ml-2 h-3.5 w-3.5"
|
||||
title="Upload failed"
|
||||
src={ExitIcon}
|
||||
alt={t('conversation.attachments.remove')}
|
||||
className="h-2.5 w-2.5 filter dark:invert"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(attachment.status === 'uploading' ||
|
||||
attachment.status === 'processing') && (
|
||||
<div className="relative ml-2 h-4 w-4">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
className="text-gray-200 dark:text-gray-700"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
/>
|
||||
<circle
|
||||
className="text-blue-600 dark:text-blue-400"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
strokeDasharray="62.83"
|
||||
strokeDashoffset={62.83 * (1 - attachment.progress / 100)}
|
||||
transform="rotate(-90 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -245,7 +245,8 @@
|
||||
"promptName": "Prompt Name",
|
||||
"promptText": "Prompt Text",
|
||||
"save": "Save",
|
||||
"nameExists": "Name already exists"
|
||||
"nameExists": "Name already exists",
|
||||
"deleteConfirmation": "Are you sure you want to delete the prompt '{{name}}'?"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "Add Chunk",
|
||||
|
||||
@@ -245,7 +245,8 @@
|
||||
"promptName": "Nombre del Prompt",
|
||||
"promptText": "Texto del Prompt",
|
||||
"save": "Guardar",
|
||||
"nameExists": "El nombre ya existe"
|
||||
"nameExists": "El nombre ya existe",
|
||||
"deleteConfirmation": "¿Estás seguro de que deseas eliminar el prompt '{{name}}'?"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "Agregar Fragmento",
|
||||
|
||||
@@ -245,7 +245,8 @@
|
||||
"promptName": "プロンプト名",
|
||||
"promptText": "プロンプトテキスト",
|
||||
"save": "保存",
|
||||
"nameExists": "名前が既に存在します"
|
||||
"nameExists": "名前が既に存在します",
|
||||
"deleteConfirmation": "プロンプト「{{name}}」を削除してもよろしいですか?"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "チャンクを追加",
|
||||
|
||||
@@ -245,7 +245,8 @@
|
||||
"promptName": "Название подсказки",
|
||||
"promptText": "Текст подсказки",
|
||||
"save": "Сохранить",
|
||||
"nameExists": "Название уже существует"
|
||||
"nameExists": "Название уже существует",
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить подсказку «{{name}}»?"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "Добавить фрагмент",
|
||||
|
||||
@@ -245,7 +245,8 @@
|
||||
"promptName": "提示名稱",
|
||||
"promptText": "提示文字",
|
||||
"save": "儲存",
|
||||
"nameExists": "名稱已存在"
|
||||
"nameExists": "名稱已存在",
|
||||
"deleteConfirmation": "您確定要刪除提示「{{name}}」嗎?"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "新增區塊",
|
||||
|
||||
@@ -245,7 +245,8 @@
|
||||
"promptName": "提示名称",
|
||||
"promptText": "提示文本",
|
||||
"save": "保存",
|
||||
"nameExists": "名称已存在"
|
||||
"nameExists": "名称已存在",
|
||||
"deleteConfirmation": "您确定要删除提示'{{name}}'吗?"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "添加块",
|
||||
|
||||
@@ -184,29 +184,54 @@ export default function PromptsModal({
|
||||
setEditPromptName: (name: string) => void;
|
||||
editPromptContent: string;
|
||||
setEditPromptContent: (content: string) => void;
|
||||
currentPromptEdit: { name: string; id: string; type: string };
|
||||
currentPromptEdit: {
|
||||
name: string;
|
||||
id: string;
|
||||
type: string;
|
||||
content?: string;
|
||||
};
|
||||
handleAddPrompt?: () => void;
|
||||
handleEditPrompt?: (id: string, type: string) => void;
|
||||
}) {
|
||||
const [disableSave, setDisableSave] = React.useState(true);
|
||||
const handlePrompNameChange = (edit: boolean, newName: string) => {
|
||||
const nameExists = existingPrompts.find(
|
||||
(prompt) => newName === prompt.name,
|
||||
);
|
||||
|
||||
if (newName && !nameExists) {
|
||||
setDisableSave(false);
|
||||
} else {
|
||||
setDisableSave(true);
|
||||
}
|
||||
|
||||
const handlePromptNameChange = (edit: boolean, newName: string) => {
|
||||
if (edit) {
|
||||
const nameExists = existingPrompts.find(
|
||||
(prompt) =>
|
||||
newName === prompt.name && prompt.id !== currentPromptEdit.id,
|
||||
);
|
||||
const nameValid = newName && !nameExists;
|
||||
const contentChanged = editPromptContent !== currentPromptEdit.content;
|
||||
|
||||
setDisableSave(!(nameValid || contentChanged));
|
||||
setEditPromptName(newName);
|
||||
} else {
|
||||
const nameExists = existingPrompts.find(
|
||||
(prompt) => newName === prompt.name,
|
||||
);
|
||||
setDisableSave(!(newName && !nameExists));
|
||||
setNewPromptName(newName);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContentChange = (edit: boolean, newContent: string) => {
|
||||
if (edit) {
|
||||
const contentChanged = newContent !== currentPromptEdit.content;
|
||||
const nameValid =
|
||||
editPromptName &&
|
||||
!existingPrompts.find(
|
||||
(prompt) =>
|
||||
editPromptName === prompt.name &&
|
||||
prompt.id !== currentPromptEdit.id,
|
||||
);
|
||||
|
||||
setDisableSave(!(nameValid || contentChanged));
|
||||
setEditPromptContent(newContent);
|
||||
} else {
|
||||
setNewPromptContent(newContent);
|
||||
}
|
||||
};
|
||||
|
||||
let view;
|
||||
|
||||
if (type === 'ADD') {
|
||||
@@ -215,9 +240,9 @@ export default function PromptsModal({
|
||||
setModalState={setModalState}
|
||||
handleAddPrompt={handleAddPrompt}
|
||||
newPromptName={newPromptName}
|
||||
setNewPromptName={handlePrompNameChange.bind(null, false)}
|
||||
setNewPromptName={handlePromptNameChange.bind(null, false)}
|
||||
newPromptContent={newPromptContent}
|
||||
setNewPromptContent={setNewPromptContent}
|
||||
setNewPromptContent={handleContentChange.bind(null, false)}
|
||||
disableSave={disableSave}
|
||||
/>
|
||||
);
|
||||
@@ -227,9 +252,9 @@ export default function PromptsModal({
|
||||
setModalState={setModalState}
|
||||
handleEditPrompt={handleEditPrompt}
|
||||
editPromptName={editPromptName}
|
||||
setEditPromptName={handlePrompNameChange.bind(null, true)}
|
||||
setEditPromptName={handlePromptNameChange.bind(null, true)}
|
||||
editPromptContent={editPromptContent}
|
||||
setEditPromptContent={setEditPromptContent}
|
||||
setEditPromptContent={handleContentChange.bind(null, true)}
|
||||
currentPromptEdit={currentPromptEdit}
|
||||
disableSave={disableSave}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Dropdown from '../components/Dropdown';
|
||||
import { ActiveState, PromptProps } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import PromptsModal from '../preferences/PromptsModal';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
|
||||
export default function Prompts({
|
||||
prompts,
|
||||
@@ -40,6 +41,11 @@ export default function Prompts({
|
||||
const [modalState, setModalState] = React.useState<ActiveState>('INACTIVE');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [promptToDelete, setPromptToDelete] = React.useState<{
|
||||
id: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleAddPrompt = async () => {
|
||||
try {
|
||||
const response = await userService.createPrompt(
|
||||
@@ -69,20 +75,37 @@ export default function Prompts({
|
||||
};
|
||||
|
||||
const handleDeletePrompt = (id: string) => {
|
||||
setPrompts(prompts.filter((prompt) => prompt.id !== id));
|
||||
userService
|
||||
.deletePrompt({ id }, token)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete prompt');
|
||||
}
|
||||
if (prompts.length > 0) {
|
||||
onSelectPrompt(prompts[0].name, prompts[0].id, prompts[0].type);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
const promptToRemove = prompts.find((prompt) => prompt.id === id);
|
||||
if (promptToRemove) {
|
||||
setPromptToDelete({ id, name: promptToRemove.name });
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeletePrompt = () => {
|
||||
if (promptToDelete) {
|
||||
setPrompts(prompts.filter((prompt) => prompt.id !== promptToDelete.id));
|
||||
userService
|
||||
.deletePrompt({ id: promptToDelete.id }, token)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete prompt');
|
||||
}
|
||||
if (prompts.length > 0) {
|
||||
const firstPrompt = prompts.find((p) => p.id !== promptToDelete.id);
|
||||
if (firstPrompt) {
|
||||
onSelectPrompt(
|
||||
firstPrompt.name,
|
||||
firstPrompt.id,
|
||||
firstPrompt.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
setPromptToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchPromptContent = async (id: string) => {
|
||||
@@ -202,6 +225,19 @@ export default function Prompts({
|
||||
handleAddPrompt={handleAddPrompt}
|
||||
handleEditPrompt={handleSaveChanges}
|
||||
/>
|
||||
{promptToDelete && (
|
||||
<ConfirmationModal
|
||||
message={t('modals.prompts.deleteConfirmation', {
|
||||
name: promptToDelete.name,
|
||||
})}
|
||||
modalState="ACTIVE"
|
||||
setModalState={() => setPromptToDelete(null)}
|
||||
submitLabel={t('modals.deleteConv.delete')}
|
||||
handleSubmit={confirmDeletePrompt}
|
||||
handleCancel={() => setPromptToDelete(null)}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
prefListenerMiddleware,
|
||||
prefSlice,
|
||||
} from './preferences/preferenceSlice';
|
||||
import uploadReducer from './upload/uploadSlice';
|
||||
|
||||
const key = localStorage.getItem('DocsGPTApiKey');
|
||||
const prompt = localStorage.getItem('DocsGPTPrompt');
|
||||
@@ -52,6 +53,7 @@ const store = configureStore({
|
||||
preference: prefSlice.reducer,
|
||||
conversation: conversationSlice.reducer,
|
||||
sharedConversation: sharedConversationSlice.reducer,
|
||||
upload: uploadReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(prefListenerMiddleware.middleware),
|
||||
|
||||
69
frontend/src/upload/uploadSlice.ts
Normal file
69
frontend/src/upload/uploadSlice.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../store';
|
||||
|
||||
export interface Attachment {
|
||||
fileName: string;
|
||||
progress: number;
|
||||
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||
taskId: string;
|
||||
id?: string;
|
||||
token_count?: number;
|
||||
}
|
||||
|
||||
interface UploadState {
|
||||
attachments: Attachment[];
|
||||
}
|
||||
|
||||
const initialState: UploadState = {
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
export const uploadSlice = createSlice({
|
||||
name: 'upload',
|
||||
initialState,
|
||||
reducers: {
|
||||
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,
|
||||
);
|
||||
},
|
||||
clearAttachments: (state) => {
|
||||
state.attachments = state.attachments.filter(
|
||||
(att) => att.status === 'uploading' || att.status === 'processing',
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
addAttachment,
|
||||
updateAttachment,
|
||||
removeAttachment,
|
||||
clearAttachments,
|
||||
} = uploadSlice.actions;
|
||||
|
||||
export const selectAttachments = (state: RootState) => state.upload.attachments;
|
||||
export const selectCompletedAttachments = (state: RootState) =>
|
||||
state.upload.attachments.filter((att) => att.status === 'completed');
|
||||
|
||||
export default uploadSlice.reducer;
|
||||
Reference in New Issue
Block a user