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.status === '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}
+
+

+
+
+ {file.fileName}
+
-
-
- >
+ ))}
+
)}
+
+
+ }
+ />
+ {!isEditClicked && (
+ <>
+
+
+ >
+ )}
+
{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;