diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 83c3db6f..469ea98c 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -164,6 +164,7 @@ def save_conversation( agent_id=None, is_shared_usage=False, shared_token=None, + attachment_ids=None, ): current_time = datetime.datetime.now(datetime.timezone.utc) if conversation_id is not None and index is not None: @@ -177,6 +178,7 @@ def save_conversation( f"queries.{index}.sources": source_log_docs, f"queries.{index}.tool_calls": tool_calls, f"queries.{index}.timestamp": current_time, + f"queries.{index}.attachments": attachment_ids, } }, ) @@ -197,6 +199,7 @@ def save_conversation( "sources": source_log_docs, "tool_calls": tool_calls, "timestamp": current_time, + "attachments": attachment_ids, } } }, @@ -233,6 +236,7 @@ def save_conversation( "sources": source_log_docs, "tool_calls": tool_calls, "timestamp": current_time, + "attachments": attachment_ids, } ], } @@ -273,20 +277,13 @@ def complete_stream( isNoneDoc=False, index=None, should_save_conversation=True, - attachments=None, + attachment_ids=None, agent_id=None, is_shared_usage=False, shared_token=None, ): try: response_full, thought, source_log_docs, tool_calls = "", "", [], [] - attachment_ids = [] - - if attachments: - attachment_ids = [attachment["id"] for attachment in attachments] - logger.info( - f"Processing request with {len(attachments)} attachments: {attachment_ids}" - ) answer = agent.gen(query=question, retriever=retriever) @@ -341,6 +338,7 @@ def complete_stream( decoded_token, index, api_key=user_api_key, + attachment_ids=attachment_ids, agent_id=agent_id, is_shared_usage=is_shared_usage, shared_token=shared_token, @@ -538,6 +536,7 @@ class Stream(Resource): isNoneDoc=data.get("isNoneDoc"), index=index, should_save_conversation=save_conv, + attachment_ids=attachment_ids, agent_id=agent_id, is_shared_usage=is_shared_usage, shared_token=shared_token, diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 5c9975fb..d98e4092 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -45,6 +45,7 @@ shared_conversations_collections = db["shared_conversations"] users_collection = db["users"] user_logs_collection = db["user_logs"] user_tools_collection = db["user_tools"] +attachments_collection = db["attachments"] agents_collection.create_index( [("shared", 1)], @@ -252,13 +253,34 @@ class GetSingleConversation(Resource): ) if not conversation: return make_response(jsonify({"status": "not found"}), 404) + + # Process queries to include attachment names + queries = conversation["queries"] + for query in queries: + if "attachments" in query and query["attachments"]: + attachment_details = [] + for attachment_id in query["attachments"]: + try: + attachment = attachments_collection.find_one( + {"_id": ObjectId(attachment_id)} + ) + if attachment: + attachment_details.append({ + "id": str(attachment["_id"]), + "fileName": attachment.get("filename", "Unknown file") + }) + except Exception as e: + current_app.logger.error( + f"Error retrieving attachment {attachment_id}: {e}", exc_info=True + ) + query["attachments"] = attachment_details except Exception as err: current_app.logger.error( f"Error retrieving conversation: {err}", exc_info=True ) return make_response(jsonify({"success": False}), 400) data = { - "queries": conversation["queries"], + "queries": queries, "agent_id": conversation.get("agent_id"), "is_shared_usage": conversation.get("is_shared_usage", False), "shared_token": conversation.get("shared_token", None), @@ -2205,7 +2227,7 @@ class GetPubliclySharedConversations(Resource): return make_response( jsonify( { - "sucess": False, + "success": False, "error": "might have broken url or the conversation does not exist", } ), @@ -2214,11 +2236,30 @@ class GetPubliclySharedConversations(Resource): conversation_queries = conversation["queries"][ : (shared["first_n_queries"]) ] + + for query in conversation_queries: + if "attachments" in query and query["attachments"]: + attachment_details = [] + for attachment_id in query["attachments"]: + try: + attachment = attachments_collection.find_one( + {"_id": ObjectId(attachment_id)} + ) + if attachment: + attachment_details.append({ + "id": str(attachment["_id"]), + "fileName": attachment.get("filename", "Unknown file") + }) + except Exception as e: + current_app.logger.error( + f"Error retrieving attachment {attachment_id}: {e}", exc_info=True + ) + query["attachments"] = attachment_details else: return make_response( jsonify( { - "sucess": False, + "success": False, "error": "might have broken url or the conversation does not exist", } ), diff --git a/application/worker.py b/application/worker.py index 13c0ca30..b652109b 100755 --- a/application/worker.py +++ b/application/worker.py @@ -477,6 +477,7 @@ def attachment_worker(self, file_info, user): "_id": doc_id, "user": user, "path": relative_path, + "filename": filename, "content": content, "token_count": token_count, "mime_type": mime_type, diff --git a/frontend/src/assets/alert.svg b/frontend/src/assets/alert.svg index 05c7634b..07721b57 100644 --- a/frontend/src/assets/alert.svg +++ b/frontend/src/assets/alert.svg @@ -1,3 +1,3 @@ - + diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 79487188..fb405316 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -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) => (
+
+ {attachment.status === 'completed' && ( + Attachment + )} + + {attachment.status === 'failed' && ( + Failed + )} + + {(attachment.status === 'uploading' || + attachment.status === 'processing') && ( +
+ + + + +
+ )} +
+ {attachment.fileName} - {attachment.status === 'completed' && ( - - )} - - {attachment.status === 'failed' && ( +
))} diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 4f34c26c..a8d86049 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -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('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(() => { diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 920005e3..102a8363 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -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<
setIsQuestionHovered(true)} onMouseLeave={() => setIsQuestionHovered(false)} + className={className} > -
- - } - /> - {!isEditClicked && ( - <> -
+
+ {filesAttached && filesAttached.length > 0 && ( +
+ {filesAttached.map((file, index) => (
- {message} +
+ Attachment +
+ + {file.fileName} +
-
- - + ))} +
)} +
+ + } + /> + {!isEditClicked && ( + <> +
+
+ {message} +
+
+ + + )} +
{isEditClicked && (
{renderResponseView(query, index)} diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index 3627c048..22efae4e 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -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(); @@ -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(() => { diff --git a/frontend/src/conversation/conversationHandlers.ts b/frontend/src/conversation/conversationHandlers.ts index 8aa7e424..71f539e5 100644 --- a/frontend/src/conversation/conversationHandlers.ts +++ b/frontend/src/conversation/conversationHandlers.ts @@ -279,11 +279,12 @@ export function handleSendFeedback( }); } -export function handleFetchSharedAnswerStreaming( //for shared conversations +export function handleFetchSharedAnswerStreaming( question: string, signal: AbortSignal, apiKey: string, history: Array = [], + attachments: string[] = [], onEvent: (event: MessageEvent) => void, ): Promise { 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(); diff --git a/frontend/src/conversation/conversationModels.ts b/frontend/src/conversation/conversationModels.ts index 8c5a479b..b16dd6c1 100644 --- a/frontend/src/conversation/conversationModels.ts +++ b/frontend/src/conversation/conversationModels.ts @@ -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 { diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index 03532792..c1fd822f 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -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) => { - state.attachments = action.payload; - }, - addAttachment: (state, action: PayloadAction) => { - state.attachments.push(action.payload); - }, - updateAttachment: ( - state, - action: PayloadAction<{ - taskId: string; - updates: Partial; - }>, - ) => { - 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) => { - 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; diff --git a/frontend/src/conversation/sharedConversationSlice.ts b/frontend/src/conversation/sharedConversationSlice.ts index f0482fa5..df2650a3 100644 --- a/frontend/src/conversation/sharedConversationSlice.ts +++ b/frontend/src/conversation/sharedConversationSlice.ts @@ -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( 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( 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( question, signal, state.sharedConversation.apiKey, + attachmentIds, ); if (answer) { let sourcesPrepped = []; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 7fda12c8..b538cdde 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -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", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 2f0260b0..bcdebfcb 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -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", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 3c1cb64b..d004e0dc 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -245,7 +245,8 @@ "promptName": "プロンプト名", "promptText": "プロンプトテキスト", "save": "保存", - "nameExists": "名前が既に存在します" + "nameExists": "名前が既に存在します", + "deleteConfirmation": "プロンプト「{{name}}」を削除してもよろしいですか?" }, "chunk": { "add": "チャンクを追加", diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 8d62a577..95c7a228 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -245,7 +245,8 @@ "promptName": "Название подсказки", "promptText": "Текст подсказки", "save": "Сохранить", - "nameExists": "Название уже существует" + "nameExists": "Название уже существует", + "deleteConfirmation": "Вы уверены, что хотите удалить подсказку «{{name}}»?" }, "chunk": { "add": "Добавить фрагмент", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index 366d0f56..36baa8b1 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -245,7 +245,8 @@ "promptName": "提示名稱", "promptText": "提示文字", "save": "儲存", - "nameExists": "名稱已存在" + "nameExists": "名稱已存在", + "deleteConfirmation": "您確定要刪除提示「{{name}}」嗎?" }, "chunk": { "add": "新增區塊", diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 6d4e590c..804f5fb2 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -245,7 +245,8 @@ "promptName": "提示名称", "promptText": "提示文本", "save": "保存", - "nameExists": "名称已存在" + "nameExists": "名称已存在", + "deleteConfirmation": "您确定要删除提示'{{name}}'吗?" }, "chunk": { "add": "添加块", diff --git a/frontend/src/preferences/PromptsModal.tsx b/frontend/src/preferences/PromptsModal.tsx index f61ba4ee..14e05e86 100644 --- a/frontend/src/preferences/PromptsModal.tsx +++ b/frontend/src/preferences/PromptsModal.tsx @@ -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} /> diff --git a/frontend/src/settings/Prompts.tsx b/frontend/src/settings/Prompts.tsx index 055bf0b1..84b739ae 100644 --- a/frontend/src/settings/Prompts.tsx +++ b/frontend/src/settings/Prompts.tsx @@ -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('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 && ( + setPromptToDelete(null)} + submitLabel={t('modals.deleteConv.delete')} + handleSubmit={confirmDeletePrompt} + handleCancel={() => setPromptToDelete(null)} + variant="danger" + /> + )} ); } diff --git a/frontend/src/store.ts b/frontend/src/store.ts index f26ef3d2..1f25606d 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -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), diff --git a/frontend/src/upload/uploadSlice.ts b/frontend/src/upload/uploadSlice.ts new file mode 100644 index 00000000..732c69bc --- /dev/null +++ b/frontend/src/upload/uploadSlice.ts @@ -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) => { + state.attachments.push(action.payload); + }, + updateAttachment: ( + state, + action: PayloadAction<{ + taskId: string; + updates: Partial; + }>, + ) => { + 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) => { + 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;