diff --git a/CHANGELOG.md b/CHANGELOG.md index 22df15975d..8846902d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Nothing unreleased! +## [1.1.300rc0] - 2024-05-27 + +### Added + +- Debug mode when starting with `-d`. Only available if the data layer supports it. This replaces the Prompt Playground. +- `@cl.set_starters` and `cl.Starter` to suggest conversation starters to the user +- `default` theme config in `config.toml` +- If only one OAuth provider is set, automatically redirect the user to it + +### Changed + +- **[BREAKING]** Avatars have been reworked. `cl.Avatar` has been removed, instead place your avatars by name in `/public/avatars/*` +- **[BREAKING]** The `running`, `took_one` and `took_other` translations have been replaced by `used`. +- **[BREAKING]** `root` attribute of `cl.Step` has been removed. Use `cl.Message` to send root level messages. +- Chain of Thought has been reworked. Only steps of type `tool` will be displayed if `hide_cot` is false +- The `show_readme_as_default` config has been removed +- No longer collapse root level messages + +### Fixed + +- The Chat Profile description should now disappear when not hovered. +- Error handling of steps has been improved +- No longer stream the first token twice +- Copilot should now work as expected even if the user is closing/reopening it +- Copilot CSS should no longer leak/be impacted by the host website CSS +- Fix various `cl.Context` errors + ## [1.1.202] - 2024-05-22 ### Added diff --git a/backend/chainlit/__init__.py b/backend/chainlit/__init__.py index eca2420512..b5046c5396 100644 --- a/backend/chainlit/__init__.py +++ b/backend/chainlit/__init__.py @@ -28,7 +28,6 @@ from chainlit.context import context from chainlit.element import ( Audio, - Avatar, File, Image, Pdf, @@ -52,7 +51,7 @@ from chainlit.step import Step, step from chainlit.sync import make_async, run_sync from chainlit.telemetry import trace -from chainlit.types import AudioChunk, ChatProfile, ThreadDict +from chainlit.types import AudioChunk, ChatProfile, Starter, ThreadDict from chainlit.user import PersistedUser, User from chainlit.user_session import user_session from chainlit.utils import make_module_getattr, wrap_user_function @@ -208,6 +207,22 @@ def set_chat_profiles( return func +@trace +def set_starters(func: Callable[[Optional["User"]], List["Starter"]]) -> Callable: + """ + Programmatic declaration of the available starter (can depend on the User from the session if authentication is setup). + + Args: + func (Callable[[Optional["User"]], List["Starter"]]): The function declaring the starters. + + Returns: + Callable[[Optional["User"]], List["Starter"]]: The decorated function. + """ + + config.code.set_starters = wrap_user_function(func) + return func + + @trace def on_chat_end(func: Callable) -> Callable: """ @@ -348,6 +363,8 @@ def acall(self): ) __all__ = [ + "ChatProfile", + "Starter", "user_session", "CopilotFunction", "AudioChunk", @@ -359,7 +376,6 @@ def acall(self): "Plotly", "Image", "Text", - "Avatar", "Pyplot", "File", "Task", diff --git a/backend/chainlit/config.py b/backend/chainlit/config.py index 2f8a8caa96..2ca9f88647 100644 --- a/backend/chainlit/config.py +++ b/backend/chainlit/config.py @@ -4,7 +4,7 @@ import sys from importlib import util from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, Literal +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Union import tomli from chainlit.logger import logger @@ -18,7 +18,7 @@ from chainlit.action import Action from chainlit.element import ElementBased from chainlit.message import Message - from chainlit.types import AudioChunk, ChatProfile, ThreadDict + from chainlit.types import AudioChunk, ChatProfile, Starter, ThreadDict from chainlit.user import User from fastapi import Request, Response @@ -61,9 +61,6 @@ # follow_symlink = false [features] -# Show the prompt playground -prompt_playground = true - # Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript) unsafe_allow_html = false @@ -95,21 +92,15 @@ sample_rate = 44100 [UI] -# Name of the app and chatbot. -name = "Chatbot" - -# Show the readme while the thread is empty. -show_readme_as_default = true +# Name of the assistant. +name = "Assistant" -# Description of the app and chatbot. This is used for HTML tags. +# Description of the assistant. This is used for HTML tags. # description = "" # Large size content are by default collapsed for a cleaner ui default_collapse_content = true -# The default value for the expand messages settings. -default_expand_messages = false - # Hide the chain of thought details from the user in the UI. hide_cot = false @@ -136,6 +127,7 @@ # custom_build = "./public/build" [UI.theme] + default = "dark" #layout = "wide" #font_family = "Inter, sans-serif" # Override default MUI light theme. (Check theme.ts) @@ -192,11 +184,13 @@ class PaletteOptions(DataClassJsonMixin): light: Optional[str] = "" dark: Optional[str] = "" + @dataclass() class TextOptions(DataClassJsonMixin): primary: Optional[str] = "" secondary: Optional[str] = "" + @dataclass() class Palette(DataClassJsonMixin): primary: Optional[PaletteOptions] = None @@ -208,6 +202,7 @@ class Palette(DataClassJsonMixin): @dataclass() class Theme(DataClassJsonMixin): font_family: Optional[str] = None + default: Optional[Literal["light", "dark"]] = "dark" layout: Optional[Literal["default", "wide"]] = "default" light: Optional[Palette] = None dark: Optional[Palette] = None @@ -234,7 +229,6 @@ class AudioFeature(DataClassJsonMixin): @dataclass() class FeaturesSettings(DataClassJsonMixin): - prompt_playground: bool = True spontaneous_file_upload: Optional[SpontaneousFileUploadFeature] = None audio: Optional[AudioFeature] = Field(default_factory=AudioFeature) latex: bool = False @@ -245,12 +239,10 @@ class FeaturesSettings(DataClassJsonMixin): @dataclass() class UISettings(DataClassJsonMixin): name: str - show_readme_as_default: bool = True description: str = "" hide_cot: bool = False # Large size content are by default collapsed for a cleaner ui default_collapse_content: bool = True - default_expand_messages: bool = False github: Optional[str] = None theme: Optional[Theme] = None # Optional custom CSS file that allows you to customize the UI @@ -289,6 +281,7 @@ class CodeSettings: set_chat_profiles: Optional[Callable[[Optional["User"]], List["ChatProfile"]]] = ( None ) + set_starters: Optional[Callable[[Optional["User"]], List["Starter"]]] = None @dataclass() diff --git a/backend/chainlit/data/__init__.py b/backend/chainlit/data/__init__.py index 05f06aee44..964739d6aa 100644 --- a/backend/chainlit/data/__init__.py +++ b/backend/chainlit/data/__init__.py @@ -139,6 +139,9 @@ async def update_thread( async def delete_user_session(self, id: str) -> bool: return True + async def build_debug_url(self) -> str: + return "" + _data_layer: Optional[BaseDataLayer] = None @@ -225,6 +228,14 @@ def step_to_step_dict(self, step: LiteralStep) -> "StepDict": "waitForAnswer": metadata.get("waitForAnswer", False), } + async def build_debug_url(self) -> str: + try: + project_id = await self.client.api.get_my_project_id() + return f"{self.client.api.url}/projects/{project_id}/threads?threadId=[thread_id]¤tStepId=[step_id]" + except Exception as e: + logger.error(f"Error building debug url: {e}") + return "" + async def get_user(self, identifier: str) -> Optional[PersistedUser]: user = await self.client.api.get_user(identifier=identifier) if not user: @@ -456,12 +467,12 @@ async def get_thread(self, thread_id: str) -> "Optional[ThreadDict]": steps = [] # List[StepDict] if thread.steps: for step in thread.steps: - if config.ui.hide_cot and step.parent_id: + if config.ui.hide_cot and ( + step.parent_id or "message" not in step.type + ): continue for attachment in step.attachments: elements.append(self.attachment_to_element_dict(attachment)) - if not config.features.prompt_playground and step.generation: - step.generation = None steps.append(self.step_to_step_dict(step)) return { diff --git a/backend/chainlit/data/sql_alchemy.py b/backend/chainlit/data/sql_alchemy.py index 97d3f3add3..6877b37e4a 100644 --- a/backend/chainlit/data/sql_alchemy.py +++ b/backend/chainlit/data/sql_alchemy.py @@ -9,7 +9,7 @@ import aiohttp from chainlit.context import context from chainlit.data import BaseDataLayer, BaseStorageClient, queue_until_user_message -from chainlit.element import Avatar, ElementDict +from chainlit.element import ElementDict from chainlit.logger import logger from chainlit.step import StepDict from chainlit.types import ( @@ -65,6 +65,9 @@ def __init__( "SQLAlchemyDataLayer storage client is not initialized and elements will not be persisted!" ) + async def build_debug_url(self) -> str: + return "" + ###### SQL Helpers ###### async def execute_sql( self, query: str, parameters: dict @@ -373,8 +376,6 @@ async def create_element(self, element: "Element"): logger.info(f"SQLAlchemy: create_element, element_id = {element.id}") if not getattr(context.session.user, "id", None): raise ValueError("No authenticated user in context") - if isinstance(element, Avatar): # Skip creating elements of type avatar - return if not self.storage_provider: logger.warn( f"SQLAlchemy: create_element error. No blob_storage_client is configured!" diff --git a/backend/chainlit/discord/app.py b/backend/chainlit/discord/app.py index 33126a7960..1b728c9856 100644 --- a/backend/chainlit/discord/app.py +++ b/backend/chainlit/discord/app.py @@ -104,7 +104,6 @@ async def send_step(self, step_dict: StepDict): is_message = step_type in [ "user_message", "assistant_message", - "system_message", ] is_chain_of_thought = bool(step_dict.get("parentId")) is_empty_output = not step_dict.get("output") diff --git a/backend/chainlit/element.py b/backend/chainlit/element.py index 33746ae094..cfea44f57a 100644 --- a/backend/chainlit/element.py +++ b/backend/chainlit/element.py @@ -21,7 +21,7 @@ } ElementType = Literal[ - "image", "avatar", "text", "pdf", "tasklist", "audio", "video", "file", "plotly" + "image", "text", "pdf", "tasklist", "audio", "video", "file", "plotly" ] ElementDisplay = Literal["inline", "side", "page"] ElementSize = Literal["small", "medium", "large"] @@ -190,14 +190,6 @@ class Image(Element): size: ElementSize = "medium" -@dataclass -class Avatar(Element): - type: ClassVar[ElementType] = "avatar" - - async def send(self): - await super().send(for_id="") - - @dataclass class Text(Element): """Useful to send a text (not a message) to the UI.""" diff --git a/backend/chainlit/langchain/callbacks.py b/backend/chainlit/langchain/callbacks.py index 0876444929..a0605d0c4e 100644 --- a/backend/chainlit/langchain/callbacks.py +++ b/backend/chainlit/langchain/callbacks.py @@ -456,7 +456,7 @@ def _start_trace(self, run: Run) -> None: elif run.run_type == "llm": step_type = "llm" elif run.run_type == "retriever": - step_type = "retrieval" + step_type = "tool" elif run.run_type == "tool": step_type = "tool" elif run.run_type == "embedding": @@ -533,7 +533,9 @@ def _on_run_update(self, run: Run) -> None: break current_step.language = "json" - current_step.output = json.dumps(message_completion, indent=4, ensure_ascii=False) + current_step.output = json.dumps( + message_completion, indent=4, ensure_ascii=False + ) else: completion_start = self.completion_generations[str(run.id)] completion = generation.get("text", "") diff --git a/backend/chainlit/llama_index/callbacks.py b/backend/chainlit/llama_index/callbacks.py index 7da19e8987..5f5d954f2e 100644 --- a/backend/chainlit/llama_index/callbacks.py +++ b/backend/chainlit/llama_index/callbacks.py @@ -73,9 +73,9 @@ def on_event_start( step_type: StepType = "undefined" if event_type == CBEventType.RETRIEVE: - step_type = "retrieval" + step_type = "tool" elif event_type == CBEventType.QUERY: - step_type = "retrieval" + step_type = "tool" elif event_type == CBEventType.LLM: step_type = "llm" else: diff --git a/backend/chainlit/message.py b/backend/chainlit/message.py index 474f4cb33c..bc79156dfd 100644 --- a/backend/chainlit/message.py +++ b/backend/chainlit/message.py @@ -173,21 +173,21 @@ async def stream_token(self, token: str, is_sequence=False): Sends a token to the UI. This is useful for streaming messages. Once all tokens have been streamed, call .send() to end the stream and persist the message if persistence is enabled. """ - - if not self.streaming: - self.streaming = True - step_dict = self.to_dict() - await context.emitter.stream_start(step_dict) - if is_sequence: self.content = token else: self.content += token assert self.id - await context.emitter.send_token( - id=self.id, token=token, is_sequence=is_sequence - ) + + if not self.streaming: + self.streaming = True + step_dict = self.to_dict() + await context.emitter.stream_start(step_dict) + else: + await context.emitter.send_token( + id=self.id, token=token, is_sequence=is_sequence + ) class Message(MessageBase): @@ -196,7 +196,7 @@ class Message(MessageBase): Args: content (Union[str, Dict]): The content of the message. - author (str, optional): The author of the message, this will be used in the UI. Defaults to the chatbot name (see config). + author (str, optional): The author of the message, this will be used in the UI. Defaults to the assistant name (see config). language (str, optional): Language of the code is the content is code. See https://react-code-blocks-rajinwonderland.vercel.app/?path=/story/codeblock--supported-languages for a list of supported languages. actions (List[Action], optional): A list of actions to send with the message. elements (List[ElementBased], optional): A list of elements to send with the message. @@ -303,7 +303,7 @@ class ErrorMessage(MessageBase): Args: content (str): Text displayed above the upload button. - author (str, optional): The author of the message, this will be used in the UI. Defaults to the chatbot name (see config). + author (str, optional): The author of the message, this will be used in the UI. Defaults to the assistant name (see config). parent_id (str, optional): If provided, the message will be nested inside the parent in the UI. indent (int, optional): If positive, the message will be nested in the UI. """ @@ -316,7 +316,7 @@ def __init__( ): self.content = content self.author = author - self.type = "system_message" + self.type = "assistant_message" self.is_error = True self.fail_on_persist_error = fail_on_persist_error @@ -346,7 +346,7 @@ class AskUserMessage(AskMessageBase): Args: content (str): The content of the prompt. - author (str, optional): The author of the message, this will be used in the UI. Defaults to the chatbot name (see config). + author (str, optional): The author of the message, this will be used in the UI. Defaults to the assistant name (see config). disable_feedback (bool, optional): Hide the feedback buttons for this specific message timeout (int, optional): The number of seconds to wait for an answer before raising a TimeoutError. raise_on_timeout (bool, optional): Whether to raise a socketio TimeoutError if the user does not answer in time. @@ -357,7 +357,7 @@ def __init__( content: str, author: str = config.ui.name, type: MessageStepType = "assistant_message", - disable_feedback: bool = False, + disable_feedback: bool = True, timeout: int = 60, raise_on_timeout: bool = False, ): @@ -411,7 +411,7 @@ class AskFileMessage(AskMessageBase): accept (Union[List[str], Dict[str, List[str]]]): List of mime type to accept like ["text/csv", "application/pdf"] or a dict like {"text/plain": [".txt", ".py"]}. max_size_mb (int, optional): Maximum size per file in MB. Maximum value is 100. max_files (int, optional): Maximum number of files to upload. Maximum value is 10. - author (str, optional): The author of the message, this will be used in the UI. Defaults to the chatbot name (see config). + author (str, optional): The author of the message, this will be used in the UI. Defaults to the assistant name (see config). disable_feedback (bool, optional): Hide the feedback buttons for this specific message timeout (int, optional): The number of seconds to wait for an answer before raising a TimeoutError. raise_on_timeout (bool, optional): Whether to raise a socketio TimeoutError if the user does not answer in time. @@ -425,7 +425,7 @@ def __init__( max_files=1, author=config.ui.name, type: MessageStepType = "assistant_message", - disable_feedback: bool = False, + disable_feedback: bool = True, timeout=90, raise_on_timeout=False, ): @@ -501,7 +501,7 @@ def __init__( content: str, actions: List[Action], author=config.ui.name, - disable_feedback=False, + disable_feedback=True, timeout=90, raise_on_timeout=False, ): diff --git a/backend/chainlit/playground/__init__.py b/backend/chainlit/playground/__init__.py deleted file mode 100644 index b39502cb65..0000000000 --- a/backend/chainlit/playground/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .config import add_llm_provider, get_llm_providers -from .provider import * diff --git a/backend/chainlit/playground/config.py b/backend/chainlit/playground/config.py deleted file mode 100644 index dc6fd5b911..0000000000 --- a/backend/chainlit/playground/config.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Dict - -from chainlit.playground.provider import BaseProvider -from chainlit.playground.providers import ( - Anthropic, - AzureChatOpenAI, - ChatOpenAI, - ChatVertexAI, - GenerationVertexAI, - Gemini, -) - -providers = { - AzureChatOpenAI.id: AzureChatOpenAI, - ChatOpenAI.id: ChatOpenAI, - Anthropic.id: Anthropic, - ChatVertexAI.id: ChatVertexAI, - GenerationVertexAI.id: GenerationVertexAI, - Gemini.id: Gemini, -} # type: Dict[str, BaseProvider] - - -def has_llm_provider(id: str): - return id in providers - - -def add_llm_provider(provider: BaseProvider): - if not provider.is_configured(): - raise ValueError( - f"{provider.name} LLM provider requires the following environment variables: {', '.join(provider.env_vars.values())}" - ) - providers[provider.id] = provider - - -def get_llm_providers(): - return [provider for provider in providers.values() if provider.is_configured()] diff --git a/backend/chainlit/playground/provider.py b/backend/chainlit/playground/provider.py deleted file mode 100644 index 4327ef2f3a..0000000000 --- a/backend/chainlit/playground/provider.py +++ /dev/null @@ -1,108 +0,0 @@ -import os -from typing import Any, Dict, List, Optional, Union - -from chainlit.config import config -from chainlit.telemetry import trace_event -from chainlit.types import GenerationRequest -from fastapi import HTTPException -from literalai import BaseGeneration, ChatGeneration, GenerationMessage -from pydantic.dataclasses import dataclass - -from chainlit import input_widget - - -@dataclass -class BaseProvider: - id: str - name: str - env_vars: Dict[str, str] - inputs: List[input_widget.InputWidget] - is_chat: bool - - # Convert the message to string format - def message_to_string(self, message: GenerationMessage): - return message["content"] - - # Concatenate multiple messages with a joiner - def concatenate_messages(self, messages: List[GenerationMessage], joiner="\n\n"): - return joiner.join([self.message_to_string(m) for m in messages]) - - # Format the template based on the prompt inputs - def _format_template(self, template: str, inputs: Optional[Dict]): - return template.format(**(inputs or {})) - - # Create a prompt based on the request - def create_generation(self, request: GenerationRequest): - if request.chatGeneration and request.chatGeneration.messages: - messages = request.chatGeneration.messages - else: - messages = None - - if self.is_chat: - if messages: - return messages - elif request.completionGeneration and request.completionGeneration.prompt: - return [ - GenerationMessage( - content=request.completionGeneration.prompt, - role="user", - ), - ] - else: - raise HTTPException( - status_code=422, detail="Could not create generation" - ) - else: - if request.completionGeneration: - return request.completionGeneration.prompt - elif messages: - return self.concatenate_messages(messages) - else: - raise HTTPException(status_code=422, detail="Could not create prompt") - - # Create a completion event - async def create_completion(self, request: GenerationRequest): - trace_event("completion") - - # Get the environment variable based on the request - def get_var(self, request: GenerationRequest, var: str) -> Union[str, None]: - user_env = config.project.user_env or [] - - if var in user_env: - return request.userEnv.get(var) - else: - return os.environ.get(var) - - # Check if the environment variable is available - def _is_env_var_available(self, var: str) -> bool: - user_env = config.project.user_env or [] - return var in os.environ or var in user_env - - # Check if the provider is configured - def is_configured(self): - for var in self.env_vars.values(): - if not self._is_env_var_available(var): - return False - return True - - # Validate the environment variables in the request - def validate_env(self, request: GenerationRequest): - return {k: self.get_var(request, v) for k, v in self.env_vars.items()} - - # Check if the required settings are present - def require_settings(self, settings: Dict[str, Any]): - for _input in self.inputs: - if _input.id not in settings: - raise HTTPException( - status_code=422, - detail=f"Field {_input.id} is a required setting but is not found.", - ) - - # Convert the provider to dictionary format - def to_dict(self): - return { - "id": self.id, - "name": self.name, - "inputs": [input_widget.to_dict() for input_widget in self.inputs], - "is_chat": self.is_chat, - } diff --git a/backend/chainlit/playground/providers/__init__.py b/backend/chainlit/playground/providers/__init__.py deleted file mode 100644 index de5c0b7a6c..0000000000 --- a/backend/chainlit/playground/providers/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .anthropic import Anthropic -from .huggingface import HFFlanT5 -from .openai import ( - AzureChatOpenAI, - ChatOpenAI, -) -from .vertexai import ( - ChatVertexAI, - GenerationVertexAI, - Gemini -) diff --git a/backend/chainlit/playground/providers/anthropic.py b/backend/chainlit/playground/providers/anthropic.py deleted file mode 100644 index 10caa9ba27..0000000000 --- a/backend/chainlit/playground/providers/anthropic.py +++ /dev/null @@ -1,118 +0,0 @@ -from chainlit.input_widget import Select, Slider, Tags -from chainlit.playground.provider import BaseProvider -from fastapi import HTTPException -from fastapi.responses import StreamingResponse -from literalai import GenerationMessage - - -class AnthropicProvider(BaseProvider): - def message_to_string(self, message: GenerationMessage) -> str: - import anthropic - - if message["role"] == "user": - message_text = f"{anthropic.HUMAN_PROMPT} {message['content']}" - elif message["role"] == "assistant": - message_text = f"{anthropic.AI_PROMPT} {message['content']}" - elif message["role"] == "function": - message_text = f"{anthropic.AI_PROMPT} {message['content']}" - elif message["role"] == "system": - message_text = ( - f"{anthropic.HUMAN_PROMPT} {message['content']}" - ) - else: - raise HTTPException( - status_code=400, detail=f"Got unknown type {message['role']}" - ) - return message_text - - async def create_completion(self, request): - await super().create_completion(request) - import anthropic - - env_settings = self.validate_env(request=request) - - llm_settings = request.generation.settings - self.require_settings(llm_settings) - - prompt = self.concatenate_messages(self.create_generation(request), joiner="") - - if not prompt.endswith(anthropic.AI_PROMPT): - prompt += anthropic.AI_PROMPT - - client = anthropic.AsyncAnthropic(**env_settings) - - llm_settings["stream"] = True - - try: - stream = await client.completions.create(prompt=prompt, **llm_settings) - except anthropic.APIConnectionError as e: - raise HTTPException( - status_code=503, - detail=e.__cause__, - ) - except anthropic.RateLimitError as e: - raise HTTPException( - status_code=429, - ) - except anthropic.APIStatusError as e: - raise HTTPException(status_code=e.status_code, detail=e.response) - - async def create_event_stream(): - async for data in stream: - token = data.completion - yield token - - return StreamingResponse(create_event_stream()) - - -Anthropic = AnthropicProvider( - id="anthropic-chat", - name="Anthropic", - env_vars={"api_key": "ANTHROPIC_API_KEY"}, - inputs=[ - Select( - id="model", - label="Model", - values=["claude-2", "claude-instant-1"], - initial_value="claude-2", - ), - Slider( - id="max_tokens_to_sample", - label="Max Tokens To Sample", - min=1.0, - max=100000, - step=1.0, - initial=1000, - ), - Tags( - id="stop_sequences", - label="Stop Sequences", - initial=[], - ), - Slider( - id="temperature", - label="Temperature", - min=0.0, - max=1.0, - step=0.01, - initial=1, - ), - Slider( - id="top_p", - label="Top P", - min=0.0, - max=1.0, - step=0.01, - initial=0.7, - ), - Slider( - id="top_k", - label="Top K", - min=0.0, - max=2048.0, - step=1.0, - initial=0, - ), - ], - is_chat=True, -) diff --git a/backend/chainlit/playground/providers/huggingface.py b/backend/chainlit/playground/providers/huggingface.py deleted file mode 100644 index d01f2b31dc..0000000000 --- a/backend/chainlit/playground/providers/huggingface.py +++ /dev/null @@ -1,75 +0,0 @@ -from typing import Optional - -from chainlit.input_widget import Slider -from chainlit.playground.provider import BaseProvider -from chainlit.sync import make_async -from fastapi import HTTPException -from fastapi.responses import StreamingResponse -from pydantic.dataclasses import dataclass - - -@dataclass -class BaseHuggingFaceProvider(BaseProvider): - repo_id: Optional[str] = None - task = "text2text-generation" - - async def create_completion(self, request): - await super().create_completion(request) - from huggingface_hub.inference_api import InferenceApi - - env_settings = self.validate_env(request=request) - llm_settings = request.generation.settings - self.require_settings(llm_settings) - - client = InferenceApi( - repo_id=self.repo_id, - token=env_settings["api_token"], - task=self.task, - ) - - prompt = self.create_generation(request) - - response = await make_async(client)(inputs=prompt, params=llm_settings) - - if "error" in response: - raise HTTPException( - status_code=500, - detail=f"Error raised by inference API: {response['error']}", - ) - if client.task == "text2text-generation": - - def create_event_stream(): - yield response[0]["generated_text"] - - return StreamingResponse(create_event_stream()) - else: - raise HTTPException(status_code=400, detail="Unsupported task") - - -flan_hf_env_vars = {"api_token": "HUGGINGFACE_API_TOKEN"} - -HFFlanT5 = BaseHuggingFaceProvider( - id="huggingface_hub", - repo_id="declare-lab/flan-alpaca-large", - name="Flan Alpaca Large", - env_vars=flan_hf_env_vars, - inputs=[ - Slider( - id="temperature", - label="Temperature", - min=0.0, - max=1.0, - step=0.01, - initial=0.9, - ), - Slider( - id="max_length", - label="Completion max length", - min=1.0, - max=5000, - step=1.0, - initial=256, - ), - ], - is_chat=False, -) diff --git a/backend/chainlit/playground/providers/langchain.py b/backend/chainlit/playground/providers/langchain.py deleted file mode 100644 index f09c343c3d..0000000000 --- a/backend/chainlit/playground/providers/langchain.py +++ /dev/null @@ -1,89 +0,0 @@ -from typing import List, Union - -from chainlit.input_widget import InputWidget -from chainlit.playground.provider import BaseProvider -from chainlit.sync import make_async -from fastapi.responses import StreamingResponse -from literalai import GenerationMessage - - -class LangchainGenericProvider(BaseProvider): - from langchain.chat_models.base import BaseChatModel - from langchain.llms.base import LLM - from langchain.schema import BaseMessage - - llm: Union[LLM, BaseChatModel] - - def __init__( - self, - id: str, - name: str, - llm: Union[LLM, BaseChatModel], - inputs: List[InputWidget] = [], - is_chat: bool = False, - ): - super().__init__( - id=id, - name=name, - env_vars={}, - inputs=inputs, - is_chat=is_chat, - ) - self.llm = llm - - def prompt_message_to_langchain_message(self, message: GenerationMessage): - from langchain.schema.messages import ( - AIMessage, - FunctionMessage, - HumanMessage, - SystemMessage, - ) - - content = "" if message["content"] is None else message["content"] - if message["role"] == "user": - return HumanMessage(content=content) # type: ignore - elif message["role"] == "assistant": - return AIMessage(content=content) # type: ignore - elif message["role"] == "system": - return SystemMessage(content=content) # type: ignore - elif message["role"] == "tool": - return FunctionMessage( - content=content, # type: ignore - name=message["name"] if message["name"] else "function", - ) - else: - raise ValueError(f"Got unknown type {message['role']}") - - def format_message(self, message, prompt): - message = super().format_message(message, prompt) - return self.prompt_message_to_langchain_message(message) - - def message_to_string(self, message: BaseMessage) -> str: # type: ignore[override] - return str(getattr(message, "content", "")) - - async def create_completion(self, request): - from langchain.schema.messages import BaseMessageChunk - - await super().create_completion(request) - - messages = self.create_generation(request) - - # https://github.com/langchain-ai/langchain/issues/14980 - result = await make_async(self.llm.stream)( - input=messages, **request.generation.settings - ) - - def create_event_stream(): - try: - for chunk in result: - if isinstance(chunk, BaseMessageChunk): - yield chunk.content - else: - yield chunk - except Exception as e: - # The better solution would be to return a 500 error, but - # langchain raises the error in the stream, and the http - # headers have already been sent. - yield f"Failed to create completion: {str(e)}" - - return StreamingResponse(create_event_stream()) diff --git a/backend/chainlit/playground/providers/openai.py b/backend/chainlit/playground/providers/openai.py deleted file mode 100644 index 1353b5fd48..0000000000 --- a/backend/chainlit/playground/providers/openai.py +++ /dev/null @@ -1,386 +0,0 @@ -import json -from contextlib import contextmanager - -from chainlit.input_widget import Select, Slider, Tags -from chainlit.playground.provider import BaseProvider -from fastapi import HTTPException -from fastapi.responses import StreamingResponse - - -def stringify_function_call(function_call): - if isinstance(function_call, dict): - _function_call = function_call.copy() - else: - _function_call = { - "arguments": function_call.arguments, - "name": function_call.name, - } - - if "arguments" in _function_call and isinstance(_function_call["arguments"], str): - _function_call["arguments"] = json.loads(_function_call["arguments"]) - return json.dumps(_function_call, indent=4, ensure_ascii=False) - - -openai_common_inputs = [ - Slider( - id="temperature", - label="Temperature", - min=0.0, - max=2.0, - step=0.01, - initial=0.9, - tooltip="Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.", - ), - Slider( - id="max_tokens", - label="Max Tokens", - min=0.0, - max=8000, - step=1, - initial=256, - tooltip="The maximum number of tokens to generate in the chat completion.", - ), - Slider( - id="top_p", - label="Top P", - min=0.0, - max=1.0, - step=0.01, - initial=1.0, - tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.", - ), - Slider( - id="frequency_penalty", - label="Frequency Penalty", - min=-2.0, - max=2.0, - step=0.01, - initial=0.0, - tooltip="Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", - ), - Slider( - id="presence_penalty", - label="Presence Penalty", - min=-2.0, - max=2.0, - step=0.01, - initial=0.0, - tooltip="Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", - ), - Tags( - id="stop", - label="Stop Sequences", - initial=[], - tooltip="Up to 4 sequences where the API will stop generating further tokens.", - ), -] - - -@contextmanager -def handle_openai_error(): - import openai - - try: - yield - except openai.APITimeoutError as e: - raise HTTPException( - status_code=408, - detail=f"OpenAI API request timed out: {e}", - ) - except openai.APIError as e: - raise HTTPException( - status_code=500, - detail=f"OpenAI API returned an API Error: {e}", - ) - except openai.APIConnectionError as e: - raise HTTPException( - status_code=503, - detail=f"OpenAI API request failed to connect: {e}", - ) - except openai.AuthenticationError as e: - raise HTTPException( - status_code=403, - detail=f"OpenAI API request was not authorized: {e}", - ) - except openai.PermissionDeniedError as e: - raise HTTPException( - status_code=403, - detail=f"OpenAI API request was not permitted: {e}", - ) - except openai.RateLimitError as e: - raise HTTPException( - status_code=429, - detail=f"OpenAI API request exceeded rate limit: {e}", - ) - - -class ChatOpenAIProvider(BaseProvider): - def format_message(self, message, prompt): - message = super().format_message(message, prompt) - return message.to_openai() - - async def create_completion(self, request): - await super().create_completion(request) - from openai import AsyncClient - - env_settings = self.validate_env(request=request) - - client = AsyncClient(api_key=env_settings["api_key"]) - - llm_settings = request.generation.settings - - self.require_settings(llm_settings) - - messages = self.create_generation(request) - - if "stop" in llm_settings: - stop = llm_settings["stop"] - - # OpenAI doesn't support an empty stop array, clear it - if isinstance(stop, list) and len(stop) == 0: - stop = None - - llm_settings["stop"] = stop - - if request.generation.tools: - llm_settings["tools"] = request.generation.tools - llm_settings["stream"] = False - else: - llm_settings["stream"] = True - - with handle_openai_error(): - response = await client.chat.completions.create( - messages=messages, - **llm_settings, - ) - - if llm_settings["stream"]: - - async def create_event_stream(): - async for part in response: - if part.choices and part.choices[0].delta.content: - token = part.choices[0].delta.content - yield token - else: - continue - - else: - - async def create_event_stream(): - message = response.choices[0].message - if tool_calls := message.tool_calls: - yield json.dumps([tc.model_dump() for tc in tool_calls], indent=4, ensure_ascii=False) - else: - yield message.content or "" - - return StreamingResponse(create_event_stream()) - - -class OpenAIProvider(BaseProvider): - def message_to_string(self, message): - return message.to_string() - - async def create_completion(self, request): - await super().create_completion(request) - from openai import AsyncClient - - env_settings = self.validate_env(request=request) - - client = AsyncClient(api_key=env_settings["api_key"]) - - llm_settings = request.generation.settings - - self.require_settings(llm_settings) - - prompt = self.create_generation(request) - - if "stop" in llm_settings: - stop = llm_settings["stop"] - - # OpenAI doesn't support an empty stop array, clear it - if isinstance(stop, list) and len(stop) == 0: - stop = None - - llm_settings["stop"] = stop - - llm_settings["stream"] = True - - with handle_openai_error(): - response = await client.completions.create( - prompt=prompt, - **llm_settings, - ) - - async def create_event_stream(): - async for part in response: - if part.choices and part.choices[0].text: - token = part.choices[0].text - yield token - else: - continue - - return StreamingResponse(create_event_stream()) - - -class AzureOpenAIProvider(BaseProvider): - def message_to_string(self, message): - return message.to_string() - - async def create_completion(self, request): - await super().create_completion(request) - from openai import AsyncAzureOpenAI - - env_settings = self.validate_env(request=request) - - client = AsyncAzureOpenAI( - api_key=env_settings["api_key"], - api_version=env_settings["api_version"], - azure_endpoint=env_settings["azure_endpoint"], - azure_ad_token=self.get_var(request, "AZURE_AD_TOKEN"), - azure_deployment=self.get_var(request, "AZURE_DEPLOYMENT"), - ) - llm_settings = request.generation.settings - - self.require_settings(llm_settings) - - prompt = self.create_generation(request) - - if "stop" in llm_settings: - stop = llm_settings["stop"] - - # OpenAI doesn't support an empty stop array, clear it - if isinstance(stop, list) and len(stop) == 0: - stop = None - - llm_settings["stop"] = stop - - llm_settings["stream"] = True - - with handle_openai_error(): - response = await client.completions.create( - prompt=prompt, - **llm_settings, - ) - - async def create_event_stream(): - async for part in response: - if part.choices and part.choices[0].text: - token = part.choices[0].text - yield token - else: - continue - - return StreamingResponse(create_event_stream()) - - -class AzureChatOpenAIProvider(BaseProvider): - def format_message(self, message, prompt): - message = super().format_message(message, prompt) - return message.to_openai() - - async def create_completion(self, request): - await super().create_completion(request) - from openai import AsyncAzureOpenAI - - env_settings = self.validate_env(request=request) - - client = AsyncAzureOpenAI( - api_key=env_settings["api_key"], - api_version=env_settings["api_version"], - azure_endpoint=env_settings["azure_endpoint"], - azure_ad_token=self.get_var(request, "AZURE_AD_TOKEN"), - azure_deployment=self.get_var(request, "AZURE_DEPLOYMENT"), - ) - - llm_settings = request.generation.settings - - self.require_settings(llm_settings) - - messages = self.create_generation(request) - - if "stop" in llm_settings: - stop = llm_settings["stop"] - - # OpenAI doesn't support an empty stop array, clear it - if isinstance(stop, list) and len(stop) == 0: - stop = None - - llm_settings["stop"] = stop - - llm_settings["model"] = env_settings["deployment_name"] - - if request.generation.tools: - llm_settings["tools"] = request.generation.tools - llm_settings["stream"] = False - else: - llm_settings["stream"] = True - - with handle_openai_error(): - response = await client.chat.completions.create( - messages=messages, - **llm_settings, - ) - - if llm_settings["stream"]: - - async def create_event_stream(): - async for part in response: - if part.choices and part.choices[0].delta.content: - token = part.choices[0].delta.content - yield token - else: - continue - - else: - - async def create_event_stream(): - message = response.choices[0].message - if tool_calls := message.tool_calls: - yield json.dumps([tc.model_dump() for tc in tool_calls], indent=4, ensure_ascii=False) - else: - yield message.content or "" - - return StreamingResponse(create_event_stream()) - - -openai_env_vars = {"api_key": "OPENAI_API_KEY"} - -azure_openai_env_vars = { - "api_key": "AZURE_OPENAI_API_KEY", - "api_version": "AZURE_OPENAI_API_VERSION", - "azure_endpoint": "AZURE_OPENAI_ENDPOINT", - "deployment_name": "AZURE_OPENAI_DEPLOYMENT_NAME", -} - -ChatOpenAI = ChatOpenAIProvider( - id="openai-chat", - env_vars=openai_env_vars, - name="ChatOpenAI", - inputs=[ - Select( - id="model", - label="Model", - values=[ - "gpt-3.5-turbo", - "gpt-3.5-turbo-16k", - "gpt-4", - "gpt-4-32k", - "gpt-4-1106-preview", - ], - initial_value="gpt-3.5-turbo", - ), - *openai_common_inputs, - ], - is_chat=True, -) - - - - -AzureChatOpenAI = AzureChatOpenAIProvider( - id="azure-openai-chat", - env_vars=azure_openai_env_vars, - name="AzureChatOpenAI", - inputs=openai_common_inputs, - is_chat=True, -) diff --git a/backend/chainlit/playground/providers/vertexai.py b/backend/chainlit/playground/providers/vertexai.py deleted file mode 100644 index 6ab7672b12..0000000000 --- a/backend/chainlit/playground/providers/vertexai.py +++ /dev/null @@ -1,171 +0,0 @@ -import chainlit as cl -from fastapi import HTTPException - -from fastapi.responses import StreamingResponse - -from chainlit.input_widget import Select, Slider, Tags -from chainlit.playground.provider import BaseProvider - -vertexai_common_inputs = [ - Slider( - id="temperature", - label="Temperature", - min=0.0, - max=0.99, - step=0.01, - initial=0.2, - ), - Slider( - id="max_output_tokens", - label="Max Output Tokens", - min=0.0, - max=1024, - step=1, - initial=256, - ), -] - - -class ChatVertexAIProvider(BaseProvider): - async def create_completion(self, request): - await super().create_completion(request) - from vertexai.language_models import ChatModel, CodeChatModel - - self.validate_env(request=request) - - llm_settings = request.generation.settings - self.require_settings(llm_settings) - - messages = self.create_generation(request) - model_name = llm_settings["model"] - if model_name.startswith("chat-"): - model = ChatModel.from_pretrained(model_name) - elif model_name.startswith("codechat-"): - model = CodeChatModel.from_pretrained(model_name) - else: - raise HTTPException( - status_code=400, - detail=f"This model{model_name} is not implemented.", - ) - del llm_settings["model"] - chat = model.start_chat() - - async def create_event_stream(): - for response in await cl.make_async(chat.send_message_streaming)( - messages, **llm_settings - ): - yield response.text - - return StreamingResponse(create_event_stream()) - - -class GenerationVertexAIProvider(BaseProvider): - async def create_completion(self, request): - await super().create_completion(request) - from vertexai.language_models import TextGenerationModel, CodeGenerationModel - - self.validate_env(request=request) - - llm_settings = request.generation.settings - self.require_settings(llm_settings) - - messages = self.create_generation(request) - model_name = llm_settings["model"] - if model_name.startswith("text-"): - model = TextGenerationModel.from_pretrained(model_name) - elif model_name.startswith("code-"): - model = CodeGenerationModel.from_pretrained(model_name) - else: - raise HTTPException( - status_code=400, - detail=f"This model{model_name} is not implemented.", - ) - del llm_settings["model"] - - async def create_event_stream(): - for response in await cl.make_async(model.predict_streaming)( - messages, **llm_settings - ): - yield response.text - - return StreamingResponse(create_event_stream()) - - -class GeminiProvider(BaseProvider): - async def create_completion(self, request): - await super().create_completion(request) - from vertexai.preview.generative_models import GenerativeModel - from google.cloud import aiplatform - import os - - self.validate_env(request=request) - - llm_settings = request.generation.settings - self.require_settings(llm_settings) - - messages = self.create_generation(request) - aiplatform.init( # TODO: remove this when Gemini is released in all the regions - project=os.environ["GCP_PROJECT_ID"], - location='us-central1', - ) - model = GenerativeModel(llm_settings["model"]) - del llm_settings["model"] - - async def create_event_stream(): - for response in await cl.make_async(model.generate_content)( - messages, stream=True, generation_config=llm_settings - ): - yield response.candidates[0].content.parts[0].text - - return StreamingResponse(create_event_stream()) - - -gcp_env_vars = {"google_application_credentials": "GOOGLE_APPLICATION_CREDENTIALS"} - -ChatVertexAI = ChatVertexAIProvider( - id="chat-vertexai", - env_vars=gcp_env_vars, - name="ChatVertexAI", - inputs=[ - Select( - id="model", - label="Model", - values=["chat-bison", "codechat-bison"], - initial_value="chat-bison", - ), - *vertexai_common_inputs, - ], - is_chat=True, -) - -GenerationVertexAI = GenerationVertexAIProvider( - id="generation-vertexai", - env_vars=gcp_env_vars, - name="GenerationVertexAI", - inputs=[ - Select( - id="model", - label="Model", - values=["text-bison", "code-bison"], - initial_value="text-bison", - ), - *vertexai_common_inputs, - ], - is_chat=False, -) - -Gemini = GeminiProvider( - id="gemini", - env_vars=gcp_env_vars, - name="Gemini", - inputs=[ - Select( - id="model", - label="Model", - values=["gemini-pro", "gemini-pro-vision"], - initial_value="gemini-pro", - ), - *vertexai_common_inputs, - ], - is_chat=False, -) diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index c932a25bba..fc877d23ea 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -33,7 +33,6 @@ from chainlit.data.acl import is_thread_author from chainlit.logger import logger from chainlit.markdown import get_markdown_str -from chainlit.playground.config import get_llm_providers from chainlit.telemetry import trace_event from chainlit.types import ( DeleteFeedbackRequest, @@ -234,7 +233,9 @@ def get_html_template(): CSS_PLACEHOLDER = "" default_url = "https://github.com/Chainlit/chainlit" - default_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png" + default_meta_image_url = ( + "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png" + ) url = config.ui.github or default_url meta_image_url = config.ui.custom_meta_image_url or default_meta_image_url @@ -496,40 +497,6 @@ async def oauth_callback( return response -@app.post("/generation") -async def generation( - request: GenerationRequest, - current_user: Annotated[Union[User, PersistedUser], Depends(get_current_user)], -): - """Handle a completion request from the prompt playground.""" - - providers = get_llm_providers() - - try: - provider = [p for p in providers if p.id == request.generation.provider][0] - except IndexError: - raise HTTPException( - status_code=404, - detail=f"LLM provider '{request.generation.provider}' not found", - ) - - trace_event("pp_create_completion") - response = await provider.create_completion(request) - - return response - - -@app.get("/project/llm-providers") -async def get_providers( - current_user: Annotated[Union[User, PersistedUser], Depends(get_current_user)] -): - """List the providers.""" - trace_event("pp_get_llm_providers") - providers = get_llm_providers() - providers = [p.to_dict() for p in providers] - return JSONResponse(content={"providers": providers}) - - @app.get("/project/translations") async def project_translations( language: str = Query(default="en-US", description="Language code"), @@ -562,9 +529,21 @@ async def project_settings( if chat_profiles: profiles = [p.to_dict() for p in chat_profiles] + starters = [] + if config.code.set_starters: + starters = await config.code.set_starters(current_user) + if starters: + starters = [s.to_dict() for s in starters] + if config.code.on_audio_chunk: config.features.audio.enabled = True + debug_url = None + data_layer = get_data_layer() + + if data_layer and config.run.debug: + debug_url = await data_layer.build_debug_url() + return JSONResponse( content={ "ui": config.ui.to_dict(), @@ -574,6 +553,8 @@ async def project_settings( "threadResumable": bool(config.code.on_chat_resume), "markdown": markdown, "chatProfiles": profiles, + "starters": starters, + "debugUrl": debug_url, } ) @@ -812,6 +793,25 @@ async def get_logo(theme: Optional[Theme] = Query(Theme.light)): return FileResponse(logo_path, media_type=media_type) +@app.get("/avatars/{avatar_id}") +async def get_avatar(avatar_id: str): + if avatar_id == "default": + avatar_id = config.ui.name + + 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: + avatar_path = files[0] + media_type, _ = mimetypes.guess_type(avatar_path) + return FileResponse(avatar_path, media_type=media_type) + else: + return await get_favicon() + + @app.head("/") def status_check(): return {"message": "Site is operational"} diff --git a/backend/chainlit/slack/app.py b/backend/chainlit/slack/app.py index 8fd82f6449..88cbb9e761 100644 --- a/backend/chainlit/slack/app.py +++ b/backend/chainlit/slack/app.py @@ -71,7 +71,6 @@ async def send_step(self, step_dict: StepDict): is_message = step_type in [ "user_message", "assistant_message", - "system_message", ] is_chain_of_thought = bool(step_dict.get("parentId")) is_empty_output = not step_dict.get("output") diff --git a/backend/chainlit/socket.py b/backend/chainlit/socket.py index 61808efe94..7bc44413b8 100644 --- a/backend/chainlit/socket.py +++ b/backend/chainlit/socket.py @@ -233,9 +233,7 @@ async def stop(sid): trace_event("stop_task") init_ws_context(session) - await Message( - author="System", content="Task manually stopped.", disable_feedback=True - ).send() + await Message(content="Task manually stopped.", disable_feedback=True).send() if session.current_task: session.current_task.cancel() diff --git a/backend/chainlit/step.py b/backend/chainlit/step.py index debc2dcf3b..17e880cd11 100644 --- a/backend/chainlit/step.py +++ b/backend/chainlit/step.py @@ -50,7 +50,6 @@ def step( id: Optional[str] = None, tags: Optional[List[str]] = None, disable_feedback: bool = True, - root: bool = False, language: Optional[str] = None, show_input: Union[bool, str] = False, ): @@ -72,7 +71,6 @@ async def async_wrapper(*args, **kwargs): name=name, id=id, disable_feedback=disable_feedback, - root=root, tags=tags, language=language, show_input=show_input, @@ -85,8 +83,9 @@ async def async_wrapper(*args, **kwargs): try: if result and not step.output: step.output = result - except: - pass + except Exception as e: + step.is_error = True + step.output = str(e) return result return async_wrapper @@ -99,7 +98,6 @@ def sync_wrapper(*args, **kwargs): name=name, id=id, disable_feedback=disable_feedback, - root=root, tags=tags, language=language, show_input=show_input, @@ -113,7 +111,8 @@ def sync_wrapper(*args, **kwargs): if result and not step.output: step.output = result except: - pass + step.is_error = True + step.output = str(e) return result return sync_wrapper @@ -136,7 +135,6 @@ class Step: streaming: bool persisted: bool - root: bool show_input: Union[bool, str] is_error: Optional[bool] @@ -161,7 +159,6 @@ def __init__( metadata: Optional[Dict] = None, tags: Optional[List[str]] = None, disable_feedback: bool = True, - root: bool = False, language: Optional[str] = None, show_input: Union[bool, str] = False, ): @@ -179,7 +176,6 @@ def __init__( self.is_error = False self.show_input = show_input self.parent_id = parent_id - self.root = root self.language = language self.generation = None @@ -299,11 +295,8 @@ async def update(self): tasks = [el.send(for_id=self.id) for el in self.elements] await asyncio.gather(*tasks) - if config.ui.hide_cot and (self.parent_id or not self.root): - return - - if not config.features.prompt_playground and "generation" in step_dict: - step_dict.pop("generation", None) + if config.ui.hide_cot and (self.parent_id or "message" not in self.type): + return True await context.emitter.update_step(step_dict) @@ -356,12 +349,9 @@ async def send(self): tasks = [el.send(for_id=self.id) for el in self.elements] await asyncio.gather(*tasks) - if config.ui.hide_cot and (self.parent_id or not self.root): + if config.ui.hide_cot and (self.parent_id or "message" not in self.type): return self - if not config.features.prompt_playground and "generation" in step_dict: - step_dict.pop("generation", None) - await context.emitter.send_step(step_dict) return self @@ -378,17 +368,17 @@ async def stream_token(self, token: str, is_sequence=False): assert self.id - if config.ui.hide_cot and (self.parent_id or not self.root): + if config.ui.hide_cot and (self.parent_id or "message" not in self.type): return if not self.streaming: self.streaming = True step_dict = self.to_dict() await context.emitter.stream_start(step_dict) - - await context.emitter.send_token( - id=self.id, token=token, is_sequence=is_sequence - ) + else: + await context.emitter.send_token( + id=self.id, token=token, is_sequence=is_sequence + ) # Handle parameter less decorator def __call__(self, func): @@ -408,7 +398,7 @@ async def __aenter__(self): previous_steps = local_steps.get() or [] parent_step = previous_steps[-1] if previous_steps else None - if not self.parent_id and not self.root: + if not self.parent_id: if parent_step: self.parent_id = parent_step.id elif context.session.root_message: @@ -421,6 +411,10 @@ async def __aenter__(self): async def __aexit__(self, exc_type, exc_val, exc_tb): self.end = utc_now() + if exc_type: + self.output = str(exc_val) + self.is_error = True + if self in context.active_steps: context.active_steps.remove(self) @@ -437,7 +431,7 @@ def __enter__(self): previous_steps = local_steps.get() or [] parent_step = previous_steps[-1] if previous_steps else None - if not self.parent_id and not self.root: + if not self.parent_id: if parent_step: self.parent_id = parent_step.id elif context.session.root_message: @@ -450,6 +444,11 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.end = utc_now() + + if exc_type: + self.output = str(exc_val) + self.is_error = True + if self in context.active_steps: context.active_steps.remove(self) diff --git a/backend/chainlit/translations/en-US.json b/backend/chainlit/translations/en-US.json index 8a66cb12e9..2836200e17 100644 --- a/backend/chainlit/translations/en-US.json +++ b/backend/chainlit/translations/en-US.json @@ -41,9 +41,7 @@ }, "detailsButton": { "using": "Using", - "running": "Running", - "took_one": "Took {{count}} step", - "took_other": "Took {{count}} steps" + "used": "Used" }, "auth": { "authLogin": { diff --git a/backend/chainlit/types.py b/backend/chainlit/types.py index 9884753e81..9bfa1ae9ad 100644 --- a/backend/chainlit/types.py +++ b/backend/chainlit/types.py @@ -226,6 +226,15 @@ class Theme(str, Enum): dark = "dark" +@dataclass +class Starter(DataClassJsonMixin): + """Specification for a starter that can be chosen by the user at the thread start.""" + + label: str + message: str + icon: Optional[str] = None + + @dataclass class ChatProfile(DataClassJsonMixin): """Specification for a chat profile that can be chosen by the user at the thread start.""" @@ -234,6 +243,7 @@ class ChatProfile(DataClassJsonMixin): markdown_description: str icon: Optional[str] = None default: bool = False + starters: Optional[List[Starter]] = None FeedbackStrategy = Literal["BINARY"] diff --git a/backend/chainlit/utils.py b/backend/chainlit/utils.py index 922ddd4138..310a279250 100644 --- a/backend/chainlit/utils.py +++ b/backend/chainlit/utils.py @@ -44,9 +44,10 @@ async def wrapper(*args): pass except Exception as e: logger.exception(e) - await ErrorMessage( - content=str(e) or e.__class__.__name__, author="Error" - ).send() + if with_task: + await ErrorMessage( + content=str(e) or e.__class__.__name__, author="Error" + ).send() finally: if with_task: await context.emitter.task_end() diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c9bc92e106..844efd7644 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chainlit" -version = "1.1.202" +version = "1.1.300rc0" keywords = [ 'LLM', 'Agents', @@ -27,7 +27,7 @@ chainlit = 'chainlit.cli:cli' [tool.poetry.dependencies] python = ">=3.8.1,<4.0.0" httpx = ">=0.23.0" -literalai = "0.0.601" +literalai = "0.0.602" dataclasses_json = "^0.5.7" fastapi = "^0.110.1" starlette = "^0.37.2" diff --git a/cypress/e2e/author_rename/.chainlit/config.toml b/cypress/e2e/author_rename/.chainlit/config.toml deleted file mode 100644 index 0c509af72c..0000000000 --- a/cypress/e2e/author_rename/.chainlit/config.toml +++ /dev/null @@ -1,62 +0,0 @@ -[project] -# Whether to enable telemetry (default: true). No personal data is collected. -enable_telemetry = true - -# List of environment variables to be provided by each user to use the app. -user_env = [] - -# Duration (in seconds) during which the session is saved when the connection is lost -session_timeout = 3600 - -# Enable third parties caching (e.g LangChain cache) -cache = false - -# Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) -# follow_symlink = false - -[features] -# Show the prompt playground -prompt_playground = true - -[UI] -# Name of the app and chatbot. -name = "Chatbot" - -# Description of the app and chatbot. This is used for HTML tags. -# description = "" - -# Large size content are by default collapsed for a cleaner ui -default_collapse_content = true - -# The default value for the expand messages settings. -default_expand_messages = false - -# Hide the chain of thought details from the user in the UI. -hide_cot = false - -# Link to your github repo. This will add a github button in the UI's header. -# github = "" - -# Override default MUI light theme. (Check theme.ts) -[UI.theme.light] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.light.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - -# Override default MUI dark theme. (Check theme.ts) -[UI.theme.dark] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.dark.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - - -[meta] -generated_by = "0.6.402" diff --git a/cypress/e2e/author_rename/main.py b/cypress/e2e/author_rename/main.py deleted file mode 100644 index eddd2d3419..0000000000 --- a/cypress/e2e/author_rename/main.py +++ /dev/null @@ -1,19 +0,0 @@ -import chainlit as cl - - -@cl.author_rename -def rename(orig_author: str): - rename_dict = {"LLMMathChain": "Albert Einstein", "Chatbot": "Assistant"} - return rename_dict.get(orig_author, orig_author) - - -@cl.step -def LLMMathChain(): - return "2+2=4" - - -@cl.on_chat_start -async def main(): - await cl.Message(author="LLMMathChain", content="2+2=4").send() - LLMMathChain() - await cl.Message(content="The response is 4").send() diff --git a/cypress/e2e/author_rename/spec.cy.ts b/cypress/e2e/author_rename/spec.cy.ts deleted file mode 100644 index 60b30cbf77..0000000000 --- a/cypress/e2e/author_rename/spec.cy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { runTestServer } from '../../support/testUtils'; - -describe('Author rename', () => { - before(() => { - runTestServer(); - }); - - it('should be able to rename authors', () => { - cy.get('.step').eq(0).should('contain', 'Albert Einstein'); - cy.get('.step').eq(1).should('contain', 'Assistant'); - }); -}); diff --git a/cypress/e2e/avatar/.chainlit/config.toml b/cypress/e2e/avatar/.chainlit/config.toml deleted file mode 100644 index 0c509af72c..0000000000 --- a/cypress/e2e/avatar/.chainlit/config.toml +++ /dev/null @@ -1,62 +0,0 @@ -[project] -# Whether to enable telemetry (default: true). No personal data is collected. -enable_telemetry = true - -# List of environment variables to be provided by each user to use the app. -user_env = [] - -# Duration (in seconds) during which the session is saved when the connection is lost -session_timeout = 3600 - -# Enable third parties caching (e.g LangChain cache) -cache = false - -# Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) -# follow_symlink = false - -[features] -# Show the prompt playground -prompt_playground = true - -[UI] -# Name of the app and chatbot. -name = "Chatbot" - -# Description of the app and chatbot. This is used for HTML tags. -# description = "" - -# Large size content are by default collapsed for a cleaner ui -default_collapse_content = true - -# The default value for the expand messages settings. -default_expand_messages = false - -# Hide the chain of thought details from the user in the UI. -hide_cot = false - -# Link to your github repo. This will add a github button in the UI's header. -# github = "" - -# Override default MUI light theme. (Check theme.ts) -[UI.theme.light] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.light.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - -# Override default MUI dark theme. (Check theme.ts) -[UI.theme.dark] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.dark.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - - -[meta] -generated_by = "0.6.402" diff --git a/cypress/e2e/avatar/main.py b/cypress/e2e/avatar/main.py deleted file mode 100644 index f1e9d8dc4e..0000000000 --- a/cypress/e2e/avatar/main.py +++ /dev/null @@ -1,32 +0,0 @@ -import chainlit as cl - - -@cl.on_chat_start -async def start(): - await cl.Avatar( - name="Tool 1", - url="https://avatars.githubusercontent.com/u/128686189?s=400&u=a1d1553023f8ea0921fba0debbe92a8c5f840dd9&v=4", - ).send() - - await cl.Avatar(name="Cat", path="./public/cat.jpeg").send() - await cl.Avatar(name="Cat 2", url="/public/cat.jpeg").send() - - await cl.Message( - content="This message should not have an avatar!", author="Tool 0" - ).send() - - await cl.Message( - content="Tool 1! This message should have an avatar!", author="Tool 1" - ).send() - - await cl.Message( - content="This message should not have an avatar!", author="Tool 2" - ).send() - - await cl.Message( - content="This message should have a cat avatar!", author="Cat" - ).send() - - await cl.Message( - content="This message should have a cat avatar!", author="Cat 2" - ).send() diff --git a/cypress/e2e/avatar/public/cat.jpeg b/cypress/e2e/avatar/public/cat.jpeg deleted file mode 100644 index 53834a3d82..0000000000 Binary files a/cypress/e2e/avatar/public/cat.jpeg and /dev/null differ diff --git a/cypress/e2e/avatar/spec.cy.ts b/cypress/e2e/avatar/spec.cy.ts deleted file mode 100644 index 41041559cd..0000000000 --- a/cypress/e2e/avatar/spec.cy.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { runTestServer } from '../../support/testUtils'; - -describe('Avatar', () => { - before(() => { - runTestServer(); - }); - - it('should be able to display avatars', () => { - cy.get('.step').should('have.length', 5); - - cy.get('.step').eq(0).find('img').should('have.length', 0); - cy.get('.step').eq(1).find('img').should('have.length', 1); - cy.get('.step').eq(2).find('img').should('have.length', 0); - cy.get('.step').eq(3).find('img').should('have.length', 1); - cy.get('.step').eq(4).find('img').should('have.length', 1); - - cy.get('.element-link').should('have.length', 0); - }); -}); diff --git a/cypress/e2e/chat_profiles/main.py b/cypress/e2e/chat_profiles/main.py index bce724285d..04446a42c0 100644 --- a/cypress/e2e/chat_profiles/main.py +++ b/cypress/e2e/chat_profiles/main.py @@ -2,6 +2,19 @@ import chainlit as cl +starters = [ + cl.Starter( + label="Say hi", + message="Start a conversation with a greeting", + icon="https://picsum.photos/300", + ), + cl.Starter( + label="Ask for help", + message="Ask for help with something", + icon="https://picsum.photos/350", + ), +] + @cl.set_chat_profiles async def chat_profile(current_user: cl.User): @@ -11,17 +24,21 @@ async def chat_profile(current_user: cl.User): return [ cl.ChatProfile( name="GPT-3.5", + icon="https://picsum.photos/250", markdown_description="The underlying LLM model is **GPT-3.5**, a *175B parameter model* trained on 410GB of text data.", + starters=starters, ), cl.ChatProfile( name="GPT-4", markdown_description="The underlying LLM model is **GPT-4**, a *1.5T parameter model* trained on 3.5TB of text data.", icon="https://picsum.photos/250", + starters=starters, ), cl.ChatProfile( name="GPT-5", markdown_description="The underlying LLM model is **GPT-5**.", icon="https://picsum.photos/200", + starters=starters, ), ] @@ -34,8 +51,8 @@ def auth_callback(username: str, password: str) -> Optional[cl.User]: return None -@cl.on_chat_start -async def on_chat_start(): +@cl.on_message +async def on_message(): user = cl.user_session.get("user") chat_profile = cl.user_session.get("chat_profile") await cl.Message( diff --git a/cypress/e2e/chat_profiles/spec.cy.ts b/cypress/e2e/chat_profiles/spec.cy.ts index 95c7a476ac..f80cb5f0da 100644 --- a/cypress/e2e/chat_profiles/spec.cy.ts +++ b/cypress/e2e/chat_profiles/spec.cy.ts @@ -12,9 +12,16 @@ describe('Chat profiles', () => { cy.get("button[type='submit']").click(); cy.get('#chat-input').should('exist'); + cy.wait(1000); + cy.get('#starter-say-hi').should('exist').click(); + cy.get('.step') - .should('have.length', 1) + .should('have.length', 2) .eq(0) + .should('contain', 'Start a conversation with a greeting'); + + cy.get('.step') + .eq(1) .should( 'contain', 'starting chat with admin using the GPT-3.5 chat profile' @@ -28,10 +35,18 @@ describe('Chat profiles', () => { // Change chat profile cy.get('[data-test="select-item:GPT-4"]').click(); + cy.get('#confirm').click(); + + cy.wait(1000); + cy.get('#starter-ask-for-help').should('exist').click(); cy.get('.step') - .should('have.length', 1) + .should('have.length', 2) .eq(0) + .should('contain', 'Ask for help with something'); + + cy.get('.step') + .eq(1) .should( 'contain', 'starting chat with admin using the GPT-4 chat profile' @@ -40,26 +55,16 @@ describe('Chat profiles', () => { cy.get('#header').get('#new-chat-button').click({ force: true }); cy.get('#confirm').click(); - cy.get('.step') - .should('have.length', 1) - .eq(0) - .should( - 'contain', - 'starting chat with admin using the GPT-4 chat profile' - ); + cy.get('#starter-ask-for-help').should('exist'); + + cy.get('.step').should('have.length', 0); submitMessage('hello'); - cy.get('.step').should('have.length', 2).eq(1).should('contain', 'hello'); + cy.get('.step').should('have.length', 2).eq(0).should('contain', 'hello'); cy.get('#chat-profile-selector').parent().click(); cy.get('[data-test="select-item:GPT-5"]').click(); cy.get('#confirm').click(); - cy.get('.step') - .should('have.length', 1) - .eq(0) - .should( - 'contain', - 'starting chat with admin using the GPT-5 chat profile' - ); + cy.get('#starter-ask-for-help').should('exist'); }); }); diff --git a/cypress/e2e/chat_settings/.chainlit/config.toml b/cypress/e2e/chat_settings/.chainlit/config.toml index 8c46822f75..e2a2b2a16d 100644 --- a/cypress/e2e/chat_settings/.chainlit/config.toml +++ b/cypress/e2e/chat_settings/.chainlit/config.toml @@ -2,6 +2,7 @@ # Whether to enable telemetry (default: true). No personal data is collected. enable_telemetry = true + # List of environment variables to be provided by each user to use the app. user_env = [] @@ -11,12 +12,42 @@ session_timeout = 3600 # Enable third parties caching (e.g LangChain cache) cache = false +# Authorized origins +allow_origins = ["*"] + # Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) # follow_symlink = false [features] -# Show the prompt playground -prompt_playground = true +# Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript) +unsafe_allow_html = false + +# Process and display mathematical expressions. This can clash with "$" characters in messages. +latex = false + +# Automatically tag threads with the current chat profile (if a chat profile is used) +auto_tag_thread = true + +# Authorize users to spontaneously upload files with messages +[features.spontaneous_file_upload] + enabled = true + accept = ["*/*"] + max_files = 20 + max_size_mb = 500 + +[features.audio] + # Threshold for audio recording + min_decibels = -45 + # Delay for the user to start speaking in MS + initial_silence_timeout = 3000 + # Delay for the user to continue speaking in MS. If the user stops speaking for this duration, the recording will stop. + silence_timeout = 1500 + # Above this duration (MS), the recording will forcefully stop. + max_duration = 15000 + # Duration of the audio chunks in MS + chunk_duration = 1000 + # Sample rate of the audio + sample_rate = 44100 [UI] # Name of the app and chatbot. @@ -28,9 +59,6 @@ name = "Chatbot" # Large size content are by default collapsed for a cleaner ui default_collapse_content = true -# The default value for the expand messages settings. -default_expand_messages = false - # Hide the chain of thought details from the user in the UI. hide_cot = false @@ -41,6 +69,25 @@ hide_cot = false # The CSS file can be served from the public directory or via an external link. # custom_css = "/public/test.css" +# Specify a Javascript file that can be used to customize the user interface. +# The Javascript file can be served from the public directory. +# custom_js = "/public/test.js" + +# Specify a custom font url. +# custom_font = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" + +# Specify a custom meta image url. +# custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png" + +# Specify a custom build directory for the frontend. +# This can be used to customize the frontend code. +# Be careful: If this is a relative path, it should not start with a slash. +# custom_build = "./public/build" + +[UI.theme] + default = "dark" + #layout = "wide" + #font_family = "Inter, sans-serif" # Override default MUI light theme. (Check theme.ts) [UI.theme.light] #background = "#FAFAFA" @@ -50,6 +97,9 @@ hide_cot = false #main = "#F80061" #dark = "#980039" #light = "#FFE7EB" + [UI.theme.light.text] + #primary = "#212121" + #secondary = "#616161" # Override default MUI dark theme. (Check theme.ts) [UI.theme.dark] @@ -60,7 +110,9 @@ hide_cot = false #main = "#F80061" #dark = "#980039" #light = "#FFE7EB" - + [UI.theme.dark.text] + #primary = "#EEEEEE" + #secondary = "#BDBDBD" [meta] -generated_by = "0.7.1" +generated_by = "1.0.504" diff --git a/cypress/e2e/copilot/spec.cy.ts b/cypress/e2e/copilot/spec.cy.ts index 058ff15c8e..ceefa32348 100644 --- a/cypress/e2e/copilot/spec.cy.ts +++ b/cypress/e2e/copilot/spec.cy.ts @@ -30,18 +30,19 @@ describe('Copilot', () => { }); it('should be able to embed the copilot', () => { - cy.get('#chainlit-copilot-button').should('be.visible').click(); - cy.get('#chainlit-copilot-popover').should('be.visible'); + const opts = { includeShadowDom: true }; + cy.get('#chainlit-copilot-button', opts).click(); + cy.get('#chainlit-copilot-popover', opts).should('be.visible'); - cy.get('#chainlit-copilot-popover').within(() => { - cy.get('.step').should('have.length', 1); - cy.contains('.step', 'Hi from copilot!').should('be.visible'); + cy.get('#chainlit-copilot-popover', opts).within(() => { + cy.get('.step', opts).should('have.length', 1); + cy.contains('.step', 'Hi from copilot!', opts).should('be.visible'); }); submitMessageCopilot('Call func!'); - cy.get('#chainlit-copilot-popover').within(() => { - cy.get('.step').should('have.length', 3); - cy.contains('.step', 'Function called with: Call func!').should( + cy.get('#chainlit-copilot-popover', opts).within(() => { + cy.get('.step', opts).should('have.length', 3); + cy.contains('.step', 'Function called with: Call func!', opts).should( 'be.visible' ); }); diff --git a/cypress/e2e/data_layer/main.py b/cypress/e2e/data_layer/main.py index 937fd56212..33ba753c10 100644 --- a/cypress/e2e/data_layer/main.py +++ b/cypress/e2e/data_layer/main.py @@ -150,7 +150,7 @@ async def handle_message(): # Wait for queue to be flushed await cl.sleep(2) await send_count() - async with cl.Step(root=True, disable_feedback=True) as step: + async with cl.Step(type="tool", name="thinking") as step: step.output = "Thinking..." await cl.Message("Ok!").send() await send_count() diff --git a/cypress/e2e/data_layer/spec.cy.ts b/cypress/e2e/data_layer/spec.cy.ts index 78b2c7e892..479c23b941 100644 --- a/cypress/e2e/data_layer/spec.cy.ts +++ b/cypress/e2e/data_layer/spec.cy.ts @@ -23,7 +23,7 @@ function feedback() { function threadQueue() { cy.get('.step').eq(1).should('contain', 'Create step counter: 0'); cy.get('.step').eq(3).should('contain', 'Create step counter: 3'); - cy.get('.step').eq(6).should('contain', 'Create step counter: 6'); + cy.get('.step').eq(5).should('contain', 'Create step counter: 6'); } function threadList() { @@ -68,8 +68,6 @@ function resumeThread() { cy.get('.step').eq(0).should('contain', 'Hello'); cy.get('.step').eq(5).should('contain', 'Welcome back to Hello'); - // Because the Thread was closed, the metadata should have been updated automatically - cy.get('.step').eq(6).should('contain', 'metadata'); cy.get('.step').eq(6).should('contain', 'chat_profile'); } diff --git a/cypress/e2e/default_expand_cot/.chainlit/config.toml b/cypress/e2e/default_expand_cot/.chainlit/config.toml deleted file mode 100644 index 46b7efb610..0000000000 --- a/cypress/e2e/default_expand_cot/.chainlit/config.toml +++ /dev/null @@ -1,62 +0,0 @@ -[project] -# Whether to enable telemetry (default: true). No personal data is collected. -enable_telemetry = true - -# List of environment variables to be provided by each user to use the app. -user_env = [] - -# Duration (in seconds) during which the session is saved when the connection is lost -session_timeout = 3600 - -# Enable third parties caching (e.g LangChain cache) -cache = false - -# Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) -# follow_symlink = false - -[features] -# Show the prompt playground -prompt_playground = true - -[UI] -# Name of the app and chatbot. -name = "Chatbot" - -# Description of the app and chatbot. This is used for HTML tags. -# description = "" - -# Large size content are by default collapsed for a cleaner ui -default_collapse_content = true - -# The default value for the expand messages settings. -default_expand_messages = true - -# Hide the chain of thought details from the user in the UI. -hide_cot = false - -# Link to your github repo. This will add a github button in the UI's header. -# github = "" - -# Override default MUI light theme. (Check theme.ts) -[UI.theme.light] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.light.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - -# Override default MUI dark theme. (Check theme.ts) -[UI.theme.dark] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.dark.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - - -[meta] -generated_by = "0.6.402" diff --git a/cypress/e2e/default_expand_cot/main.py b/cypress/e2e/default_expand_cot/main.py deleted file mode 100644 index 83f6d578b2..0000000000 --- a/cypress/e2e/default_expand_cot/main.py +++ /dev/null @@ -1,27 +0,0 @@ -import chainlit as cl - - -@cl.step(name="Tool 3", type="tool") -async def tool_3(): - return "Response from tool 3" - - -@cl.step(name="Tool 2", type="tool") -async def tool_2(): - await tool_3() - return "Response from tool 2" - - -@cl.step(name="Tool 1", type="tool") -async def tool_1(): - await tool_2() - return "Response from tool 1" - - -@cl.on_message -async def main(message: cl.Message): - await tool_1() - - await cl.Message( - content="Final response", - ).send() diff --git a/cypress/e2e/default_expand_cot/spec.cy.ts b/cypress/e2e/default_expand_cot/spec.cy.ts deleted file mode 100644 index 1464d441b9..0000000000 --- a/cypress/e2e/default_expand_cot/spec.cy.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { runTestServer, submitMessage } from '../../support/testUtils'; - -describe('Default Expand', () => { - before(() => { - runTestServer(); - }); - - it('should be able to set the default_expand_messages field in the config to have the CoT expanded by default', () => { - submitMessage('Hello'); - - cy.get(".step:contains('Hello')").contains('Response from tool 1'); - cy.get(".step:contains('Response from tool 1')").contains( - 'Response from tool 2' - ); - cy.get(".step:contains('Hello')").contains('Response from tool 3'); - cy.get(".step:contains('Final response')"); - }); -}); diff --git a/cypress/e2e/elements/main.py b/cypress/e2e/elements/main.py index aab0cd2cca..6369df22f0 100644 --- a/cypress/e2e/elements/main.py +++ b/cypress/e2e/elements/main.py @@ -3,25 +3,22 @@ import chainlit as cl -@cl.step +@cl.step(type="tool") async def gen_img(): - if current_step := context.current_step: - current_step.elements = [ - cl.Image(path="./cat.jpeg", name="image1", display="inline") - ] - return "Here is a cat!" + + return cl.Image(path="./cat.jpeg", name="image1", display="inline") @cl.on_chat_start async def start(): + + img = await gen_img() + # Element should not be inlined or referenced await cl.Message( - content="Here is image1, a nice image of a cat! As well as text1 and text2!", + content="Here is image1, a nice image of a cat!", elements=[img] ).send() - # Step should be able to have elements - await gen_img() - # Image should be inlined even if not referenced await cl.Message( content="Here a nice image of a cat! As well as text1 and text2!", diff --git a/cypress/e2e/elements/spec.cy.ts b/cypress/e2e/elements/spec.cy.ts index 9e4ede1c3c..77beb6d277 100644 --- a/cypress/e2e/elements/spec.cy.ts +++ b/cypress/e2e/elements/spec.cy.ts @@ -10,7 +10,6 @@ describe('Elements', () => { cy.get('.step').eq(0).find('.element-link').should('have.length', 0); cy.get('.step').eq(0).find('.inline-pdf').should('have.length', 0); - cy.get('#gen_img-done').should('exist').click(); cy.get('.step').eq(1).find('.inline-image').should('have.length', 1); cy.get('.step').eq(2).find('.inline-image').should('have.length', 1); diff --git a/cypress/e2e/hide_prompt_playground/.chainlit/config.toml b/cypress/e2e/hide_prompt_playground/.chainlit/config.toml deleted file mode 100644 index c1b3ecf175..0000000000 --- a/cypress/e2e/hide_prompt_playground/.chainlit/config.toml +++ /dev/null @@ -1,62 +0,0 @@ -[project] -# Whether to enable telemetry (default: true). No personal data is collected. -enable_telemetry = true - -# List of environment variables to be provided by each user to use the app. -user_env = [] - -# Duration (in seconds) during which the session is saved when the connection is lost -session_timeout = 3600 - -# Enable third parties caching (e.g LangChain cache) -cache = false - -# Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) -# follow_symlink = false - -[features] -# Show the prompt playground -prompt_playground = false - -[UI] -# Name of the app and chatbot. -name = "Chatbot" - -# Description of the app and chatbot. This is used for HTML tags. -# description = "" - -# Large size content are by default collapsed for a cleaner ui -default_collapse_content = true - -# The default value for the expand messages settings. -default_expand_messages = false - -# Hide the chain of thought details from the user in the UI. -hide_cot = false - -# Link to your github repo. This will add a github button in the UI's header. -# github = "" - -# Override default MUI light theme. (Check theme.ts) -[UI.theme.light] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.light.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - -# Override default MUI dark theme. (Check theme.ts) -[UI.theme.dark] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.dark.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - - -[meta] -generated_by = "0.6.402" diff --git a/cypress/e2e/hide_prompt_playground/main.py b/cypress/e2e/hide_prompt_playground/main.py deleted file mode 100644 index 85b9b549cf..0000000000 --- a/cypress/e2e/hide_prompt_playground/main.py +++ /dev/null @@ -1,29 +0,0 @@ -import chainlit as cl - -template = """Hello, this is a template.""" - -inputs = { - "variable1": "variable1 value", - "variable2": "variable2 value", - "variable3": "{{variable3 value}}", -} - -completion = "This is the original completion" - - -@cl.step(type="llm") -async def gen_response(): - res = "This is a message with a basic prompt" - if current_step := cl.context.current_step: - current_step.generation = cl.CompletionGeneration( - prompt=template, variables=inputs, completion=res - ) - return res - - -@cl.on_chat_start -async def start(): - content = await gen_response() - await cl.Message( - content=content, - ).send() diff --git a/cypress/e2e/hide_prompt_playground/spec.cy.ts b/cypress/e2e/hide_prompt_playground/spec.cy.ts deleted file mode 100644 index 39534c273c..0000000000 --- a/cypress/e2e/hide_prompt_playground/spec.cy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { runTestServer } from '../../support/testUtils'; - -describe('DisablePromptPlayground', () => { - before(() => { - runTestServer(); - }); - - it('should not display the playground button', () => { - cy.wait(2000); - cy.get('.playground-button').should('not.exist'); - }); -}); diff --git a/cypress/e2e/llama_index_cb/.chainlit/config.toml b/cypress/e2e/llama_index_cb/.chainlit/config.toml index a5eee5dfa4..54a6f2c7d9 100644 --- a/cypress/e2e/llama_index_cb/.chainlit/config.toml +++ b/cypress/e2e/llama_index_cb/.chainlit/config.toml @@ -19,9 +19,6 @@ allow_origins = ["*"] # follow_symlink = false [features] -# Show the prompt playground -prompt_playground = true - # Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript) unsafe_allow_html = false @@ -56,18 +53,12 @@ auto_tag_thread = true # Name of the app and chatbot. name = "Chatbot" -# Show the readme while the thread is empty. -show_readme_as_default = true - # Description of the app and chatbot. This is used for HTML tags. # description = "" # Large size content are by default collapsed for a cleaner ui default_collapse_content = true -# The default value for the expand messages settings. -default_expand_messages = false - # Hide the chain of thought details from the user in the UI. hide_cot = false @@ -85,14 +76,19 @@ hide_cot = false # Specify a custom font url. # custom_font = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" +# Specify a custom meta image url. +# custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png" + # Specify a custom build directory for the frontend. # This can be used to customize the frontend code. # Be careful: If this is a relative path, it should not start with a slash. # custom_build = "./public/build" -# Override default MUI light theme. (Check theme.ts) [UI.theme] + default = "dark" + #layout = "wide" #font_family = "Inter, sans-serif" +# Override default MUI light theme. (Check theme.ts) [UI.theme.light] #background = "#FAFAFA" #paper = "#FFFFFF" @@ -101,6 +97,9 @@ hide_cot = false #main = "#F80061" #dark = "#980039" #light = "#FFE7EB" + [UI.theme.light.text] + #primary = "#212121" + #secondary = "#616161" # Override default MUI dark theme. (Check theme.ts) [UI.theme.dark] @@ -111,7 +110,9 @@ hide_cot = false #main = "#F80061" #dark = "#980039" #light = "#FFE7EB" - + [UI.theme.dark.text] + #primary = "#EEEEEE" + #secondary = "#BDBDBD" [meta] -generated_by = "1.0.504" +generated_by = "1.1.0rc1" diff --git a/cypress/e2e/llama_index_cb/spec.cy.ts b/cypress/e2e/llama_index_cb/spec.cy.ts index 408eafba71..90366ce5e5 100644 --- a/cypress/e2e/llama_index_cb/spec.cy.ts +++ b/cypress/e2e/llama_index_cb/spec.cy.ts @@ -8,26 +8,16 @@ describe('Llama Index Callback', () => { it('should be able to send messages to the UI with prompts and elements', () => { cy.get('.step').should('have.length', 1); - cy.get('#llm-done').should('exist').click(); + const toolCall = cy.get('#tool-call-retrieve'); - cy.get('.step').should('have.length', 3); + toolCall.should('exist').click(); - cy.get('.step') - .eq(1) - .find('.element-link') - .eq(0) - .should('contain', 'Source 0'); - - cy.get('.playground-button').eq(0).should('exist').click(); - - cy.get('.formatted-editor [contenteditable]') - .should('exist') - .should('contain', 'This is the LLM prompt'); + const toolCallContent = toolCall.get('.message-content').eq(0); - cy.get('.completion-editor [contenteditable]') + toolCallContent .should('exist') - .should('contain', 'This is the LLM response'); - - cy.get('#close-playground').should('exist').click(); + .get('.element-link') + .eq(0) + .should('contain', 'Source 0'); }); }); diff --git a/cypress/e2e/prompt_playground/.chainlit/config.toml b/cypress/e2e/prompt_playground/.chainlit/config.toml deleted file mode 100644 index 0c509af72c..0000000000 --- a/cypress/e2e/prompt_playground/.chainlit/config.toml +++ /dev/null @@ -1,62 +0,0 @@ -[project] -# Whether to enable telemetry (default: true). No personal data is collected. -enable_telemetry = true - -# List of environment variables to be provided by each user to use the app. -user_env = [] - -# Duration (in seconds) during which the session is saved when the connection is lost -session_timeout = 3600 - -# Enable third parties caching (e.g LangChain cache) -cache = false - -# Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317) -# follow_symlink = false - -[features] -# Show the prompt playground -prompt_playground = true - -[UI] -# Name of the app and chatbot. -name = "Chatbot" - -# Description of the app and chatbot. This is used for HTML tags. -# description = "" - -# Large size content are by default collapsed for a cleaner ui -default_collapse_content = true - -# The default value for the expand messages settings. -default_expand_messages = false - -# Hide the chain of thought details from the user in the UI. -hide_cot = false - -# Link to your github repo. This will add a github button in the UI's header. -# github = "" - -# Override default MUI light theme. (Check theme.ts) -[UI.theme.light] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.light.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - -# Override default MUI dark theme. (Check theme.ts) -[UI.theme.dark] - #background = "#FAFAFA" - #paper = "#FFFFFF" - - [UI.theme.dark.primary] - #main = "#F80061" - #dark = "#980039" - #light = "#FFE7EB" - - -[meta] -generated_by = "0.6.402" diff --git a/cypress/e2e/prompt_playground/main.py b/cypress/e2e/prompt_playground/main.py deleted file mode 100644 index b1e9ea611b..0000000000 --- a/cypress/e2e/prompt_playground/main.py +++ /dev/null @@ -1,34 +0,0 @@ -from provider import ChatTestLLM, TestLLM - -import chainlit as cl - -formatted = "This is a test formatted prompt" - -inputs = { - "variable1": "variable1 value", - "variable2": "variable2 value", - "variable3": "{{variable3 value}}", -} - -completion = "This is the original completion" - - -@cl.on_chat_start -async def start(): - async with cl.Step() as step: - step.generation = cl.CompletionGeneration( - provider=TestLLM.id, completion=completion, prompt=formatted - ) - step.output = "This is a message with only a formatted basic prompt" - - async with cl.Step() as step: - step.generation = cl.ChatGeneration( - provider=ChatTestLLM.id, - completion=completion, - inputs=inputs, - messages=[ - cl.GenerationMessage(content=formatted, role="system"), - cl.GenerationMessage(content=formatted, role="system"), - ], - ) - step.output = "This is a message with only a formatted chat prompt" diff --git a/cypress/e2e/prompt_playground/provider.py b/cypress/e2e/prompt_playground/provider.py deleted file mode 100644 index ff5aeb072b..0000000000 --- a/cypress/e2e/prompt_playground/provider.py +++ /dev/null @@ -1,89 +0,0 @@ -import os - -from chainlit.input_widget import Select, Slider -from chainlit.playground.config import BaseProvider, add_llm_provider -from chainlit.playground.providers.langchain import LangchainGenericProvider -from fastapi.responses import StreamingResponse -from langchain.llms.fake import FakeListLLM - -import chainlit as cl - -os.environ["TEST_LLM_API_KEY"] = "sk..." - - -class TestLLMProvider(BaseProvider): - async def create_completion(self, request): - await super().create_completion(request) - - self.create_generation(request) - self.require_settings(request.generation.settings) - - stream = ["This ", "is ", "the ", "test ", "completion"] - - async def create_event_stream(): - for token in stream: - await cl.sleep(0.1) - yield token - - return StreamingResponse(create_event_stream()) - - -TestLLM = TestLLMProvider( - id="test", - name="Test", - env_vars={"api_key": "TEST_LLM_API_KEY"}, - inputs=[ - Select( - id="model", - label="Model", - values=["test-model-1", "test-model-2"], - initial_value="test-model-2", - ), - Slider( - id="temperature", - label="Temperature", - min=0.0, - max=1.0, - step=0.01, - initial=1, - ), - ], - is_chat=False, -) - -ChatTestLLM = TestLLMProvider( - id="test-chat", - name="TestChat", - env_vars={"api_key": "TEST_LLM_API_KEY"}, - inputs=[ - Select( - id="model", - label="Model", - values=["test-model-chat-1", "test-model-chat-2"], - initial_value="test-model-chat-2", - ), - Slider( - id="temperature", - label="Temperature", - min=0.0, - max=1.0, - step=0.01, - initial=1, - ), - ], - is_chat=True, -) - -llm = FakeListLLM(responses=["This is the test completion"]) - -LangchainTestLLM = LangchainGenericProvider( - id="test-langchain", - name="TestLangchain", - llm=llm, - is_chat=False, -) - - -add_llm_provider(TestLLM) -add_llm_provider(ChatTestLLM) -add_llm_provider(LangchainTestLLM) diff --git a/cypress/e2e/prompt_playground/spec.cy.ts b/cypress/e2e/prompt_playground/spec.cy.ts deleted file mode 100644 index 9a015f3d15..0000000000 --- a/cypress/e2e/prompt_playground/spec.cy.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { runTestServer } from '../../support/testUtils'; - -function openPlayground(index) { - cy.get('.playground-button').eq(index).should('exist').click(); -} - -const expectedFormatted = `This is a test formatted prompt`; - -const expectedCompletion = 'This is the test completion'; - -function testFormatted() { - // it("should display the missing template warning", () => { - // cy.get("#template-warning").should("exist"); - // }); - - it('should display the formatted prompt', () => { - cy.get('.formatted-editor [contenteditable]') - .should('exist') - .should('contain', expectedFormatted); - }); - - it('should let the user update the formatted prompt', () => { - cy.get('.formatted-editor [contenteditable]') - .eq(0) - .type('foobar') - .should('contain', 'foobar' + expectedFormatted); - }); -} - -function testCompletion() { - it('should be able to call the LLM provider and stream the completion', () => { - // Wait for the llm provider - cy.wait(1000); - cy.get('#submit-prompt').should('exist').click(); - cy.get('.completion-editor [contenteditable]').should( - 'contain', - expectedCompletion - ); - }); -} - -function testSettings(chat?: boolean) { - it('should be able to switch providers and preserve settings', () => { - const initialModel = chat ? 'test-model-chat-2' : 'test-model-2'; - const nextModel = chat ? 'test-model-2' : 'test-model-chat-2'; - - const optionTarget = chat ? '[data-value=test]' : '[data-value=test-chat]'; - - cy.get('#model').invoke('val').should('equal', initialModel); - cy.get('#temperature').invoke('val').should('equal', '1'); - cy.get('#llm-providers').parent().click(); - cy.get(optionTarget).click(); - cy.get('#model').invoke('val').should('equal', nextModel); - cy.get('#temperature').invoke('val').should('equal', '1'); - }); -} - -describe('PromptPlayground', () => { - before(() => { - runTestServer(); - }); - - describe('Completion', () => { - beforeEach(() => { - cy.visit('/'); - openPlayground(0); - }); - - testFormatted(); - testCompletion(); - testSettings(false); - }); - - describe('Chat', () => { - beforeEach(() => { - cy.visit('/'); - openPlayground(1); - }); - - testFormatted(); - testCompletion(); - testSettings(true); - }); - - describe('Langchain provider', () => { - beforeEach(() => { - cy.visit('/'); - openPlayground(1); - cy.get('#llm-providers').parent().click(); - cy.get('[data-value=test-langchain]').click(); - }); - - testCompletion(); - }); -}); diff --git a/cypress/e2e/readme/spec.cy.ts b/cypress/e2e/readme/spec.cy.ts index c729328a2d..ad8c2fac6b 100644 --- a/cypress/e2e/readme/spec.cy.ts +++ b/cypress/e2e/readme/spec.cy.ts @@ -1,34 +1,41 @@ import { runTestServer } from '../../support/testUtils'; +function openReadme() { + cy.get('#open-sidebar-button').click(); + cy.get('#readme-button').click(); +} + describe('readme_language', () => { before(() => { runTestServer(); }); it('should show default markdown on open', () => { - cy.visit('/readme'); + openReadme(); cy.contains('Welcome to Chainlit!'); }); it('should show Portguese markdown on pt-BR language', () => { - cy.visit('/readme', { + cy.visit('/', { onBeforeLoad(win) { Object.defineProperty(win.navigator, 'language', { value: 'pt-BR' }); } }); + openReadme(); cy.contains('Bem-vindo ao Chainlit!'); }); it('should fallback to default markdown on Klingon language', () => { - cy.visit('/readme', { + cy.visit('/', { onBeforeLoad(win) { Object.defineProperty(win.navigator, 'language', { value: 'Klingon' }); } }); + openReadme(); cy.contains('Welcome to Chainlit!'); }); }); diff --git a/cypress/e2e/remove_elements/main.py b/cypress/e2e/remove_elements/main.py index 2ef09eb115..62e3fc7725 100644 --- a/cypress/e2e/remove_elements/main.py +++ b/cypress/e2e/remove_elements/main.py @@ -10,7 +10,7 @@ async def start(): name="image1", display="inline", path="../../fixtures/cat.jpeg" ) - async with cl.Step() as step: + async with cl.Step(type="tool", name="tool1") as step: step.elements = [ step_image, cl.Image(name="image2", display="inline", path="../../fixtures/cat.jpeg"), diff --git a/cypress/e2e/remove_elements/spec.cy.ts b/cypress/e2e/remove_elements/spec.cy.ts index 8947478f7a..f767e96a0f 100644 --- a/cypress/e2e/remove_elements/spec.cy.ts +++ b/cypress/e2e/remove_elements/spec.cy.ts @@ -6,8 +6,14 @@ describe('remove_elements', () => { }); it('should be able to remove elements', () => { + cy.get('#tool-call-tool1').should('exist'); + cy.get('#tool-call-tool1').click(); + cy.get('#tool-call-tool1') + .parent() + .find('.inline-image') + .should('have.length', 1); + cy.get('.step').should('have.length', 2); - cy.get('.step').eq(0).find('.inline-image').should('have.length', 1); cy.get('.step').eq(1).find('.inline-image').should('have.length', 1); }); }); diff --git a/cypress/e2e/remove_step/main.py b/cypress/e2e/remove_step/main.py index 00ff44d12d..e57be8da5c 100644 --- a/cypress/e2e/remove_step/main.py +++ b/cypress/e2e/remove_step/main.py @@ -6,7 +6,7 @@ async def main(): msg1 = cl.Message(content="Message 1") await msg1.send() - async with cl.Step() as child1: + async with cl.Step(type="tool", name="tool1") as child1: child1.output = "Child 1" await cl.sleep(1) diff --git a/cypress/e2e/remove_step/spec.cy.ts b/cypress/e2e/remove_step/spec.cy.ts index 3e0b16ef07..6993c5dee8 100644 --- a/cypress/e2e/remove_step/spec.cy.ts +++ b/cypress/e2e/remove_step/spec.cy.ts @@ -9,11 +9,11 @@ describe('Remove Step', () => { cy.get('.step').should('have.length', 1); cy.get('.step').eq(0).should('contain', 'Message 1'); - cy.get('#chatbot-loading').should('exist'); - cy.get('#chatbot-loading').click(); - cy.get('.step').eq(1).should('contain', 'Child 1'); + cy.get('#tool-call-tool1').should('exist'); + cy.get('#tool-call-tool1').click(); + cy.get('.message-content').eq(0).should('contain', 'Child 1'); - cy.get('.step').should('have.length', 2); + cy.get('#tool-call-tool1').should('not.exist'); cy.get('.step').eq(1).should('contain', 'Message 2'); cy.get('.step').should('have.length', 1); diff --git a/cypress/e2e/starters/main.py b/cypress/e2e/starters/main.py new file mode 100644 index 0000000000..80098636e4 --- /dev/null +++ b/cypress/e2e/starters/main.py @@ -0,0 +1,15 @@ +import chainlit as cl + + +@cl.set_starters +async def starters(): + return [ + cl.Starter(label="test1", message="Running starter 1"), + cl.Starter(label="test2", message="Running starter 2"), + cl.Starter(label="test3", message="Running starter 3"), + ] + + +@cl.on_message +async def on_message(msg: cl.Message): + await cl.Message(msg.content).send() diff --git a/cypress/e2e/starters/spec.cy.ts b/cypress/e2e/starters/spec.cy.ts new file mode 100644 index 0000000000..a5fd1bf68c --- /dev/null +++ b/cypress/e2e/starters/spec.cy.ts @@ -0,0 +1,16 @@ +import { runTestServer } from '../../support/testUtils'; + +describe('Starters', () => { + before(() => { + runTestServer(); + }); + + it('should be able to use a starter', () => { + cy.wait(1000); + cy.get('#starter-test1').should('exist').click(); + cy.get('.step').should('have.length', 2); + + cy.get('.step').eq(0).contains('Running starter 1'); + cy.get('.step').eq(1).contains('Running starter 1'); + }); +}); diff --git a/cypress/e2e/step/main.py b/cypress/e2e/step/main.py index 543ad1ad7b..3ca09ed7cf 100644 --- a/cypress/e2e/step/main.py +++ b/cypress/e2e/step/main.py @@ -2,19 +2,19 @@ def tool_3(): - with cl.Step(name="Tool 3", type="tool") as s: + with cl.Step(name="tool3", type="tool") as s: cl.run_sync(cl.sleep(2)) s.output = "Response from tool 3" -@cl.step +@cl.step(name="tool2", type="tool") def tool_2(): tool_3() cl.run_sync(cl.Message(content="Message from tool 2").send()) return "Response from tool 2" -@cl.step(name="Tool 1", type="tool") +@cl.step(name="tool1", type="tool") def tool_1(): tool_2() return "Response from tool 1" diff --git a/cypress/e2e/step/main_async.py b/cypress/e2e/step/main_async.py index bb8218cc12..7422871185 100644 --- a/cypress/e2e/step/main_async.py +++ b/cypress/e2e/step/main_async.py @@ -2,19 +2,19 @@ async def tool_3(): - async with cl.Step(name="Tool 3", type="tool") as s: + async with cl.Step(name="tool3", type="tool") as s: await cl.sleep(2) s.output = "Response from tool 3" -@cl.step +@cl.step(name="tool2", type="tool") async def tool_2(): await tool_3() await cl.Message(content="Message from tool 2").send() return "Response from tool 2" -@cl.step(name="Tool 1", type="tool") +@cl.step(name="tool1", type="tool") async def tool_1(): await tool_2() return "Response from tool 1" diff --git a/cypress/e2e/step/spec.cy.ts b/cypress/e2e/step/spec.cy.ts index 705aed2266..3de8b62816 100644 --- a/cypress/e2e/step/spec.cy.ts +++ b/cypress/e2e/step/spec.cy.ts @@ -12,19 +12,12 @@ describeSyncAsync('Step', () => { it('should be able to nest steps', () => { submitMessage('Hello'); - cy.get('#tool-1-loading').should('exist'); - cy.get('#tool-1-loading').click(); + cy.get('#tool-call-tool1').should('exist').click(); - cy.get('#tool_2-loading').should('exist'); - cy.get('#tool_2-loading').click(); + cy.get('#tool-call-tool2').should('exist').click(); - cy.get('#tool-3-loading').should('exist'); - cy.get('#tool-3-loading').click(); + cy.get('#tool-call-tool3').should('exist').click(); - cy.get('#tool-1-done').should('exist'); - cy.get('#tool_2-done').should('exist'); - cy.get('#tool-3-done').should('exist'); - - cy.get('.step').should('have.length', 5); + cy.get('.step').should('have.length', 2); }); }); diff --git a/cypress/e2e/stop_task/main_async.py b/cypress/e2e/stop_task/main_async.py index 12ed98bd23..17e8b044e3 100644 --- a/cypress/e2e/stop_task/main_async.py +++ b/cypress/e2e/stop_task/main_async.py @@ -1,13 +1,8 @@ import chainlit as cl -@cl.on_chat_start -async def start(): +@cl.on_message +async def message(message: cl.Message): await cl.Message(content="Message 1").send() await cl.sleep(1) await cl.Message(content="Message 2").send() - - -@cl.on_message -async def message(message: cl.Message): - await cl.Message(content="World").send() diff --git a/cypress/e2e/stop_task/main_sync.py b/cypress/e2e/stop_task/main_sync.py index 2af957552d..be74d6d254 100644 --- a/cypress/e2e/stop_task/main_sync.py +++ b/cypress/e2e/stop_task/main_sync.py @@ -7,13 +7,8 @@ def sync_function(): time.sleep(1) -@cl.on_chat_start -async def start(): +@cl.on_message +async def message(message: cl.Message): await cl.Message(content="Message 1").send() await cl.make_async(sync_function)() await cl.Message(content="Message 2").send() - - -@cl.on_message -async def message(message: cl.Message): - await cl.Message(content="World").send() diff --git a/cypress/e2e/stop_task/spec.cy.ts b/cypress/e2e/stop_task/spec.cy.ts index 78144d55e1..3bc0d71efc 100644 --- a/cypress/e2e/stop_task/spec.cy.ts +++ b/cypress/e2e/stop_task/spec.cy.ts @@ -10,24 +10,12 @@ describeSyncAsync('Stop task', (mode) => { }); it('should be able to stop a task', () => { - cy.get('.step').should('have.length', 1); - - cy.get('.step').last().should('contain.text', 'Message 1'); + submitMessage('Hello'); cy.get('#stop-button').should('exist').click(); cy.get('#stop-button').should('not.exist'); - cy.get('.step').should('have.length', 2); - + cy.wait(1000); + cy.get('.step').should('have.length', 3); cy.get('.step').last().should('contain.text', 'Task manually stopped.'); - - cy.wait(5000); - - cy.get('.step').should('have.length', 2); - - submitMessage('Hello'); - - cy.get('.step').should('have.length', 4); - - cy.get('.step').last().should('contain.text', 'World'); }); }); diff --git a/cypress/e2e/streaming/main.py b/cypress/e2e/streaming/main.py index 88dd019e4c..124edaa4c1 100644 --- a/cypress/e2e/streaming/main.py +++ b/cypress/e2e/streaming/main.py @@ -21,14 +21,14 @@ async def main(): await msg.send() - step = cl.Step() + step = cl.Step(type="tool", name="tool1") for token in token_list: await step.stream_token(token) await cl.sleep(0.2) await step.send() - step = cl.Step() + step = cl.Step(type="tool", name="tool2") for seq in sequence_list: await step.stream_token(token=seq, is_sequence=True) await cl.sleep(0.2) diff --git a/cypress/e2e/streaming/spec.cy.ts b/cypress/e2e/streaming/spec.cy.ts index a3bfb1c330..8552257fea 100644 --- a/cypress/e2e/streaming/spec.cy.ts +++ b/cypress/e2e/streaming/spec.cy.ts @@ -1,13 +1,23 @@ import { runTestServer } from '../../support/testUtils'; -function testStreamedTest(index: number) { - const tokenList = ['the', 'quick', 'brown', 'fox']; +const tokenList = ['the', 'quick', 'brown', 'fox']; + +function messageStream(index: number) { for (const token of tokenList) { cy.get('.step').eq(index).should('contain', token); } cy.get('.step').eq(index).should('contain', tokenList.join(' ')); } +function toolStream(tool: string) { + const toolCall = cy.get(`#tool-call-${tool}`); + toolCall.click(); + for (const token of tokenList) { + toolCall.parent().should('contain', token); + } + toolCall.parent().should('contain', tokenList.join(' ')); +} + describe('Streaming', () => { before(() => { runTestServer(); @@ -16,20 +26,18 @@ describe('Streaming', () => { it('should be able to stream a message', () => { cy.get('.step').should('have.length', 1); - testStreamedTest(0); + messageStream(0); cy.get('.step').should('have.length', 1); - testStreamedTest(1); + messageStream(1); cy.get('.step').should('have.length', 2); - testStreamedTest(2); - - cy.get('.step').should('have.length', 3); + toolStream('tool1'); - testStreamedTest(3); + toolStream('tool2'); - cy.get('.step').should('have.length', 4); + cy.get('.step').should('have.length', 3); }); }); diff --git a/cypress/e2e/update_step/main.py b/cypress/e2e/update_step/main.py index fb407253cc..5ac3723a9c 100644 --- a/cypress/e2e/update_step/main.py +++ b/cypress/e2e/update_step/main.py @@ -6,7 +6,7 @@ async def main(): msg = cl.Message(content="Hello!") await msg.send() - async with cl.Step() as step: + async with cl.Step(type="tool", name="tool1") as step: step.output = "Foo" await cl.sleep(1) diff --git a/cypress/e2e/update_step/spec.cy.ts b/cypress/e2e/update_step/spec.cy.ts index cc8fd02bec..795c0eb47d 100644 --- a/cypress/e2e/update_step/spec.cy.ts +++ b/cypress/e2e/update_step/spec.cy.ts @@ -6,13 +6,12 @@ describe('Update Step', () => { }); it('should be able to update a step', () => { + cy.get(`#tool-call-tool1`).click(); cy.get('.step').should('have.length', 1); - cy.get('#chatbot-loading').should('exist').click(); - cy.get('.step').should('have.length', 2); cy.get('.step').eq(0).should('contain', 'Hello!'); - cy.get('.step').eq(1).should('contain', 'Foo'); + cy.get(`#tool-call-tool1`).parent().should('contain', 'Foo'); cy.get('.step').eq(0).should('contain', 'Hello again!'); - cy.get('.step').eq(1).should('contain', 'Foo Bar'); + cy.get(`#tool-call-tool1`).parent().should('contain', 'Foo Bar'); }); }); diff --git a/cypress/support/testUtils.ts b/cypress/support/testUtils.ts index 3586ed8b1c..458a300477 100644 --- a/cypress/support/testUtils.ts +++ b/cypress/support/testUtils.ts @@ -10,10 +10,11 @@ export function submitMessage(message: string) { export function submitMessageCopilot(message: string) { cy.wait(1000); - cy.get(`#copilot-chat-input`).should('not.be.disabled'); - cy.get(`#copilot-chat-input`).type(`${message}{enter}`, { - scrollBehavior: false - }); + cy.get(`#copilot-chat-input`, { includeShadowDom: true }) + .should('not.be.disabled') + .type(`${message}{enter}`, { + scrollBehavior: false + }); } export function openHistory() { diff --git a/frontend/package.json b/frontend/package.json index cebc2196ee..3a66753b03 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,6 @@ "@mui/icons-material": "^5.14.9", "@mui/lab": "^5.0.0-alpha.122", "@mui/material": "^5.14.10", - "draft-js": "^0.11.7", "formik": "^2.4.3", "highlight.js": "^11.9.0", "i18next": "^23.7.16", diff --git a/frontend/src/App.css b/frontend/src/App.css index 8e1b199df1..d00655f0e4 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,5 +1,6 @@ .markdown-body { overflow-x: auto; + overflow-y: hidden; } .markdown-body img { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b4d5508858..71006e5036 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,10 +11,7 @@ import { Theme, ThemeProvider } from '@mui/material/styles'; import { useChatSession } from '@chainlit/react-client'; -import Hotkeys from 'components/Hotkeys'; -import SettingsModal from 'components/molecules/settingsModal'; import ChatSettingsModal from 'components/organisms/chat/settings'; -import PromptPlayground from 'components/organisms/playground'; import { apiClientState } from 'state/apiClient'; import { projectSettingsState } from 'state/project'; @@ -45,6 +42,7 @@ declare global { interface Window { renderingCodeBlock?: boolean; theme?: { + default: string; light?: ThemOverride; dark?: ThemOverride; }; @@ -150,10 +148,7 @@ function App() { width="100vw" sx={{ overflowX: 'hidden' }} > - - - diff --git a/frontend/src/AppWrapper.tsx b/frontend/src/AppWrapper.tsx index 019b8b9a41..8f572be1ab 100644 --- a/frontend/src/AppWrapper.tsx +++ b/frontend/src/AppWrapper.tsx @@ -52,9 +52,7 @@ export default function AppWrapper() { setProjectSettings(data); setAppSettings((prev) => ({ ...prev, - defaultCollapseContent: data.ui.default_collapse_content ?? true, - expandAll: !!data.ui.default_expand_messages, - hideCot: !!data.ui.hide_cot + defaultCollapseContent: data.ui.default_collapse_content ?? true })); }, [data, setProjectSettings, setAppSettings]); diff --git a/frontend/src/assets/Image.tsx b/frontend/src/assets/Image.tsx new file mode 100644 index 0000000000..f0c966d72e --- /dev/null +++ b/frontend/src/assets/Image.tsx @@ -0,0 +1,22 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const ImageIcon = (props: SvgIconProps) => { + return ( + + + + + ); +}; + +export default ImageIcon; diff --git a/frontend/src/assets/debug.tsx b/frontend/src/assets/debug.tsx new file mode 100644 index 0000000000..fb8520e9a1 --- /dev/null +++ b/frontend/src/assets/debug.tsx @@ -0,0 +1,31 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const DebugIcon = (props: SvgIconProps) => { + return ( + + + + + + + + + + + + + {' '} + + + ); +}; + +export default DebugIcon; diff --git a/frontend/src/components/Hotkeys.tsx b/frontend/src/components/Hotkeys.tsx deleted file mode 100644 index 6c30a6037c..0000000000 --- a/frontend/src/components/Hotkeys.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useHotkeys } from 'react-hotkeys-hook'; -import { useSetRecoilState } from 'recoil'; - -import { settingsState } from 'state/settings'; - -export default function Hotkeys() { - const setSettings = useSetRecoilState(settingsState); - useHotkeys('s', () => setSettings((old) => ({ ...old, open: !old.open }))); - - return null; -} diff --git a/frontend/src/components/atoms/buttons/githubButton.tsx b/frontend/src/components/atoms/buttons/githubButton.tsx index db3f6ad228..8febff943a 100644 --- a/frontend/src/components/atoms/buttons/githubButton.tsx +++ b/frontend/src/components/atoms/buttons/githubButton.tsx @@ -1,23 +1,31 @@ import { useRecoilValue } from 'recoil'; -import { IconButton, IconButtonProps, Tooltip } from '@mui/material'; +import { Button, ButtonProps } from '@mui/material'; import GithubIcon from 'assets/github'; import { projectSettingsState } from 'state/project'; -interface Props extends IconButtonProps {} +interface Props extends ButtonProps {} export default function GithubButton({ ...props }: Props) { const pSettings = useRecoilValue(projectSettingsState); const href = pSettings?.ui.github; if (!href) return null; return ( - - {/* @ts-expect-error href breaks IconButton props */} - - - - + //@ts-expect-error href is not a valid prop for Button + ); } diff --git a/frontend/src/components/atoms/buttons/userButton/avatar.tsx b/frontend/src/components/atoms/buttons/userButton/avatar.tsx index 1c8952121b..a608df68e3 100644 --- a/frontend/src/components/atoms/buttons/userButton/avatar.tsx +++ b/frontend/src/components/atoms/buttons/userButton/avatar.tsx @@ -1,40 +1,29 @@ import { useAuth } from 'api/auth'; -import SettingsIcon from '@mui/icons-material/Settings'; -import { Avatar, Button, ButtonProps, Typography } from '@mui/material'; +import { Avatar, IconButton, IconButtonProps } from '@mui/material'; -export default function UserAvatar(props: ButtonProps) { +import UserIcon from 'assets/user'; + +export default function UserAvatar(props: IconButtonProps) { const { user } = useAuth(); return ( - + + )} + + ); } diff --git a/frontend/src/components/atoms/buttons/userButton/index.tsx b/frontend/src/components/atoms/buttons/userButton/index.tsx index bd266ee7e8..9a0ca0e65d 100644 --- a/frontend/src/components/atoms/buttons/userButton/index.tsx +++ b/frontend/src/components/atoms/buttons/userButton/index.tsx @@ -16,7 +16,6 @@ export default function UserButton() { return ( <> ); - const settingsItem = ( - { - setAppSettings((old) => ({ ...old, open: true })); - handleClose(); - }} - > + const themeItem = ( + - + - - - - - - - + + } + /> + + { + const variant = settings.theme === 'light' ? 'dark' : 'light'; + localStorage.setItem('themeVariant', variant); + setSettings((old) => ({ ...old, theme: variant })); + }} + checked={settings.theme === 'dark'} + inputProps={{ + 'aria-labelledby': 'switch-theme' + }} + /> + + ); const apiKeysItem = requiredKeys && ( @@ -87,12 +97,9 @@ export default function UserMenu({ anchorEl, open, handleClose }: Props) { ); - const menuItems = [ - userNameItem, - settingsItem, - apiKeysItem, - logoutItem - ].filter((i) => !!i); + const menuItems = [userNameItem, themeItem, apiKeysItem, logoutItem].filter( + (i) => !!i + ); const itemsWithDivider = menuItems.reduce((acc, curr, i) => { if (i === menuItems.length - 1) { @@ -138,8 +145,8 @@ export default function UserMenu({ anchorEl, open, handleClose }: Props) { } } }} - transformOrigin={{ horizontal: 'center', vertical: 'bottom' }} - anchorOrigin={{ horizontal: 'center', vertical: 'top' }} + transformOrigin={{ horizontal: 'center', vertical: 'top' }} + anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }} > {itemsWithDivider} diff --git a/frontend/src/components/atoms/elements/Avatar.tsx b/frontend/src/components/atoms/elements/Avatar.tsx deleted file mode 100644 index c86eac1d15..0000000000 --- a/frontend/src/components/atoms/elements/Avatar.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Avatar from '@mui/material/Avatar'; - -import { type IAvatarElement } from 'client-types/'; - -interface Props { - author: string; - bgColor?: string; - element?: IAvatarElement; -} - -const AvatarElement = ({ element, author, bgColor }: Props) => { - let avatar: JSX.Element; - const sx = { - width: 26, - height: 26, - fontSize: '0.75rem', - mt: '-2px', - bgcolor: element?.url ? 'transparent' : bgColor - }; - if (element?.url) { - avatar = ; - } else { - avatar = {author[0]?.toUpperCase()}; - } - return {avatar}; -}; - -export { AvatarElement }; diff --git a/frontend/src/components/atoms/elements/index.ts b/frontend/src/components/atoms/elements/index.ts index 9a55c750f7..0a520ff7e3 100644 --- a/frontend/src/components/atoms/elements/index.ts +++ b/frontend/src/components/atoms/elements/index.ts @@ -1,5 +1,4 @@ export { AudioElement } from './Audio'; -export { AvatarElement } from './Avatar'; export { Element } from './Element'; export { ElementSideView } from './ElementSideView'; export { ElementView } from './ElementView'; diff --git a/frontend/src/components/atoms/inputs/selects/SelectInput.tsx b/frontend/src/components/atoms/inputs/selects/SelectInput.tsx index 25d202fcd0..8eba4897ee 100644 --- a/frontend/src/components/atoms/inputs/selects/SelectInput.tsx +++ b/frontend/src/components/atoms/inputs/selects/SelectInput.tsx @@ -1,4 +1,4 @@ -import { MouseEvent } from 'react'; +import { MouseEvent, useState } from 'react'; import { grey, primary } from 'theme/index'; import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; @@ -58,6 +58,20 @@ const SelectInput = ({ }: SelectInputProps): JSX.Element => { const isDarkMode = useIsDarkMode(); + const [menuOpen, setMenuOpen] = useState(false); + + const handleMenuOpen = () => { + setMenuOpen(true); + }; + + const handleMenuClose = (event: React.SyntheticEvent) => { + setMenuOpen(false); + + if (onClose) { + onClose(event); + } + }; + return ( onItemMouseEnter?.(e, item.label)} - onMouseLeave={(e) => onItemMouseLeave?.(e)} + onMouseEnter={(e) => + menuOpen && onItemMouseEnter?.(e, item.label) + } + onMouseLeave={(e) => menuOpen && onItemMouseLeave?.(e)} item={item} selected={item.value === value} key={item.value} diff --git a/frontend/src/components/molecules/Code.tsx b/frontend/src/components/molecules/Code.tsx index 1c213c22dc..06c59926ca 100644 --- a/frontend/src/components/molecules/Code.tsx +++ b/frontend/src/components/molecules/Code.tsx @@ -92,7 +92,7 @@ const Code = ({ children, ...props }: any) => { > { + if (!onPasswordSignIn && onOAuthSignIn && providers.length === 1) { + onOAuthSignIn(providers[0], callbackUrl); + } + }, [onPasswordSignIn, onOAuthSignIn, providers]); + const formik = useFormik({ initialValues: { email: '', diff --git a/frontend/src/components/molecules/chatProfiles.tsx b/frontend/src/components/molecules/chatProfiles.tsx index de729a2cee..722c26440f 100644 --- a/frontend/src/components/molecules/chatProfiles.tsx +++ b/frontend/src/components/molecules/chatProfiles.tsx @@ -14,11 +14,13 @@ import { import { SelectInput } from 'components/atoms/inputs'; import { Markdown } from 'components/molecules/Markdown'; +import { apiClientState } from 'state/apiClient'; import { projectSettingsState } from 'state/project'; import NewChatDialog from './newChatDialog'; export default function ChatProfiles() { + const apiClient = useRecoilValue(apiClientState); const pSettings = useRecoilValue(projectSettingsState); const { chatProfile, setChatProfile } = useChatSession(); const { firstInteraction } = useChatMessages(); @@ -60,22 +62,27 @@ export default function ChatProfiles() { const popoverOpen = Boolean(anchorEl); - const items = pSettings.chatProfiles.map((item) => ({ - label: item.name, - value: item.name, - icon: item.icon ? ( - - ) : undefined - })); + const items = pSettings.chatProfiles.map((item) => { + const icon = item.icon?.startsWith('/public') + ? apiClient.buildEndpoint(item.icon) + : item.icon; + return { + label: item.name, + value: item.name, + icon: icon ? ( + + ) : undefined + }; + }); return ( <> @@ -88,7 +95,8 @@ export default function ChatProfiles() { boxShadow: (theme) => theme.palette.mode === 'light' ? '0px 2px 4px 0px #0000000D' - : '0px 10px 10px 0px #0000000D' + : '0px 10px 10px 0px #0000000D', + ml: 2 } }} sx={{ @@ -103,7 +111,6 @@ export default function ChatProfiles() { vertical: 'center', horizontal: 'left' }} - onClose={() => setAnchorEl(null)} disableRestoreFocus > setAnchorEl(null)} /> { + ({ message, showAvatar, elements, actions, isRunning, isLast }: Props) => { const { - expandAll, - hideCot, highlightedMessage, defaultCollapseContent, allowHtml, @@ -52,19 +39,11 @@ const Message = memo( } = useContext(MessageContext); const layoutMaxWidth = useLayoutMaxWidth(); - const [showDetails, setShowDetails] = useState(expandAll); - - useEffect(() => { - setShowDetails(expandAll); - }, [expandAll]); - - if (hideCot && indent) { - return null; - } - const isAsk = message.waitForAnswer; const isUserMessage = message.type === 'user_message'; + const forceDisplayCursor = + isLast && isRunning && (!message.streaming || window.renderingCodeBlock); return ( - showBorder ? `1px solid ${theme.palette.divider}` : 'none', animation: message.id && highlightedMessage === message.id ? `3s ease-in-out 0.1s ${flash}` @@ -120,40 +97,21 @@ const Message = memo( latex={latex} /> - setShowDetails(!showDetails)} - loading={isRunning && isLast} - /> - {message.steps && showDetails && ( - + {forceDisplayCursor && ( + + + )} ) : ( - - setShowDetails(!showDetails)} - loading={isRunning && isLast} - /> - {message.steps && showDetails && ( - + + + - )} - )} + {forceDisplayCursor && ( + + + + )} {actions?.length ? ( ) : null} - + )} - {isLast && - isRunning && - (!message.streaming || window.renderingCodeBlock) && ( - - - - )} ); diff --git a/frontend/src/components/molecules/messages/MessageContainer.tsx b/frontend/src/components/molecules/messages/MessageContainer.tsx index d4ad654e2d..bb9ba7261a 100644 --- a/frontend/src/components/molecules/messages/MessageContainer.tsx +++ b/frontend/src/components/molecules/messages/MessageContainer.tsx @@ -1,7 +1,5 @@ import { MessageContext, defaultMessageContext } from 'contexts/MessageContext'; -import { memo, useEffect, useRef } from 'react'; - -import Box from '@mui/material/Box'; +import { memo } from 'react'; import type { IAction, IMessageElement, IStep } from 'client-types/'; import { IMessageContext } from 'types/messageContext'; @@ -10,61 +8,21 @@ import { Messages } from './Messages'; interface Props { actions: IAction[]; - autoScroll?: boolean; context: IMessageContext; elements: IMessageElement[]; messages: IStep[]; - setAutoScroll?: (autoScroll: boolean) => void; } const MessageContainer = memo( - ({ - actions, - autoScroll, - context, - elements, - messages, - setAutoScroll - }: Props) => { - const ref = useRef(); - - useEffect(() => { - setTimeout(() => { - if (!ref.current || !autoScroll) { - return; - } - ref.current.scrollTop = ref.current.scrollHeight; - }, 0); - }, [messages, autoScroll]); - - const handleScroll = () => { - if (!ref.current || !setAutoScroll) return; - - const { scrollTop, scrollHeight, clientHeight } = ref.current; - const atBottom = scrollTop + clientHeight >= scrollHeight - 10; - setAutoScroll(atBottom); - }; - + ({ actions, context, elements, messages }: Props) => { return ( - - - + ); } diff --git a/frontend/src/components/molecules/messages/Messages.tsx b/frontend/src/components/molecules/messages/Messages.tsx index 0a609e92bb..9fb45e4fec 100644 --- a/frontend/src/components/molecules/messages/Messages.tsx +++ b/frontend/src/components/molecules/messages/Messages.tsx @@ -18,7 +18,6 @@ const Messages = memo( const messageContext = useContext(MessageContext); const isRoot = indent === 0; - let previousAuthor = ''; const filtered = messages.filter((m, i) => { const content = m.output; @@ -26,7 +25,7 @@ const Messages = memo( const hasInlinedElement = elements.find( (el) => el.display === 'inline' && el.forId === m.id ); - const hasChildren = !!m.steps?.length && !messageContext.hideCot; + const hasChildren = !!m.steps?.length; const isLast = i === messages.length - 1; const messageRunning = isRunning === undefined @@ -43,23 +42,26 @@ const Messages = memo( return ( <> {filtered.map((m, i) => { - const author = m.name; + const previousMessage = i > 0 ? filtered[i - 1] : undefined; + const typeIsDifferent = previousMessage?.type !== m.type; + const authorIsDifferent = + !!m.name && + !!previousMessage?.name && + previousMessage.name !== m.name; + const showAvatar = typeIsDifferent || authorIsDifferent; + const isLast = filtered.length - 1 === i; let messageRunning = isRunning === undefined ? messageContext.loading : isRunning; if (isRoot) { messageRunning = messageRunning && isLast; } - const showAvatar = author !== previousAuthor; - const showBorder = false; - previousAuthor = author; return ( void; + autoScroll?: boolean; + children: React.ReactNode; +} + +export default function ScrollContainer({ + setAutoScroll, + autoScroll, + children +}: Props) { + const ref = useRef(); + const { messages } = useChatMessages(); + const { session } = useChatSession(); + + useEffect(() => { + setAutoScroll?.(true); + }, [session?.socket.id]); + + useEffect(() => { + setTimeout(() => { + if (!ref.current || !autoScroll) { + return; + } + ref.current.scrollTop = ref.current.scrollHeight; + }, 0); + }, [messages, autoScroll]); + + const handleScroll = () => { + if (!ref.current || !setAutoScroll) return; + + const { scrollTop, scrollHeight, clientHeight } = ref.current; + const atBottom = scrollTop + clientHeight >= scrollHeight - 10; + setAutoScroll(atBottom); + }; + + return ( + + {children} + + ); +} diff --git a/frontend/src/components/molecules/messages/ToolCall.tsx b/frontend/src/components/molecules/messages/ToolCall.tsx new file mode 100644 index 0000000000..190973e890 --- /dev/null +++ b/frontend/src/components/molecules/messages/ToolCall.tsx @@ -0,0 +1,118 @@ +import { useMemo, useState } from 'react'; + +import Box from '@mui/material/Box'; +import LinearProgress from '@mui/material/LinearProgress'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +import { MessageContent } from './components/MessageContent'; +import { Translator } from 'components/i18n'; + +import ChevronDownIcon from 'assets/chevronDown'; +import ChevronUpIcon from 'assets/chevronUp'; + +import type { IMessageElement, IStep } from 'client-types/'; + +interface Props { + steps: IStep[]; + elements: IMessageElement[]; + isRunning?: boolean; +} + +export default function ToolCall({ steps, elements, isRunning }: Props) { + const [open, setOpen] = useState(false); + const [hover, setHover] = useState(false); + const using = useMemo(() => { + return ( + isRunning && + steps.find((step) => step.start && !step.end && !step.isError) + ); + }, [steps, isRunning]); + + const hasOutput = steps.some((step) => step.output); + const isError = steps.length ? steps[steps.length - 1].isError : false; + + if (!steps.length) { + return null; + } + + const toolName = steps[0].name; + + return ( + + setHover(true)} + onMouseOut={() => setHover(false)} + onClick={() => setOpen(!open)} + id={`tool-call-${toolName}`} + > + + theme.typography.fontFamily + }} + > + {using ? ( + <> + {' '} + {toolName} + + ) : ( + <> + {' '} + {toolName} + + )} + {' '} + {using && ( + + )} + + + {hasOutput && (hover || open) ? ( + open ? ( + + ) : ( + + ) + ) : null} + + {open && ( + `1px solid ${theme.palette.primary.main}`, + boxSizing: 'border-box', + pl: 1 + }} + > + {steps + .filter((step) => step.output) + .map((step) => ( + + ))} + + )} + + ); +} diff --git a/frontend/src/components/molecules/messages/ToolCalls.tsx b/frontend/src/components/molecules/messages/ToolCalls.tsx new file mode 100644 index 0000000000..d739b76d05 --- /dev/null +++ b/frontend/src/components/molecules/messages/ToolCalls.tsx @@ -0,0 +1,72 @@ +import { useMemo } from 'react'; + +import Stack from '@mui/material/Stack'; + +import type { IMessageElement, IStep } from 'client-types/'; + +import ToolCall from './ToolCall'; + +interface Props { + message: IStep; + elements: IMessageElement[]; + isRunning?: boolean; +} + +function groupToolSteps(step: IStep) { + const groupedSteps: IStep[][] = []; + + let currentGroup: IStep[] = []; + + function traverseAndGroup(currentStep: IStep) { + if (currentStep.type === 'tool') { + if ( + currentGroup.length === 0 || + currentGroup[0].name === currentStep.name + ) { + currentGroup.push(currentStep); + } else { + groupedSteps.push(currentGroup); + + currentGroup = [currentStep]; + } + } + + if (currentStep.steps) { + for (const childStep of currentStep.steps) { + traverseAndGroup(childStep); + } + } + } + + traverseAndGroup(step); + + // Push the last group if it exists + if (currentGroup.length > 0) { + groupedSteps.push(currentGroup); + } + + return groupedSteps; +} + +export default function ToolCalls({ message, elements, isRunning }: Props) { + const toolCalls = useMemo(() => { + return message.steps ? groupToolSteps(message) : []; + }, [message]); + + if (!toolCalls.length) { + return null; + } + + return ( + + {toolCalls.map((toolCall, index) => ( + + ))} + + ); +} diff --git a/frontend/src/components/molecules/messages/components/Author.tsx b/frontend/src/components/molecules/messages/components/Author.tsx deleted file mode 100644 index 62cad183b5..0000000000 --- a/frontend/src/components/molecules/messages/components/Author.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { MessageContext } from 'contexts/MessageContext'; -import { useContext } from 'react'; - -import Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; - -import { AvatarElement } from 'components/atoms/elements'; - -import { useColorForName } from 'hooks/useColors'; - -import type { IStep } from 'client-types/'; - -import { MessageTime } from './MessageTime'; - -interface Props { - message: IStep; - show?: boolean; - children?: React.ReactNode; -} - -export const AUTHOR_BOX_WIDTH = 26; - -const Author = ({ message, show, children }: Props) => { - const context = useContext(MessageContext); - const getColorForName = useColorForName(context.uiName); - - const isUser = message.type === 'user_message'; - const author = isUser ? 'You' : message.name; - - const avatarEl = context.avatars.find((e) => e.name === author); - const avatar = show ? ( - - - {(!!message.indent || message.parentId) && ( - - )} - - ) : ( - - ); - const name = ( - - {show ? ( - - {author} - - ) : null} - - - ); - - return ( - - {avatar} - - {name} - {children} - - - ); -}; - -export { Author }; diff --git a/frontend/src/components/molecules/messages/components/Avatar.tsx b/frontend/src/components/molecules/messages/components/Avatar.tsx new file mode 100644 index 0000000000..943c15738c --- /dev/null +++ b/frontend/src/components/molecules/messages/components/Avatar.tsx @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; + +import Avatar from '@mui/material/Avatar'; + +import { useChatSession } from '@chainlit/react-client'; + +import { apiClientState } from 'state/apiClient'; +import { projectSettingsState } from 'state/project'; + +interface Props { + author: string; + hide?: boolean; +} + +const MessageAvatar = ({ author, hide }: Props) => { + const { chatProfile } = useChatSession(); + const pSettings = useRecoilValue(projectSettingsState); + + const selectedChatProfile = useMemo(() => { + return pSettings?.chatProfiles.find( + (profile) => profile.name === chatProfile + ); + }, [pSettings, chatProfile]); + + const apiClient = useRecoilValue(apiClientState); + + const avatarUrl = useMemo(() => { + const isAssistant = !author || author === pSettings?.ui.name; + if (isAssistant && selectedChatProfile?.icon) { + return selectedChatProfile.icon; + } + return apiClient?.buildEndpoint(`/avatars/${author || 'default'}`); + }, [apiClient, selectedChatProfile, pSettings, author]); + + return ( + + + + ); +}; + +export { MessageAvatar }; diff --git a/frontend/src/components/molecules/messages/components/DebugButton.tsx b/frontend/src/components/molecules/messages/components/DebugButton.tsx new file mode 100644 index 0000000000..7bb6431218 --- /dev/null +++ b/frontend/src/components/molecules/messages/components/DebugButton.tsx @@ -0,0 +1,35 @@ +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; + +import DebugIcon from 'assets/debug'; + +import type { IStep } from 'client-types/'; + +interface Props { + debugUrl: string; + step: IStep; +} + +const DebugButton = ({ step, debugUrl }: Props) => { + let stepId = step.id; + if (stepId.startsWith('wrap_')) { + stepId = stepId.replace('wrap_', ''); + } + const href = debugUrl + .replace('[thread_id]', step.threadId!) + .replace('[step_id]', stepId); + return ( + + + + + + ); +}; + +export { DebugButton }; diff --git a/frontend/src/components/molecules/messages/components/DetailsButton.tsx b/frontend/src/components/molecules/messages/components/DetailsButton.tsx deleted file mode 100644 index c0c54006c3..0000000000 --- a/frontend/src/components/molecules/messages/components/DetailsButton.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { MessageContext } from 'contexts/MessageContext'; -import { useContext } from 'react'; - -import { GreyButton } from 'components/atoms/buttons/GreyButton'; -import { Translator } from 'components/i18n'; - -import ChevronDownIcon from 'assets/chevronDown'; -import ChevronUpIcon from 'assets/chevronUp'; - -import type { IStep } from 'client-types/'; - -interface Props { - message: IStep; - opened: boolean; - loading?: boolean; - onClick: () => void; -} - -const DetailsButton = ({ message, opened, onClick, loading }: Props) => { - const messageContext = useContext(MessageContext); - const nestedCount = message.steps?.length; - const nested = !!nestedCount && !messageContext.hideCot; - - const lastStep = nested ? message.steps![nestedCount - 1] : undefined; - - const tool = lastStep ? lastStep.name : undefined; - - if (!tool) { - return null; - } - - // Don't count empty steps - const stepCount = nestedCount - ? message.steps!.filter((m) => !!m.output || m.steps?.length).length - : 0; - - const text = ( - - {loading ? ( - <> - {tool} - - ) : ( - <> - - - )} - - ); - - let id = ''; - if (tool) { - id = tool.trim().toLowerCase().replaceAll(' ', '-'); - } - if (loading) { - id += '-loading'; - } else { - id += '-done'; - } - - return ( - - ) : ( - - ) - ) : undefined - } - onClick={tool ? onClick : undefined} - > - {text} - - ); -}; - -export { DetailsButton }; diff --git a/frontend/src/components/molecules/messages/components/MessageButtons.tsx b/frontend/src/components/molecules/messages/components/MessageButtons.tsx index cf3fdf365c..18937ddc14 100644 --- a/frontend/src/components/molecules/messages/components/MessageButtons.tsx +++ b/frontend/src/components/molecules/messages/components/MessageButtons.tsx @@ -1,17 +1,22 @@ import { MessageContext } from 'contexts/MessageContext'; import { useContext } from 'react'; +import { useRecoilValue } from 'recoil'; import { grey } from 'theme/palette'; import Stack from '@mui/material/Stack'; +import { useChatMessages } from '@chainlit/react-client'; + import { ClipboardCopy } from 'components/atoms/ClipboardCopy'; import { useIsDarkMode } from 'hooks/useIsDarkMode'; -import type { IStep } from 'client-types/'; +import { projectSettingsState } from 'state/project'; + +import { type IStep } from 'client-types/'; +import { DebugButton } from './DebugButton'; import { FeedbackButtons } from './FeedbackButtons'; -import { PlaygroundButton } from './PlaygroundButton'; interface Props { message: IStep; @@ -20,8 +25,9 @@ interface Props { const MessageButtons = ({ message }: Props) => { const isDark = useIsDarkMode(); const { showFeedbackButtons: showFbButtons } = useContext(MessageContext); + const pSettings = useRecoilValue(projectSettingsState); + const { firstInteraction } = useChatMessages(); - const showPlaygroundButton = !!message.generation; const isUser = message.type === 'user_message'; const isAsk = message.waitForAnswer; const hasContent = !!message.output; @@ -35,7 +41,10 @@ const MessageButtons = ({ message }: Props) => { !isAsk && hasContent; - const show = showCopyButton || showPlaygroundButton || showFeedbackButtons; + const showDebugButton = + !!pSettings?.debugUrl && !!message.threadId && !!firstInteraction; + + const show = showCopyButton || showDebugButton || showFeedbackButtons; if (!show) { return null; @@ -50,7 +59,9 @@ const MessageButtons = ({ message }: Props) => { > {showCopyButton ? : null} {showFeedbackButtons ? : null} - {showPlaygroundButton ? : null} + {showDebugButton ? ( + + ) : null} ); }; diff --git a/frontend/src/components/molecules/messages/components/MessageContent.tsx b/frontend/src/components/molecules/messages/components/MessageContent.tsx index 2e43532ca4..fb3f968c9b 100644 --- a/frontend/src/components/molecules/messages/components/MessageContent.tsx +++ b/frontend/src/components/molecules/messages/components/MessageContent.tsx @@ -29,9 +29,10 @@ const MessageContent = memo( let lineCount = 0; let contentLength = 0; - const content = message.streaming - ? message.output + CURSOR_PLACEHOLDER - : message.output; + const content = + message.streaming && message.output + ? message.output + CURSOR_PLACEHOLDER + : message.output; const { preparedContent: output, @@ -102,8 +103,8 @@ const MessageContent = memo( ); const collapse = - lineCount > COLLAPSE_MIN_LINES || contentLength > COLLAPSE_MIN_LENGTH; - + !message.type.includes('message') && + (lineCount > COLLAPSE_MIN_LINES || contentLength > COLLAPSE_MIN_LENGTH); const messageContent = collapse ? ( {markdownContent} ) : ( @@ -111,7 +112,7 @@ const MessageContent = memo( ); return ( - + {output ? messageContent : null} diff --git a/frontend/src/components/molecules/messages/components/PlaygroundButton.tsx b/frontend/src/components/molecules/messages/components/PlaygroundButton.tsx deleted file mode 100644 index 53f06f22f6..0000000000 --- a/frontend/src/components/molecules/messages/components/PlaygroundButton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { MessageContext } from 'contexts/MessageContext'; -import { useContext } from 'react'; - -import Terminal from '@mui/icons-material/Terminal'; -import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; - -import type { IStep } from 'client-types/'; - -interface Props { - step: IStep; -} - -const PlaygroundButton = ({ step }: Props) => { - const { onPlaygroundButtonClick } = useContext(MessageContext); - - return ( - - { - onPlaygroundButtonClick && onPlaygroundButtonClick(step); - }} - > - - - - ); -}; - -export { PlaygroundButton }; diff --git a/frontend/src/components/molecules/playground/actionBar.tsx b/frontend/src/components/molecules/playground/actionBar.tsx deleted file mode 100644 index e5ed67aaf2..0000000000 --- a/frontend/src/components/molecules/playground/actionBar.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import Stack from '@mui/material/Stack'; - -export default function ActionBar({ children }: PropsWithChildren) { - return ( - theme.palette.background.paper, - padding: '16px 24px', - alignItems: 'center', - justifyContent: 'flex-end', - gap: 2 - }} - > - {children} - - ); -} diff --git a/frontend/src/components/molecules/playground/basic.tsx b/frontend/src/components/molecules/playground/basic.tsx deleted file mode 100644 index 88b428913c..0000000000 --- a/frontend/src/components/molecules/playground/basic.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { EditorState } from 'draft-js'; -import { useContext } from 'react'; - -import Alert from '@mui/material/Alert'; -import Stack from '@mui/material/Stack'; - -import type { ICompletionGeneration } from 'client-types/'; - -import Completion from './editor/completion'; -import FormattedEditor from './editor/formatted'; - -interface Props { - generation: ICompletionGeneration; - hasTemplate: boolean; - restoredTime: number; -} - -export default function BasicPromptPlayground({ - generation, - restoredTime -}: Props) { - const { promptMode, setPlayground } = useContext(PlaygroundContext); - - const onFormattedChange = (nextState: EditorState) => { - const formatted = nextState.getCurrentContent().getPlainText(); - setPlayground((old) => ({ - ...old, - generation: { - ...old!.generation!, - formatted - } - })); - }; - - const renderFormatted = () => { - if (typeof generation.prompt === 'string') { - return ( - - ); - } else { - return ( - - Neither template or formatted prompt provided. - - ); - } - }; - - return ( - - {promptMode === 'Formatted' ? renderFormatted() : null} - - - ); -} diff --git a/frontend/src/components/molecules/playground/chat.tsx b/frontend/src/components/molecules/playground/chat.tsx deleted file mode 100644 index d28f9a65fa..0000000000 --- a/frontend/src/components/molecules/playground/chat.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { EditorState } from 'draft-js'; -import { Fragment, forwardRef, useContext } from 'react'; -import { grey } from 'theme'; - -import AddCircleOutlined from '@mui/icons-material/AddCircleOutlined'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; - -import type { IChatGeneration } from 'client-types/'; - -import Completion from './editor/completion'; -import PromptMessage from './editor/promptMessage'; - -interface Props { - generation: IChatGeneration; - hasTemplate: boolean; - restoredTime: number; -} - -export const ChatPromptPlayground = forwardRef( - ({ hasTemplate, generation, restoredTime }: Props, ref) => { - const { promptMode, setPlayground } = useContext(PlaygroundContext); - const messages = generation.messages; - - const onChange = (index: number, nextState: EditorState) => { - const text = nextState.getCurrentContent().getPlainText(); - - setPlayground((old) => ({ - ...old, - generation: { - ...old!.generation!, - messages: (old!.generation! as IChatGeneration).messages?.map( - (message, mIndex) => { - if (mIndex === index) { - return { - ...message, - content: text - }; - } - return message; - } - ) - } - })); - }; - - const title = - promptMode === 'Formatted' - ? hasTemplate - ? 'Formatted messages [Read Only]' - : 'Formatted messages' - : 'Prompt messages'; - - const completionContent = generation.messageCompletion?.content; - const completionToolCalls = generation.messageCompletion?.tool_calls; - - const completion = completionContent - ? completionContent - : completionToolCalls - ? JSON.stringify(completionToolCalls, null, 2) - : ''; - - return ( - - - {title} - - - {messages?.length ? ( - - {messages.map((message, index) => ( - - - - ))} - - ) : null} - - - - - - - ); - } -); - -export default ChatPromptPlayground; diff --git a/frontend/src/components/molecules/playground/editor/EditorWrapper.tsx b/frontend/src/components/molecules/playground/editor/EditorWrapper.tsx deleted file mode 100644 index d2344b4406..0000000000 --- a/frontend/src/components/molecules/playground/editor/EditorWrapper.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import merge from 'lodash/merge'; -import { grey } from 'theme'; - -import Box, { BoxProps } from '@mui/material/Box'; -import Stack, { StackProps } from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Theme, useTheme } from '@mui/material/styles'; - -import { ClipboardCopy } from 'components/atoms/ClipboardCopy'; - -interface Props { - className?: string; - clipboardValue?: string; - sx?: StackProps['sx']; - sxChildren?: BoxProps['sx']; - title?: string; -} - -export default function EditorWrapper({ - children, - className, - clipboardValue, - sx, - sxChildren, - title -}: React.PropsWithChildren) { - const theme = useTheme(); - - return ( - - - {title} - - theme.typography.fontFamily, - fontSize: '16px', - lineHeight: '24px', - padding: 3, - paddingRight: 4, - border: `1.5px solid ${theme.palette.divider}`, - borderRadius: '8px', - overflowY: 'auto', - flexGrow: 1, - caretColor: theme.palette.text.primary, - backgroundColor: theme.palette.background.paper, - '&:hover': { - borderColor: theme.palette.mode === 'light' ? grey[400] : 'white' - } - }, - sxChildren - )} - > - {clipboardValue ? ( - - - - ) : null} - {children} - - - ); -} diff --git a/frontend/src/components/molecules/playground/editor/MessageWrapper.tsx b/frontend/src/components/molecules/playground/editor/MessageWrapper.tsx deleted file mode 100644 index c6918d3734..0000000000 --- a/frontend/src/components/molecules/playground/editor/MessageWrapper.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import React, { useContext, useState } from 'react'; - -import RemoveCircleOutlineOutlined from '@mui/icons-material/RemoveCircleOutlineOutlined'; -import Box from '@mui/material/Box'; -import IconButton from '@mui/material/IconButton'; -import { SelectChangeEvent } from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; - -import { SelectInput } from 'components/atoms/inputs'; - -import type { - GenerationMessageRole, - IChatGeneration, - IGenerationMessage -} from 'client-types/'; - -const roles = ['Assistant', 'System', 'User']; - -interface MessageWrapperProps { - canSelectRole?: boolean; - children: React.ReactElement; - index?: number; - message?: IGenerationMessage; - role?: string; - name?: string; -} - -const MessageWrapper = ({ - canSelectRole, - children, - index, - message, - role, - name -}: MessageWrapperProps): JSX.Element => { - const [showSelectRole, setShowSelectRole] = useState(false); - const { setPlayground } = useContext(PlaygroundContext); - - const onRoleSelected = (event: SelectChangeEvent) => { - const role = event.target.value as GenerationMessageRole; - - if (role) { - setPlayground((old) => ({ - ...old, - generation: { - ...old!.generation!, - messages: (old!.generation! as IChatGeneration).messages?.map( - (message, mIndex) => ({ - ...message, - ...(mIndex === index ? { role } : {}) // Update role if it's the selected message - }) - ) - } - })); - } - - setShowSelectRole(false); - }; - - const onRemove = () => { - if (index !== undefined) { - setPlayground((old) => ({ - ...old, - generation: { - ...old!.generation!, - messages: [ - ...(old!.generation! as IChatGeneration).messages!.slice(0, index), - ...(old!.generation! as IChatGeneration).messages!.slice(index + 1) - ] - } - })); - } - }; - - const roleSelect = ( - - {!showSelectRole ? ( - canSelectRole && setShowSelectRole(true)} - color="text.primary" - sx={{ - p: 1, - borderRadius: 0.5, - marginTop: 1, - cursor: canSelectRole ? 'pointer' : 'default', - fontSize: '12px', - fontWeight: 700, - width: 'fit-content', - ...(canSelectRole && { - '&:hover': { - backgroundColor: (theme) => theme.palette.divider - } - }) - }} - > - {role} - - ) : ( - setShowSelectRole(false)} - defaultOpen - items={roles.map((role) => ({ - label: role, - value: role.toLowerCase() - }))} - id="role-select" - value={message?.role} - onChange={onRoleSelected} - sx={{ width: 'fit-content' }} - iconSx={{ - px: 0, - marginRight: '2px !important' - }} - /> - )} - {name ? ( - - {name} - - ) : null} - - ); - - return ( - - `1px solid ${theme.palette.divider}`, - borderRadius: 1 - }} - /> - theme.palette.background.paper - } - }} - > - {roleSelect} - {children} - {index !== undefined ? ( - - - - - - ) : null} - - - ); -}; - -export default MessageWrapper; diff --git a/frontend/src/components/molecules/playground/editor/completion.tsx b/frontend/src/components/molecules/playground/editor/completion.tsx deleted file mode 100644 index 56223870f5..0000000000 --- a/frontend/src/components/molecules/playground/editor/completion.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Editor, EditorState, Modifier, SelectionState } from 'draft-js'; -import { OrderedSet } from 'immutable'; -import { useEffect, useState } from 'react'; -import { grey } from 'theme'; - -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Box'; -import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; - -import ChevronDownIcon from 'assets/chevronDown'; -import ChevronUpIcon from 'assets/chevronUp'; - -import 'draft-js/dist/Draft.css'; - -import EditorWrapper from './EditorWrapper'; -import MessageWrapper from './MessageWrapper'; - -interface Props { - completion?: string; - chatMode?: boolean; -} - -export default function Completion({ completion, chatMode }: Props) { - const [state, setState] = useState(EditorState.createEmpty()); - const [isCompletionOpen, setCompletionOpen] = useState(true); - - useEffect(() => { - let _state = EditorState.createEmpty(); - if (completion) { - _state = insertCompletion(_state, completion); - } - - setState(_state); - setCompletionOpen(true); - }, [completion]); - - const insertCompletion = (state: EditorState, completion: string) => { - const contentState = state.getCurrentContent(); - - const blockMap = contentState.getBlockMap(); - const key = blockMap.last().getKey(); - const length = blockMap.last().getLength(); - const selection = new SelectionState({ - anchorKey: key, - anchorOffset: length, - focusKey: key, - focusOffset: length - }); - - const ncs = Modifier.insertText( - contentState, - selection, - completion, - OrderedSet.of('COMPLETION') - ); - const es = EditorState.push(state, ncs, 'insert-characters'); - return EditorState.forceSelection(es, ncs.getSelectionAfter()); - }; - - const renderEditor = () => ( - theme.palette.success.main, - '&:hover': { - borderColor: (theme) => theme.palette.success.main - } - }} - > - { - // Read only mode, force content but preserve selection - nextState = EditorState.push( - nextState, - state.getCurrentContent(), - 'insert-characters' - ); - setState(nextState); - }} - /> - - ); - - return !chatMode ? ( - - - - Completion - - setCompletionOpen(!isCompletionOpen)}> - {isCompletionOpen ? : } - - - `1px solid ${theme.palette.divider}`, - borderRadius: 1, - marginTop: 1 - }} - /> - - {renderEditor()} - - - ) : ( - {renderEditor()} - ); -} diff --git a/frontend/src/components/molecules/playground/editor/formatted.tsx b/frontend/src/components/molecules/playground/editor/formatted.tsx deleted file mode 100644 index d0c2f2b12f..0000000000 --- a/frontend/src/components/molecules/playground/editor/formatted.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { - ContentState, - Editor, - EditorState, - Modifier, - SelectionState -} from 'draft-js'; -import { OrderedSet } from 'immutable'; -import isEqual from 'lodash/isEqual'; -import merge from 'lodash/merge'; -import { useContext, useRef, useState } from 'react'; -import { useIsFirstRender } from 'usehooks-ts'; - -import EditorWrapper from 'components/molecules/playground/editor/EditorWrapper'; -import { - buildEscapeReplaceRegexp, - buildTemplatePlaceholderRegexp, - escape, - validateVariablePlaceholder -} from 'components/molecules/playground/helpers/format'; - -import { useColors } from 'hooks/useColors'; - -import type { IGeneration } from 'client-types/'; - -import 'draft-js/dist/Draft.css'; - -export interface IVariable { - name: string; - styleIndex: number; - content?: string; -} - -interface Props { - template?: string; - formatted?: string; - format: string; - inputs: IGeneration['variables']; - readOnly?: boolean; - onChange?: (state: EditorState) => void; - showTitle?: boolean; - sxEditorChildren?: any; -} - -function useCustomStyleMap() { - const colors = useColors(true); - - const customStyleMap: Record> = {}; - - for (let i = 0; i < colors.length; i++) { - customStyleMap[i.toString()] = { - background: colors[i], - borderRadius: '2px', - cursor: 'pointer' - }; - } - - return customStyleMap; -} - -/* This function takes a draftjs block content and matches all escaping and interpolation - * candidates using a regexp specific to the current template format. f-string example: - * "Hello this is a {{{{variable}}}}" would match "{{{{variable}}}}". - */ -function matchToEscapeOrReplace(text: string, format: string) { - const regexp = buildEscapeReplaceRegexp(format); - const matches: RegExpExecArray[] = []; - let match: RegExpExecArray | null; - - while ((match = regexp.exec(text)) !== null) { - if (match.index > -1) { - matches.push(match); - } - } - - return matches; -} - -// This function takes a text and tries to match a variable placeholder to replace -function matchVariable(text: string, variableName: string, format: string) { - // Get the regex based on the current template format - const regex = buildTemplatePlaceholderRegexp(variableName, format); - const match = regex.exec(text); - const matchedVariable = match?.[0]; - if (matchedVariable) { - // We found a variable candidate for instance {{{{variable}}}}". - // We now need to validate that we need to replace it. - const { ok } = validateVariablePlaceholder( - variableName, - matchedVariable, - format - ); - return { match: matchedVariable, ok }; - } else { - return { match: '', ok: false }; - } -} - -function formatTemplate( - state: EditorState, - variables: IVariable[], - format: string -) { - let contentState = state.getCurrentContent(); - let nextState = state; - - // Iterate each block in the editor. - // At this point the editor content is still the template - contentState.getBlockMap().forEach((contentBlock) => { - if (!contentBlock) { - return; - } - - const key = contentBlock.getKey(); - const text = contentBlock.getText(); - - // Get the substrings of the block to escape/replace - const ssmToEscapeOrReplace = matchToEscapeOrReplace(text, format); - - // We start with escaping - - // Each escaping will change the block text length. - // We need to keep track of the length diff (offset) to keep the escaping accurate. - let escapeOffset = 0; - - const ssmToEscapeOrReplaceWithVariable = ssmToEscapeOrReplace.map((ssm) => { - const ss = ssm[0]; - - let variableFound: { variable: IVariable; match: string } | undefined = - undefined; - // Iterate each variable and try to match it. - // If there is a match, flag it for replacement later on and break. - for (const variable of variables) { - const { match, ok } = matchVariable(ss, variable.name, format); - if (ok) { - variableFound = { variable, match }; - break; - } - } - - // start index of the selection to escape, accounting for offset - const startIndex = ssm.index + escapeOffset; - // end index of the selection to scape, accounting for offset - const endIndex = startIndex + ss.length; - - // Define the selection of the template to escape - const selectionToEscape = new SelectionState({ - anchorKey: key, - anchorOffset: startIndex, - focusKey: key, - focusOffset: endIndex - }); - - // Escape the substring - const content = escape(ss, format); - - // Update the offset (new value length - old value length) - escapeOffset += content.length - ss.length; - - // Perform the replace operation - contentState = Modifier.replaceText( - contentState, - selectionToEscape, - content - ); - nextState = EditorState.push(state, contentState, 'apply-entity'); - - // Update the substring match since we just updated it - ssm[0] = content; - ssm.index = startIndex; - - return { ssm, variableFound }; - }); - - // At this point the template has been escaped - // We now perform replace operations - - // Each replace will change the block text length. - // We need to keep track of the length diff (offset) to keep the replace accurate. - let replaceOffset = 0; - - ssmToEscapeOrReplaceWithVariable.forEach(({ ssm, variableFound }) => { - if (!variableFound) { - // Nothing to replace - return; - } - - const { variable } = variableFound; - - const ss = ssm[0]; - - // It is important to preserve the selection to keep the text selectable (copy paste for instance) - const currentSelection = nextState.getSelection(); - - // We know the variable is here but we need to know the exact range to replace - // for instance {{{var1}}} was escaped to {{var1}} so we need to replace {var1} - const { localEndIndex, localStartIndex } = validateVariablePlaceholder( - variable.name, - ss, - format - ); - - // The start of the range is the - // start index of the whole variable + the local start index - // of the exact variable match + the offset - const startIndex = ssm.index + localStartIndex + replaceOffset; - - // Same for the end index - const endIndex = ssm.index + localEndIndex + replaceOffset; - - // Define the selection to replace - const selectionToHighlight = new SelectionState({ - anchorKey: key, - anchorOffset: startIndex, - focusKey: key, - focusOffset: endIndex - }); - - const content = variable.content || ''; - - // Update the offset - replaceOffset += content.length - (localEndIndex - localStartIndex); - - // Perform the replace operation - contentState = nextState.getCurrentContent(); - contentState = contentState.createEntity('TOKEN', 'SEGMENTED', variable); - const entityKey = contentState.getLastCreatedEntityKey(); - contentState = Modifier.replaceText( - contentState, - selectionToHighlight, - content, - OrderedSet.of(variable.styleIndex.toString()), - entityKey - ); - nextState = EditorState.push(nextState, contentState, 'apply-entity'); - nextState = EditorState.forceSelection(nextState, currentSelection); - }); - }); - - return nextState; -} - -function getEntityAtSelection(editorState: EditorState) { - const selectionState = editorState.getSelection(); - const selectionKey = selectionState.getStartKey(); - const contentstate = editorState.getCurrentContent(); - - // The block in which the selection starts - const block = contentstate.getBlockForKey(selectionKey); - - if (!block) { - return; - } - - // Entity key at the start selection - const entityKey = block.getEntityAt(selectionState.getStartOffset()); - if (entityKey) { - // The actual entity instance - const entityInstance = contentstate.getEntity(entityKey); - const entityInfo = { - type: entityInstance.getType(), - mutability: entityInstance.getMutability(), - data: entityInstance.getData() - }; - return entityInfo; - } -} - -export default function FormattedEditor({ - template, - formatted, - inputs, - format, - readOnly, - onChange, - showTitle = false, - sxEditorChildren -}: Props) { - const editorRef = useRef(null); - const { setVariableName, onNotification } = useContext(PlaygroundContext); - - const [state, setState] = useState(); - const [prevInputs, setPrevInputs] = useState>(); - - const customStyleMap = useCustomStyleMap(); - const isFirstRender = useIsFirstRender(); - - if (isFirstRender || !isEqual(inputs, prevInputs)) { - if (typeof template === 'string') { - inputs = inputs || {}; - - const variableNames = Object.keys(inputs); - const variables: IVariable[] = []; - - for (let i = 0; i < variableNames.length; i++) { - const variableName = variableNames[i]; - - const variableContent = inputs[variableName]; - - variables.push({ - name: variableName, - styleIndex: i, - content: variableContent - }); - } - - const sortedVariables = variables.sort( - (a, b) => b.name.length - a.name.length - ); - - const state = EditorState.createWithContent( - ContentState.createFromText(template) - ); - const nextState = formatTemplate(state, sortedVariables, format); - - setState(nextState); - } else if (typeof formatted === 'string') { - const nextState = EditorState.createWithContent( - ContentState.createFromText(formatted) - ); - setState(nextState); - } - setPrevInputs(inputs); - } - - const handleOnEditorChange = (nextState: EditorState) => { - const hasFocus = nextState.getSelection().getHasFocus(); - const isCollapsed = nextState.getSelection().isCollapsed(); - const entity = getEntityAtSelection(nextState); - if (entity && hasFocus && isCollapsed && editorRef.current) { - // Open the variable modal - setVariableName(entity.data.name); - - // If we do not blur the selection stay the same - // And we keep opening the variable - editorRef.current.blur(); - } - - if (!readOnly) { - // update editor - onChange && onChange(nextState); - } else if (state) { - const currentContent = state.getCurrentContent(); - const nextContent = nextState.getCurrentContent(); - - if (currentContent !== nextContent) { - onNotification('error', 'Formatted prompt is read only.'); - } - - // Read only mode, force content but preserve selection - nextState = EditorState.push( - nextState, - currentContent, - 'insert-characters' - ); - } - setState(nextState); - }; - - if (!state) { - return null; - } - - const title = readOnly ? 'Formatted prompt [Read Only]' : 'Formatted prompt'; - - return ( - - - - ); -} diff --git a/frontend/src/components/molecules/playground/editor/functionModal.tsx b/frontend/src/components/molecules/playground/editor/functionModal.tsx deleted file mode 100644 index 6f5e23103b..0000000000 --- a/frontend/src/components/molecules/playground/editor/functionModal.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { useFormik } from 'formik'; -import { useContext } from 'react'; -import { grey } from 'theme'; -import * as yup from 'yup'; - -import Box from '@mui/material/Box'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { useTheme } from '@mui/material/styles'; - -import { AccentButton } from 'components/atoms/buttons'; -import { TextInput } from 'components/atoms/inputs'; - -const FunctionModal = (): JSX.Element | null => { - const { setPlayground, playground, functionIndex, setFunctionIndex } = - useContext(PlaygroundContext); - - const theme = useTheme(); - const hasIndex = functionIndex !== undefined; - const functions = playground?.generation?.tools || []; - const fn = hasIndex ? functions[functionIndex].function : undefined; - - const updateFunction = (name: string, description: string) => { - if (functionIndex !== undefined && fn) { - const nextFn = { ...fn, name, description }; - setPlayground((old) => { - if (!old?.generation) return old; - - return { - ...old, - generation: { - ...old.generation, - tools: old.generation.tools - ? old.generation.tools.map((tool, index) => - index === functionIndex ? { ...tool, function: nextFn } : tool - ) - : undefined - } - }; - }); - setFunctionIndex(undefined); - } - }; - - const formik = useFormik({ - initialValues: { - name: fn?.name, - description: fn?.description - }, - validationSchema: yup.object({ - name: yup.string().required(), - description: yup.string().required() - }), - enableReinitialize: true, - onSubmit: async (values) => { - updateFunction(values.name!, values.description!); - } - }); - - return ( - setFunctionIndex(undefined)} - fullWidth - maxWidth="md" - sx={{ - border: (theme) => - theme.palette.mode === 'dark' ? `1px solid ${grey[800]}` : null, - borderRadius: 1 - }} - > - - - - {`Edit function ${fn?.name}`} - - - - - formik.setFieldValue('name', e.target.value)} - value={formik.values['name']} - hasError={!!formik.errors.name} - /> - - formik.setFieldValue('description', e.target.value) - } - value={formik.values['description']} - hasError={!!formik.errors.description} - /> - - - - formik.submitForm()} - > - Save - - - - - ); -}; - -export default FunctionModal; diff --git a/frontend/src/components/molecules/playground/editor/promptMessage.tsx b/frontend/src/components/molecules/playground/editor/promptMessage.tsx deleted file mode 100644 index 2a07a02cb7..0000000000 --- a/frontend/src/components/molecules/playground/editor/promptMessage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { EditorState } from 'draft-js'; - -import Alert from '@mui/material/Alert'; -import { useTheme } from '@mui/material/styles'; - -import type { IChatGeneration, IGenerationMessage } from 'client-types/'; -import { PromptMode } from 'types/playground'; - -import MessageWrapper from './MessageWrapper'; -import FormattedEditor from './formatted'; - -interface Props { - message: IGenerationMessage; - generation: IChatGeneration; - mode: PromptMode; - index: number; - onChange: (index: number, nextState: EditorState) => void; -} - -export default function PromptMessage({ - message, - generation, - mode, - index, - onChange -}: Props) { - const theme = useTheme(); - - const templateProps = { - inputs: generation.variables || {}, - format: 'f-string', - sxEditorChildren: { - padding: theme.spacing(2), - backgroundColor: '', - '&:hover': { - background: theme.palette.background.paper - } - } - }; - - const renderFormatted = () => { - if (typeof message.content === 'string') { - return ( - onChange(index, state)} - formatted={message.content} - readOnly={false} - showTitle={false} - /> - ); - } - - return ( - - Neither template or formatted prompt provided. - - ); - }; - - return ( - - <>{mode === 'Formatted' ? renderFormatted() : null} - - ); -} diff --git a/frontend/src/components/molecules/playground/editor/template/index.tsx b/frontend/src/components/molecules/playground/editor/template/index.tsx deleted file mode 100644 index 54a797b18a..0000000000 --- a/frontend/src/components/molecules/playground/editor/template/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { - CompositeDecorator, - ContentBlock, - ContentState, - DraftDecorator, - Editor, - EditorState -} from 'draft-js'; -import { useState } from 'react'; -import { useIsFirstRender } from 'usehooks-ts'; - -import EditorWrapper from 'components/molecules/playground/editor/EditorWrapper'; -import { - buildTemplatePlaceholdersRegexp, - validateVariablePlaceholder -} from 'components/molecules/playground/helpers/format'; - -import type { IGeneration } from 'client-types/'; - -import Variable from './variable'; - -const findVariable = ( - regex: RegExp | undefined, - format: string, - contentBlock: ContentBlock, - callback: (start: number, end: number) => void -) => { - if (!regex) { - return; - } - const text = contentBlock.getText(); - let matchArr: RegExpExecArray | null; - while ((matchArr = regex.exec(text)) !== null) { - const { ok, localEndIndex, localStartIndex } = validateVariablePlaceholder( - matchArr[1], - matchArr[0], - format - ); - if (!ok) { - continue; - } - const start = matchArr.index + localStartIndex; - const end = matchArr.index + localEndIndex; - callback(start, end); - } -}; - -interface Props { - inputs: IGeneration['variables']; - format: string; - template: string; - onChange(nextState: EditorState): void; - showTitle?: boolean; - sxEditorChildren?: any; -} - -export default function TemplateEditor({ - inputs, - format, - template, - onChange, - showTitle = true, - sxEditorChildren -}: Props) { - const [state, setState] = useState(); - const isFirstRender = useIsFirstRender(); - - if (isFirstRender) { - const contentState = ContentState.createFromText(template); - const variableDecorator: DraftDecorator = { - strategy: (contentBlock, callback) => { - findVariable( - buildTemplatePlaceholdersRegexp(inputs || {}, format), - format, - contentBlock, - callback - ); - }, - component: Variable - }; - - const decorators = new CompositeDecorator([variableDecorator]); - setState(EditorState.createWithContent(contentState, decorators)); - } - - if (!state) { - return null; - } - - return ( - - { - setState(nextState); - onChange && onChange(nextState); - }} - /> - - ); -} diff --git a/frontend/src/components/molecules/playground/editor/template/variable.tsx b/frontend/src/components/molecules/playground/editor/template/variable.tsx deleted file mode 100644 index 15a5318802..0000000000 --- a/frontend/src/components/molecules/playground/editor/template/variable.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import React, { useContext, useEffect, useState } from 'react'; - -import Tooltip from '@mui/material/Tooltip'; - -import { buildVariablePlaceholder } from 'components/molecules/playground/helpers/format'; - -import { useColors } from 'hooks/useColors'; - -interface Props { - decoratedText: string; -} - -function truncate(str: string, n = 200) { - return str.length > n ? str.slice(0, n - 1) + '...' : str; -} - -export default function Variable({ - children, - decoratedText -}: React.PropsWithChildren) { - const { setVariableName, playground } = useContext(PlaygroundContext); - const colors = useColors(true); - const [variableIndex, setVariableIndex] = useState(); - const [styles, setStyles] = useState({}); - - const generation = playground?.generation; - - useEffect(() => { - if (generation?.variables && decoratedText) { - const index = Object.keys(generation.variables).findIndex( - (name) => buildVariablePlaceholder(name, 'f-string') === decoratedText - ); - if (index > -1) { - setVariableIndex(index); - const colorIndex = index % (colors.length - 1); - setStyles({ - backgroundColor: colors[colorIndex], - borderRadius: '2px', - cursor: 'pointer' - }); - } - } - }, [colors, decoratedText, generation]); - - if (!generation) { - return null; - } - - const [varName, varValue] = - variableIndex !== undefined - ? Object.entries(generation.variables || {})[variableIndex] - : []; - - return ( - - setVariableName(varName)} - > - {children} - - - ); -} diff --git a/frontend/src/components/molecules/playground/editor/variableModal.tsx b/frontend/src/components/molecules/playground/editor/variableModal.tsx deleted file mode 100644 index ecf3386127..0000000000 --- a/frontend/src/components/molecules/playground/editor/variableModal.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { ContentState, Editor, EditorState } from 'draft-js'; -import { useContext, useEffect, useState } from 'react'; -import { grey } from 'theme'; - -import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import Typography from '@mui/material/Typography'; -import { useTheme } from '@mui/material/styles'; - -import { AccentButton } from 'components/atoms/buttons'; - -import EditorWrapper from './EditorWrapper'; - -const VariableModal = (): JSX.Element | null => { - const [state, setState] = useState(); - const { setPlayground, playground, variableName, setVariableName } = - useContext(PlaygroundContext); - - const theme = useTheme(); - - useEffect(() => { - if (variableName && playground?.generation?.variables) { - setState( - EditorState.createWithContent( - ContentState.createFromText( - playground.generation.variables[variableName] - ) - ) - ); - } - }, [variableName]); - - const updateVariable = () => { - if (variableName) { - setPlayground((old) => { - if (!old?.generation) return old; - - return { - ...old, - generation: { - ...old.generation, - inputs: { - ...old?.generation?.variables, - [variableName]: state?.getCurrentContent().getPlainText() || '' - } - } - }; - }); - setVariableName(undefined); - } - }; - - const resetVariableName = () => { - setVariableName(undefined); - }; - - if (!variableName) return null; - - return ( - - theme.palette.mode === 'dark' ? `1px solid ${grey[800]}` : null, - borderRadius: 1 - }} - > - - - - {`Edit ${variableName}`} - - - - - Editing a variable will update its value in the formatted view. If - you want to update the template instead, go to the template view. - - {state ? ( - - ({ - color: theme.palette.text.primary, - padding: '2px' - })} - editorState={state} - onChange={(nextState) => { - nextState && setState(nextState); - }} - /> - - ) : null} - - - - Save - - - - - ); -}; - -export default VariableModal; diff --git a/frontend/src/components/molecules/playground/functionInput.tsx b/frontend/src/components/molecules/playground/functionInput.tsx deleted file mode 100644 index d73f914334..0000000000 --- a/frontend/src/components/molecules/playground/functionInput.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { useContext } from 'react'; - -import { SelectInput } from 'components/atoms/inputs/selects/SelectInput'; - -const FunctionInput = (): JSX.Element | null => { - const { functionIndex, setFunctionIndex, playground } = - useContext(PlaygroundContext); - - const functions = playground?.generation?.tools || []; - - const options = functions.map((fn, index) => ({ - label: fn.function.name, - value: index.toString() - })); - - return functions?.length > 0 ? ( - setFunctionIndex(parseInt(e.target.value, 10))} - sx={{ maxWidth: '270px' }} - /> - ) : null; -}; - -export default FunctionInput; diff --git a/frontend/src/components/molecules/playground/header.tsx b/frontend/src/components/molecules/playground/header.tsx deleted file mode 100644 index a53edeb686..0000000000 --- a/frontend/src/components/molecules/playground/header.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { grey } from 'theme'; - -import CloseIcon from '@mui/icons-material/Close'; -import TuneIcon from '@mui/icons-material/Tune'; -import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; - -import { AccentButton } from 'components/atoms/buttons'; - -import FunctionInput from './functionInput'; -import VariableInput from './variableInput'; - -interface Props { - hasTemplate?: boolean; - showToggleDrawerButton?: boolean; - toggleDrawer: () => void; - handleClose: () => void; -} - -export default function PlaygroundHeader({ - showToggleDrawerButton, - toggleDrawer, - handleClose -}: Props) { - return ( - - - {/* */} - - - - - - Help - - - {showToggleDrawerButton ? ( - - - - ) : null} - - - - - - ); -} diff --git a/frontend/src/components/molecules/playground/helpers/format.ts b/frontend/src/components/molecules/playground/helpers/format.ts deleted file mode 100644 index b7e6b6755a..0000000000 --- a/frontend/src/components/molecules/playground/helpers/format.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Helper function to match the placeholders for a given variable in the template -export function buildTemplatePlaceholderRegexp( - variable: string, - format: string -) { - switch (format) { - case 'f-string': { - return new RegExp(`\\{+(${variable}+)\\}+`, 'g'); - } - default: - throw new Error(`Unsupported template format ${format}`); - } -} - -// Helper function to match the placeholders for a all variables in the template -export function buildTemplatePlaceholdersRegexp( - inputs: object, - format: string -) { - const variables = Object.keys(inputs).sort((a, b) => b.length - a.length); - if (!variables.length) { - return undefined; - } - switch (format) { - case 'f-string': { - // Create a regex pattern from the variables array - const regexPattern = variables.map((v) => `${v}`).join('|'); - return buildTemplatePlaceholderRegexp(regexPattern, format); - } - default: - throw new Error(`Unsupported template format ${format}`); - } -} - -// Helper function to escape the template -export function escape(str: string, format: string) { - switch (format) { - case 'f-string': { - str = str.replaceAll('{{', '{'); - str = str.replaceAll('}}', '}'); - return str; - } - default: - throw new Error(`Unsupported template format ${format}`); - } -} - -// Helper function to match all substrings to escape and or replace -export function buildEscapeReplaceRegexp(format: string) { - switch (format) { - case 'f-string': { - // Match wrapped by {} or opening or closing braces - return /\{+([^{}]+)\}+|{{|}}/g; - } - default: - throw new Error(`Unsupported template format ${format}`); - } -} - -// Helper function to build the template placeholder of a variable -export function buildVariablePlaceholder(variable: string, format: string) { - switch (format) { - case 'f-string': { - return `{${variable}}`; - } - default: - throw new Error(`Unsupported template format ${format}`); - } -} - -export function validateVariablePlaceholder( - variableName: string, - match: string, - format: string -) { - switch (format) { - case 'f-string': { - // leading curly braces - const prefixBracesCount = match.split(variableName)[0].length; - // tailing curly braces - const suffixBracesCount = match.split(variableName)[1].length; - const isOdd = prefixBracesCount % 2; - const ok = isOdd && prefixBracesCount === suffixBracesCount; - const placeholder = buildVariablePlaceholder(variableName, format); - const localStartIndex = match.indexOf(placeholder); - const localEndIndex = localStartIndex + placeholder.length; - return { - ok, - localStartIndex, - localEndIndex - }; - } - default: - throw new Error(`Unsupported template format ${format}`); - } -} diff --git a/frontend/src/components/molecules/playground/helpers/provider.ts b/frontend/src/components/molecules/playground/helpers/provider.ts deleted file mode 100644 index 8ba9b5a0f6..0000000000 --- a/frontend/src/components/molecules/playground/helpers/provider.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ILLMProvider, IPlayground } from 'types/playground'; - -const getProviders = (playground: IPlayground) => { - const providers = playground?.providers || []; - - if (!providers?.length) { - throw new Error('No LLM provider available'); - } - - let provider = providers.find( - (provider) => provider.id === playground.generation?.provider - ); - - const providerFound = !!provider; - - provider = provider || providers[0]; - - return { - provider, - providerFound, - providers - }; -}; - -const getDefaultSettings = (providerId: string, providers?: ILLMProvider[]) => { - if (!providers || providers.length === 0) return {}; - - const defaultSettings: { [key: string]: any } = {}; - const provider = providers?.find((provider) => provider.id === providerId); - - provider?.inputs?.forEach( - (input) => (defaultSettings[input.id] = input.initial) - ); - - return defaultSettings; -}; - -export { getDefaultSettings, getProviders }; diff --git a/frontend/src/components/molecules/playground/index.tsx b/frontend/src/components/molecules/playground/index.tsx deleted file mode 100644 index d5e9bf62c8..0000000000 --- a/frontend/src/components/molecules/playground/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { useContext, useRef, useState } from 'react'; -import { useToggle } from 'usehooks-ts'; - -import RestoreIcon from '@mui/icons-material/Restore'; -import { Chip } from '@mui/material'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; -import Tooltip from '@mui/material/Tooltip'; -import { useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; - -import { ErrorBoundary } from 'components/atoms/ErrorBoundary'; - -import { IPlayground } from 'types/playground'; -import { IPlaygroundContext } from 'types/playgroundContext'; - -import ActionBar from './actionBar'; -import BasicPromptPlayground from './basic'; -import ChatPromptPlayground from './chat'; -import FunctionModal from './editor/functionModal'; -import VariableModal from './editor/variableModal'; -import PlaygroundHeader from './header'; -import ModelSettings from './modelSettings'; -import SubmitButton from './submitButton'; - -interface Props { - context: IPlaygroundContext; -} - -function _PromptPlayground() { - const { playground, setPlayground } = useContext(PlaygroundContext); - const [restoredTime, setRestoredTime] = useState(0); - const [isDrawerOpen, toggleDrawer] = useToggle(false); - const chatPromptScrollRef = useRef(null); - - const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); - - const showToggleDrawerButton = isSmallScreen && !!playground?.providers; - - const restore = () => { - if (playground?.generation) { - setPlayground((old?: IPlayground) => ({ - ...old, - generation: old?.originalGeneration - })); - setRestoredTime((old) => old + 1); - } - }; - - const handleClose = () => { - setPlayground((old) => ({ - ...old, - generation: undefined, - originalGeneration: undefined - })); - }; - - const generation = playground?.generation; - - const isChat = generation?.type === 'CHAT'; - - const hasTemplate = false; - - if (!generation) { - return null; - } - - return ( - - - - - - - - - - - {isChat ? ( - - ) : ( - - )} - - - - - - - - {generation.tokenCount ? ( - - ) : null} - - - - - - { - if (!chatPromptScrollRef?.current) return; - - chatPromptScrollRef.current.scrollTop = - chatPromptScrollRef.current.scrollHeight; - }} - /> - - - ); -} - -const PromptPlayground = ({ context }: Props) => { - return ( - - <_PromptPlayground /> - - ); -}; - -export { PromptPlayground }; diff --git a/frontend/src/components/molecules/playground/modeToggle.tsx b/frontend/src/components/molecules/playground/modeToggle.tsx deleted file mode 100644 index 818a0c6312..0000000000 --- a/frontend/src/components/molecules/playground/modeToggle.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { useContext } from 'react'; - -import Tooltip from '@mui/material/Tooltip'; - -import { Toggle } from 'components/atoms/buttons'; - -import { PromptMode } from 'types/playground'; - -interface Props { - hasTemplate?: boolean; -} -export default function PromptModeToggle({ hasTemplate }: Props) { - const { setPromptMode, promptMode } = useContext(PlaygroundContext); - const disabled = !hasTemplate; - - const toggle = ( - setPromptMode(v as PromptMode)} - /> - ); - - if (disabled) { - return ( - - {toggle} - - ); - } else { - return toggle; - } -} diff --git a/frontend/src/components/molecules/playground/modelSettings.tsx b/frontend/src/components/molecules/playground/modelSettings.tsx deleted file mode 100644 index 9a1029eae5..0000000000 --- a/frontend/src/components/molecules/playground/modelSettings.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import { useFormik } from 'formik'; -import cloneDeep from 'lodash/cloneDeep'; -import merge from 'lodash/merge'; -import { useContext, useEffect } from 'react'; -import * as yup from 'yup'; - -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; -import Drawer from '@mui/material/Drawer'; -import IconButton from '@mui/material/IconButton'; -import { SelectChangeEvent } from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; - -import { FormInput, SelectInput, TFormInput } from 'components/atoms/inputs'; -import { getProviders } from 'components/molecules/playground/helpers/provider'; - -import type { ILLMSettings } from 'client-types/'; -import { ILLMProvider } from 'types/playground'; - -type Schema = { - [key: string]: yup.Schema; -}; - -interface IFormProps { - settings: ILLMSettings; - schema: Schema; - provider: ILLMProvider; - providers: ILLMProvider[]; - providerWarning: JSX.Element | null; - providerTooltip?: string; -} - -const SettingsForm = ({ - settings, - schema, - provider, - providers, - providerTooltip, - providerWarning -}: IFormProps) => { - const { setPlayground } = useContext(PlaygroundContext); - - const formik = useFormik({ - initialValues: settings, - validationSchema: schema, - enableReinitialize: true, - onSubmit: async () => undefined - }); - - useEffect(() => { - const debounceTimeout = setTimeout(() => { - setPlayground((old) => ({ - ...old, - generation: { - ...old!.generation!, - settings: formik.values - } - })); - }, 500); - - return () => { - clearTimeout(debounceTimeout); - }; - }, [formik.values]); - - const onSelectedProviderChange = (event: SelectChangeEvent) => { - setPlayground((old) => - merge(cloneDeep(old), { - generation: { - provider: event.target.value - } - }) - ); - }; - - return ( - - - Settings - - - ({ - label: provider.name, - value: provider.id - }))} - id="llm-providers" - value={provider.id} - label="LLM Provider" - tooltip={providerTooltip} - onChange={onSelectedProviderChange} - /> - {providerWarning} - {provider.inputs.map((input: TFormInput, index: number) => ( - - - - ))} - - - ); -}; - -const ModelSettings = () => { - const { playground } = useContext(PlaygroundContext); - - if (!playground || !playground?.providers) { - return null; - } - - const { provider, providerFound, providers } = getProviders(playground); - - if (!provider) { - return null; - } - - const buildProviderTooltip = () => { - if (provider.is_chat && playground.generation?.type !== 'CHAT') { - return `${provider.name} is chat-based. This prompt will be wrapped in a message before being sent to ${provider.name}.`; - } else if (!provider.is_chat && playground.generation?.type === 'CHAT') { - return `${provider.name} is completion-based. The messages will converted to a single prompt before being sent to ${provider.name}.`; - } else { - return undefined; - } - }; - - const providerWarning = !providerFound ? ( - - {playground.generation?.provider - ? `${playground?.generation?.provider} provider is not found, using - ${provider.name} instead.` - : `Provider not specified, using ${provider.name} instead.`} - - ) : null; - - const settings: ILLMSettings = {}; - const currentSettings = playground?.generation?.settings || {}; - const origSettings = playground?.originalGeneration?.settings || {}; - - const isSettingCompatible = ( - value: string | number | boolean | string[], - input: TFormInput - ) => { - if (input.type === 'select') { - return !!input?.items?.find((i) => i.value === value); - } - return true; - }; - - const schema = yup.object( - provider.inputs.reduce((object: Schema, input: TFormInput) => { - const settingValue = - currentSettings[input.id] !== undefined - ? currentSettings[input.id] - : origSettings[input.id]; - - if ( - settingValue !== undefined && - isSettingCompatible(settingValue, input) - ) { - settings[input.id] = settingValue; - } else if (input.initial !== undefined) { - settings[input.id] = input.initial; - } - - switch (input.type) { - case 'select': - object[input.id] = yup.string(); - break; - case 'slider': { - let schema = yup.number(); - if (input.min) { - schema = schema.min(input.min); - } - if (input.max) { - schema = schema.max(input.max); - } - object[input.id] = schema; - break; - } - case 'tags': - object[input.id] = yup.array().of(yup.string()); - break; - } - - return object; - }, {}) - ); - - return ( - - ); -}; - -interface Props { - isSmallScreen: boolean; - isDrawerOpen: boolean; - toggleDrawer: () => void; -} - -export default function ResponsiveModelSettings({ - isSmallScreen, - isDrawerOpen, - toggleDrawer -}: Props) { - return !isSmallScreen ? ( - - - - ) : ( - - - - - - - - - - - ); -} diff --git a/frontend/src/components/molecules/playground/submitButton.tsx b/frontend/src/components/molecules/playground/submitButton.tsx deleted file mode 100644 index 8941d0d0ac..0000000000 --- a/frontend/src/components/molecules/playground/submitButton.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import cloneDeep from 'lodash/cloneDeep'; -import { useContext, useState } from 'react'; - -import { AccentButton, RegularButton } from 'components/atoms/buttons'; -import { getProviders } from 'components/molecules/playground/helpers/provider'; - -export default function SubmitButton({ onSubmit }: { onSubmit: () => void }) { - const [completionController, setCompletionController] = useState< - AbortController | undefined - >(); - const { playground, setPlayground, createCompletion } = - useContext(PlaygroundContext); - - if (!playground || !playground.providers || !createCompletion) { - return null; - } - - const submit = async () => { - try { - const { provider } = getProviders(playground); - const generation = cloneDeep(playground.generation)!; - generation.provider = provider.id; - const controller = new AbortController(); - - setCompletionController(controller); - setPlayground((old) => { - if (!old?.generation) return old; - - if (old.generation.type === 'CHAT') { - return { - ...old, - generation: { - ...old.generation!, - messageCompletion: { - ...(old.generation?.messageCompletion || { role: 'assistant' }), - content: '' - } - } - }; - } else { - return { - ...old, - generation: { - ...old.generation!, - completion: '' - } - }; - } - }); - - await createCompletion( - generation, - controller, - (done: boolean, token: string) => { - onSubmit && onSubmit(); - - if (done) { - setCompletionController(undefined); - return; - } - setPlayground((old) => { - if (!old?.generation) return old; - if (old.generation.type === 'CHAT') { - return { - ...old, - generation: { - ...old.generation!, - messageCompletion: { - ...(old.generation?.messageCompletion || { - role: 'assistant' - }), - content: - (old.generation?.messageCompletion?.content || '') + token - } - } - }; - } else { - return { - ...old, - generation: { - ...old.generation!, - completion: (old.generation?.completion || '') + token - } - }; - } - }); - } - ); - } catch (err) { - setCompletionController(undefined); - } - }; - - if (completionController) { - return ( - { - completionController.abort(); - setCompletionController(undefined); - }} - > - Cancel - - ); - } else { - return ( - - Submit - - ); - } -} diff --git a/frontend/src/components/molecules/playground/variableInput.tsx b/frontend/src/components/molecules/playground/variableInput.tsx deleted file mode 100644 index 091f50e9fb..0000000000 --- a/frontend/src/components/molecules/playground/variableInput.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { PlaygroundContext } from 'contexts/PlaygroundContext'; -import map from 'lodash/map'; -import { useContext } from 'react'; - -import { SelectInput } from 'components/atoms/inputs/selects/SelectInput'; - -const VariableInput = (): JSX.Element | null => { - const { variableName, setVariableName, playground } = - useContext(PlaygroundContext); - - const variables = map(playground?.generation?.variables, (input, index) => ({ - label: index, - value: index - })); - - return variables?.length > 0 ? ( - setVariableName(e.target.value)} - sx={{ maxWidth: '270px' }} - /> - ) : null; -}; - -export default VariableInput; diff --git a/frontend/src/components/molecules/settingsModal.tsx b/frontend/src/components/molecules/settingsModal.tsx deleted file mode 100644 index dbac2465eb..0000000000 --- a/frontend/src/components/molecules/settingsModal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useRecoilState, useRecoilValue } from 'recoil'; - -import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'; -import EmojiObjectsIcon from '@mui/icons-material/EmojiObjects'; -import ExpandIcon from '@mui/icons-material/Expand'; -import { - Box, - Dialog, - DialogContent, - List, - ListItem, - ListItemIcon, - ListItemText, - ListSubheader -} from '@mui/material'; - -import { SwitchInput } from 'components/atoms/inputs/SwitchInput'; -import { Translator } from 'components/i18n'; - -import { projectSettingsState } from 'state/project'; -import { settingsState } from 'state/settings'; - -export default function SettingsModal() { - const projectSettings = useRecoilValue(projectSettingsState); - const [settings, setSettings] = useRecoilState(settingsState); - - return ( - setSettings((old) => ({ ...old, open: false }))} - id="settings-dialog" - PaperProps={{ - sx: { - backgroundImage: 'none' - } - }} - > - - - - - } - > - - - - - - } - /> - - - setSettings((old) => ({ ...old, expandAll: !old.expandAll })) - } - checked={settings.expandAll} - inputProps={{ - 'aria-labelledby': 'switch-expand-all' - }} - /> - - - {projectSettings?.ui.hide_cot ? null : ( - - - - - - } - /> - - - setSettings((old) => ({ ...old, hideCot: !old.hideCot })) - } - checked={settings.hideCot} - inputProps={{ - 'aria-labelledby': 'hide-cot' - }} - /> - - - )} - - - - - - } - /> - - { - const variant = settings.theme === 'light' ? 'dark' : 'light'; - localStorage.setItem('themeVariant', variant); - setSettings((old) => ({ ...old, theme: variant })); - }} - checked={settings.theme === 'dark'} - inputProps={{ - 'aria-labelledby': 'switch-theme' - }} - /> - - - - - - ); -} diff --git a/frontend/src/components/organisms/chat/Messages/container.tsx b/frontend/src/components/organisms/chat/Messages/container.tsx index c974e0158d..1674d58b51 100644 --- a/frontend/src/components/organisms/chat/Messages/container.tsx +++ b/frontend/src/components/organisms/chat/Messages/container.tsx @@ -6,12 +6,9 @@ import { toast } from 'sonner'; import { IAction, IAsk, - IAvatarElement, IFeedback, - IFunction, IMessageElement, IStep, - ITool, useChatInteract } from '@chainlit/react-client'; import { sideViewState } from '@chainlit/react-client'; @@ -19,7 +16,6 @@ import { sideViewState } from '@chainlit/react-client'; import { MessageContainer as CMessageContainer } from 'components/molecules/messages/MessageContainer'; import { apiClientState } from 'state/apiClient'; -import { playgroundState } from 'state/playground'; import { highlightMessage } from 'state/project'; import { projectSettingsState } from 'state/project'; import { settingsState } from 'state/settings'; @@ -28,10 +24,8 @@ interface Props { loading: boolean; actions: IAction[]; elements: IMessageElement[]; - avatars: IAvatarElement[]; messages: IStep[]; askUser?: IAsk; - autoScroll?: boolean; onFeedbackUpdated: ( message: IStep, onSuccess: () => void, @@ -43,26 +37,21 @@ interface Props { feedback: string ) => void; callAction?: (action: IAction) => void; - setAutoScroll?: (autoScroll: boolean) => void; } const MessageContainer = memo( ({ askUser, loading, - avatars, actions, - autoScroll, elements, messages, onFeedbackUpdated, onFeedbackDeleted, - callAction, - setAutoScroll + callAction }: Props) => { const appSettings = useRecoilValue(settingsState); const projectSettings = useRecoilValue(projectSettingsState); - const setPlayground = useSetRecoilState(playgroundState); const setSideView = useSetRecoilState(sideViewState); const highlightedMessage = useRecoilValue(highlightMessage); const { uploadFile: _uploadFile } = useChatInteract(); @@ -79,42 +68,6 @@ const MessageContainer = memo( const navigate = useNavigate(); - const onPlaygroundButtonClick = useCallback( - (message: IStep) => { - setPlayground((old) => { - const generation = message.generation; - let functions = - (generation?.settings?.functions as unknown as IFunction[]) || []; - const tools = - (generation?.settings?.tools as unknown as ITool[]) || []; - if (tools.length) { - functions = [ - ...functions, - ...tools - .filter((t) => t.type === 'function') - .map((t) => t.function) - ]; - } - return { - ...old, - generation: generation - ? { - ...generation, - functions - } - : undefined, - originalGeneration: generation - ? { - ...generation, - functions - } - : undefined - }; - }); - }, - [setPlayground] - ); - const onElementRefClick = useCallback( (element: IMessageElement) => { let path = `/element/${element.id}`; @@ -160,10 +113,7 @@ const MessageContainer = memo( askUser, allowHtml: projectSettings?.features?.unsafe_allow_html, latex: projectSettings?.features?.latex, - avatars, defaultCollapseContent: appSettings.defaultCollapseContent, - expandAll: appSettings.expandAll, - hideCot: appSettings.hideCot, highlightedMessage, loading, showFeedbackButtons: enableFeedback, @@ -171,15 +121,11 @@ const MessageContainer = memo( onElementRefClick, onError, onFeedbackUpdated, - onFeedbackDeleted, - onPlaygroundButtonClick + onFeedbackDeleted }; }, [ appSettings.defaultCollapseContent, - appSettings.expandAll, - appSettings.hideCot, askUser, - avatars, enableFeedback, highlightedMessage, loading, @@ -187,8 +133,7 @@ const MessageContainer = memo( projectSettings?.features?.unsafe_allow_html, onElementRefClick, onError, - onFeedbackUpdated, - onPlaygroundButtonClick + onFeedbackUpdated ]); return ( @@ -196,8 +141,6 @@ const MessageContainer = memo( actions={messageActions} elements={elements} messages={messages} - autoScroll={autoScroll} - setAutoScroll={setAutoScroll} context={memoizedContext} /> ); diff --git a/frontend/src/components/organisms/chat/Messages/index.tsx b/frontend/src/components/organisms/chat/Messages/index.tsx index 717de5c419..28d6149c2c 100644 --- a/frontend/src/components/organisms/chat/Messages/index.tsx +++ b/frontend/src/components/organisms/chat/Messages/index.tsx @@ -11,33 +11,19 @@ import { updateMessageById, useChatData, useChatInteract, - useChatMessages, - useChatSession + useChatMessages } from '@chainlit/react-client'; import { useTranslation } from 'components/i18n/Translator'; import { apiClientState } from 'state/apiClient'; -import { IProjectSettings } from 'state/project'; import MessageContainer from './container'; -import WelcomeScreen from './welcomeScreen'; -interface MessagesProps { - autoScroll: boolean; - projectSettings?: IProjectSettings; - setAutoScroll: (autoScroll: boolean) => void; -} - -const Messages = ({ - autoScroll, - projectSettings, - setAutoScroll -}: MessagesProps): JSX.Element => { - const { elements, askUser, avatars, loading, actions } = useChatData(); +const Messages = (): JSX.Element => { + const { elements, askUser, loading, actions } = useChatData(); const { messages } = useChatMessages(); const { callAction } = useChatInteract(); - const { idToResume } = useChatSession(); const accessToken = useRecoilValue(accessTokenState); const setMessages = useSetRecoilState(messagesState); const apiClient = useRecoilValue(apiClientState); @@ -135,28 +121,16 @@ const Messages = ({ [] ); - return !idToResume && - !messages.length && - projectSettings?.ui.show_readme_as_default ? ( - - ) : ( + return ( ); }; diff --git a/frontend/src/components/organisms/chat/dropScreen.tsx b/frontend/src/components/organisms/chat/dropScreen.tsx index a577189918..d0fc3501f0 100644 --- a/frontend/src/components/organisms/chat/dropScreen.tsx +++ b/frontend/src/components/organisms/chat/dropScreen.tsx @@ -1,8 +1,9 @@ -import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import { Backdrop, Stack, Typography } from '@mui/material'; import { Translator } from 'components/i18n'; +import ImageIcon from 'assets/Image'; + export default function DropScreen() { return ( - - + + diff --git a/frontend/src/components/organisms/chat/index.tsx b/frontend/src/components/organisms/chat/index.tsx index 6e25e71744..2a47aa4634 100644 --- a/frontend/src/components/organisms/chat/index.tsx +++ b/frontend/src/components/organisms/chat/index.tsx @@ -1,3 +1,4 @@ +import { useAuth } from 'api/auth'; import { useUpload } from 'hooks'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -18,6 +19,7 @@ import { import { ErrorBoundary } from 'components/atoms/ErrorBoundary'; import { Translator } from 'components/i18n'; import { useTranslation } from 'components/i18n/Translator'; +import ScrollContainer from 'components/molecules/messages/ScrollContainer'; import { TaskList } from 'components/molecules/tasklist/TaskList'; import { useLayoutMaxWidth } from 'hooks/useLayoutMaxWidth'; @@ -29,8 +31,10 @@ import { projectSettingsState } from 'state/project'; import Messages from './Messages'; import DropScreen from './dropScreen'; import InputBox from './inputBox'; +import WelcomeScreen from './welcomeScreen'; const Chat = () => { + const { user } = useAuth(); const { idToResume } = useChatSession(); const projectSettings = useRecoilValue(projectSettingsState); @@ -162,6 +166,7 @@ const Chat = () => { useEffect(() => { const currentPage = new URL(window.location.href); if ( + user && projectSettings?.dataPersistence && threadId && currentPage.pathname === '/' @@ -228,11 +233,14 @@ const Chat = () => { ) : null} - + > + + + + { const { loading } = useChatData(); + const { firstInteraction } = useChatMessages(); const { stopTask } = useChatInteract(); const handleClick = () => { @@ -28,24 +33,24 @@ const SubmitButton = ({ disabled, onSubmit }: SubmitButtonProps) => { color: 'text.secondary' }} > - {!loading ? ( + {loading && firstInteraction ? ( + } > - - + + ) : ( + } > - - + + )} diff --git a/frontend/src/components/organisms/chat/inputBox/index.tsx b/frontend/src/components/organisms/chat/inputBox/index.tsx index 8176188b48..fbf702670a 100644 --- a/frontend/src/components/organisms/chat/inputBox/index.tsx +++ b/frontend/src/components/organisms/chat/inputBox/index.tsx @@ -104,7 +104,8 @@ const InputBox = memo( position="relative" flexDirection="column" gap={1} - p={2} + pb={2} + px={2} sx={{ boxSizing: 'border-box', width: '100%', diff --git a/frontend/src/components/organisms/chat/welcomeScreen/index.tsx b/frontend/src/components/organisms/chat/welcomeScreen/index.tsx new file mode 100644 index 0000000000..771783e7eb --- /dev/null +++ b/frontend/src/components/organisms/chat/welcomeScreen/index.tsx @@ -0,0 +1,110 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { Avatar, Grid, Stack, Typography } from '@mui/material'; +import Fade from '@mui/material/Fade'; + +import { useChatMessages, useChatSession } from '@chainlit/react-client'; + +import { apiClientState } from 'state/apiClient'; +import { projectSettingsState } from 'state/project'; + +import Starter from './starter'; + +interface Props { + hideLogo?: boolean; +} + +export default function WelcomeScreen({ hideLogo }: Props) { + const { messages } = useChatMessages(); + const [show, setShow] = useState(true); + const { chatProfile } = useChatSession(); + const pSettings = useRecoilValue(projectSettingsState); + const apiClient = useRecoilValue(apiClientState); + const defaultIconUrl = apiClient?.buildEndpoint(`/avatars/default`); + + useEffect(() => { + if (messages.length > 0) { + setShow(false); + } else { + setShow(true); + } + }, [messages]); + + const selectedChatProfile = useMemo(() => { + return pSettings?.chatProfiles.find( + (profile) => profile.name === chatProfile + ); + }, [pSettings, chatProfile]); + + const logo = useMemo(() => { + const name = selectedChatProfile?.name; + let icon = selectedChatProfile?.icon || defaultIconUrl; + + if (icon?.startsWith('/public')) { + icon = apiClient.buildEndpoint(icon); + } + + return ( + + + {name ? ( + + {name} + + ) : null} + + ); + }, [pSettings, chatProfile, selectedChatProfile]); + + const starters = useMemo(() => { + if (chatProfile) { + const selectedChatProfile = pSettings?.chatProfiles.find( + (profile) => profile.name === chatProfile + ); + if (selectedChatProfile?.starters) { + return selectedChatProfile.starters.slice(0, 4); + } + } + return pSettings?.starters; + }, [pSettings, chatProfile]); + + if (!starters?.length) { + return null; + } + + return ( + + + {hideLogo ? null : {logo}} + + {starters?.map((starter, i) => ( + + + + + + ))} + + + + ); +} diff --git a/frontend/src/components/organisms/chat/welcomeScreen/starter.tsx b/frontend/src/components/organisms/chat/welcomeScreen/starter.tsx new file mode 100644 index 0000000000..0dc90c1354 --- /dev/null +++ b/frontend/src/components/organisms/chat/welcomeScreen/starter.tsx @@ -0,0 +1,86 @@ +import { useAuth } from 'api/auth'; +import { useCallback } from 'react'; +import { useRecoilValue } from 'recoil'; +import { v4 as uuidv4 } from 'uuid'; + +import { Box, Button, Stack, Typography } from '@mui/material'; + +import { IStep, useChatData } from '@chainlit/react-client'; +import { useChatInteract } from '@chainlit/react-client'; + +import { apiClientState } from 'state/apiClient'; +import type { IStarter } from 'state/project'; + +interface Props { + starter: IStarter; +} + +export default function Starter({ starter }: Props) { + const apiClient = useRecoilValue(apiClientState); + const { sendMessage } = useChatInteract(); + const { loading } = useChatData(); + const { user } = useAuth(); + + const onSubmit = useCallback(async () => { + const message: IStep = { + threadId: '', + id: uuidv4(), + name: user?.identifier || 'User', + type: 'user_message', + output: starter.message, + createdAt: new Date().toISOString() + }; + + sendMessage(message, []); + }, [user, sendMessage, starter]); + + return ( + + ); +} diff --git a/frontend/src/components/organisms/header.tsx b/frontend/src/components/organisms/header.tsx index 4b34666a42..515f6c450b 100644 --- a/frontend/src/components/organisms/header.tsx +++ b/frontend/src/components/organisms/header.tsx @@ -4,7 +4,8 @@ import { useRecoilValue } from 'recoil'; import { Box, Stack } from '@mui/material'; import useMediaQuery from '@mui/material/useMediaQuery'; -import GithubButton from 'components/atoms/buttons/githubButton'; +import UserButton from 'components/atoms/buttons/userButton'; +import { Logo } from 'components/atoms/logo'; import ChatProfiles from 'components/molecules/chatProfiles'; import NewChatButton from 'components/molecules/newChatButton'; @@ -24,21 +25,32 @@ const Header = memo(() => { height="45px" alignItems="center" flexDirection="row" - justifyContent={isMobile ? 'space-between' : 'initial'} + justifyContent="space-between" color="text.primary" gap={2} id="header" + position="relative" > - {isMobile ? : null} - {!isMobile && !isChatHistoryOpen ? : null} - + + + {isMobile ? ( - - - - - ) : null} - {!isMobile ? : null} + + ) : isChatHistoryOpen ? null : ( + + )} + + + + + ); }); diff --git a/frontend/src/components/organisms/playground/index.tsx b/frontend/src/components/organisms/playground/index.tsx deleted file mode 100644 index 50621478eb..0000000000 --- a/frontend/src/components/organisms/playground/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useCallback } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { toast } from 'sonner'; -import { IPlaygroundContext } from 'types'; - -import { IGeneration, accessTokenState } from '@chainlit/react-client'; - -import { PromptPlayground } from 'components/molecules/playground'; - -import { useLLMProviders } from 'hooks/useLLMProviders'; - -import { apiClientState } from 'state/apiClient'; -import { - functionState, - modeState, - playgroundState, - variableState -} from 'state/playground'; -import { userEnvState } from 'state/user'; - -export default function PlaygroundWrapper() { - const accessToken = useRecoilValue(accessTokenState); - const userEnv = useRecoilValue(userEnvState); - const [variableName, setVariableName] = useRecoilState(variableState); - const [functionIndex, setFunctionIndex] = useRecoilState(functionState); - const [playground, setPlayground] = useRecoilState(playgroundState); - const [promptMode, setPromptMode] = useRecoilState(modeState); - const apiClient = useRecoilValue(apiClientState); - - const shoulFetchProviders = - playground?.generation && !playground?.providers?.length; - - useLLMProviders(shoulFetchProviders); - - const onNotification: IPlaygroundContext['onNotification'] = useCallback( - (type, content) => { - switch (type) { - case 'error': - toast.error(content); - return; - case 'success': - toast.success(content); - return; - default: - return; - } - }, - [] - ); - - const createCompletion = useCallback( - ( - generation: IGeneration, - controller: AbortController, - cb: (done: boolean, token: string) => void - ) => { - return apiClient.getGeneration( - generation, - userEnv, - controller, - accessToken, - cb - ); - }, - [accessToken] - ); - - return ( - - ); -} diff --git a/frontend/src/components/organisms/chat/Messages/welcomeScreen.tsx b/frontend/src/components/organisms/readme.tsx similarity index 82% rename from frontend/src/components/organisms/chat/Messages/welcomeScreen.tsx rename to frontend/src/components/organisms/readme.tsx index 25f4aab58d..0bd059c4e1 100644 --- a/frontend/src/components/organisms/chat/Messages/welcomeScreen.tsx +++ b/frontend/src/components/organisms/readme.tsx @@ -6,17 +6,15 @@ import { Markdown } from 'components/molecules/Markdown'; import { useLayoutMaxWidth } from 'hooks/useLayoutMaxWidth'; -const WelcomeScreen = memo( +const Readme = memo( ({ markdown, allowHtml, - latex, - variant + latex }: { markdown?: string; allowHtml?: boolean; latex?: boolean; - variant: 'app' | 'copilot'; }) => { const layoutMaxWidth = useLayoutMaxWidth(); @@ -27,13 +25,11 @@ const WelcomeScreen = memo(