mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
feat: Enhance agent selection and conversation handling
- Added functionality to select agents in the Navigation component, allowing users to reset conversations and set the selected agent. - Updated the MessageInput component to conditionally show source and tool buttons based on the selected agent. - Modified the Conversation component to handle agent-specific queries and manage file uploads. - Improved conversation fetching logic to include agent IDs and handle attachments. - Introduced new types for conversation summaries and results to streamline API responses. - Refactored Redux slices to manage selected agent state and improve overall state management. - Enhanced error handling and loading states across components for better user experience.
This commit is contained in:
@@ -10,6 +10,7 @@ from application.core.mongo_db import MongoDB
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.logging import build_stack_data, log_activity, LogContext
|
||||
from application.retriever.base import BaseRetriever
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
|
||||
class BaseAgent(ABC):
|
||||
@@ -23,7 +24,7 @@ class BaseAgent(ABC):
|
||||
prompt: str = "",
|
||||
chat_history: Optional[List[Dict]] = None,
|
||||
decoded_token: Optional[Dict] = None,
|
||||
attachments: Optional[List[Dict]]=None,
|
||||
attachments: Optional[List[Dict]] = None,
|
||||
):
|
||||
self.endpoint = endpoint
|
||||
self.llm_name = llm_name
|
||||
@@ -58,6 +59,27 @@ class BaseAgent(ABC):
|
||||
) -> Generator[Dict, None, None]:
|
||||
pass
|
||||
|
||||
def _get_tools(self, api_key: str = None) -> Dict[str, Dict]:
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
agents_collection = db["agents"]
|
||||
tools_collection = db["user_tools"]
|
||||
|
||||
agent_data = agents_collection.find_one({"key": api_key or self.user_api_key})
|
||||
tool_ids = agent_data.get("tools", []) if agent_data else []
|
||||
|
||||
tools = (
|
||||
tools_collection.find(
|
||||
{"_id": {"$in": [ObjectId(tool_id) for tool_id in tool_ids]}}
|
||||
)
|
||||
if tool_ids
|
||||
else []
|
||||
)
|
||||
tools = list(tools)
|
||||
tools_by_id = {str(tool["_id"]): tool for tool in tools} if tools else {}
|
||||
|
||||
return tools_by_id
|
||||
|
||||
def _get_user_tools(self, user="local"):
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
@@ -243,9 +265,11 @@ class BaseAgent(ABC):
|
||||
tools_dict: Dict,
|
||||
messages: List[Dict],
|
||||
log_context: Optional[LogContext] = None,
|
||||
attachments: Optional[List[Dict]] = None
|
||||
attachments: Optional[List[Dict]] = None,
|
||||
):
|
||||
resp = self.llm_handler.handle_response(self, resp, tools_dict, messages, attachments)
|
||||
resp = self.llm_handler.handle_response(
|
||||
self, resp, tools_dict, messages, attachments
|
||||
)
|
||||
if log_context:
|
||||
data = build_stack_data(self.llm_handler)
|
||||
log_context.stacks.append({"component": "llm_handler", "data": data})
|
||||
|
||||
@@ -5,21 +5,25 @@ from application.logging import LogContext
|
||||
|
||||
from application.retriever.base import BaseRetriever
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClassicAgent(BaseAgent):
|
||||
def _gen_inner(
|
||||
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
||||
) -> Generator[Dict, None, None]:
|
||||
retrieved_data = self._retriever_search(retriever, query, log_context)
|
||||
|
||||
tools_dict = self._get_user_tools(self.user)
|
||||
if self.user_api_key:
|
||||
tools_dict = self._get_tools(self.user_api_key)
|
||||
else:
|
||||
tools_dict = self._get_user_tools(self.user)
|
||||
self._prepare_tools(tools_dict)
|
||||
|
||||
messages = self._build_messages(self.prompt, query, retrieved_data)
|
||||
|
||||
resp = self._llm_gen(messages, log_context)
|
||||
|
||||
|
||||
attachments = self.attachments
|
||||
|
||||
if isinstance(resp, str):
|
||||
@@ -33,7 +37,7 @@ class ClassicAgent(BaseAgent):
|
||||
yield {"answer": resp.message.content}
|
||||
return
|
||||
|
||||
resp = self._llm_handler(resp, tools_dict, messages, log_context,attachments)
|
||||
resp = self._llm_handler(resp, tools_dict, messages, log_context, attachments)
|
||||
|
||||
if isinstance(resp, str):
|
||||
yield {"answer": resp}
|
||||
|
||||
@@ -30,7 +30,10 @@ class ReActAgent(BaseAgent):
|
||||
) -> Generator[Dict, None, None]:
|
||||
retrieved_data = self._retriever_search(retriever, query, log_context)
|
||||
|
||||
tools_dict = self._get_user_tools(self.user)
|
||||
if self.user_api_key:
|
||||
tools_dict = self._get_tools(self.user_api_key)
|
||||
else:
|
||||
tools_dict = self._get_user_tools(self.user)
|
||||
self._prepare_tools(tools_dict)
|
||||
|
||||
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
|
||||
|
||||
@@ -86,6 +86,20 @@ def run_async_chain(chain, question, chat_history):
|
||||
return result
|
||||
|
||||
|
||||
def get_agent_key(agent_id, user_id):
|
||||
if not agent_id:
|
||||
return None
|
||||
|
||||
agent = agents_collection.find_one({"_id": ObjectId(agent_id)})
|
||||
if agent is None:
|
||||
raise Exception("Agent not found", 404)
|
||||
|
||||
if agent.get("is_public") or agent.get("user") == user_id:
|
||||
return str(agent["key"])
|
||||
|
||||
raise Exception("Unauthorized access to the agent", 403)
|
||||
|
||||
|
||||
def get_data_from_api_key(api_key):
|
||||
data = agents_collection.find_one({"key": api_key})
|
||||
if not data:
|
||||
@@ -129,6 +143,7 @@ def save_conversation(
|
||||
decoded_token,
|
||||
index=None,
|
||||
api_key=None,
|
||||
agent_id=None,
|
||||
):
|
||||
current_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
if conversation_id is not None and index is not None:
|
||||
@@ -202,6 +217,8 @@ def save_conversation(
|
||||
],
|
||||
}
|
||||
if api_key:
|
||||
if agent_id:
|
||||
conversation_data["agent_id"] = agent_id
|
||||
api_key_doc = agents_collection.find_one({"key": api_key})
|
||||
if api_key_doc:
|
||||
conversation_data["api_key"] = api_key_doc["key"]
|
||||
@@ -234,6 +251,7 @@ def complete_stream(
|
||||
index=None,
|
||||
should_save_conversation=True,
|
||||
attachments=None,
|
||||
agent_id=None,
|
||||
):
|
||||
try:
|
||||
response_full, thought, source_log_docs, tool_calls = "", "", [], []
|
||||
@@ -297,6 +315,7 @@ def complete_stream(
|
||||
decoded_token,
|
||||
index,
|
||||
api_key=user_api_key,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
else:
|
||||
conversation_id = None
|
||||
@@ -404,7 +423,14 @@ class Stream(Resource):
|
||||
chunks = int(data.get("chunks", 2))
|
||||
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
|
||||
retriever_name = data.get("retriever", "classic")
|
||||
agent_id = data.get("agent_id", None)
|
||||
agent_type = settings.AGENT_NAME
|
||||
agent_key = get_agent_key(agent_id, request.decoded_token.get("sub"))
|
||||
|
||||
if agent_key:
|
||||
data.update({"api_key": agent_key})
|
||||
else:
|
||||
agent_id = None
|
||||
|
||||
if "api_key" in data:
|
||||
data_key = get_data_from_api_key(data["api_key"])
|
||||
@@ -479,6 +505,7 @@ class Stream(Resource):
|
||||
isNoneDoc=data.get("isNoneDoc"),
|
||||
index=index,
|
||||
should_save_conversation=save_conv,
|
||||
agent_id=agent_id,
|
||||
),
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
|
||||
@@ -138,14 +138,24 @@ class GetConversations(Resource):
|
||||
try:
|
||||
conversations = (
|
||||
conversations_collection.find(
|
||||
{"api_key": {"$exists": False}, "user": decoded_token.get("sub")}
|
||||
{
|
||||
"$or": [
|
||||
{"api_key": {"$exists": False}},
|
||||
{"agent_id": {"$exists": True}},
|
||||
],
|
||||
"user": decoded_token.get("sub"),
|
||||
}
|
||||
)
|
||||
.sort("date", -1)
|
||||
.limit(30)
|
||||
)
|
||||
|
||||
list_conversations = [
|
||||
{"id": str(conversation["_id"]), "name": conversation["name"]}
|
||||
{
|
||||
"id": str(conversation["_id"]),
|
||||
"name": conversation["name"],
|
||||
"agent_id": conversation.get("agent_id", None),
|
||||
}
|
||||
for conversation in conversations
|
||||
]
|
||||
except Exception as err:
|
||||
@@ -179,7 +189,12 @@ class GetSingleConversation(Resource):
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error retrieving conversation: {err}")
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify(conversation["queries"]), 200)
|
||||
|
||||
data = {
|
||||
"queries": conversation["queries"],
|
||||
"agent_id": conversation.get("agent_id"),
|
||||
}
|
||||
return make_response(jsonify(data), 200)
|
||||
|
||||
|
||||
@user_ns.route("/api/update_conversation_name")
|
||||
|
||||
@@ -14,8 +14,8 @@ import Github from './assets/github.svg';
|
||||
import Hamburger from './assets/hamburger.svg';
|
||||
import openNewChat from './assets/openNewChat.svg';
|
||||
import Robot from './assets/robot.svg';
|
||||
import Spark from './assets/spark.svg';
|
||||
import SettingGear from './assets/settingGear.svg';
|
||||
import Spark from './assets/spark.svg';
|
||||
import SpinnerDark from './assets/spinner-dark.svg';
|
||||
import Spinner from './assets/spinner.svg';
|
||||
import Twitter from './assets/TwitterX.svg';
|
||||
@@ -35,13 +35,14 @@ import JWTModal from './modals/JWTModal';
|
||||
import { ActiveState } from './models/misc';
|
||||
import { getConversations } from './preferences/preferenceApi';
|
||||
import {
|
||||
selectApiKeyStatus,
|
||||
selectConversationId,
|
||||
selectConversations,
|
||||
selectModalStateDeleteConv,
|
||||
selectSelectedAgent,
|
||||
selectToken,
|
||||
setConversations,
|
||||
setModalStateDeleteConv,
|
||||
setSelectedAgent,
|
||||
} from './preferences/preferenceSlice';
|
||||
import Upload from './upload/Upload';
|
||||
|
||||
@@ -59,7 +60,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
const token = useSelector(selectToken);
|
||||
const queries = useSelector(selectQueries);
|
||||
const conversations = useSelector(selectConversations);
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
@@ -84,14 +87,14 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
});
|
||||
}
|
||||
|
||||
const getAgents = async () => {
|
||||
async function getAgents() {
|
||||
const response = await userService.getAgents(token);
|
||||
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||
const data = await response.json();
|
||||
setRecentAgents(
|
||||
data.filter((agent: Agent) => agent.status === 'published'),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (recentAgents.length === 0) getAgents();
|
||||
@@ -120,18 +123,34 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
.catch((error) => console.error(error));
|
||||
};
|
||||
|
||||
const handleAgentClick = (agent: Agent) => {
|
||||
resetConversation();
|
||||
dispatch(setSelectedAgent(agent));
|
||||
if (isMobile) setNavOpen(!navOpen);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const handleConversationClick = (index: string) => {
|
||||
conversationService
|
||||
.getConversation(index, token)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
navigate('/');
|
||||
dispatch(setConversation(data));
|
||||
dispatch(setConversation(data.queries));
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: index },
|
||||
}),
|
||||
);
|
||||
if (data.agent_id) {
|
||||
userService.getAgent(data.agent_id, token).then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((agent: Agent) => {
|
||||
dispatch(setSelectedAgent(agent));
|
||||
});
|
||||
}
|
||||
});
|
||||
} else dispatch(setSelectedAgent(null));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -143,6 +162,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
query: { conversationId: null },
|
||||
}),
|
||||
);
|
||||
dispatch(setSelectedAgent(null));
|
||||
};
|
||||
|
||||
const newChat = () => {
|
||||
@@ -284,55 +304,73 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
{recentAgents?.length > 0 ? (
|
||||
<div>
|
||||
<div className="mx-4 my-auto mt-2 flex h-6 items-center">
|
||||
<p className="ml-4 mt-1 text-sm font-semibold">Agents</p>
|
||||
</div>
|
||||
<div className="agents-container">
|
||||
{recentAgents?.length > 0 ? (
|
||||
<div>
|
||||
{recentAgents.map((agent, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||
>
|
||||
<div className="flex w-6 justify-center">
|
||||
<img
|
||||
src={agent.image ?? Robot}
|
||||
alt="agent-logo"
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||
{agent.name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
{recentAgents.map((agent, idx) => (
|
||||
<div
|
||||
className="mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4"
|
||||
onClick={() => navigate('/agents')}
|
||||
key={idx}
|
||||
className={`mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal ${
|
||||
agent.id === selectedAgent?.id && !conversationId
|
||||
? 'bg-bright-gray dark:bg-dark-charcoal'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => handleAgentClick(agent)}
|
||||
>
|
||||
<div className="flex w-6 justify-center">
|
||||
<img
|
||||
src={Spark}
|
||||
alt="manage-agents"
|
||||
className="h-[18px] w-[18px]"
|
||||
src={agent.image ?? Robot}
|
||||
alt="agent-logo"
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||
Manage Agents
|
||||
{agent.name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||
onClick={() => {
|
||||
dispatch(setSelectedAgent(null));
|
||||
navigate('/agents');
|
||||
}}
|
||||
>
|
||||
<div className="flex w-6 justify-center">
|
||||
<img
|
||||
src={Spark}
|
||||
alt="manage-agents"
|
||||
className="h-[18px] w-[18px]"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-sm text-gray-500">
|
||||
No agents available.
|
||||
</div>
|
||||
)}
|
||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||
Manage Agents
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
) : (
|
||||
<div
|
||||
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4"
|
||||
onClick={() => navigate('/agents')}
|
||||
>
|
||||
<div className="flex w-6 justify-center">
|
||||
<img
|
||||
src={Spark}
|
||||
alt="manage-agents"
|
||||
className="h-[18px] w-[18px]"
|
||||
/>
|
||||
</div>
|
||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black hover:text-purple-30 dark:text-bright-gray hover:dark:text-purple-30">
|
||||
Manage Agents
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{conversations?.data && conversations.data.length > 0 ? (
|
||||
<div className="mt-7">
|
||||
<div className="mx-4 my-auto mt-2 flex h-6 items-center justify-between gap-4 rounded-3xl">
|
||||
|
||||
@@ -1,43 +1,51 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import userService from '../api/services/userService';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import endpoints from '../api/endpoints';
|
||||
import userService from '../api/services/userService';
|
||||
import ClipIcon from '../assets/clip.svg';
|
||||
import PaperPlane from '../assets/paper_plane.svg';
|
||||
import SourceIcon from '../assets/source.svg';
|
||||
import ToolIcon from '../assets/tool.svg';
|
||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import ToolIcon from '../assets/tool.svg';
|
||||
import { setAttachments } from '../conversation/conversationSlice';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import {
|
||||
selectSelectedDocs,
|
||||
selectToken,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import Upload from '../upload/Upload';
|
||||
import SourcesPopup from './SourcesPopup';
|
||||
import ToolsPopup from './ToolsPopup';
|
||||
import { selectSelectedDocs, selectToken } from '../preferences/preferenceSlice';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import Upload from '../upload/Upload';
|
||||
import ClipIcon from '../assets/clip.svg';
|
||||
import { setAttachments } from '../conversation/conversationSlice';
|
||||
|
||||
interface MessageInputProps {
|
||||
type MessageInputProps = {
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
onSubmit: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
showSourceButton?: boolean;
|
||||
showToolButton?: boolean;
|
||||
};
|
||||
|
||||
interface UploadState {
|
||||
type UploadState = {
|
||||
taskId: string;
|
||||
fileName: string;
|
||||
progress: number;
|
||||
attachment_id?: string;
|
||||
token_count?: number;
|
||||
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||
}
|
||||
};
|
||||
|
||||
export default function MessageInput({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
loading,
|
||||
showSourceButton = true,
|
||||
showToolButton = true,
|
||||
}: MessageInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
@@ -46,12 +54,13 @@ export default function MessageInput({
|
||||
const toolButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false);
|
||||
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
|
||||
const [uploadModalState, setUploadModalState] = useState<ActiveState>('INACTIVE');
|
||||
const [uploadModalState, setUploadModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const [uploads, setUploads] = useState<UploadState[]>([]);
|
||||
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
const token = useSelector(selectToken);
|
||||
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -68,20 +77,20 @@ export default function MessageInput({
|
||||
taskId: '',
|
||||
fileName: file.name,
|
||||
progress: 0,
|
||||
status: 'uploading'
|
||||
status: 'uploading',
|
||||
};
|
||||
|
||||
setUploads(prev => [...prev, uploadState]);
|
||||
setUploads((prev) => [...prev, uploadState]);
|
||||
const uploadIndex = uploads.length;
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const progress = Math.round((event.loaded / event.total) * 100);
|
||||
setUploads(prev => prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, progress }
|
||||
: upload
|
||||
));
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex ? { ...upload, progress } : upload,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -91,28 +100,30 @@ export default function MessageInput({
|
||||
console.log('File uploaded successfully:', response);
|
||||
|
||||
if (response.task_id) {
|
||||
setUploads(prev => prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, taskId: response.task_id, status: 'processing' }
|
||||
: upload
|
||||
));
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, taskId: response.task_id, status: 'processing' }
|
||||
: upload,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setUploads(prev => prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, status: 'failed' }
|
||||
: upload
|
||||
));
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex ? { ...upload, status: 'failed' } : upload,
|
||||
),
|
||||
);
|
||||
console.error('Error uploading file:', xhr.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
setUploads(prev => prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, status: 'failed' }
|
||||
: upload
|
||||
));
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex ? { ...upload, status: 'failed' } : upload,
|
||||
),
|
||||
);
|
||||
console.error('Network error during file upload');
|
||||
};
|
||||
|
||||
@@ -126,59 +137,64 @@ export default function MessageInput({
|
||||
let timeoutIds: number[] = [];
|
||||
|
||||
const checkTaskStatus = () => {
|
||||
const processingUploads = uploads.filter(upload =>
|
||||
upload.status === 'processing' && upload.taskId
|
||||
const processingUploads = uploads.filter(
|
||||
(upload) => upload.status === 'processing' && upload.taskId,
|
||||
);
|
||||
|
||||
processingUploads.forEach(upload => {
|
||||
processingUploads.forEach((upload) => {
|
||||
userService
|
||||
.getTaskStatus(upload.taskId, null)
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
console.log('Task status:', data);
|
||||
|
||||
setUploads(prev => prev.map(u => {
|
||||
if (u.taskId !== upload.taskId) return u;
|
||||
setUploads((prev) =>
|
||||
prev.map((u) => {
|
||||
if (u.taskId !== upload.taskId) return u;
|
||||
|
||||
if (data.status === 'SUCCESS') {
|
||||
return {
|
||||
...u,
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
attachment_id: data.result?.attachment_id,
|
||||
token_count: data.result?.token_count
|
||||
};
|
||||
} else if (data.status === 'FAILURE') {
|
||||
return { ...u, status: 'failed' };
|
||||
} else if (data.status === 'PROGRESS' && data.result?.current) {
|
||||
return { ...u, progress: data.result.current };
|
||||
}
|
||||
return u;
|
||||
}));
|
||||
if (data.status === 'SUCCESS') {
|
||||
return {
|
||||
...u,
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
attachment_id: data.result?.attachment_id,
|
||||
token_count: data.result?.token_count,
|
||||
};
|
||||
} else if (data.status === 'FAILURE') {
|
||||
return { ...u, status: 'failed' };
|
||||
} else if (data.status === 'PROGRESS' && data.result?.current) {
|
||||
return { ...u, progress: data.result.current };
|
||||
}
|
||||
return u;
|
||||
}),
|
||||
);
|
||||
|
||||
if (data.status !== 'SUCCESS' && data.status !== 'FAILURE') {
|
||||
const timeoutId = window.setTimeout(() => checkTaskStatus(), 2000);
|
||||
const timeoutId = window.setTimeout(
|
||||
() => checkTaskStatus(),
|
||||
2000,
|
||||
);
|
||||
timeoutIds.push(timeoutId);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error checking task status:', error);
|
||||
setUploads(prev => prev.map(u =>
|
||||
u.taskId === upload.taskId
|
||||
? { ...u, status: 'failed' }
|
||||
: u
|
||||
));
|
||||
setUploads((prev) =>
|
||||
prev.map((u) =>
|
||||
u.taskId === upload.taskId ? { ...u, status: 'failed' } : u,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (uploads.some(upload => upload.status === 'processing')) {
|
||||
if (uploads.some((upload) => upload.status === 'processing')) {
|
||||
const timeoutId = window.setTimeout(checkTaskStatus, 2000);
|
||||
timeoutIds.push(timeoutId);
|
||||
}
|
||||
|
||||
return () => {
|
||||
timeoutIds.forEach(id => clearTimeout(id));
|
||||
timeoutIds.forEach((id) => clearTimeout(id));
|
||||
};
|
||||
}, [uploads]);
|
||||
|
||||
@@ -213,29 +229,30 @@ export default function MessageInput({
|
||||
console.log('Selected document:', doc);
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = () => {
|
||||
const completedAttachments = uploads
|
||||
.filter(upload => upload.status === 'completed' && upload.attachment_id)
|
||||
.map(upload => ({
|
||||
.filter((upload) => upload.status === 'completed' && upload.attachment_id)
|
||||
.map((upload) => ({
|
||||
fileName: upload.fileName,
|
||||
id: upload.attachment_id as string
|
||||
id: upload.attachment_id as string,
|
||||
}));
|
||||
|
||||
|
||||
dispatch(setAttachments(completedAttachments));
|
||||
|
||||
|
||||
onSubmit();
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col w-full mx-2">
|
||||
<div className="flex flex-col w-full rounded-[23px] border dark:border-grey border-dark-gray bg-lotion dark:bg-transparent relative">
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2 px-4 sm:px-6 pt-3 pb-0">
|
||||
<div className="mx-2 flex w-full flex-col">
|
||||
<div className="relative flex w-full flex-col rounded-[23px] border border-dark-gray bg-lotion dark:border-grey dark:bg-transparent">
|
||||
<div className="flex flex-wrap gap-1.5 px-4 pb-0 pt-3 sm:gap-2 sm:px-6">
|
||||
{uploads.map((upload, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center px-2 sm:px-3 py-1 sm:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe bg-white dark:bg-[#1F2028] text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray"
|
||||
className="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]"
|
||||
>
|
||||
<span className="font-medium truncate max-w-[120px] sm:max-w-[150px]">{upload.fileName}</span>
|
||||
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
|
||||
{upload.fileName}
|
||||
</span>
|
||||
|
||||
{upload.status === 'completed' && (
|
||||
<span className="ml-2 text-green-500">✓</span>
|
||||
@@ -245,9 +262,10 @@ export default function MessageInput({
|
||||
<span className="ml-2 text-red-500">✗</span>
|
||||
)}
|
||||
|
||||
{(upload.status === 'uploading' || upload.status === 'processing') && (
|
||||
<div className="ml-2 w-4 h-4 relative">
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24">
|
||||
{(upload.status === 'uploading' ||
|
||||
upload.status === 'processing') && (
|
||||
<div className="relative ml-2 h-4 w-4">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="text-gray-200 dark:text-gray-700"
|
||||
cx="12"
|
||||
@@ -287,41 +305,57 @@ export default function MessageInput({
|
||||
onChange={onChange}
|
||||
tabIndex={1}
|
||||
placeholder={t('inputPlaceholder')}
|
||||
className="inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion dark:bg-transparent py-3 sm:py-5 text-base leading-tight opacity-100 focus:outline-none dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 px-4 sm:px-6 no-scrollbar"
|
||||
className="inputbox-style no-scrollbar w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion px-4 py-3 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 sm:px-6 sm:py-5"
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label={t('inputPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center px-3 sm:px-4 py-1.5 sm:py-2">
|
||||
<div className="flex-grow flex flex-wrap gap-1 sm:gap-2">
|
||||
<button
|
||||
ref={sourceButtonRef}
|
||||
className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[130px] xs:max-w-[150px]"
|
||||
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
||||
>
|
||||
<img src={SourceIcon} alt="Sources" className="w-3.5 sm:w-4 h-3.5 sm:h-4 mr-1 sm:mr-1.5 flex-shrink-0" />
|
||||
<span className="text-[10px] xs:text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium truncate overflow-hidden">
|
||||
{selectedDocs
|
||||
? selectedDocs.name
|
||||
: t('conversation.sources.title')}
|
||||
</span>
|
||||
</button>
|
||||
<div className="flex items-center px-3 py-1.5 sm:px-4 sm:py-2">
|
||||
<div className="flex flex-grow flex-wrap gap-1 sm:gap-2">
|
||||
{showSourceButton && (
|
||||
<button
|
||||
ref={sourceButtonRef}
|
||||
className="xs:px-3 xs:py-1.5 xs:max-w-[150px] flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]"
|
||||
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
||||
>
|
||||
<img
|
||||
src={SourceIcon}
|
||||
alt="Sources"
|
||||
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||
/>
|
||||
<span className="xs:text-[12px] overflow-hidden truncate text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||
{selectedDocs
|
||||
? selectedDocs.name
|
||||
: t('conversation.sources.title')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
ref={toolButtonRef}
|
||||
className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[130px] xs:max-w-[150px]"
|
||||
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
||||
>
|
||||
<img src={ToolIcon} alt="Tools" className="w-3.5 sm:w-4 h-3.5 sm:h-4 mr-1 sm:mr-1.5 flex-shrink-0" />
|
||||
<span className="text-[10px] xs:text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium truncate overflow-hidden">
|
||||
{t('settings.tools.label')}
|
||||
</span>
|
||||
</button>
|
||||
<label className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors cursor-pointer">
|
||||
<img src={ClipIcon} alt="Attach" className="w-3.5 sm:w-4 h-3.5 sm:h-4 mr-1 sm:mr-1.5" />
|
||||
<span className="text-[10px] xs:text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium">
|
||||
{showToolButton && (
|
||||
<button
|
||||
ref={toolButtonRef}
|
||||
className="xs:px-3 xs:py-1.5 xs:max-w-[150px] flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]"
|
||||
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
||||
>
|
||||
<img
|
||||
src={ToolIcon}
|
||||
alt="Tools"
|
||||
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||
/>
|
||||
<span className="xs:text-[12px] overflow-hidden truncate text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||
{t('settings.tools.label')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<label className="xs:px-3 xs:py-1.5 flex cursor-pointer items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]">
|
||||
<img
|
||||
src={ClipIcon}
|
||||
alt="Attach"
|
||||
className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||
/>
|
||||
<span className="xs:text-[12px] text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||
Attach
|
||||
</span>
|
||||
<input
|
||||
@@ -337,18 +371,18 @@ export default function MessageInput({
|
||||
<button
|
||||
onClick={loading ? undefined : handleSubmit}
|
||||
aria-label={loading ? t('loading') : t('send')}
|
||||
className={`flex items-center justify-center p-2 sm:p-2.5 rounded-full ${loading ? 'bg-gray-300 dark:bg-gray-600' : 'bg-black dark:bg-white'} ml-auto flex-shrink-0`}
|
||||
className={`flex items-center justify-center rounded-full p-2 sm:p-2.5 ${loading ? 'bg-gray-300 dark:bg-gray-600' : 'bg-black dark:bg-white'} ml-auto flex-shrink-0`}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<img
|
||||
src={isDarkTheme ? SpinnerDark : Spinner}
|
||||
className="w-3.5 sm:w-4 h-3.5 sm:h-4 animate-spin"
|
||||
className="h-3.5 w-3.5 animate-spin sm:h-4 sm:w-4"
|
||||
alt={t('loading')}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className={`w-3.5 sm:w-4 h-3.5 sm:h-4 ${isDarkTheme ? 'filter invert' : ''}`}
|
||||
className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${isDarkTheme ? 'invert filter' : ''}`}
|
||||
src={PaperPlane}
|
||||
alt={t('send')}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
||||
import DragFileUpload from '../assets/DragFileUpload.svg';
|
||||
import newChatIcon from '../assets/openNewChat.svg';
|
||||
import ShareIcon from '../assets/share.svg';
|
||||
import MessageInput from '../components/MessageInput';
|
||||
import { useMediaQuery } from '../hooks';
|
||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import {
|
||||
selectConversationId,
|
||||
selectSelectedAgent,
|
||||
selectToken,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import { AppDispatch } from '../store';
|
||||
import Upload from '../upload/Upload';
|
||||
import { handleSendFeedback } from './conversationHandlers';
|
||||
import ConversationMessages from './ConversationMessages';
|
||||
import { FEEDBACK, Query } from './conversationModels';
|
||||
import {
|
||||
addQuery,
|
||||
@@ -24,28 +30,29 @@ import {
|
||||
updateConversationId,
|
||||
updateQuery,
|
||||
} from './conversationSlice';
|
||||
import Upload from '../upload/Upload';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import ConversationMessages from './ConversationMessages';
|
||||
import MessageInput from '../components/MessageInput';
|
||||
|
||||
export default function Conversation() {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const token = useSelector(selectToken);
|
||||
const queries = useSelector(selectQueries);
|
||||
const status = useSelector(selectStatus);
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const [input, setInput] = useState('');
|
||||
const fetchStream = useRef<any>(null);
|
||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [uploadModalState, setUploadModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] =
|
||||
useState<boolean>(false);
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
const [handleDragActive, setHandleDragActive] = useState<boolean>(false);
|
||||
|
||||
const fetchStream = useRef<any>(null);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setUploadModalState('ACTIVE');
|
||||
setFiles(acceptedFiles);
|
||||
@@ -83,35 +90,36 @@ export default function Conversation() {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (queries.length) {
|
||||
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
||||
queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry
|
||||
}
|
||||
}, [queries[queries.length - 1]]);
|
||||
const handleFetchAnswer = useCallback(
|
||||
({ question, index }: { question: string; index?: number }) => {
|
||||
fetchStream.current = dispatch(fetchAnswer({ question, indx: index }));
|
||||
},
|
||||
[dispatch, selectedAgent],
|
||||
);
|
||||
|
||||
const handleQuestion = ({
|
||||
question,
|
||||
isRetry = false,
|
||||
updated = null,
|
||||
indx = undefined,
|
||||
}: {
|
||||
question: string;
|
||||
isRetry?: boolean;
|
||||
updated?: boolean | null;
|
||||
indx?: number;
|
||||
}) => {
|
||||
if (updated === true) {
|
||||
!isRetry &&
|
||||
dispatch(resendQuery({ index: indx as number, prompt: question }));
|
||||
fetchStream.current = dispatch(fetchAnswer({ question, indx }));
|
||||
} else {
|
||||
question = question.trim();
|
||||
if (question === '') return;
|
||||
!isRetry && dispatch(addQuery({ prompt: question }));
|
||||
fetchStream.current = dispatch(fetchAnswer({ question }));
|
||||
}
|
||||
};
|
||||
const handleQuestion = useCallback(
|
||||
({
|
||||
question,
|
||||
isRetry = false,
|
||||
index = undefined,
|
||||
}: {
|
||||
question: string;
|
||||
isRetry?: boolean;
|
||||
index?: number;
|
||||
}) => {
|
||||
const trimmedQuestion = question.trim();
|
||||
if (trimmedQuestion === '') return;
|
||||
|
||||
if (index !== undefined) {
|
||||
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
|
||||
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||
} else {
|
||||
if (!isRetry) dispatch(addQuery({ prompt: trimmedQuestion }));
|
||||
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||
}
|
||||
},
|
||||
[dispatch, handleFetchAnswer],
|
||||
);
|
||||
|
||||
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
|
||||
const prevFeedback = query.feedback;
|
||||
@@ -143,10 +151,9 @@ export default function Conversation() {
|
||||
indx?: number,
|
||||
) => {
|
||||
if (updated === true) {
|
||||
handleQuestion({ question: updatedQuestion as string, updated, indx });
|
||||
handleQuestion({ question: updatedQuestion as string, index: indx });
|
||||
} else if (input && status !== 'loading') {
|
||||
if (lastQueryReturnedErr) {
|
||||
// update last failed query with new prompt
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: queries.length - 1,
|
||||
@@ -160,13 +167,14 @@ export default function Conversation() {
|
||||
isRetry: true,
|
||||
});
|
||||
} else {
|
||||
handleQuestion({
|
||||
handleQuestion({
|
||||
question: input,
|
||||
});
|
||||
}
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const resetConversation = () => {
|
||||
dispatch(setConversation([]));
|
||||
dispatch(
|
||||
@@ -175,22 +183,29 @@ export default function Conversation() {
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const newChat = () => {
|
||||
if (queries && queries.length > 0) resetConversation();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (queries.length) {
|
||||
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
||||
queries[queries.length - 1].response && setLastQueryReturnedErr(false);
|
||||
}
|
||||
}, [queries[queries.length - 1]]);
|
||||
return (
|
||||
<div className="flex flex-col gap-1 h-full justify-end">
|
||||
<div className="flex h-full flex-col justify-end gap-1">
|
||||
{conversationId && queries.length > 0 && (
|
||||
<div className="absolute top-4 right-20">
|
||||
<div className="flex mt-2 items-center gap-4">
|
||||
<div className="absolute right-20 top-4">
|
||||
<div className="mt-2 flex items-center gap-4">
|
||||
{isMobile && queries.length > 0 && (
|
||||
<button
|
||||
title="Open New Chat"
|
||||
onClick={() => {
|
||||
newChat();
|
||||
}}
|
||||
className="hover:bg-bright-gray dark:hover:bg-[#28292E] rounded-full p-2"
|
||||
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img
|
||||
className="h-5 w-5 filter dark:invert"
|
||||
@@ -205,7 +220,7 @@ export default function Conversation() {
|
||||
onClick={() => {
|
||||
setShareModalState(true);
|
||||
}}
|
||||
className="hover:bg-bright-gray dark:hover:bg-[#28292E] rounded-full p-2"
|
||||
className="rounded-full p-2 hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img
|
||||
className="h-5 w-5 filter dark:invert"
|
||||
@@ -233,7 +248,7 @@ export default function Conversation() {
|
||||
status={status}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-end self-center rounded-2xl bg-opacity-0 z-3 w-full md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12 max-w-[1300px] h-auto py-1">
|
||||
<div className="z-3 flex h-auto w-full max-w-[1300px] flex-col items-end self-center rounded-2xl bg-opacity-0 py-1 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="flex w-full items-center rounded-[40px]"
|
||||
@@ -247,20 +262,22 @@ export default function Conversation() {
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onSubmit={handleQuestionSubmission}
|
||||
loading={status === 'loading'}
|
||||
showSourceButton={selectedAgent ? false : true}
|
||||
showToolButton={selectedAgent ? false : true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-4000 hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs dark:text-sonic-silver md:inline md:w-full">
|
||||
<p className="hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs text-gray-4000 dark:text-sonic-silver md:inline md:w-full">
|
||||
{t('tagline')}
|
||||
</p>
|
||||
</div>
|
||||
{handleDragActive && (
|
||||
<div className="pointer-events-none fixed top-0 left-0 z-30 flex flex-col size-full items-center justify-center bg-opacity-50 bg-white dark:bg-gray-alpha">
|
||||
<div className="pointer-events-none fixed left-0 top-0 z-30 flex size-full flex-col items-center justify-center bg-white bg-opacity-50 dark:bg-gray-alpha">
|
||||
<img className="filter dark:invert" src={DragFileUpload} />
|
||||
<span className="px-2 text-2xl font-bold text-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.drag.title')}
|
||||
</span>
|
||||
<span className="p-2 text-s w-48 text-center text-outer-space dark:text-silver">
|
||||
<span className="text-s w-48 p-2 text-center text-outer-space dark:text-silver">
|
||||
{t('modals.uploadDoc.drag.description')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ interface ConversationMessagesProps {
|
||||
handleFeedback?: (query: Query, feedback: FEEDBACK, index: number) => void;
|
||||
queries: Query[];
|
||||
status: Status;
|
||||
showHeroOnEmpty?: boolean;
|
||||
}
|
||||
|
||||
export default function ConversationMessages({
|
||||
@@ -31,6 +32,7 @@ export default function ConversationMessages({
|
||||
queries,
|
||||
status,
|
||||
handleFeedback,
|
||||
showHeroOnEmpty = true,
|
||||
}: ConversationMessagesProps) {
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -141,7 +143,7 @@ export default function ConversationMessages({
|
||||
ref={conversationRef}
|
||||
onWheel={handleUserInterruption}
|
||||
onTouchMove={handleUserInterruption}
|
||||
className="flex justify-center w-full overflow-y-auto h-screen sm:pt-12"
|
||||
className="flex justify-center w-full overflow-y-auto h-full sm:pt-12"
|
||||
>
|
||||
{queries.length > 0 && !hasScrolledToLast && (
|
||||
<button
|
||||
@@ -161,7 +163,6 @@ export default function ConversationMessages({
|
||||
{queries.length > 0 ? (
|
||||
queries.map((query, index) => (
|
||||
<Fragment key={index}>
|
||||
|
||||
<ConversationBubble
|
||||
className={'first:mt-5'}
|
||||
key={`${index}QUESTION`}
|
||||
@@ -175,9 +176,9 @@ export default function ConversationMessages({
|
||||
{prepResponseView(query, index)}
|
||||
</Fragment>
|
||||
))
|
||||
) : (
|
||||
) : showHeroOnEmpty ? (
|
||||
<Hero handleQuestion={handleQuestion} />
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ export function handleFetchAnswer(
|
||||
promptId: string | null,
|
||||
chunks: string,
|
||||
token_limit: number,
|
||||
agentId?: string,
|
||||
attachments?: string[],
|
||||
): Promise<
|
||||
| {
|
||||
@@ -50,13 +51,14 @@ export function handleFetchAnswer(
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
agent_id: agentId,
|
||||
};
|
||||
|
||||
|
||||
// Add attachments to payload if they exist
|
||||
if (attachments && attachments.length > 0) {
|
||||
payload.attachments = attachments;
|
||||
}
|
||||
|
||||
|
||||
if (selectedDocs && 'id' in selectedDocs) {
|
||||
payload.active_docs = selectedDocs.id as string;
|
||||
}
|
||||
@@ -97,6 +99,7 @@ export function handleFetchAnswerSteaming(
|
||||
token_limit: number,
|
||||
onEvent: (event: MessageEvent) => void,
|
||||
indx?: number,
|
||||
agentId?: string,
|
||||
attachments?: string[],
|
||||
): Promise<Answer> {
|
||||
history = history.map((item) => {
|
||||
@@ -116,13 +119,14 @@ export function handleFetchAnswerSteaming(
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
index: indx,
|
||||
agent_id: agentId,
|
||||
};
|
||||
|
||||
|
||||
// Add attachments to payload if they exist
|
||||
if (attachments && attachments.length > 0) {
|
||||
payload.attachments = attachments;
|
||||
}
|
||||
|
||||
|
||||
if (selectedDocs && 'id' in selectedDocs) {
|
||||
payload.active_docs = selectedDocs.id as string;
|
||||
}
|
||||
|
||||
@@ -51,5 +51,6 @@ export interface RetrievalPayload {
|
||||
token_limit: number;
|
||||
isNoneDoc: boolean;
|
||||
index?: number;
|
||||
agent_id?: string;
|
||||
attachments?: string[];
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ export const fetchAnswer = createAsyncThunk<
|
||||
|
||||
let isSourceUpdated = false;
|
||||
const state = getState() as RootState;
|
||||
const attachments = state.conversation.attachments?.map(a => a.id) || [];
|
||||
|
||||
const attachments = state.conversation.attachments?.map((a) => a.id) || [];
|
||||
|
||||
if (state.preference) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchAnswerSteaming(
|
||||
@@ -80,7 +80,6 @@ export const fetchAnswer = createAsyncThunk<
|
||||
);
|
||||
} else if (data.type === 'thought') {
|
||||
const result = data.thought;
|
||||
console.log('thought', result);
|
||||
dispatch(
|
||||
updateThought({
|
||||
index: indx ?? state.conversation.queries.length - 1,
|
||||
@@ -122,7 +121,8 @@ export const fetchAnswer = createAsyncThunk<
|
||||
}
|
||||
},
|
||||
indx,
|
||||
attachments
|
||||
state.preference.selectedAgent?.id,
|
||||
attachments,
|
||||
);
|
||||
} else {
|
||||
const answer = await handleFetchAnswer(
|
||||
@@ -135,7 +135,8 @@ export const fetchAnswer = createAsyncThunk<
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
attachments
|
||||
state.preference.selectedAgent?.id,
|
||||
attachments,
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
@@ -286,7 +287,10 @@ export const conversationSlice = createSlice({
|
||||
const { index, message } = action.payload;
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
setAttachments: (state, action: PayloadAction<{ fileName: string; id: string }[]>) => {
|
||||
setAttachments: (
|
||||
state,
|
||||
action: PayloadAction<{ fileName: string; id: string }[]>,
|
||||
) => {
|
||||
state.attachments = action.payload;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import conversationService from '../api/services/conversationService';
|
||||
import userService from '../api/services/userService';
|
||||
import { Doc, GetDocsResponse } from '../models/misc';
|
||||
import { GetConversationsResult, ConversationSummary } from './types';
|
||||
|
||||
//Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later.
|
||||
export async function getDocs(token: string | null): Promise<Doc[] | null> {
|
||||
@@ -49,23 +50,37 @@ export async function getDocsWithPagination(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConversations(token: string | null): Promise<{
|
||||
data: { name: string; id: string }[] | null;
|
||||
loading: boolean;
|
||||
}> {
|
||||
export async function getConversations(
|
||||
token: string | null,
|
||||
): Promise<GetConversationsResult> {
|
||||
try {
|
||||
const response = await conversationService.getConversations(token);
|
||||
const data = await response.json();
|
||||
|
||||
const conversations: { name: string; id: string }[] = [];
|
||||
if (!response.ok) {
|
||||
console.error('Error fetching conversations:', response.statusText);
|
||||
return { data: null, loading: false };
|
||||
}
|
||||
|
||||
data.forEach((conversation: object) => {
|
||||
conversations.push(conversation as { name: string; id: string });
|
||||
});
|
||||
const rawData: unknown = await response.json();
|
||||
if (!Array.isArray(rawData)) {
|
||||
console.error(
|
||||
'Invalid data format received from API: Expected an array.',
|
||||
rawData,
|
||||
);
|
||||
return { data: null, loading: false };
|
||||
}
|
||||
|
||||
const conversations: ConversationSummary[] = rawData.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
agent_id: item.agent_id ?? null,
|
||||
}));
|
||||
return { data: conversations, loading: false };
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(
|
||||
'An unexpected error occurred while fetching conversations:',
|
||||
error,
|
||||
);
|
||||
return { data: null, loading: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
PayloadAction,
|
||||
createListenerMiddleware,
|
||||
createSlice,
|
||||
isAnyOf,
|
||||
PayloadAction,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { setLocalApiKey, setLocalRecentDocs } from './preferenceApi';
|
||||
import { RootState } from '../store';
|
||||
|
||||
import { Agent } from '../agents/types';
|
||||
import { ActiveState, Doc } from '../models/misc';
|
||||
import { RootState } from '../store';
|
||||
import { setLocalApiKey, setLocalRecentDocs } from './preferenceApi';
|
||||
|
||||
export interface Preference {
|
||||
apiKey: string;
|
||||
@@ -22,6 +24,7 @@ export interface Preference {
|
||||
token: string | null;
|
||||
modalState: ActiveState;
|
||||
paginatedDocuments: Doc[] | null;
|
||||
selectedAgent: Agent | null;
|
||||
}
|
||||
|
||||
const initialState: Preference = {
|
||||
@@ -46,6 +49,7 @@ const initialState: Preference = {
|
||||
token: localStorage.getItem('authToken') || null,
|
||||
modalState: 'INACTIVE',
|
||||
paginatedDocuments: null,
|
||||
selectedAgent: null,
|
||||
};
|
||||
|
||||
export const prefSlice = createSlice({
|
||||
@@ -82,6 +86,9 @@ export const prefSlice = createSlice({
|
||||
setModalStateDeleteConv: (state, action: PayloadAction<ActiveState>) => {
|
||||
state.modalState = action.payload;
|
||||
},
|
||||
setSelectedAgent: (state, action) => {
|
||||
state.selectedAgent = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -96,6 +103,7 @@ export const {
|
||||
setTokenLimit,
|
||||
setModalStateDeleteConv,
|
||||
setPaginatedDocuments,
|
||||
setSelectedAgent,
|
||||
} = prefSlice.actions;
|
||||
export default prefSlice.reducer;
|
||||
|
||||
@@ -170,3 +178,5 @@ export const selectTokenLimit = (state: RootState) =>
|
||||
state.preference.token_limit;
|
||||
export const selectPaginatedDocuments = (state: RootState) =>
|
||||
state.preference.paginatedDocuments;
|
||||
export const selectSelectedAgent = (state: RootState) =>
|
||||
state.preference.selectedAgent;
|
||||
|
||||
10
frontend/src/preferences/types/index.ts
Normal file
10
frontend/src/preferences/types/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type ConversationSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
agent_id: string | null;
|
||||
};
|
||||
|
||||
export type GetConversationsResult = {
|
||||
data: ConversationSummary[] | null;
|
||||
loading: boolean;
|
||||
};
|
||||
@@ -40,6 +40,7 @@ const preloadedState: { preference: Preference } = {
|
||||
],
|
||||
modalState: 'INACTIVE',
|
||||
paginatedDocuments: null,
|
||||
selectedAgent: null,
|
||||
},
|
||||
};
|
||||
const store = configureStore({
|
||||
|
||||
Reference in New Issue
Block a user