Merge branch 'main' into refactor/llm-handler

This commit is contained in:
Siddhant Rai
2025-06-11 19:27:33 +05:30
committed by GitHub
23 changed files with 429 additions and 193 deletions

View File

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

View File

@@ -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",
}
),

View File

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

View File

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

View File

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

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';
@@ -59,6 +59,7 @@ const ConversationBubble = forwardRef<
updated?: boolean,
index?: number,
) => void;
filesAttached?: { id: string; fileName: string }[];
}
>(function ConversationBubble(
{
@@ -74,6 +75,7 @@ const ConversationBubble = forwardRef<
questionNumber,
isStreaming,
handleUpdatedQuestionSubmission,
filesAttached,
},
ref,
) {
@@ -105,41 +107,66 @@ const ConversationBubble = forwardRef<
<div
onMouseEnter={() => setIsQuestionHovered(true)}
onMouseLeave={() => setIsQuestionHovered(false)}
className={className}
>
<div
ref={ref}
className={`flex flex-row-reverse justify-items-start ${className}`}
>
<Avatar
size="SMALL"
className="mt-2 flex-shrink-0 text-2xl"
avatar={
<img className="mr-1 rounded-full" width={30} src={UserIcon} />
}
/>
{!isEditClicked && (
<>
<div className="mr-2 flex flex-col">
<div className="flex flex-col items-end">
{filesAttached && filesAttached.length > 0 && (
<div className="mb-4 mr-12 flex flex-wrap justify-end gap-2">
{filesAttached.map((file, index) => (
<div
style={{
wordBreak: 'break-word',
}}
className="ml-2 mr-2 flex max-w-full items-center whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-[19px] py-[14px] text-sm leading-normal text-white sm:text-base"
key={index}
title={file.fileName}
className="flex items-center rounded-xl bg-[#EFF3F4] p-2 text-[14px] text-[#5D5D5D] dark:bg-[#393B3D] dark:text-bright-gray"
>
{message}
<div className="mr-2 items-center justify-center rounded-lg bg-purple-30 p-[5.5px]">
<img
src={DocumentationDark}
alt="Attachment"
className="h-[15px] w-[15px] object-fill"
/>
</div>
<span className="max-w-[150px] truncate font-normal">
{file.fileName}
</span>
</div>
</div>
<button
onClick={() => {
setIsEditClicked(true);
setEditInputBox(message ?? '');
}}
className={`mt-3 flex h-fit flex-shrink-0 cursor-pointer items-center rounded-full p-2 hover:bg-light-silver dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
>
<img src={Edit} alt="Edit" className="cursor-pointer" />
</button>
</>
))}
</div>
)}
<div
ref={ref}
className={`flex flex-row-reverse justify-items-start`}
>
<Avatar
size="SMALL"
className="mt-2 flex-shrink-0 text-2xl"
avatar={
<img className="mr-1 rounded-full" width={30} src={UserIcon} />
}
/>
{!isEditClicked && (
<>
<div className="mr-2 flex flex-col">
<div
style={{
wordBreak: 'break-word',
}}
className="ml-2 mr-2 flex max-w-full items-center whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-[19px] py-[14px] text-sm leading-normal text-white sm:text-base"
>
{message}
</div>
</div>
<button
onClick={() => {
setIsEditClicked(true);
setEditInputBox(message ?? '');
}}
className={`mt-3 flex h-fit flex-shrink-0 cursor-pointer items-center rounded-full p-2 hover:bg-light-silver dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
>
<img src={Edit} alt="Edit" className="cursor-pointer" />
</button>
</>
)}
</div>
{isEditClicked && (
<div
ref={editableQueryRef}

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;
@@ -319,39 +327,11 @@ export const conversationSlice = createSlice({
const { index, message } = action.payload;
state.queries[index].error = message;
},
setAttachments: (state, action: PayloadAction<Attachment[]>) => {
state.attachments = action.payload;
},
addAttachment: (state, action: PayloadAction<Attachment>) => {
state.attachments.push(action.payload);
},
updateAttachment: (
state,
action: PayloadAction<{
taskId: string;
updates: Partial<Attachment>;
}>,
) => {
const index = state.attachments.findIndex(
(att) => att.taskId === action.payload.taskId,
);
if (index !== -1) {
state.attachments[index] = {
...state.attachments[index],
...action.payload.updates,
};
}
},
removeAttachment: (state, action: PayloadAction<string>) => {
state.attachments = state.attachments.filter(
(att) => att.taskId !== action.payload && att.id !== action.payload,
);
},
resetConversation: (state) => {
state.queries = initialState.queries;
state.status = initialState.status;
state.conversationId = initialState.conversationId;
state.attachments = initialState.attachments;
handleAbort();
},
},
@@ -377,11 +357,6 @@ export const selectQueries = (state: RootState) => state.conversation.queries;
export const selectStatus = (state: RootState) => state.conversation.status;
export const selectAttachments = (state: RootState) =>
state.conversation.attachments;
export const selectCompletedAttachments = (state: RootState) =>
state.conversation.attachments.filter((att) => att.status === 'completed');
export const {
addQuery,
updateQuery,
@@ -392,10 +367,8 @@ export const {
updateStreamingSource,
updateToolCall,
setConversation,
setAttachments,
addAttachment,
updateAttachment,
removeAttachment,
setStatus,
raiseError,
resetConversation,
} = conversationSlice.actions;
export default conversationSlice.reducer;

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -245,7 +245,8 @@
"promptName": "プロンプト名",
"promptText": "プロンプトテキスト",
"save": "保存",
"nameExists": "名前が既に存在します"
"nameExists": "名前が既に存在します",
"deleteConfirmation": "プロンプト「{{name}}」を削除してもよろしいですか?"
},
"chunk": {
"add": "チャンクを追加",

View File

@@ -245,7 +245,8 @@
"promptName": "Название подсказки",
"promptText": "Текст подсказки",
"save": "Сохранить",
"nameExists": "Название уже существует"
"nameExists": "Название уже существует",
"deleteConfirmation": "Вы уверены, что хотите удалить подсказку «{{name}}»?"
},
"chunk": {
"add": "Добавить фрагмент",

View File

@@ -245,7 +245,8 @@
"promptName": "提示名稱",
"promptText": "提示文字",
"save": "儲存",
"nameExists": "名稱已存在"
"nameExists": "名稱已存在",
"deleteConfirmation": "您確定要刪除提示「{{name}}」嗎?"
},
"chunk": {
"add": "新增區塊",

View File

@@ -245,7 +245,8 @@
"promptName": "提示名称",
"promptText": "提示文本",
"save": "保存",
"nameExists": "名称已存在"
"nameExists": "名称已存在",
"deleteConfirmation": "您确定要删除提示'{{name}}'吗?"
},
"chunk": {
"add": "添加块",

View File

@@ -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}
/>

View File

@@ -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"
/>
)}
</>
);
}

View File

@@ -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),

View 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;