From 81d21d151a57298eaa95d9e7cd7d5f5df0265e40 Mon Sep 17 00:00:00 2001 From: modenter Date: Thu, 22 Aug 2024 18:55:41 +0200 Subject: [PATCH 01/30] feat: v0 of chat assistants feature --- backend/chainlit/__init__.py | 32 +++ backend/chainlit/assistant.py | 36 +++ backend/chainlit/assistant_settings.py | 35 +++ backend/chainlit/config.py | 8 + backend/chainlit/emitter.py | 7 + backend/chainlit/markdown.py | 16 +- backend/chainlit/session.py | 19 ++ backend/chainlit/socket.py | 27 ++ backend/chainlit/translations/en-US.json | 6 + backend/chainlit/translations/fr-FR.json | 236 ++++++++++++++++++ backend/chainlit/user_session.py | 3 + frontend/src/assets/squarePlus.tsx | 52 ++++ .../molecules/AssistantProfiles.tsx | 67 +++++ .../molecules/newAssistantButton.tsx | 52 ++++ .../organisms/chat/inputBox/SubmitButton.tsx | 8 +- .../organisms/chat/newAssistant.tsx | 119 +++++++++ frontend/src/components/organisms/header.tsx | 4 + frontend/src/state/project.ts | 14 ++ libs/react-client/src/state.ts | 31 +++ libs/react-client/src/useChatData.ts | 12 +- libs/react-client/src/useChatInteract.ts | 34 ++- libs/react-client/src/useChatSession.ts | 9 + 22 files changed, 808 insertions(+), 19 deletions(-) create mode 100644 backend/chainlit/assistant.py create mode 100644 backend/chainlit/assistant_settings.py create mode 100644 backend/chainlit/translations/fr-FR.json create mode 100644 frontend/src/assets/squarePlus.tsx create mode 100644 frontend/src/components/molecules/AssistantProfiles.tsx create mode 100644 frontend/src/components/molecules/newAssistantButton.tsx create mode 100644 frontend/src/components/organisms/chat/newAssistant.tsx diff --git a/backend/chainlit/__init__.py b/backend/chainlit/__init__.py index 00b969994b..0cd78ff954 100644 --- a/backend/chainlit/__init__.py +++ b/backend/chainlit/__init__.py @@ -24,6 +24,10 @@ import chainlit.input_widget as input_widget from chainlit.action import Action + +# import BaseAssistant +from chainlit.assistant import BaseAssistant +from chainlit.assistant_settings import AssistantSettings from chainlit.cache import cache from chainlit.chat_context import chat_context from chainlit.chat_settings import ChatSettings @@ -66,6 +70,27 @@ logger.info("Loaded .env file") +# assistant-related callbacks setters +# --------------------------------- +@trace +def on_create_assistant( + func: Callable[[Optional[User], AssistantSettings], Any] +) -> Callable[[Optional[User], AssistantSettings], Any]: + config.code.on_create_assistant = wrap_user_function(func) + return func + + +@trace +def on_list_assistants( + func: Callable[[Optional[User]], List[BaseAssistant]] +) -> Callable[[Optional[User]], List[BaseAssistant]]: + config.code.on_list_assistants = wrap_user_function(func) + return func + + +# --------------------------------- + + @trace def password_auth_callback(func: Callable[[str, str], Optional[User]]) -> Callable: """ @@ -402,6 +427,9 @@ def acall(self): "TaskStatus", "Video", "ChatSettings", + "AssistantSettings", + # assistant + "BaseAssistant", "input_widget", "Message", "ErrorMessage", @@ -421,6 +449,10 @@ def acall(self): "action_callback", "author_rename", "on_settings_update", + # assistant-related callbacks setters + "on_create_assistant", + "on_list_assistants", + # end of assistant-related callbacks setters "password_auth_callback", "header_auth_callback", "sleep", diff --git a/backend/chainlit/assistant.py b/backend/chainlit/assistant.py new file mode 100644 index 0000000000..6b54f731d1 --- /dev/null +++ b/backend/chainlit/assistant.py @@ -0,0 +1,36 @@ +from typing import List, Optional + +from chainlit.input_widget import InputWidget +from dataclasses_json import DataClassJsonMixin +from pydantic.dataclasses import Field, dataclass + + +@dataclass +class BaseAssistant(DataClassJsonMixin): + """ + An abstract base class for assistants that can be extended. + """ + + name: str + markdown_description: str + icon: str + + def __init__(self, name: str, markdown_description: str, icon: str): + """ + Initialize the BaseAssistant. + + Args: + name (str): The name of the assistant. + markdown_description (str): A markdown description of the assistant. + icon (Optional[str], optional): An optional icon for the assistant. Defaults to None. + """ + self.name = name + self.markdown_description = markdown_description + self.icon = icon + + async def run(self, *args, **kwargs): + """ + An abstract method that should be implemented by subclasses. + This method defines the main functionality of the assistant. + """ + pass diff --git a/backend/chainlit/assistant_settings.py b/backend/chainlit/assistant_settings.py new file mode 100644 index 0000000000..9442104b01 --- /dev/null +++ b/backend/chainlit/assistant_settings.py @@ -0,0 +1,35 @@ +import logging +from typing import List + +from chainlit.context import context +from chainlit.input_widget import InputWidget +from pydantic.dataclasses import Field, dataclass + + +@dataclass +class AssistantSettings: + """Useful to create chat settings that the user can change.""" + + inputs: List[InputWidget] = Field(default_factory=list, exclude=True) + + def __init__( + self, + inputs: List[InputWidget], + ) -> None: + self.inputs = inputs + + def settings(self): + return dict( + [(input_widget.id, input_widget.initial) for input_widget in self.inputs] + ) + + async def send(self): + settings = self.settings() + context.emitter.set_assistant_settings(settings) + + inputs_content = [input_widget.to_dict() for input_widget in self.inputs] + # logging.info(f"Sending assistant settings: {inputs_content}") + await context.emitter.emit("assistant_settings", inputs_content) + + # logging.info(f"Assistant settings sent: {settings}") + return settings diff --git a/backend/chainlit/config.py b/backend/chainlit/config.py index f43482b637..6c0fd08140 100644 --- a/backend/chainlit/config.py +++ b/backend/chainlit/config.py @@ -21,7 +21,9 @@ from chainlit.types import AudioChunk, ChatProfile, Starter, ThreadDict from chainlit.user import User from fastapi import Request, Response + from chainlit.assistant import BaseAssistant +from chainlit.assistant_settings import AssistantSettings BACKEND_ROOT = os.path.dirname(__file__) PACKAGE_ROOT = os.path.dirname(os.path.dirname(BACKEND_ROOT)) @@ -291,6 +293,12 @@ class CodeSettings: ) set_starters: Optional[Callable[[Optional["User"]], List["Starter"]]] = None + # assistant-related callback function + on_create_assistant: Optional[Callable[[Optional["User"], Any], Any]] = None + on_list_assistants: Optional[ + Callable[[Optional["User"]], List["AssistantSettings"]] + ] = None + @dataclass() class ProjectSettings(DataClassJsonMixin): diff --git a/backend/chainlit/emitter.py b/backend/chainlit/emitter.py index e80bd46dd2..2906c54def 100644 --- a/backend/chainlit/emitter.py +++ b/backend/chainlit/emitter.py @@ -114,6 +114,10 @@ async def set_chat_settings(self, settings: dict): """Stub method to set chat settings.""" pass + async def set_assistant_settings(self, settings: dict): + """Stub method to send assistant settings to the UI.""" + pass + async def send_action_response( self, id: str, status: bool, response: Optional[str] = None ): @@ -361,6 +365,9 @@ def send_token(self, id: str, token: str, is_sequence=False, is_input=False): def set_chat_settings(self, settings: Dict[str, Any]): self.session.chat_settings = settings + def set_assistant_settings(self, settings: Dict[str, Any]): + self.session.assistant_settings = settings + def send_action_response( self, id: str, status: bool, response: Optional[str] = None ): diff --git a/backend/chainlit/markdown.py b/backend/chainlit/markdown.py index 224a1373fd..9efa529469 100644 --- a/backend/chainlit/markdown.py +++ b/backend/chainlit/markdown.py @@ -3,20 +3,8 @@ from chainlit.logger import logger # Default chainlit.md file created if none exists -DEFAULT_MARKDOWN_STR = """# Welcome to Chainlit! 🚀🤖 - -Hi there, Developer! 👋 We're excited to have you on board. Chainlit is a powerful tool designed to help you prototype, debug and share applications built on top of LLMs. - -## Useful Links 🔗 - -- **Documentation:** Get started with our comprehensive [Chainlit Documentation](https://docs.chainlit.io) 📚 -- **Discord Community:** Join our friendly [Chainlit Discord](https://discord.gg/k73SQ3FyUh) to ask questions, share your projects, and connect with other developers! 💬 - -We can't wait to see what you create with Chainlit! Happy coding! 💻😊 - -## Welcome screen - -To modify the welcome screen, edit the `chainlit.md` file at the root of your project. If you do not want a welcome screen, just leave this file empty. +DEFAULT_MARKDOWN_STR = """# It worked! +I apparently successfuly built chainlit and runned it. """ diff --git a/backend/chainlit/session.py b/backend/chainlit/session.py index f23e39ca40..7bfe3058fd 100644 --- a/backend/chainlit/session.py +++ b/backend/chainlit/session.py @@ -24,6 +24,9 @@ from chainlit.types import FileDict, FileReference from chainlit.user import PersistedUser, User +# import the BaseAssistant class +from chainlit.assistant import BaseAssistant + ClientType = Literal["webapp", "copilot", "teams", "slack", "discord"] @@ -72,8 +75,12 @@ def __init__( user_env: Optional[Dict[str, str]], # Chat profile selected before the session was created chat_profile: Optional[str] = None, + # Selected assistant + selected_assistant: Optional[str] = None, # Origin of the request http_referer: Optional[str] = None, + # assistant settings + assistant_settings: Optional[Dict[str, Any]] = None, ): if thread_id: self.thread_id_to_resume = thread_id @@ -90,7 +97,9 @@ def __init__( self.id = id + self.assistant_settings = assistant_settings self.chat_settings: Dict[str, Any] = {} + self.selected_assistant = selected_assistant @property def files_dir(self): @@ -153,6 +162,7 @@ def to_persistable(self) -> Dict: user_session = user_sessions.get(self.id) or {} # type: Dict user_session["chat_settings"] = self.chat_settings user_session["chat_profile"] = self.chat_profile + user_session["selected_assistant"] = self.selected_assistant user_session["http_referer"] = self.http_referer user_session["client_type"] = self.client_type metadata = clean_metadata(user_session) @@ -176,6 +186,8 @@ def __init__( user_env: Optional[Dict[str, str]] = None, # Origin of the request http_referer: Optional[str] = None, + # assistant settings + assistant_settings: Optional[Dict[str, Any]] = None, ): super().__init__( id=id, @@ -185,6 +197,7 @@ def __init__( client_type=client_type, user_env=user_env, http_referer=http_referer, + assistant_settings=assistant_settings, ) def delete(self): @@ -228,10 +241,14 @@ def __init__( token: Optional[str] = None, # Chat profile selected before the session was created chat_profile: Optional[str] = None, + # Selected assistant + selected_assistant: Optional[str] = None, # Languages of the user's browser languages: Optional[str] = None, # Origin of the request http_referer: Optional[str] = None, + # chat settings + assistant_settings: Optional[Dict[str, Any]] = None, ): super().__init__( id=id, @@ -241,7 +258,9 @@ def __init__( user_env=user_env, client_type=client_type, chat_profile=chat_profile, + selected_assistant=selected_assistant, http_referer=http_referer, + assistant_settings=assistant_settings, ) self.socket_id = socket_id diff --git a/backend/chainlit/socket.py b/backend/chainlit/socket.py index f2979e9e21..4258f255af 100644 --- a/backend/chainlit/socket.py +++ b/backend/chainlit/socket.py @@ -6,6 +6,7 @@ from urllib.parse import unquote from chainlit.action import Action +from chainlit.assistant_settings import AssistantSettings from chainlit.auth import get_current_user, require_login from chainlit.chat_context import chat_context from chainlit.config import config @@ -406,3 +407,29 @@ async def change_settings(sid, settings: Dict[str, Any]): if config.code.on_settings_update: await config.code.on_settings_update(settings) + + +# when signal "on_create_assistant" is received, call the callback with user and assistant +@sio.on("on_create_assistant") +async def on_create_assistant(sid, options): + context = init_ws_context(sid) + logger.info(f"Received request to create assistant with options: {options}") + if config.code.on_create_assistant: + await config.code.on_create_assistant(context.session.user, options) + logger.info("Assistant creation process completed") + + +# when signal "on_list_assistants" is received, call the callback with user +@sio.on("on_list_assistants") +async def on_list_assistants(sid): + context = init_ws_context(sid) + if config.code.on_list_assistants: + return await config.code.on_list_assistants(context.session.user) + + +@sio.on("select_assistant") +async def select_assistant(sid, assistant_name: str): + # store the assistant name in the session + context = init_ws_context(sid) + context.session.selected_assistant = assistant_name + logger.info(f"Selected assistant: {assistant_name}") diff --git a/backend/chainlit/translations/en-US.json b/backend/chainlit/translations/en-US.json index 2bb9b6f4d8..a259866845 100644 --- a/backend/chainlit/translations/en-US.json +++ b/backend/chainlit/translations/en-US.json @@ -16,6 +16,9 @@ "newChatButton": { "newChat": "New Chat" }, + "newAssistantButton": { + "newAssistant": "New Assistant" + }, "tasklist": { "TaskList": { "title": "🗒️ Task List", @@ -109,6 +112,9 @@ } }, "organisms": { + "assistantCreationModal": { + "title": "Create new assistant" + }, "chat": { "history": { "index": { diff --git a/backend/chainlit/translations/fr-FR.json b/backend/chainlit/translations/fr-FR.json new file mode 100644 index 0000000000..bcbf36db20 --- /dev/null +++ b/backend/chainlit/translations/fr-FR.json @@ -0,0 +1,236 @@ +{ + "components": { + "atoms": { + "buttons": { + "userButton": { + "menu": { + "settings": "Paramètres", + "settingsKey": "P", + "APIKeys": "Clés API", + "logout": "Déconnexion" + } + } + } + }, + "molecules": { + "newChatButton": { + "newChat": "Nouveau Chat" + }, + "newAssistantButton": { + "newAssistant": "Nouveau Chatbot" + }, + "tasklist": { + "TaskList": { + "title": "🗒️ Liste des Tâches", + "loading": "Chargement...", + "error": "Une erreur s'est produite" + } + }, + "attachments": { + "cancelUpload": "Annuler le téléchargement", + "removeAttachment": "Supprimer la pièce jointe" + }, + "newChatDialog": { + "createNewChat": "Créer un nouveau chat ?", + "clearChat": "Cela effacera les messages actuels et commencera un nouveau chat.", + "cancel": "Annuler", + "confirm": "Confirmer" + }, + "settingsModal": { + "settings": "Paramètres", + "expandMessages": "Développer les Messages", + "hideChainOfThought": "Masquer la Chaîne de Pensée", + "darkMode": "Mode Sombre" + }, + "detailsButton": { + "using": "Utilisation", + "used": "Utilisé" + }, + "auth": { + "authLogin": { + "title": "Connectez-vous pour accéder à l'application.", + "form": { + "email": "Adresse e-mail", + "password": "Mot de passe", + "noAccount": "Vous n'avez pas de compte ?", + "alreadyHaveAccount": "Vous avez déjà un compte ?", + "signup": "S'inscrire", + "signin": "Se connecter", + "or": "OU", + "continue": "Continuer", + "forgotPassword": "Mot de passe oublié ?", + "passwordMustContain": "Votre mot de passe doit contenir :", + "emailRequired": "l'email est un champ obligatoire", + "passwordRequired": "le mot de passe est un champ obligatoire" + }, + "error": { + "default": "Impossible de se connecter.", + "signin": "Essayez de vous connecter avec un autre compte.", + "oauthsignin": "Essayez de vous connecter avec un autre compte.", + "redirect_uri_mismatch": "L'URI de redirection ne correspond pas à la configuration de l'application oauth.", + "oauthcallbackerror": "Essayez de vous connecter avec un autre compte.", + "oauthcreateaccount": "Essayez de vous connecter avec un autre compte.", + "emailcreateaccount": "Essayez de vous connecter avec un autre compte.", + "callback": "Essayez de vous connecter avec un autre compte.", + "oauthaccountnotlinked": "Pour confirmer votre identité, connectez-vous avec le même compte que celui utilisé à l'origine.", + "emailsignin": "L'e-mail n'a pas pu être envoyé.", + "emailverify": "Veuillez vérifier votre e-mail, un nouvel e-mail a été envoyé.", + "credentialssignin": "Échec de la connexion. Vérifiez que les informations fournies sont correctes.", + "sessionrequired": "Veuillez vous connecter pour accéder à cette page." + } + }, + "authVerifyEmail": { + "almostThere": "Vous y êtes presque ! Nous avons envoyé un e-mail à ", + "verifyEmailLink": "Veuillez cliquer sur le lien dans cet e-mail pour terminer votre inscription.", + "didNotReceive": "Vous ne trouvez pas l'e-mail ?", + "resendEmail": "Renvoyer l'e-mail", + "goBack": "Retour", + "emailSent": "E-mail envoyé avec succès.", + "verifyEmail": "Vérifiez votre adresse e-mail" + }, + "providerButton": { + "continue": "Continuer avec {{provider}}", + "signup": "S'inscrire avec {{provider}}" + }, + "authResetPassword": { + "newPasswordRequired": "Le nouveau mot de passe est un champ obligatoire", + "passwordsMustMatch": "Les mots de passe doivent correspondre", + "confirmPasswordRequired": "La confirmation du mot de passe est un champ obligatoire", + "newPassword": "Nouveau mot de passe", + "confirmPassword": "Confirmer le mot de passe", + "resetPassword": "Réinitialiser le mot de passe" + }, + "authForgotPassword": { + "email": "Adresse e-mail", + "emailRequired": "l'email est un champ obligatoire", + "emailSent": "Veuillez vérifier l'adresse e-mail {{email}} pour les instructions de réinitialisation de votre mot de passe.", + "enterEmail": "Entrez votre adresse e-mail et nous vous enverrons des instructions pour réinitialiser votre mot de passe.", + "resendEmail": "Renvoyer l'e-mail", + "continue": "Continuer", + "goBack": "Retour" + } + } + }, + "organisms": { + "assistantCreationModal": { + "title": "Créer un nouveau chatbot" + }, + "chat": { + "history": { + "index": { + "showHistory": "Afficher l'historique", + "lastInputs": "Dernières Entrées", + "noInputs": "Si vide...", + "loading": "Chargement..." + } + }, + "inputBox": { + "input": { + "placeholder": "Tapez votre message ici..." + }, + "speechButton": { + "start": "Commencer l'enregistrement", + "stop": "Arrêter l'enregistrement" + }, + "SubmitButton": { + "sendMessage": "Envoyer le message", + "stopTask": "Arrêter la Tâche" + }, + "UploadButton": { + "attachFiles": "Joindre des fichiers" + }, + "waterMark": { + "text": "Construit avec" + } + }, + "Messages": { + "index": { + "running": "En cours", + "executedSuccessfully": "exécuté avec succès", + "failed": "échoué", + "feedbackUpdated": "Retour mis à jour", + "updating": "Mise à jour" + } + }, + "dropScreen": { + "dropYourFilesHere": "Déposez vos fichiers ici" + }, + "index": { + "failedToUpload": "Échec du téléchargement", + "cancelledUploadOf": "Téléchargement annulé de", + "couldNotReachServer": "Impossible de joindre le serveur", + "continuingChat": "Continuer le chat précédent" + }, + "settings": { + "settingsPanel": "Panneau de paramètres", + "reset": "Réinitialiser", + "cancel": "Annuler", + "confirm": "Confirmer" + } + }, + "threadHistory": { + "sidebar": { + "filters": { + "FeedbackSelect": { + "feedbackAll": "Retour : Tout", + "feedbackPositive": "Retour : Positif", + "feedbackNegative": "Retour : Négatif" + }, + "SearchBar": { + "search": "Rechercher" + } + }, + "DeleteThreadButton": { + "confirmMessage": "Cela supprimera le fil ainsi que ses messages et éléments.", + "cancel": "Annuler", + "confirm": "Confirmer", + "deletingChat": "Suppression du chat", + "chatDeleted": "Chat supprimé" + }, + "index": { + "pastChats": "Chats Passés" + }, + "ThreadList": { + "empty": "Vide...", + "today": "Aujourd'hui", + "yesterday": "Hier", + "previous7days": "7 derniers jours", + "previous30days": "30 derniers jours" + }, + "TriggerButton": { + "closeSidebar": "Fermer la barre latérale", + "openSidebar": "Ouvrir la barre latérale" + } + }, + "Thread": { + "backToChat": "Retour au chat", + "chatCreatedOn": "Ce chat a été créé le" + } + }, + "header": { + "chat": "Chat", + "readme": "Lisez-moi" + } + } + }, + "hooks": { + "useLLMProviders": { + "failedToFetchProviders": "Échec de la récupération des fournisseurs :" + } + }, + "pages": { + "Design": {}, + "Env": { + "savedSuccessfully": "Enregistré avec succès", + "requiredApiKeys": "Clés API Requises", + "requiredApiKeysInfo": "Pour utiliser cette application, les clés API suivantes sont requises. Les clés sont stockées dans le stockage local de votre appareil." + }, + "Page": { + "notPartOfProject": "Vous ne faites pas partie de ce projet." + }, + "ResumeButton": { + "resumeChat": "Reprendre le Chat" + } + } + } + \ No newline at end of file diff --git a/backend/chainlit/user_session.py b/backend/chainlit/user_session.py index 7d08d2e6c0..db258898f5 100644 --- a/backend/chainlit/user_session.py +++ b/backend/chainlit/user_session.py @@ -30,6 +30,9 @@ def get(self, key, default=None): user_session["http_referer"] = context.session.http_referer user_session["client_type"] = context.session.client_type + # store assistant selected by the user + user_session["selected_assistant"] = context.session.selected_assistant + if isinstance(context.session, WebsocketSession): user_session["languages"] = context.session.languages diff --git a/frontend/src/assets/squarePlus.tsx b/frontend/src/assets/squarePlus.tsx new file mode 100644 index 0000000000..3e469efd41 --- /dev/null +++ b/frontend/src/assets/squarePlus.tsx @@ -0,0 +1,52 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const SquarePlusIcon = (props: SvgIconProps) => { + return ( + + + + + + + + ); +}; + +export default SquarePlusIcon; diff --git a/frontend/src/components/molecules/AssistantProfiles.tsx b/frontend/src/components/molecules/AssistantProfiles.tsx new file mode 100644 index 0000000000..d85190a9b6 --- /dev/null +++ b/frontend/src/components/molecules/AssistantProfiles.tsx @@ -0,0 +1,67 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; + +import { Box } from '@mui/material'; +import { SelectChangeEvent } from '@mui/material/Select'; + +import { useChatInteract, useConfig, useChatData } from '@chainlit/react-client'; + +import { SelectInput } from 'components/atoms/inputs'; + +import { Assistant, assistantsState } from 'state/project'; + +export default function AssistantProfiles() { + const { config } = useConfig(); + const { listAssistants, setSelectedAssistant } = useChatInteract(); + const [assistants, setAssistants] = useRecoilState(assistantsState); + const [assistant, setAssistant] = useState(''); + const { assistantSettingsInputs } = useChatData(); + + const fetchAssistants = useCallback(async () => { + try { + const assistantsList = (await listAssistants()) as Assistant[]; + setAssistants(assistantsList); + + // Set initial assistant if available + if (assistantsList.length > 0 && !assistant) { + setAssistant(assistantsList[0].name); + setSelectedAssistant(assistantsList[0].name); + } + } catch (error) { + console.error('Error fetching assistants:', error); + setAssistants([]); + } + }, [listAssistants, setSelectedAssistant, assistant, setAssistants]); + + useEffect(() => { + fetchAssistants(); + }, [fetchAssistants]); + + if (typeof config === 'undefined' || !assistants || assistants.length === 0 || !assistantSettingsInputs || assistantSettingsInputs.length === 0) { + return null; + } + + const items = assistants.map((assistant) => ({ + label: assistant.name, + value: assistant.name + })); + + const handleChange = (e: SelectChangeEvent) => { + setAssistant(e.target.value); + setSelectedAssistant(e.target.value); + }; + + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/molecules/newAssistantButton.tsx b/frontend/src/components/molecules/newAssistantButton.tsx new file mode 100644 index 0000000000..51a16edf7c --- /dev/null +++ b/frontend/src/components/molecules/newAssistantButton.tsx @@ -0,0 +1,52 @@ +import { useRecoilState } from 'recoil'; + +import { Box, IconButton, IconButtonProps, Tooltip } from '@mui/material'; + +import { Translator } from 'components/i18n'; +import AssistantCreationModal from 'components/organisms/chat/newAssistant'; + +import SquarePlusIcon from 'assets/squarePlus'; + +import { newAssistantOpenState } from 'state/project'; +import { useChatData } from '@chainlit/react-client'; + +export default function NewAssistantButton(props: IconButtonProps) { + const [newAssistantOpen, setNewAssistantOpen] = useRecoilState( + newAssistantOpenState + ); + const { assistantSettingsInputs } = useChatData(); + + const handleClickOpen = () => { + setNewAssistantOpen(true); + }; + + const handleClose = () => { + setNewAssistantOpen(false); + }; + + if (!assistantSettingsInputs || assistantSettingsInputs.length === 0) { + return null; + } + + return ( + + + } + > + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/organisms/chat/inputBox/SubmitButton.tsx b/frontend/src/components/organisms/chat/inputBox/SubmitButton.tsx index 993df6fec7..fea908d7cb 100644 --- a/frontend/src/components/organisms/chat/inputBox/SubmitButton.tsx +++ b/frontend/src/components/organisms/chat/inputBox/SubmitButton.tsx @@ -49,9 +49,11 @@ const SubmitButton = ({ disabled, onSubmit }: SubmitButtonProps) => { } > - - - + + + + + )} diff --git a/frontend/src/components/organisms/chat/newAssistant.tsx b/frontend/src/components/organisms/chat/newAssistant.tsx new file mode 100644 index 0000000000..425912fa4e --- /dev/null +++ b/frontend/src/components/organisms/chat/newAssistant.tsx @@ -0,0 +1,119 @@ +import { useFormik } from 'formik'; +import mapValues from 'lodash/mapValues'; +import { useRecoilState } from 'recoil'; + +import { + Box, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Typography +} from '@mui/material'; + +import { useChatData, useChatInteract } from '@chainlit/react-client'; + +import { AccentButton, RegularButton } from 'components/atoms/buttons'; +import { FormInput, TFormInputValue } from 'components/atoms/inputs'; +import { Translator } from 'components/i18n'; + +import { Assistant, assistantsState } from 'state/project'; + +interface AssistantCreationModalProps { + open: boolean; + handleClose: () => void; +} + +export default function AssistantCreationModal({ + open, + handleClose +}: AssistantCreationModalProps) { + const { assistantSettingsInputs, assistantSettingsDefaultValue } = + useChatData(); + const { createAssistant, listAssistants } = useChatInteract(); + const [, setAssistants] = useRecoilState(assistantsState); + + const formik = useFormik({ + initialValues: assistantSettingsDefaultValue, + enableReinitialize: true, + onSubmit: async () => undefined + }); + + const handleConfirm = async () => { + const values = mapValues(formik.values, (x: TFormInputValue) => + x !== '' ? x : null + ); + await createAssistant(values); + const updatedAssistants = (await listAssistants()) as Assistant[]; + setAssistants(updatedAssistants); + formik.resetForm(); + handleClose(); + }; + + const handleReset = () => { + formik.resetForm(); + }; + + return ( + + + { + + } + + + + {assistantSettingsInputs && assistantSettingsInputs.length > 0 ? ( + assistantSettingsInputs.map((input: any, index: number) => ( + + )) + ) : ( + No assistant settings available. + )} + + + + + + +
+ + + + + + + +
+ ); +} diff --git a/frontend/src/components/organisms/header.tsx b/frontend/src/components/organisms/header.tsx index d75a4d7bc5..918d2a7f1d 100644 --- a/frontend/src/components/organisms/header.tsx +++ b/frontend/src/components/organisms/header.tsx @@ -6,7 +6,9 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import UserButton from 'components/atoms/buttons/userButton'; import { Logo } from 'components/atoms/logo'; +import AssistantProfiles from 'components/molecules/AssistantProfiles'; import ChatProfiles from 'components/molecules/chatProfiles'; +import NewAssistantButton from 'components/molecules/newAssistantButton'; import NewChatButton from 'components/molecules/newChatButton'; import { settingsState } from 'state/settings'; @@ -49,6 +51,8 @@ const Header = memo(() => { + + diff --git a/frontend/src/state/project.ts b/frontend/src/state/project.ts index 82d2974cb2..e40065b387 100644 --- a/frontend/src/state/project.ts +++ b/frontend/src/state/project.ts @@ -11,3 +11,17 @@ export const chatSettingsOpenState = atom({ key: 'chatSettingsOpen', default: false }); + +export const newAssistantOpenState = atom({ + key: 'newAssistantOpen', + default: false +}); + +export interface Assistant { + name: string; +} + +export const assistantsState = atom({ + key: 'Assistants', + default: [] +}); diff --git a/libs/react-client/src/state.ts b/libs/react-client/src/state.ts index 54ca14973c..16fe8d9b72 100644 --- a/libs/react-client/src/state.ts +++ b/libs/react-client/src/state.ts @@ -104,6 +104,37 @@ export const chatSettingsValueState = atom({ default: chatSettingsDefaultValueSelector }); +// assistants settings inputs +export const assistantSettingsInputsState = atom({ + key: 'AssistantSettings', + default: [] +}); + +// assistants settings default value +export const assistantSettingsDefaultValueSelector = selector({ + key: 'AssistantSettingsValue/Default', + get: ({ get }) => { + const assistantSettings = get(assistantSettingsInputsState); + return assistantSettings.reduce( + (form: { [key: string]: any }, input: any) => ( + (form[input.id] = input.initial), form + ), + {} + ); + } +}); + +export const selectedAssistantState = atom({ + key: 'SelectedAssistant', + default: undefined +}); + +// // assistant settings value +// export const assistantSettingsValueState = atom({ +// key: 'AssistantSettingsValue', +// default: assistantSettingsDefaultValueSelector +// }); + export const elementState = atom({ key: 'DisplayElements', default: [] diff --git a/libs/react-client/src/useChatData.ts b/libs/react-client/src/useChatData.ts index ba3e37a98c..a790edf6fa 100644 --- a/libs/react-client/src/useChatData.ts +++ b/libs/react-client/src/useChatData.ts @@ -3,6 +3,8 @@ import { useRecoilValue } from 'recoil'; import { actionState, askUserState, + assistantSettingsDefaultValueSelector, + assistantSettingsInputsState, callFnState, chatSettingsDefaultValueSelector, chatSettingsInputsState, @@ -34,6 +36,12 @@ const useChatData = () => { chatSettingsDefaultValueSelector ); + // assistants-related + const assistantSettingsInputs = useRecoilValue(assistantSettingsInputsState); + const assistantSettingsDefaultValue = useRecoilValue( + assistantSettingsDefaultValueSelector + ); + const connected = session?.socket.connected && !session?.error; const disabled = !connected || @@ -53,7 +61,9 @@ const useChatData = () => { elements, error: session?.error, loading, - tasklists + tasklists, + assistantSettingsInputs, + assistantSettingsDefaultValue }; }; diff --git a/libs/react-client/src/useChatInteract.ts b/libs/react-client/src/useChatInteract.ts index 5aeb34be3c..0d7b2d7f74 100644 --- a/libs/react-client/src/useChatInteract.ts +++ b/libs/react-client/src/useChatInteract.ts @@ -126,6 +126,35 @@ const useChatInteract = () => { [session?.socket] ); + // create a new assistant + const createAssistant = useCallback( + (values: object) => { + session?.socket.emit('on_create_assistant', values); + }, + [session?.socket] + ); + + // get the list of assistants from the server and return a list of assistants + const listAssistants = useCallback(() => { + return new Promise((resolve, reject) => { + session?.socket.emit('on_list_assistants', (response) => { + if (response) { + resolve(response); + } else { + reject(new Error('Failed to fetch assistants')); + } + }); + }); + }, [session?.socket]); + + // set the selected assistant (emit a message to the server with the assistant name) + const setSelectedAssistant = useCallback( + (assistantName: string) => { + session?.socket.emit('select_assistant', assistantName); + }, + [session?.socket] + ); + const stopTask = useCallback(() => { setMessages((oldMessages) => oldMessages.map((m) => { @@ -183,7 +212,10 @@ const useChatInteract = () => { endAudioStream, stopTask, setIdToResume, - updateChatSettings + updateChatSettings, + createAssistant, + listAssistants, + setSelectedAssistant }; }; diff --git a/libs/react-client/src/useChatSession.ts b/libs/react-client/src/useChatSession.ts index 6ff4e77bb8..4deef774fa 100644 --- a/libs/react-client/src/useChatSession.ts +++ b/libs/react-client/src/useChatSession.ts @@ -10,6 +10,7 @@ import io from 'socket.io-client'; import { actionState, askUserState, + assistantSettingsInputsState, callFnState, chatProfileState, chatSettingsInputsState, @@ -64,6 +65,10 @@ const useChatSession = () => { const [chatProfile, setChatProfile] = useRecoilState(chatProfileState); const idToResume = useRecoilValue(threadIdToResumeState); const setCurrentThreadId = useSetRecoilState(currentThreadIdState); + const setAssistantSettingsInputs = useSetRecoilState( + assistantSettingsInputsState + ); + const _connect = useCallback( ({ userEnv, @@ -218,6 +223,10 @@ const useChatSession = () => { resetChatSettingsValue(); }); + socket.on('assistant_settings', (inputs: any) => { + setAssistantSettingsInputs(inputs); + }); + socket.on('element', (element: IElement) => { if (!element.url && element.chainlitKey) { element.url = client.getElementUrl(element.chainlitKey, sessionId); From dcfe390197cd3fa7101203dc632c45c2a91b6a97 Mon Sep 17 00:00:00 2001 From: modenter Date: Fri, 23 Aug 2024 10:39:36 +0200 Subject: [PATCH 02/30] fix: brought back markdown description, enlarged the new assistant popup width --- backend/chainlit/markdown.py | 16 ++++++++++++++-- .../components/organisms/chat/newAssistant.tsx | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/chainlit/markdown.py b/backend/chainlit/markdown.py index 9efa529469..224a1373fd 100644 --- a/backend/chainlit/markdown.py +++ b/backend/chainlit/markdown.py @@ -3,8 +3,20 @@ from chainlit.logger import logger # Default chainlit.md file created if none exists -DEFAULT_MARKDOWN_STR = """# It worked! -I apparently successfuly built chainlit and runned it. +DEFAULT_MARKDOWN_STR = """# Welcome to Chainlit! 🚀🤖 + +Hi there, Developer! 👋 We're excited to have you on board. Chainlit is a powerful tool designed to help you prototype, debug and share applications built on top of LLMs. + +## Useful Links 🔗 + +- **Documentation:** Get started with our comprehensive [Chainlit Documentation](https://docs.chainlit.io) 📚 +- **Discord Community:** Join our friendly [Chainlit Discord](https://discord.gg/k73SQ3FyUh) to ask questions, share your projects, and connect with other developers! 💬 + +We can't wait to see what you create with Chainlit! Happy coding! 💻😊 + +## Welcome screen + +To modify the welcome screen, edit the `chainlit.md` file at the root of your project. If you do not want a welcome screen, just leave this file empty. """ diff --git a/frontend/src/components/organisms/chat/newAssistant.tsx b/frontend/src/components/organisms/chat/newAssistant.tsx index 425912fa4e..c434b19fe0 100644 --- a/frontend/src/components/organisms/chat/newAssistant.tsx +++ b/frontend/src/components/organisms/chat/newAssistant.tsx @@ -75,8 +75,8 @@ export default function AssistantCreationModal({ sx={{ display: 'flex', flexDirection: 'column', - minWidth: '20vw', - maxHeight: '70vh', + minWidth: '30vw', + maxHeight: '90vh', gap: '15px' }} > From 2b1d0ac1f3e617e3939f32ba25a4d7978f4b3d11 Mon Sep 17 00:00:00 2001 From: modenter Date: Fri, 23 Aug 2024 13:15:35 +0200 Subject: [PATCH 03/30] change: moved the assistant related ui elements to the sidebar, change the dropdownlist to a list --- backend/chainlit/config.py | 2 +- .../molecules/AssistantProfiles.tsx | 79 +++++++++++-------- .../molecules/newAssistantButton.tsx | 19 +++-- frontend/src/components/organisms/header.tsx | 4 +- .../components/organisms/sidebar/index.tsx | 10 ++- frontend/src/state/project.ts | 8 +- frontend/src/state/settings.ts | 2 +- 7 files changed, 78 insertions(+), 46 deletions(-) diff --git a/backend/chainlit/config.py b/backend/chainlit/config.py index 6c0fd08140..473562792e 100644 --- a/backend/chainlit/config.py +++ b/backend/chainlit/config.py @@ -296,7 +296,7 @@ class CodeSettings: # assistant-related callback function on_create_assistant: Optional[Callable[[Optional["User"], Any], Any]] = None on_list_assistants: Optional[ - Callable[[Optional["User"]], List["AssistantSettings"]] + Callable[[Optional["User"]], List["BaseAssistant"]] ] = None diff --git a/frontend/src/components/molecules/AssistantProfiles.tsx b/frontend/src/components/molecules/AssistantProfiles.tsx index d85190a9b6..52202e78ee 100644 --- a/frontend/src/components/molecules/AssistantProfiles.tsx +++ b/frontend/src/components/molecules/AssistantProfiles.tsx @@ -1,37 +1,28 @@ import { useCallback, useEffect, useState } from 'react'; import { useRecoilState } from 'recoil'; -import { Box } from '@mui/material'; -import { SelectChangeEvent } from '@mui/material/Select'; +import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Button } from '@mui/material'; import { useChatInteract, useConfig, useChatData } from '@chainlit/react-client'; -import { SelectInput } from 'components/atoms/inputs'; - -import { Assistant, assistantsState } from 'state/project'; +import { BaseAssistant, assistantsState } from 'state/project'; export default function AssistantProfiles() { const { config } = useConfig(); const { listAssistants, setSelectedAssistant } = useChatInteract(); const [assistants, setAssistants] = useRecoilState(assistantsState); - const [assistant, setAssistant] = useState(''); const { assistantSettingsInputs } = useChatData(); + const [showAll, setShowAll] = useState(false); const fetchAssistants = useCallback(async () => { try { - const assistantsList = (await listAssistants()) as Assistant[]; + const assistantsList = (await listAssistants()) as BaseAssistant[]; setAssistants(assistantsList); - - // Set initial assistant if available - if (assistantsList.length > 0 && !assistant) { - setAssistant(assistantsList[0].name); - setSelectedAssistant(assistantsList[0].name); - } } catch (error) { console.error('Error fetching assistants:', error); setAssistants([]); } - }, [listAssistants, setSelectedAssistant, assistant, setAssistants]); + }, [listAssistants, setAssistants]); useEffect(() => { fetchAssistants(); @@ -41,27 +32,53 @@ export default function AssistantProfiles() { return null; } - const items = assistants.map((assistant) => ({ - label: assistant.name, - value: assistant.name - })); - - const handleChange = (e: SelectChangeEvent) => { - setAssistant(e.target.value); - setSelectedAssistant(e.target.value); + const handleAssistantClick = (assistantName: string) => { + setSelectedAssistant(assistantName); }; + const displayedAssistants = showAll ? assistants : assistants.slice(0, 5); + return ( - + + {displayedAssistants.map((assistant) => ( + + handleAssistantClick(assistant.name)} + sx={{ + borderRadius: '12px', + '&:hover': { + backgroundColor: 'action.hover', + }, + }} + > + {assistant.icon && ( + + {assistant.name} + + )} + + + + ))} + + {assistants.length > 5 && ( + + )} ); } \ No newline at end of file diff --git a/frontend/src/components/molecules/newAssistantButton.tsx b/frontend/src/components/molecules/newAssistantButton.tsx index 51a16edf7c..8c43f36751 100644 --- a/frontend/src/components/molecules/newAssistantButton.tsx +++ b/frontend/src/components/molecules/newAssistantButton.tsx @@ -1,6 +1,6 @@ import { useRecoilState } from 'recoil'; -import { Box, IconButton, IconButtonProps, Tooltip } from '@mui/material'; +import { Box, Button, ButtonProps, Tooltip } from '@mui/material'; import { Translator } from 'components/i18n'; import AssistantCreationModal from 'components/organisms/chat/newAssistant'; @@ -10,7 +10,7 @@ import SquarePlusIcon from 'assets/squarePlus'; import { newAssistantOpenState } from 'state/project'; import { useChatData } from '@chainlit/react-client'; -export default function NewAssistantButton(props: IconButtonProps) { +export default function NewAssistantButton(props: ButtonProps) { const [newAssistantOpen, setNewAssistantOpen] = useRecoilState( newAssistantOpenState ); @@ -35,13 +35,22 @@ export default function NewAssistantButton(props: IconButtonProps) { } > - } + sx={{ + justifyContent: 'flex-start', + textTransform: 'none', + width: '100%', + '&:hover': { + backgroundColor: 'action.hover' + } + }} {...props} > - - + New Assistant + { - - + {/* + */} diff --git a/frontend/src/components/organisms/sidebar/index.tsx b/frontend/src/components/organisms/sidebar/index.tsx index dd5e7d5cba..a0993bf5e1 100644 --- a/frontend/src/components/organisms/sidebar/index.tsx +++ b/frontend/src/components/organisms/sidebar/index.tsx @@ -12,6 +12,8 @@ import { useAuth, useConfig } from '@chainlit/react-client'; import GithubButton from 'components/atoms/buttons/githubButton'; import { Logo } from 'components/atoms/logo'; import ReadmeButton from 'components/organisms/readmeButton'; +import NewAssistantButton from 'components/molecules/newAssistantButton'; +import AssistantProfiles from 'components/molecules/AssistantProfiles'; import { settingsState } from 'state/settings'; @@ -32,9 +34,9 @@ const SideBar = () => { if (isMobile) { setChatHistoryOpen(false); } else { - setChatHistoryOpen(enableHistory); + setChatHistoryOpen(true); } - }, [enableHistory]); + }, [enableHistory, isMobile]); const setChatHistoryOpen = (open: boolean) => setSettings((prev) => ({ ...prev, isChatHistoryOpen: open })); @@ -80,6 +82,8 @@ const SideBar = () => { > + + {enableHistory ? ( ) : ( @@ -112,4 +116,4 @@ const SideBar = () => { ); }; -export { SideBar }; +export { SideBar }; \ No newline at end of file diff --git a/frontend/src/state/project.ts b/frontend/src/state/project.ts index e40065b387..7f053f2bd2 100644 --- a/frontend/src/state/project.ts +++ b/frontend/src/state/project.ts @@ -17,11 +17,13 @@ export const newAssistantOpenState = atom({ default: false }); -export interface Assistant { +export interface BaseAssistant { name: string; + markdown_description: string; + icon: string; } -export const assistantsState = atom({ +export const assistantsState = atom({ key: 'Assistants', default: [] -}); +}); \ No newline at end of file diff --git a/frontend/src/state/settings.ts b/frontend/src/state/settings.ts index 49484a4fe4..cbbbb90e16 100644 --- a/frontend/src/state/settings.ts +++ b/frontend/src/state/settings.ts @@ -15,7 +15,7 @@ export const defaultSettingsState = { defaultCollapseContent: true, isChatHistoryOpen: true, language: 'en-US', - theme + theme: theme }; export const settingsState = atom<{ From b825360144759a094c2c6ada5aec8746119a7a5b Mon Sep 17 00:00:00 2001 From: modenter Date: Fri, 23 Aug 2024 13:59:04 +0200 Subject: [PATCH 04/30] fix: list_assistants returns a baseassistant --- backend/chainlit/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/chainlit/config.py b/backend/chainlit/config.py index 473562792e..244b8fa991 100644 --- a/backend/chainlit/config.py +++ b/backend/chainlit/config.py @@ -23,8 +23,6 @@ from fastapi import Request, Response from chainlit.assistant import BaseAssistant -from chainlit.assistant_settings import AssistantSettings - BACKEND_ROOT = os.path.dirname(__file__) PACKAGE_ROOT = os.path.dirname(os.path.dirname(BACKEND_ROOT)) TRANSLATIONS_DIR = os.path.join(BACKEND_ROOT, "translations") From 4c8b11979c6fd4f2563b13c5ab3f0471e564563d Mon Sep 17 00:00:00 2001 From: modenter Date: Fri, 23 Aug 2024 14:00:26 +0200 Subject: [PATCH 05/30] fix: list_assistants returns a baseassistant --- frontend/src/components/organisms/chat/newAssistant.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/organisms/chat/newAssistant.tsx b/frontend/src/components/organisms/chat/newAssistant.tsx index c434b19fe0..648b5ffbe9 100644 --- a/frontend/src/components/organisms/chat/newAssistant.tsx +++ b/frontend/src/components/organisms/chat/newAssistant.tsx @@ -17,7 +17,7 @@ import { AccentButton, RegularButton } from 'components/atoms/buttons'; import { FormInput, TFormInputValue } from 'components/atoms/inputs'; import { Translator } from 'components/i18n'; -import { Assistant, assistantsState } from 'state/project'; +import { BaseAssistant, assistantsState } from 'state/project'; interface AssistantCreationModalProps { open: boolean; @@ -44,7 +44,7 @@ export default function AssistantCreationModal({ x !== '' ? x : null ); await createAssistant(values); - const updatedAssistants = (await listAssistants()) as Assistant[]; + const updatedAssistants = (await listAssistants()) as BaseAssistant[]; setAssistants(updatedAssistants); formik.resetForm(); handleClose(); From 1d749f6de0721f5bb1d7cc9e52ac5fb740d4d9bb Mon Sep 17 00:00:00 2001 From: modenter Date: Mon, 26 Aug 2024 15:22:04 +0200 Subject: [PATCH 06/30] feat: assistant avatars work, folder /publid/avatars --- backend/chainlit/input_widget.py | 22 +++++++ backend/chainlit/server.py | 42 ++++++++++--- .../atoms/inputs/FileUploadInput.tsx | 58 ++++++++++++++++++ .../src/components/atoms/inputs/FormInput.tsx | 15 ++++- .../organisms/chat/inputBox/input.tsx | 2 +- .../organisms/chat/newAssistant.tsx | 60 ++++++++++++++----- libs/react-client/src/api/index.tsx | 58 +++++++++++++++++- libs/react-client/src/useChatInteract.ts | 16 +++-- 8 files changed, 241 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/atoms/inputs/FileUploadInput.tsx diff --git a/backend/chainlit/input_widget.py b/backend/chainlit/input_widget.py index 58ee591653..f08a07c048 100644 --- a/backend/chainlit/input_widget.py +++ b/backend/chainlit/input_widget.py @@ -161,6 +161,28 @@ def to_dict(self) -> Dict[str, Any]: "description": self.description, } +@dataclass +class FileUploadInput(InputWidget): + """Useful to create a file upload input.""" + + type: InputWidgetType = "fileupload" + initial: Optional[str] = None + placeholder: Optional[str] = None + accept: List[str] = Field(default_factory=lambda: []) + max_size_mb: Optional[int] = None + max_files: Optional[int] = None + + def to_dict(self) -> Dict[str, Any]: + return { + "type": self.type, + "id": self.id, + "label": self.label, + "initial": self.initial, + "placeholder": self.placeholder, + "tooltip": self.tooltip, + "description": self.description, + } + @dataclass class Tags(InputWidget): diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index e28414d576..6ebdeb2c4d 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -5,6 +5,8 @@ import shutil import urllib.parse from typing import Any, Optional, Union +import uuid +import os from chainlit.oauth_providers import get_oauth_provider from chainlit.secret import random_secret @@ -194,11 +196,11 @@ def get_build_dir(local_target: str, packaged_target: str): router = APIRouter(prefix=ROOT_PATH) -app.mount( - f"{ROOT_PATH}/public", - StaticFiles(directory="public", check_dir=False), - name="public", -) +# app.mount( +# f"{ROOT_PATH}/static", +# StaticFiles(directory="static", check_dir=False), +# name="static", +# ) app.mount( f"{ROOT_PATH}/assets", @@ -218,7 +220,6 @@ def get_build_dir(local_target: str, packaged_target: str): name="copilot", ) - # ------------------------------------------------------------------------------- # SLACK HANDLER # ------------------------------------------------------------------------------- @@ -921,7 +922,6 @@ async def get_avatar(avatar_id: str): avatar_id = avatar_id.strip().lower().replace(" ", "_") avatar_path = os.path.join(APP_ROOT, "public", "avatars", f"{avatar_id}.*") - files = glob.glob(avatar_path) if files: @@ -931,6 +931,34 @@ async def get_avatar(avatar_id: str): else: return await get_favicon() +# post avatar/{avatar_id} (only for authenticated users) +@router.post("/avatars/{avatar_id}") +async def upload_avatar( + avatar_id: str, + file: UploadFile, + current_user: Annotated[ + Union[None, User, PersistedUser], Depends(get_current_user) + ], +): + # if not current_user: + # raise HTTPException(status_code=401, detail="Not authenticated") + + # Save the file to the avatars directory + try: + file_extension = os.path.splitext(file.filename)[1] + avatar_filename = f"{avatar_id}{file_extension}" + avatar_path = os.path.join(APP_ROOT, "public", "avatars", avatar_filename) + + # Ensure the avatars directory exists + os.makedirs(os.path.dirname(avatar_path), exist_ok=True) + + with open(avatar_path, "wb") as f: + f.write(await file.read()) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + return {"id": avatar_id} + @router.head("/") def status_check(): diff --git a/frontend/src/components/atoms/inputs/FileUploadInput.tsx b/frontend/src/components/atoms/inputs/FileUploadInput.tsx new file mode 100644 index 0000000000..a22c6cff43 --- /dev/null +++ b/frontend/src/components/atoms/inputs/FileUploadInput.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Button, Typography, IconButton, Box, Avatar } from '@mui/material'; +import AttachmentIcon from '@mui/icons-material/Attachment'; +import CloseIcon from '@mui/icons-material/Close'; + +export interface FileUploadInputProps { + id: string; + label: string; + onFileSelect: (file: File | null) => void; + value?: File; +} + +export const FileUploadInput: React.FC = ({ id, label, onFileSelect, value }) => { + const fileInputRef = React.useRef(null); + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + onFileSelect(file); + } + }; + + const handleRemoveFile = () => { + onFileSelect(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
+ + + {value && ( + + + + {value.name} + + + + + + )} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/atoms/inputs/FormInput.tsx b/frontend/src/components/atoms/inputs/FormInput.tsx index f6ff425755..5524aacc0a 100644 --- a/frontend/src/components/atoms/inputs/FormInput.tsx +++ b/frontend/src/components/atoms/inputs/FormInput.tsx @@ -7,8 +7,9 @@ import { SwitchInput, SwitchInputProps } from './SwitchInput'; import { TagsInput, TagsInputProps } from './TagsInput'; import { TextInput, TextInputProps } from './TextInput'; import { SelectInput, SelectInputProps } from './selects/SelectInput'; +import { FileUploadInput, FileUploadInputProps } from './FileUploadInput' -type TFormInputValue = string | number | boolean | string[] | undefined; +type TFormInputValue = string | number | boolean | string[] | File | undefined; interface IFormInput extends IInput { type: T; @@ -23,7 +24,8 @@ type TFormInput = | (Omit & IFormInput<'tags', string[]>) | (Omit & IFormInput<'select', string>) | (Omit & IFormInput<'textinput', string>) - | (Omit & IFormInput<'numberinput', number>); + | (Omit & IFormInput<'numberinput', number>) + | (Omit & IFormInput<'fileupload', File>); const FormInput = ({ element }: { element: TFormInput }): JSX.Element => { switch (element?.type) { @@ -62,6 +64,13 @@ const FormInput = ({ element }: { element: TFormInput }): JSX.Element => { value={element.value?.toString() ?? '0'} /> ); + case 'fileupload': + return ( + file && element.setField?.(element.id, file)} + /> + ); default: // If the element type is not recognized, we indicate an unimplemented type. // This code path should not normally occur and serves as a fallback. @@ -71,4 +80,4 @@ const FormInput = ({ element }: { element: TFormInput }): JSX.Element => { }; export { FormInput }; -export type { IFormInput, TFormInput, TFormInputValue }; +export type { IFormInput, TFormInput, TFormInputValue }; \ No newline at end of file diff --git a/frontend/src/components/organisms/chat/inputBox/input.tsx b/frontend/src/components/organisms/chat/inputBox/input.tsx index 78f64c2e0b..67096fea65 100644 --- a/frontend/src/components/organisms/chat/inputBox/input.tsx +++ b/frontend/src/components/organisms/chat/inputBox/input.tsx @@ -154,7 +154,7 @@ const Input = memo( onFileUploadError={onFileUploadError} onFileUpload={onFileUpload} /> - {chatSettingsInputs.length > 0 && ( + {chatSettingsInputs && chatSettingsInputs.length > 0 && ( { - const values = mapValues(formik.values, (x: TFormInputValue) => - x !== '' ? x : null - ); - await createAssistant(values); - const updatedAssistants = (await listAssistants()) as BaseAssistant[]; - setAssistants(updatedAssistants); - formik.resetForm(); - handleClose(); + var values = mapValues(formik.values, (x: TFormInputValue, key: string) => { + if (x instanceof File) { + return x.name.split('.')[0] + } + return x !== '' ? x : null; + }); + + // Handle icon upload + if (formik.values.icon instanceof File) { + const newFileName = `${formik.values.icon.name}`; + const { promise } = uploadFile( + formik.values.icon, + () => {}, + `/avatars/${newFileName}` + ); + try { + await promise; + } catch (error) { + console.error('Failed to upload avatar:', error); + return; + } + } + + try { + await createAssistant(values); + const updatedAssistants = (await listAssistants()) as BaseAssistant[]; + setAssistants(updatedAssistants); + formik.resetForm(); + handleClose(); // Close the modal after successful creation + } catch (error) { + console.error('Failed to create assistant:', error); + // Optionally, you can show an error message to the user here + } }; const handleReset = () => { @@ -75,9 +101,10 @@ export default function AssistantCreationModal({ sx={{ display: 'flex', flexDirection: 'column', - minWidth: '30vw', + minWidth: '50vh', maxHeight: '90vh', - gap: '15px' + gap: '15px', + margin: '0, 20px' }} > {assistantSettingsInputs && assistantSettingsInputs.length > 0 ? ( @@ -87,8 +114,13 @@ export default function AssistantCreationModal({ element={{ ...input, value: formik.values[input.id], - onChange: formik.handleChange, - setField: formik.setFieldValue + onChange: input.id === 'icon' + ? (file: File | null) => { + formik.setFieldValue(input.id, file); + } + : formik.handleChange, + setField: formik.setFieldValue, + type: input.id === 'icon' ? 'fileupload' : input.type }} /> )) @@ -116,4 +148,4 @@ export default function AssistantCreationModal({ ); -} +} \ No newline at end of file diff --git a/libs/react-client/src/api/index.tsx b/libs/react-client/src/api/index.tsx index fb42000c58..189284f4b8 100644 --- a/libs/react-client/src/api/index.tsx +++ b/libs/react-client/src/api/index.tsx @@ -196,7 +196,8 @@ export class ChainlitAPI extends APIBase { uploadFile( file: File, onProgress: (progress: number) => void, - sessionId: string, + endpoint: string = '/project/file', + sessionId?: string, token?: string ) { const xhr = new XMLHttpRequest(); @@ -205,9 +206,13 @@ export class ChainlitAPI extends APIBase { const formData = new FormData(); formData.append('file', file); + const url = sessionId + ? `${endpoint}?session_id=${sessionId}` + : endpoint; + xhr.open( 'POST', - this.buildEndpoint(`/project/file?session_id=${sessionId}`), + this.buildEndpoint(url), true ); @@ -254,4 +259,51 @@ export class ChainlitAPI extends APIBase { getOAuthEndpoint(provider: string) { return this.buildEndpoint(`/auth/oauth/${provider}`); } -} + + uploadAssistantIcon( + file: File, + onProgress: (progress: number) => void, + token?: string + ) { + const xhr = new XMLHttpRequest(); + + const promise = new Promise<{ filePath: string }>((resolve, reject) => { + const formData = new FormData(); + formData.append('file', file); + + xhr.open( + 'POST', + this.buildEndpoint(`/upload-assistant-icon`), + true + ); + + if (token) { + xhr.setRequestHeader('Authorization', this.checkToken(token)); + } + + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + const percentage = (event.loaded / event.total) * 100; + onProgress(percentage); + } + }; + + xhr.onload = function () { + if (xhr.status === 200) { + const response = JSON.parse(xhr.responseText); + resolve(response); + } else { + reject('Upload failed'); + } + }; + + xhr.onerror = function () { + reject('Upload error'); + }; + + xhr.send(formData); + }); + + return { xhr, promise }; + } +} \ No newline at end of file diff --git a/libs/react-client/src/useChatInteract.ts b/libs/react-client/src/useChatInteract.ts index 0d7b2d7f74..0cf5ac6fb8 100644 --- a/libs/react-client/src/useChatInteract.ts +++ b/libs/react-client/src/useChatInteract.ts @@ -195,12 +195,19 @@ const useChatInteract = () => { ); const uploadFile = useCallback( - (file: File, onProgress: (progress: number) => void) => { - return client.uploadFile(file, onProgress, sessionId, accessToken); + (file: File, onProgress: (progress: number) => void, endpoint?: string) => { + return client.uploadFile(file, onProgress, endpoint, sessionId, accessToken); }, [sessionId, accessToken] ); + const uploadAssistantIcon = useCallback( + (file: File, onProgress: (progress: number) => void) => { + return client.uploadAssistantIcon(file, onProgress, accessToken); + }, + [accessToken] + ); + return { uploadFile, callAction, @@ -215,8 +222,9 @@ const useChatInteract = () => { updateChatSettings, createAssistant, listAssistants, - setSelectedAssistant + setSelectedAssistant, + uploadAssistantIcon }; }; -export { useChatInteract }; +export { useChatInteract }; \ No newline at end of file From c041027ba4eff0d8c91e78f4d810c29559e458e3 Mon Sep 17 00:00:00 2001 From: modenter Date: Tue, 27 Aug 2024 10:18:23 +0200 Subject: [PATCH 07/30] feat: added id and created_by atttributes --- backend/chainlit/assistant.py | 9 ++++++++- backend/chainlit/server.py | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/chainlit/assistant.py b/backend/chainlit/assistant.py index 6b54f731d1..676de8ca06 100644 --- a/backend/chainlit/assistant.py +++ b/backend/chainlit/assistant.py @@ -4,6 +4,8 @@ from dataclasses_json import DataClassJsonMixin from pydantic.dataclasses import Field, dataclass +from chainlit.user import User + @dataclass class BaseAssistant(DataClassJsonMixin): @@ -14,8 +16,11 @@ class BaseAssistant(DataClassJsonMixin): name: str markdown_description: str icon: str + created_by: Optional[User] + id: Optional[str] + - def __init__(self, name: str, markdown_description: str, icon: str): + def __init__(self, name: str, markdown_description: str, icon: str, created_by: User, id: str): """ Initialize the BaseAssistant. @@ -27,6 +32,8 @@ def __init__(self, name: str, markdown_description: str, icon: str): self.name = name self.markdown_description = markdown_description self.icon = icon + self.created_by = created_by + self.id = id async def run(self, *args, **kwargs): """ diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 6ebdeb2c4d..05e44e3497 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -945,9 +945,9 @@ async def upload_avatar( # Save the file to the avatars directory try: - file_extension = os.path.splitext(file.filename)[1] - avatar_filename = f"{avatar_id}{file_extension}" - avatar_path = os.path.join(APP_ROOT, "public", "avatars", avatar_filename) + # file_extension = os.path.splitext(file.filename)[1] + # avatar_filename = f"{avatar_id}{file_extension}" + avatar_path = os.path.join(APP_ROOT, "public", "avatars", avatar_id) # Ensure the avatars directory exists os.makedirs(os.path.dirname(avatar_path), exist_ok=True) From 179fd65fa58e6e3e951d2e949546f2826445d62b Mon Sep 17 00:00:00 2001 From: modenter Date: Tue, 27 Aug 2024 15:49:40 +0200 Subject: [PATCH 08/30] feat: added warning banner on missing assistant field --- .../organisms/chat/newAssistant.tsx | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/organisms/chat/newAssistant.tsx b/frontend/src/components/organisms/chat/newAssistant.tsx index 4f92fb170d..7b56501f28 100644 --- a/frontend/src/components/organisms/chat/newAssistant.tsx +++ b/frontend/src/components/organisms/chat/newAssistant.tsx @@ -1,6 +1,7 @@ import { useFormik } from 'formik'; import mapValues from 'lodash/mapValues'; import { useRecoilState } from 'recoil'; +import { useState } from 'react'; import { Box, @@ -12,7 +13,6 @@ import { } from '@mui/material'; import { useChatData, useChatInteract } from '@chainlit/react-client'; -import { v4 as uuidv4 } from 'uuid'; import { AccentButton, RegularButton } from 'components/atoms/buttons'; import { FormInput, TFormInputValue } from 'components/atoms/inputs'; @@ -23,19 +23,23 @@ import { BaseAssistant, assistantsState } from 'state/project'; interface AssistantCreationModalProps { open: boolean; handleClose: () => void; + startValues: Record | null; } export default function AssistantCreationModal({ open, - handleClose + handleClose, + startValues, }: AssistantCreationModalProps) { const { assistantSettingsInputs, assistantSettingsDefaultValue } = useChatData(); const { createAssistant, listAssistants, uploadFile } = useChatInteract(); const [, setAssistants] = useRecoilState(assistantsState); + const [errorMessage, setErrorMessage] = useState(null); const formik = useFormik({ - initialValues: assistantSettingsDefaultValue, + initialValues: startValues ? startValues : assistantSettingsDefaultValue, + // initialValues: { "name": 'toto', "markdown_description": 'toto', "icon": "pfp.png" }, enableReinitialize: true, onSubmit: async () => undefined }); @@ -48,6 +52,16 @@ export default function AssistantCreationModal({ return x !== '' ? x : null; }); + // Check for required fields + if (!values.name) { + setErrorMessage('Name is mandatory'); + return; + } + if (!values.markdown_description) { + setErrorMessage('Description is mandatory'); + return; + } + // Handle icon upload if (formik.values.icon instanceof File) { const newFileName = `${formik.values.icon.name}`; @@ -69,15 +83,18 @@ export default function AssistantCreationModal({ const updatedAssistants = (await listAssistants()) as BaseAssistant[]; setAssistants(updatedAssistants); formik.resetForm(); + setErrorMessage(null); handleClose(); // Close the modal after successful creation } catch (error) { console.error('Failed to create assistant:', error); - // Optionally, you can show an error message to the user here + setErrorMessage('Failed to create assistant. Please try again.'); } + console.log(assistantSettingsDefaultValue); }; const handleReset = () => { formik.resetForm(); + setErrorMessage(null); }; return ( @@ -127,6 +144,19 @@ export default function AssistantCreationModal({ ) : ( No assistant settings available. )} + {errorMessage && ( + + {errorMessage} + + )} From 3876a26bd124d8c85db7c93bf1f53629b82bac22 Mon Sep 17 00:00:00 2001 From: modenter Date: Tue, 27 Aug 2024 19:32:27 +0200 Subject: [PATCH 09/30] feat: edit assistants --- backend/chainlit/__init__.py | 3 +- .../atoms/inputs/FileUploadInput.tsx | 24 +++++-- .../molecules/AssistantProfiles.tsx | 70 ++++++++++++++----- .../molecules/newAssistantButton.tsx | 1 + 4 files changed, 76 insertions(+), 22 deletions(-) diff --git a/backend/chainlit/__init__.py b/backend/chainlit/__init__.py index 0cd78ff954..41da668868 100644 --- a/backend/chainlit/__init__.py +++ b/backend/chainlit/__init__.py @@ -47,6 +47,7 @@ Text, Video, ) +from chainlit.assistant import BaseAssistant from chainlit.logger import logger from chainlit.message import ( AskActionMessage, @@ -428,7 +429,7 @@ def acall(self): "Video", "ChatSettings", "AssistantSettings", - # assistant + "Assistant", "BaseAssistant", "input_widget", "Message", diff --git a/frontend/src/components/atoms/inputs/FileUploadInput.tsx b/frontend/src/components/atoms/inputs/FileUploadInput.tsx index a22c6cff43..54245f2281 100644 --- a/frontend/src/components/atoms/inputs/FileUploadInput.tsx +++ b/frontend/src/components/atoms/inputs/FileUploadInput.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Button, Typography, IconButton, Box, Avatar } from '@mui/material'; import AttachmentIcon from '@mui/icons-material/Attachment'; import CloseIcon from '@mui/icons-material/Close'; @@ -7,11 +7,24 @@ export interface FileUploadInputProps { id: string; label: string; onFileSelect: (file: File | null) => void; - value?: File; + value?: File | string; } export const FileUploadInput: React.FC = ({ id, label, onFileSelect, value }) => { const fileInputRef = React.useRef(null); + const [preview, setPreview] = useState(null); + + useEffect(() => { + if (typeof value === 'string') { + setPreview(value); + } else if (value instanceof File) { + const objectUrl = URL.createObjectURL(value); + setPreview(objectUrl); + return () => URL.revokeObjectURL(objectUrl); + } else { + setPreview(null); + } + }, [value]); const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -22,6 +35,7 @@ export const FileUploadInput: React.FC = ({ id, label, onF const handleRemoveFile = () => { onFileSelect(null); + setPreview(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } @@ -42,11 +56,11 @@ export const FileUploadInput: React.FC = ({ id, label, onF {label} - {value && ( + {preview && ( - + - {value.name} + {value instanceof File ? value.name : 'Current Icon'} diff --git a/frontend/src/components/molecules/AssistantProfiles.tsx b/frontend/src/components/molecules/AssistantProfiles.tsx index 52202e78ee..b0148fb6e1 100644 --- a/frontend/src/components/molecules/AssistantProfiles.tsx +++ b/frontend/src/components/molecules/AssistantProfiles.tsx @@ -1,22 +1,27 @@ import { useCallback, useEffect, useState } from 'react'; import { useRecoilState } from 'recoil'; - -import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Button } from '@mui/material'; - -import { useChatInteract, useConfig, useChatData } from '@chainlit/react-client'; - +import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Button, IconButton, Avatar } from '@mui/material'; +import SettingsIcon from '@mui/icons-material/Settings'; +import { useChatInteract, useChatData } from '@chainlit/react-client'; import { BaseAssistant, assistantsState } from 'state/project'; +import AssistantCreationModal from 'components/organisms/chat/newAssistant'; +import AttachmentIcon from '@mui/icons-material/Attachment'; export default function AssistantProfiles() { - const { config } = useConfig(); const { listAssistants, setSelectedAssistant } = useChatInteract(); const [assistants, setAssistants] = useRecoilState(assistantsState); const { assistantSettingsInputs } = useChatData(); const [showAll, setShowAll] = useState(false); + const [newAssistantOpen, setNewAssistantOpen] = useState(false); + + // assistant to edit (set when clicking on the settings icon, just before opening the modal) + const [editAssistant, setEditAssistant] = useState(null); const fetchAssistants = useCallback(async () => { try { - const assistantsList = (await listAssistants()) as BaseAssistant[]; + const assistantsList = (await listAssistants()) as any[]; + console.log(assistantsList) +; setAssistants(assistantsList); } catch (error) { console.error('Error fetching assistants:', error); @@ -28,7 +33,7 @@ export default function AssistantProfiles() { fetchAssistants(); }, [fetchAssistants]); - if (typeof config === 'undefined' || !assistants || assistants.length === 0 || !assistantSettingsInputs || assistantSettingsInputs.length === 0) { + if (!assistants || assistants.length === 0 || !assistantSettingsInputs || assistantSettingsInputs.length === 0) { return null; } @@ -36,6 +41,26 @@ export default function AssistantProfiles() { setSelectedAssistant(assistantName); }; + const handleEditAssistant = async (assistant: BaseAssistant) => { + try { + const assistantsList = await listAssistants() as any[]; + const fullAssistant = assistantsList.find(a => a.name === assistant.name); + if (fullAssistant) { + console.log(fullAssistant) + setEditAssistant(fullAssistant); + setNewAssistantOpen(true); + } else { + console.error('Assistant not found in the list'); + } + } catch (error) { + console.error('Error fetching assistants:', error); + } + }; + + const handleCloseModal = () => { + setNewAssistantOpen(false); + }; + const displayedAssistants = showAll ? assistants : assistants.slice(0, 5); return ( @@ -52,21 +77,29 @@ export default function AssistantProfiles() { }, }} > - {assistant.icon && ( - - + {assistant.icon ? ( + - - )} + ) : ( + + + + )} + + { + e.stopPropagation(); + handleEditAssistant(assistant); + }}> + + ))} @@ -79,6 +112,11 @@ export default function AssistantProfiles() { {showAll ? 'Show Less' : 'Show More'} )} + ); } \ No newline at end of file diff --git a/frontend/src/components/molecules/newAssistantButton.tsx b/frontend/src/components/molecules/newAssistantButton.tsx index 8c43f36751..a00199a65c 100644 --- a/frontend/src/components/molecules/newAssistantButton.tsx +++ b/frontend/src/components/molecules/newAssistantButton.tsx @@ -55,6 +55,7 @@ export default function NewAssistantButton(props: ButtonProps) { ); From eab3e47c773c6e7b661ee55a3d6b43ed319fb287 Mon Sep 17 00:00:00 2001 From: modenter Date: Wed, 28 Aug 2024 15:36:47 +0200 Subject: [PATCH 10/30] feat: changed the assistant paradigm, from inheritance to extendability --- backend/chainlit/__init__.py | 10 ++-- backend/chainlit/assistant.py | 55 +++++-------------- backend/chainlit/config.py | 4 +- backend/chainlit/session.py | 3 - backend/chainlit/socket.py | 15 +++-- .../molecules/AssistantProfiles.tsx | 32 ++++++----- .../molecules/newAssistantButton.tsx | 1 + .../organisms/chat/newAssistant.tsx | 16 ++++-- 8 files changed, 59 insertions(+), 77 deletions(-) diff --git a/backend/chainlit/__init__.py b/backend/chainlit/__init__.py index 0cd78ff954..73129a605c 100644 --- a/backend/chainlit/__init__.py +++ b/backend/chainlit/__init__.py @@ -25,8 +25,8 @@ import chainlit.input_widget as input_widget from chainlit.action import Action -# import BaseAssistant -from chainlit.assistant import BaseAssistant +# import Assistant +from chainlit.assistant import Assistant from chainlit.assistant_settings import AssistantSettings from chainlit.cache import cache from chainlit.chat_context import chat_context @@ -82,8 +82,8 @@ def on_create_assistant( @trace def on_list_assistants( - func: Callable[[Optional[User]], List[BaseAssistant]] -) -> Callable[[Optional[User]], List[BaseAssistant]]: + func: Callable[[Optional[User]], List[Assistant]] +) -> Callable[[Optional[User]], List[Assistant]]: config.code.on_list_assistants = wrap_user_function(func) return func @@ -429,7 +429,7 @@ def acall(self): "ChatSettings", "AssistantSettings", # assistant - "BaseAssistant", + "Assistant", "input_widget", "Message", "ErrorMessage", diff --git a/backend/chainlit/assistant.py b/backend/chainlit/assistant.py index 676de8ca06..131f1ae91b 100644 --- a/backend/chainlit/assistant.py +++ b/backend/chainlit/assistant.py @@ -1,43 +1,16 @@ -from typing import List, Optional - +from typing import List, Dict from chainlit.input_widget import InputWidget -from dataclasses_json import DataClassJsonMixin -from pydantic.dataclasses import Field, dataclass - -from chainlit.user import User - - -@dataclass -class BaseAssistant(DataClassJsonMixin): - """ - An abstract base class for assistants that can be extended. - """ - - name: str - markdown_description: str - icon: str - created_by: Optional[User] - id: Optional[str] - - - def __init__(self, name: str, markdown_description: str, icon: str, created_by: User, id: str): - """ - Initialize the BaseAssistant. - - Args: - name (str): The name of the assistant. - markdown_description (str): A markdown description of the assistant. - icon (Optional[str], optional): An optional icon for the assistant. Defaults to None. - """ - self.name = name - self.markdown_description = markdown_description - self.icon = icon - self.created_by = created_by - self.id = id - async def run(self, *args, **kwargs): - """ - An abstract method that should be implemented by subclasses. - This method defines the main functionality of the assistant. - """ - pass +class Assistant: + input_widgets: List[InputWidget] = [] + settings_values: Dict = {} + + def __init__(self, input_widgets: List[InputWidget], settings_values: Dict): + self.input_widgets = input_widgets + self.settings_values = settings_values + + def to_dict(self): + return { + "input_widgets": [widget.__repr__() for widget in self.input_widgets], + "settings_values": self.settings_values + } diff --git a/backend/chainlit/config.py b/backend/chainlit/config.py index 244b8fa991..6ac513d004 100644 --- a/backend/chainlit/config.py +++ b/backend/chainlit/config.py @@ -21,7 +21,7 @@ from chainlit.types import AudioChunk, ChatProfile, Starter, ThreadDict from chainlit.user import User from fastapi import Request, Response - from chainlit.assistant import BaseAssistant + from chainlit.assistant import Assistant BACKEND_ROOT = os.path.dirname(__file__) PACKAGE_ROOT = os.path.dirname(os.path.dirname(BACKEND_ROOT)) @@ -294,7 +294,7 @@ class CodeSettings: # assistant-related callback function on_create_assistant: Optional[Callable[[Optional["User"], Any], Any]] = None on_list_assistants: Optional[ - Callable[[Optional["User"]], List["BaseAssistant"]] + Callable[[Optional["User"]], List["Assistant"]] ] = None diff --git a/backend/chainlit/session.py b/backend/chainlit/session.py index 7bfe3058fd..583000b6ec 100644 --- a/backend/chainlit/session.py +++ b/backend/chainlit/session.py @@ -24,9 +24,6 @@ from chainlit.types import FileDict, FileReference from chainlit.user import PersistedUser, User -# import the BaseAssistant class -from chainlit.assistant import BaseAssistant - ClientType = Literal["webapp", "copilot", "teams", "slack", "discord"] diff --git a/backend/chainlit/socket.py b/backend/chainlit/socket.py index 4258f255af..133166e47b 100644 --- a/backend/chainlit/socket.py +++ b/backend/chainlit/socket.py @@ -6,7 +6,7 @@ from urllib.parse import unquote from chainlit.action import Action -from chainlit.assistant_settings import AssistantSettings +from chainlit.assistant import Assistant from chainlit.auth import get_current_user, require_login from chainlit.chat_context import chat_context from chainlit.config import config @@ -409,27 +409,30 @@ async def change_settings(sid, settings: Dict[str, Any]): await config.code.on_settings_update(settings) -# when signal "on_create_assistant" is received, call the callback with user and assistant @sio.on("on_create_assistant") async def on_create_assistant(sid, options): context = init_ws_context(sid) logger.info(f"Received request to create assistant with options: {options}") if config.code.on_create_assistant: - await config.code.on_create_assistant(context.session.user, options) + new_assistant = Assistant( + input_widgets=options['input_widgets'], + settings_values=options['settings_values'] + ) + await config.code.on_create_assistant(context.session.user, new_assistant) + return new_assistant.to_dict() # Return the dictionary representation logger.info("Assistant creation process completed") -# when signal "on_list_assistants" is received, call the callback with user @sio.on("on_list_assistants") async def on_list_assistants(sid): context = init_ws_context(sid) if config.code.on_list_assistants: - return await config.code.on_list_assistants(context.session.user) + assistants = await config.code.on_list_assistants(context.session.user) + return [assistant.to_dict() for assistant in assistants] # Convert each assistant to a dictionary @sio.on("select_assistant") async def select_assistant(sid, assistant_name: str): - # store the assistant name in the session context = init_ws_context(sid) context.session.selected_assistant = assistant_name logger.info(f"Selected assistant: {assistant_name}") diff --git a/frontend/src/components/molecules/AssistantProfiles.tsx b/frontend/src/components/molecules/AssistantProfiles.tsx index 52202e78ee..187c0d065a 100644 --- a/frontend/src/components/molecules/AssistantProfiles.tsx +++ b/frontend/src/components/molecules/AssistantProfiles.tsx @@ -1,13 +1,15 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useContext } from 'react'; import { useRecoilState } from 'recoil'; import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Button } from '@mui/material'; -import { useChatInteract, useConfig, useChatData } from '@chainlit/react-client'; +import { useChatInteract, useConfig, useChatData, ChainlitContext } from '@chainlit/react-client'; -import { BaseAssistant, assistantsState } from 'state/project'; +import { Assistant, assistantsState } from 'state/project'; +import UserIcon from 'assets/user'; export default function AssistantProfiles() { + const apiClient = useContext(ChainlitContext); const { config } = useConfig(); const { listAssistants, setSelectedAssistant } = useChatInteract(); const [assistants, setAssistants] = useRecoilState(assistantsState); @@ -16,7 +18,7 @@ export default function AssistantProfiles() { const fetchAssistants = useCallback(async () => { try { - const assistantsList = (await listAssistants()) as BaseAssistant[]; + const assistantsList = (await listAssistants()) as Assistant[]; setAssistants(assistantsList); } catch (error) { console.error('Error fetching assistants:', error); @@ -41,10 +43,10 @@ export default function AssistantProfiles() { return ( - {displayedAssistants.map((assistant) => ( - + {displayedAssistants.map((assistant: Assistant) => ( + handleAssistantClick(assistant.name)} + onClick={() => handleAssistantClick(assistant.settings_values['name'])} sx={{ borderRadius: '12px', '&:hover': { @@ -52,11 +54,11 @@ export default function AssistantProfiles() { }, }} > - {assistant.icon && ( - + + {assistant.settings_values['icon'] ? ( {assistant.name} - - )} - + ) : ( + + )} + + ))} diff --git a/frontend/src/components/molecules/newAssistantButton.tsx b/frontend/src/components/molecules/newAssistantButton.tsx index 8c43f36751..6aea4e3b49 100644 --- a/frontend/src/components/molecules/newAssistantButton.tsx +++ b/frontend/src/components/molecules/newAssistantButton.tsx @@ -55,6 +55,7 @@ export default function NewAssistantButton(props: ButtonProps) { ); diff --git a/frontend/src/components/organisms/chat/newAssistant.tsx b/frontend/src/components/organisms/chat/newAssistant.tsx index 7b56501f28..0296809b4c 100644 --- a/frontend/src/components/organisms/chat/newAssistant.tsx +++ b/frontend/src/components/organisms/chat/newAssistant.tsx @@ -18,7 +18,7 @@ import { AccentButton, RegularButton } from 'components/atoms/buttons'; import { FormInput, TFormInputValue } from 'components/atoms/inputs'; import { Translator } from 'components/i18n'; -import { BaseAssistant, assistantsState } from 'state/project'; +import { Assistant, assistantsState } from 'state/project'; interface AssistantCreationModalProps { open: boolean; @@ -64,11 +64,11 @@ export default function AssistantCreationModal({ // Handle icon upload if (formik.values.icon instanceof File) { - const newFileName = `${formik.values.icon.name}`; + const newFileName = `avatars/${formik.values.icon.name}`; const { promise } = uploadFile( formik.values.icon, () => {}, - `/avatars/${newFileName}` + `/${newFileName}` ); try { await promise; @@ -79,12 +79,16 @@ export default function AssistantCreationModal({ } try { - await createAssistant(values); - const updatedAssistants = (await listAssistants()) as BaseAssistant[]; + const newAssistant = { + input_widgets: assistantSettingsInputs, + settings_values: values + }; + await createAssistant(newAssistant); + const updatedAssistants = (await listAssistants()) as Assistant[]; setAssistants(updatedAssistants); formik.resetForm(); setErrorMessage(null); - handleClose(); // Close the modal after successful creation + handleClose(); } catch (error) { console.error('Failed to create assistant:', error); setErrorMessage('Failed to create assistant. Please try again.'); From 07f283950d5446a9f870f92c2a77d724fef8b7d7 Mon Sep 17 00:00:00 2001 From: modenter Date: Wed, 28 Aug 2024 15:41:39 +0200 Subject: [PATCH 11/30] fix: fix forgot to update project.ts in previous commit --- frontend/src/state/project.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/state/project.ts b/frontend/src/state/project.ts index 7f053f2bd2..ae2de792e4 100644 --- a/frontend/src/state/project.ts +++ b/frontend/src/state/project.ts @@ -17,13 +17,12 @@ export const newAssistantOpenState = atom({ default: false }); -export interface BaseAssistant { - name: string; - markdown_description: string; - icon: string; +export interface Assistant { + input_widgets: any[]; + settings_values: Record; } -export const assistantsState = atom({ +export const assistantsState = atom({ key: 'Assistants', default: [] }); \ No newline at end of file From 2b2d4530b92a261bc1b8e7cdde95b115e923374c Mon Sep 17 00:00:00 2001 From: modenter Date: Wed, 28 Aug 2024 17:21:09 +0200 Subject: [PATCH 12/30] fix: fixed icons --- backend/chainlit/__init__.py | 1 - backend/chainlit/socket.py | 8 +- backend/chainlit/types.py | 2 +- .../molecules/AssistantProfiles.tsx | 108 +++++++++++++----- .../organisms/chat/newAssistant.tsx | 49 +++++--- 5 files changed, 115 insertions(+), 53 deletions(-) diff --git a/backend/chainlit/__init__.py b/backend/chainlit/__init__.py index 898b0218e7..73129a605c 100644 --- a/backend/chainlit/__init__.py +++ b/backend/chainlit/__init__.py @@ -47,7 +47,6 @@ Text, Video, ) -from chainlit.assistant import BaseAssistant from chainlit.logger import logger from chainlit.message import ( AskActionMessage, diff --git a/backend/chainlit/socket.py b/backend/chainlit/socket.py index 133166e47b..335c3c85a3 100644 --- a/backend/chainlit/socket.py +++ b/backend/chainlit/socket.py @@ -414,12 +414,12 @@ async def on_create_assistant(sid, options): context = init_ws_context(sid) logger.info(f"Received request to create assistant with options: {options}") if config.code.on_create_assistant: + settings_values = options["settings_values"] new_assistant = Assistant( - input_widgets=options['input_widgets'], - settings_values=options['settings_values'] + input_widgets=options["input_widgets"], settings_values=settings_values ) await config.code.on_create_assistant(context.session.user, new_assistant) - return new_assistant.to_dict() # Return the dictionary representation + return new_assistant.to_dict() logger.info("Assistant creation process completed") @@ -428,7 +428,7 @@ async def on_list_assistants(sid): context = init_ws_context(sid) if config.code.on_list_assistants: assistants = await config.code.on_list_assistants(context.session.user) - return [assistant.to_dict() for assistant in assistants] # Convert each assistant to a dictionary + return [assistant.to_dict() for assistant in assistants] @sio.on("select_assistant") diff --git a/backend/chainlit/types.py b/backend/chainlit/types.py index 1ca9d18bb6..693b6ece40 100644 --- a/backend/chainlit/types.py +++ b/backend/chainlit/types.py @@ -23,7 +23,7 @@ from pydantic.dataclasses import dataclass InputWidgetType = Literal[ - "switch", "slider", "select", "textinput", "tags", "numberinput" + "switch", "slider", "select", "textinput", "tags", "numberinput", "fileupload" ] diff --git a/frontend/src/components/molecules/AssistantProfiles.tsx b/frontend/src/components/molecules/AssistantProfiles.tsx index 649dbc5d9a..4d8329850e 100644 --- a/frontend/src/components/molecules/AssistantProfiles.tsx +++ b/frontend/src/components/molecules/AssistantProfiles.tsx @@ -1,16 +1,32 @@ -import { useCallback, useEffect, useState, useContext } from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; import { useRecoilState } from 'recoil'; -import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Button } from '@mui/material'; +import SettingsIcon from '@mui/icons-material/Settings'; +import { + Box, + Button, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText +} from '@mui/material'; +import IconButton from '@mui/material/IconButton'; -import { useChatInteract, useConfig, useChatData, ChainlitContext } from '@chainlit/react-client'; +import { + ChainlitContext, + useChatData, + useChatInteract +} from '@chainlit/react-client'; + +import AssistantCreationModal from 'components/organisms/chat/newAssistant'; -import { Assistant, assistantsState } from 'state/project'; import UserIcon from 'assets/user'; +import { Assistant, assistantsState } from 'state/project'; + export default function AssistantProfiles() { const apiClient = useContext(ChainlitContext); - const { config } = useConfig(); const { listAssistants, setSelectedAssistant } = useChatInteract(); const [assistants, setAssistants] = useRecoilState(assistantsState); const { assistantSettingsInputs } = useChatData(); @@ -34,7 +50,12 @@ export default function AssistantProfiles() { fetchAssistants(); }, [fetchAssistants]); - if (!assistants || assistants.length === 0 || !assistantSettingsInputs || assistantSettingsInputs.length === 0) { + if ( + !assistants || + assistants.length === 0 || + !assistantSettingsInputs || + assistantSettingsInputs.length === 0 + ) { return null; } @@ -42,20 +63,24 @@ export default function AssistantProfiles() { setSelectedAssistant(assistantName); }; - const handleEditAssistant = async (assistant: BaseAssistant) => { - try { - const assistantsList = await listAssistants() as any[]; - const fullAssistant = assistantsList.find(a => a.name === assistant.name); - if (fullAssistant) { - console.log(fullAssistant) - setEditAssistant(fullAssistant); - setNewAssistantOpen(true); - } else { - console.error('Assistant not found in the list'); - } - } catch (error) { - console.error('Error fetching assistants:', error); + const handleEditAssistant = async (assistant: Assistant) => { + console.log('assistant', assistant); + if (assistant.settings_values['icon'] == null) { + setEditAssistant(assistant); + } else { + const new_assistant_icon = apiClient.buildEndpoint( + `${assistant.settings_values['icon']}` + ); + const new_assistant = { + ...assistant, + settings_values: { + ...assistant.settings_values, + icon: new_assistant_icon + } + }; + setEditAssistant(new_assistant); } + setNewAssistantOpen(true); }; const handleCloseModal = () => { @@ -68,37 +93,60 @@ export default function AssistantProfiles() { {displayedAssistants.map((assistant: Assistant) => ( - + handleAssistantClick(assistant.settings_values['name'])} + onClick={() => + handleAssistantClick(assistant.settings_values['name']) + } sx={{ borderRadius: '12px', '&:hover': { - backgroundColor: 'action.hover', - }, + backgroundColor: 'action.hover' + } }} > {assistant.settings_values['icon'] ? ( {assistant.settings_values['name']} ) : ( - + )} + { + e.stopPropagation(); + handleEditAssistant(assistant); + }} + > + + ))} {assistants.length > 5 && ( -