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
+ }
+ {...props}
+ >
+ Repo
+
);
}
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 = (
-
+
+ }
+ />
+
+ {
+ 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 (
-
- );
-};
-
-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 (
-
- );
-};
-
-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 (
-
- );
-}
-
-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 (
-
- );
-}
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(
);
};
diff --git a/frontend/src/contexts/MessageContext.tsx b/frontend/src/contexts/MessageContext.tsx
index 2a7713c27d..a5b91ce48d 100644
--- a/frontend/src/contexts/MessageContext.tsx
+++ b/frontend/src/contexts/MessageContext.tsx
@@ -5,13 +5,11 @@ import { IMessageContext } from 'types/messageContext';
const defaultMessageContext = {
avatars: [],
defaultCollapseContent: false,
- expandAll: false,
hideCot: false,
highlightedMessage: null,
loading: false,
onElementRefClick: undefined,
onFeedbackUpdated: undefined,
- onPlaygroundButtonClick: undefined,
showFeedbackButtons: true,
onError: () => undefined,
uiName: ''
diff --git a/frontend/src/contexts/PlaygroundContext.tsx b/frontend/src/contexts/PlaygroundContext.tsx
deleted file mode 100644
index 86f65688e8..0000000000
--- a/frontend/src/contexts/PlaygroundContext.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { createContext } from 'react';
-
-import { IPlaygroundContext } from 'types/playgroundContext';
-
-const defaultPlaygroundContext: IPlaygroundContext = {
- setVariableName: () => undefined,
- setFunctionIndex: () => undefined,
- setPromptMode: () => undefined,
- setPlayground: () => undefined,
- onNotification: () => undefined,
- promptMode: 'Formatted'
-};
-
-const PlaygroundContext = createContext(
- defaultPlaygroundContext
-);
-
-export { PlaygroundContext, defaultPlaygroundContext };
diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts
index 79e3ddec44..7b2443e0d6 100644
--- a/frontend/src/hooks/index.ts
+++ b/frontend/src/hooks/index.ts
@@ -1,3 +1,2 @@
-export { useColors } from './useColors';
export { useIsDarkMode } from './useIsDarkMode';
export { useUpload } from './useUpload';
diff --git a/frontend/src/hooks/useColors.tsx b/frontend/src/hooks/useColors.tsx
deleted file mode 100644
index 001ae516a4..0000000000
--- a/frontend/src/hooks/useColors.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useCallback } from 'react';
-
-import { useIsDarkMode } from './useIsDarkMode';
-
-const darkColors = [
- '#fb923c',
- '#facc15',
- '#34d399',
- '#38bdf8',
- '#818cf8',
- '#c084fc',
- '#f472b6',
- '#0ea5e9'
-];
-
-const lightColors = [
- '#ea580c',
- '#ca8a04',
- '#059669',
- '#0284c7',
- '#4f46e5',
- '#9333ea',
- '#db2777',
- '#2563eb'
-];
-
-const hashCode = (str: string) => {
- const arr = str.split('');
- return arr.reduce(
- (hashCode, currentVal) =>
- (hashCode =
- currentVal.charCodeAt(0) +
- (hashCode << 6) +
- (hashCode << 16) -
- hashCode),
- 0
- );
-};
-
-const useColors = (inverted?: boolean) => {
- const isDarkMode = useIsDarkMode();
- let colors = isDarkMode ? darkColors : lightColors;
-
- if (inverted) {
- if (colors === darkColors) {
- colors = lightColors;
- } else {
- colors = darkColors;
- }
- }
- return colors;
-};
-
-const useColorForName = (uiName: string) => {
- const colors = useColors();
-
- return useCallback(
- (name: string, isUser?: boolean, isError?: boolean) => {
- if (isError) {
- return 'error.main';
- }
- if (name === uiName) {
- return 'primary.main';
- }
- if (isUser) {
- return 'text.primary';
- }
- const index = Math.abs(hashCode(name)) % colors.length;
- return colors[index];
- },
- [uiName]
- );
-};
-
-export { useColorForName, useColors };
diff --git a/frontend/src/hooks/useLLMProviders.ts b/frontend/src/hooks/useLLMProviders.ts
deleted file mode 100644
index 5383ce7151..0000000000
--- a/frontend/src/hooks/useLLMProviders.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useEffect } from 'react';
-import { useRecoilValue, useSetRecoilState } from 'recoil';
-import { toast } from 'sonner';
-
-import { useApi } from '@chainlit/react-client';
-
-import { useTranslation } from 'components/i18n/Translator';
-
-import { apiClientState } from 'state/apiClient';
-import { playgroundState } from 'state/playground';
-
-import { IPlayground } from 'types/playground';
-
-const useLLMProviders = (shouldFetch?: boolean) => {
- const apiClient = useRecoilValue(apiClientState);
-
- const { data, error } = useApi(
- apiClient,
- shouldFetch ? '/project/llm-providers' : null
- );
- const setPlayground = useSetRecoilState(playgroundState);
-
- const { t } = useTranslation();
-
- useEffect(() => {
- if (error) {
- toast.error(
- `${t('hooks.useLLMProviders.failedToFetchProviders')} ${error}`
- );
- }
- if (!data) return;
- setPlayground((old) => ({ ...old, providers: data.providers }));
- }, [data, error]);
-
- return null;
-};
-
-export { useLLMProviders };
diff --git a/frontend/src/pages/ResumeButton.tsx b/frontend/src/pages/ResumeButton.tsx
index e9a6b11990..c86119178d 100644
--- a/frontend/src/pages/ResumeButton.tsx
+++ b/frontend/src/pages/ResumeButton.tsx
@@ -1,10 +1,11 @@
+import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { toast } from 'sonner';
import { Box, Button } from '@mui/material';
-import { useChatInteract } from '@chainlit/react-client';
+import { useChatInteract, useChatSession } from '@chainlit/react-client';
import { Translator } from 'components/i18n';
import WaterMark from 'components/organisms/chat/inputBox/waterMark';
@@ -22,6 +23,18 @@ export default function ResumeButton({ threadId }: Props) {
const layoutMaxWidth = useLayoutMaxWidth();
const pSettings = useRecoilValue(projectSettingsState);
const { clear, setIdToResume } = useChatInteract();
+ const { session, idToResume } = useChatSession();
+
+ useEffect(() => {
+ if (threadId !== idToResume) {
+ return;
+ }
+ if (session?.socket.connected) {
+ toast.success('Chat resumed successfully');
+ } else if (session?.error) {
+ toast.error("Couldn't resume chat");
+ }
+ }, [session, idToResume, threadId]);
if (!threadId || !pSettings?.threadResumable) {
return null;
@@ -30,7 +43,6 @@ export default function ResumeButton({ threadId }: Props) {
const onClick = () => {
clear();
setIdToResume(threadId!);
- toast.success('Chat resumed!');
if (!pSettings?.dataPersistence) {
navigate('/');
}
diff --git a/frontend/src/state/playground.ts b/frontend/src/state/playground.ts
deleted file mode 100644
index d5f39ba35b..0000000000
--- a/frontend/src/state/playground.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { atom } from 'recoil';
-
-import { IPlayground, PromptMode } from 'types/playground';
-
-export const playgroundState = atom({
- key: 'Playground',
- default: undefined
-});
-
-export const variableState = atom({
- key: 'PlaygroundVariable',
- default: undefined
-});
-
-export const functionState = atom({
- key: 'PlaygroundFunction',
- default: undefined
-});
-
-export const modeState = atom({
- key: 'PlaygroundMode',
- default: 'Formatted'
-});
diff --git a/frontend/src/state/project.ts b/frontend/src/state/project.ts
index 6a5ade25c9..f1823777e2 100644
--- a/frontend/src/state/project.ts
+++ b/frontend/src/state/project.ts
@@ -2,22 +2,26 @@ import { atom } from 'recoil';
import { IStep } from '@chainlit/react-client';
+export interface IStarter {
+ label: string;
+ message: string;
+ icon?: string;
+}
+
export interface ChatProfile {
default: boolean;
- icon: string;
+ icon?: string;
name: string;
markdown_description: string;
+ starters?: IStarter[];
}
export interface IProjectSettings {
markdown?: string;
ui: {
name: string;
- show_readme_as_default?: boolean;
description?: string;
- hide_cot?: boolean;
default_collapse_content?: boolean;
- default_expand_messages?: boolean;
github?: string;
theme: any;
custom_css?: string;
@@ -44,10 +48,12 @@ export interface IProjectSettings {
unsafe_allow_html?: boolean;
latex?: boolean;
};
+ debugUrl?: string;
userEnv: string[];
dataPersistence: boolean;
threadResumable: boolean;
chatProfiles: ChatProfile[];
+ starters?: IStarter[];
translation: object;
}
diff --git a/frontend/src/state/settings.ts b/frontend/src/state/settings.ts
index 774979caa3..9cd0450837 100644
--- a/frontend/src/state/settings.ts
+++ b/frontend/src/state/settings.ts
@@ -2,7 +2,7 @@ import { atom } from 'recoil';
type ThemeVariant = 'dark' | 'light';
-const defaultTheme = 'dark';
+const defaultTheme = (window.theme?.default || 'dark') as ThemeVariant;
const preferredTheme = localStorage.getItem(
'themeVariant'
@@ -13,8 +13,6 @@ const theme = preferredTheme ? preferredTheme : defaultTheme;
export const defaultSettingsState = {
open: false,
defaultCollapseContent: true,
- expandAll: false,
- hideCot: false,
isChatHistoryOpen: true,
language: 'en-US',
theme
@@ -23,8 +21,6 @@ export const defaultSettingsState = {
export const settingsState = atom<{
open: boolean;
defaultCollapseContent: boolean;
- expandAll: boolean;
- hideCot: boolean;
theme: ThemeVariant;
isChatHistoryOpen: boolean;
language: string;
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 0508b0e87c..15824af1a2 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -1,5 +1,3 @@
export * from './Input';
export * from './messageContext';
export * from './NotificationCount';
-export * from './playground';
-export * from './playgroundContext';
diff --git a/frontend/src/types/messageContext.ts b/frontend/src/types/messageContext.ts
index a5d436a6f5..cd88a86255 100644
--- a/frontend/src/types/messageContext.ts
+++ b/frontend/src/types/messageContext.ts
@@ -1,6 +1,5 @@
import type {
IAsk,
- IAvatarElement,
IFeedback,
IFileRef,
IMessageElement,
@@ -13,17 +12,13 @@ interface IMessageContext {
onProgress: (progress: number) => void
) => { xhr: XMLHttpRequest; promise: Promise };
askUser?: IAsk;
- avatars: IAvatarElement[];
defaultCollapseContent: boolean;
- expandAll: boolean;
- hideCot: boolean;
highlightedMessage: string | null;
loading: boolean;
showFeedbackButtons: boolean;
uiName: string;
allowHtml?: boolean;
latex?: boolean;
- onPlaygroundButtonClick?: (step: IStep) => void;
onElementRefClick?: (element: IMessageElement) => void;
onFeedbackUpdated?: (
message: IStep,
diff --git a/frontend/src/types/playground.ts b/frontend/src/types/playground.ts
deleted file mode 100644
index d8f858fd05..0000000000
--- a/frontend/src/types/playground.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { TFormInput } from 'components/atoms/inputs';
-
-import type { IGeneration } from 'client-types/';
-
-export interface ILLMProvider {
- id: string;
- inputs: TFormInput[];
- name: string;
- settings: ILLMProviderSettings;
- is_chat: boolean;
-}
-
-export interface ILLMProviderSettings {
- settings: {
- $schema: string;
- $ref: string;
- definitions: {
- settingsSchema: {
- type: string;
- Properties: Record;
- };
- };
- };
-}
-
-export type PromptMode = 'Formatted';
-
-export interface IPlayground {
- providers?: ILLMProvider[];
- generation?: IGeneration;
- originalGeneration?: IGeneration;
-}
diff --git a/frontend/src/types/playgroundContext.ts b/frontend/src/types/playgroundContext.ts
deleted file mode 100644
index bed7c0c52e..0000000000
--- a/frontend/src/types/playgroundContext.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { IGeneration } from 'client-types/';
-
-import { IPlayground, PromptMode } from './playground';
-
-interface IPlaygroundContext {
- variableName?: string;
- setVariableName: (
- name?: string | ((name?: string) => string | undefined)
- ) => void;
- functionIndex?: number;
- setFunctionIndex: (
- index?: number | ((index?: number) => number | undefined)
- ) => void;
- promptMode: PromptMode;
- setPromptMode: (
- mode: PromptMode | ((mode: PromptMode) => PromptMode)
- ) => void;
- setPlayground: (
- playground?:
- | IPlayground
- | ((playground?: IPlayground) => IPlayground | undefined)
- ) => void;
- playground?: IPlayground;
- onNotification: (type: 'success' | 'error', content: string) => void;
- createCompletion?: (
- generation: IGeneration,
- controller: AbortController,
- cb: (done: boolean, token: string) => void
- ) => Promise;
-}
-
-export type { IPlaygroundContext };
diff --git a/frontend/src/utils/message.ts b/frontend/src/utils/message.ts
index 8c785bea07..772026d3cb 100644
--- a/frontend/src/utils/message.ts
+++ b/frontend/src/utils/message.ts
@@ -67,7 +67,7 @@ export const prepareContent = ({
});
}
- if (language) {
+ if (language && preparedContent) {
const prefix = `\`\`\`${language}`;
const suffix = '```';
if (!preparedContent.startsWith('```')) {
diff --git a/frontend/tests/content.spec.tsx b/frontend/tests/content.spec.tsx
index c49d579224..7cd5ca325c 100644
--- a/frontend/tests/content.spec.tsx
+++ b/frontend/tests/content.spec.tsx
@@ -11,7 +11,7 @@ it('renders the message content', () => {
{
{
{
expect(getByRole('link', { name: 'page{12}' })).toBeInTheDocument();
});
-it('preserves the box size when collapsing', () => {
- const { getByRole } = render(
-
- );
+// it('preserves the box size when collapsing', () => {
+// const { getByRole } = render(
+//
+// );
- expect(getByRole('button', { name: 'Collapse' })).toBeInTheDocument();
-});
+// expect(getByRole('button', { name: 'Collapse' })).toBeInTheDocument();
+// });
diff --git a/frontend/tests/message.spec.tsx b/frontend/tests/message.spec.tsx
index ee61fcbff3..00d87330d2 100644
--- a/frontend/tests/message.spec.tsx
+++ b/frontend/tests/message.spec.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render } from '@testing-library/react';
+import { render } from '@testing-library/react';
import { MessageContext, defaultMessageContext } from 'contexts/MessageContext';
import i18n from 'i18next';
import { ComponentProps } from 'react';
@@ -33,7 +33,7 @@ describe('Message', () => {
id: '2',
threadId: '1',
input: '',
- type: 'llm',
+ type: 'tool',
output: 'bar',
name: 'bar',
createdAt: '12/12/2002',
@@ -49,8 +49,6 @@ describe('Message', () => {
elements: [],
actions: [],
indent: 0,
- showAvatar: true,
- showBorder: true,
isRunning: false,
isLast: true
};
@@ -67,62 +65,42 @@ describe('Message', () => {
expect(messageContent).toBeInTheDocument();
});
- it('toggles the detail button', () => {
- const theme = createTheme({});
- const { getByRole } = render(
-
-
-
- );
- let detailsButton = getByRole('button', {});
-
- expect(detailsButton).toBeInTheDocument();
- fireEvent.click(detailsButton);
- const closeButton = getByRole('button', { name: 'Took 1 step' });
-
- expect(closeButton).toBeInTheDocument();
- fireEvent.click(closeButton);
- detailsButton = getByRole('button', { name: 'Took 1 step' });
-
- expect(detailsButton).toBeInTheDocument();
- });
-
- it('preserves the content size when message is streamed', () => {
- const theme = createTheme({});
- const { getByRole } = render(
-
-
-
- );
-
- expect(getByRole('button', { name: 'Collapse' })).toBeInTheDocument();
- });
-
- it('preserves the content size when app settings defaultCollapseContent is false', () => {
- const theme = createTheme({});
- const { getByRole } = render(
-
-
-
-
-
- );
-
- expect(getByRole('button', { name: 'Collapse' })).toBeInTheDocument();
- });
+ // it('preserves the content size when message is streamed', () => {
+ // const theme = createTheme({});
+ // const { getByRole } = render(
+ //
+ //
+ //
+ // );
+
+ // expect(getByRole('button', { name: 'Collapse' })).toBeInTheDocument();
+ // });
+
+ // it('preserves the content size when app settings defaultCollapseContent is false', () => {
+ // const theme = createTheme({});
+ // const { getByRole } = render(
+ //
+ //
+ //
+ //
+ //
+ // );
+
+ // expect(getByRole('button', { name: 'Collapse' })).toBeInTheDocument();
+ // });
});
diff --git a/libs/copilot/index.tsx b/libs/copilot/index.tsx
index 336d1bce68..a0283a03d0 100644
--- a/libs/copilot/index.tsx
+++ b/libs/copilot/index.tsx
@@ -1,22 +1,58 @@
+import createCache from '@emotion/cache';
+import { CacheProvider } from '@emotion/react';
import React from 'react';
import ReactDOM from 'react-dom/client';
+// @ts-expect-error inlined
+import clStyles from '@chainlit/app/src/App.css?inline';
+
+// @ts-expect-error inlined
+import sonnerCss from './sonner.css?inline';
+// @ts-expect-error inlined
+import hljsStyles from 'highlight.js/styles/monokai-sublime.css?inline';
+
import AppWrapper from './src/appWrapper';
import { IWidgetConfig } from './src/types';
const id = 'chainlit-copilot';
let root: ReactDOM.Root | null = null;
+declare global {
+ interface Window {
+ cl_shadowRootElement: HTMLDivElement;
+ }
+}
+
// @ts-expect-error is not a valid prop
window.mountChainlitWidget = (config: IWidgetConfig) => {
- const div = document.createElement('div');
- div.id = id;
- document.body.appendChild(div);
+ const container = document.createElement('div');
+ container.id = id;
+ document.body.appendChild(container);
+
+ const shadowContainer = container.attachShadow({ mode: 'open' });
+ const shadowRootElement = document.createElement('div');
+ shadowRootElement.id = 'cl-shadow-root';
+ shadowContainer.appendChild(shadowRootElement);
+
+ const cache = createCache({
+ key: 'css',
+ prepend: true,
+ container: shadowContainer
+ });
+
+ window.cl_shadowRootElement = shadowRootElement;
- root = ReactDOM.createRoot(div);
+ root = ReactDOM.createRoot(shadowRootElement);
root.render(
-
+
+
+
+
);
};
diff --git a/libs/copilot/package.json b/libs/copilot/package.json
index a0e8ca1f51..195f29033b 100644
--- a/libs/copilot/package.json
+++ b/libs/copilot/package.json
@@ -16,12 +16,14 @@
"dependencies": {
"@chainlit/app": "workspace:^",
"@chainlit/react-client": "workspace:^",
+ "@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.9",
"@mui/lab": "^5.0.0-alpha.122",
"@mui/material": "^5.14.10",
"formik": "^2.4.3",
+ "highlight.js": "^11.9.0",
"i18next": "^23.7.16",
"lodash": "^4.17.21",
"react": "^18.2.0",
diff --git a/libs/copilot/pnpm-lock.yaml b/libs/copilot/pnpm-lock.yaml
index 92e9f0ba08..202d980edb 100644
--- a/libs/copilot/pnpm-lock.yaml
+++ b/libs/copilot/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@chainlit/react-client':
specifier: workspace:^
version: link:../react-client
+ '@emotion/cache':
+ specifier: ^11.11.0
+ version: 11.11.0
'@emotion/react':
specifier: ^11.11.1
version: 11.11.1(@types/react@18.2.0)(react@18.2.0)
@@ -32,6 +35,9 @@ importers:
formik:
specifier: ^2.4.3
version: 2.4.3(react@18.2.0)
+ highlight.js:
+ specifier: ^11.9.0
+ version: 11.9.0
i18next:
specifier: ^23.7.16
version: 23.7.16
@@ -2666,6 +2672,10 @@ packages:
hast-util-to-string@3.0.0:
resolution: {integrity: sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==}
+ highlight.js@11.9.0:
+ resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==}
+ engines: {node: '>=12.0.0'}
+
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
@@ -7402,6 +7412,8 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
+ highlight.js@11.9.0: {}
+
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
diff --git a/libs/copilot/sonner.css b/libs/copilot/sonner.css
new file mode 100644
index 0000000000..91be78dbcf
--- /dev/null
+++ b/libs/copilot/sonner.css
@@ -0,0 +1,661 @@
+:where(html[dir='ltr']),
+:where([data-sonner-toaster][dir='ltr']) {
+ --toast-icon-margin-start: -3px;
+ --toast-icon-margin-end: 4px;
+ --toast-svg-margin-start: -1px;
+ --toast-svg-margin-end: 0px;
+ --toast-button-margin-start: auto;
+ --toast-button-margin-end: 0;
+ --toast-close-button-start: 0;
+ --toast-close-button-end: unset;
+ --toast-close-button-transform: translate(-35%, -35%);
+}
+
+:where(html[dir='rtl']),
+:where([data-sonner-toaster][dir='rtl']) {
+ --toast-icon-margin-start: 4px;
+ --toast-icon-margin-end: -3px;
+ --toast-svg-margin-start: 0px;
+ --toast-svg-margin-end: -1px;
+ --toast-button-margin-start: 0;
+ --toast-button-margin-end: auto;
+ --toast-close-button-start: unset;
+ --toast-close-button-end: 0;
+ --toast-close-button-transform: translate(35%, -35%);
+}
+
+:where([data-sonner-toaster]) {
+ position: fixed;
+ width: var(--width);
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
+ Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
+ Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
+ --gray1: hsl(0, 0%, 99%);
+ --gray2: hsl(0, 0%, 97.3%);
+ --gray3: hsl(0, 0%, 95.1%);
+ --gray4: hsl(0, 0%, 93%);
+ --gray5: hsl(0, 0%, 90.9%);
+ --gray6: hsl(0, 0%, 88.7%);
+ --gray7: hsl(0, 0%, 85.8%);
+ --gray8: hsl(0, 0%, 78%);
+ --gray9: hsl(0, 0%, 56.1%);
+ --gray10: hsl(0, 0%, 52.3%);
+ --gray11: hsl(0, 0%, 43.5%);
+ --gray12: hsl(0, 0%, 9%);
+ --border-radius: 8px;
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ outline: none;
+ z-index: 999999999;
+}
+
+:where([data-sonner-toaster][data-x-position='right']) {
+ right: max(var(--offset), env(safe-area-inset-right));
+}
+
+:where([data-sonner-toaster][data-x-position='left']) {
+ left: max(var(--offset), env(safe-area-inset-left));
+}
+
+:where([data-sonner-toaster][data-x-position='center']) {
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+:where([data-sonner-toaster][data-y-position='top']) {
+ top: max(var(--offset), env(safe-area-inset-top));
+}
+
+:where([data-sonner-toaster][data-y-position='bottom']) {
+ bottom: max(var(--offset), env(safe-area-inset-bottom));
+}
+
+:where([data-sonner-toast]) {
+ --y: translateY(100%);
+ --lift-amount: calc(var(--lift) * var(--gap));
+ z-index: var(--z-index);
+ position: absolute;
+ opacity: 0;
+ transform: var(--y);
+ filter: blur(0);
+ /* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */
+ touch-action: none;
+ transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms;
+ box-sizing: border-box;
+ outline: none;
+ overflow-wrap: anywhere;
+}
+
+:where([data-sonner-toast][data-styled='true']) {
+ padding: 16px;
+ background: var(--normal-bg);
+ border: 1px solid var(--normal-border);
+ color: var(--normal-text);
+ border-radius: var(--border-radius);
+ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
+ width: var(--width);
+ font-size: 13px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+:where([data-sonner-toast]:focus-visible) {
+ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
+}
+
+:where([data-sonner-toast][data-y-position='top']) {
+ top: 0;
+ --y: translateY(-100%);
+ --lift: 1;
+ --lift-amount: calc(1 * var(--gap));
+}
+
+:where([data-sonner-toast][data-y-position='bottom']) {
+ bottom: 0;
+ --y: translateY(100%);
+ --lift: -1;
+ --lift-amount: calc(var(--lift) * var(--gap));
+}
+
+:where([data-sonner-toast]) :where([data-description]) {
+ font-weight: 400;
+ line-height: 1.4;
+ color: inherit;
+}
+
+:where([data-sonner-toast]) :where([data-title]) {
+ font-weight: 500;
+ line-height: 1.5;
+ color: inherit;
+}
+
+:where([data-sonner-toast]) :where([data-icon]) {
+ display: flex;
+ height: 16px;
+ width: 16px;
+ position: relative;
+ justify-content: flex-start;
+ align-items: center;
+ flex-shrink: 0;
+ margin-left: var(--toast-icon-margin-start);
+ margin-right: var(--toast-icon-margin-end);
+}
+
+:where([data-sonner-toast][data-promise='true']) :where([data-icon]) > svg {
+ opacity: 0;
+ transform: scale(0.8);
+ transform-origin: center;
+ animation: sonner-fade-in 300ms ease forwards;
+}
+
+:where([data-sonner-toast]) :where([data-icon]) > * {
+ flex-shrink: 0;
+}
+
+:where([data-sonner-toast]) :where([data-icon]) svg {
+ margin-left: var(--toast-svg-margin-start);
+ margin-right: var(--toast-svg-margin-end);
+}
+
+:where([data-sonner-toast]) :where([data-content]) {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+[data-sonner-toast][data-styled='true'] [data-button] {
+ border-radius: 4px;
+ padding-left: 8px;
+ padding-right: 8px;
+ height: 24px;
+ font-size: 12px;
+ color: var(--normal-bg);
+ background: var(--normal-text);
+ margin-left: var(--toast-button-margin-start);
+ margin-right: var(--toast-button-margin-end);
+ border: none;
+ cursor: pointer;
+ outline: none;
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ transition: opacity 400ms, box-shadow 200ms;
+}
+
+:where([data-sonner-toast]) :where([data-button]):focus-visible {
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
+}
+
+:where([data-sonner-toast]) :where([data-button]):first-of-type {
+ margin-left: var(--toast-button-margin-start);
+ margin-right: var(--toast-button-margin-end);
+}
+
+:where([data-sonner-toast]) :where([data-cancel]) {
+ color: var(--normal-text);
+ background: rgba(0, 0, 0, 0.08);
+}
+
+:where([data-sonner-toast][data-theme='dark']) :where([data-cancel]) {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+:where([data-sonner-toast]) :where([data-close-button]) {
+ position: absolute;
+ left: var(--toast-close-button-start);
+ right: var(--toast-close-button-end);
+ top: 0;
+ height: 20px;
+ width: 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+ background: var(--gray1);
+ color: var(--gray12);
+ border: 1px solid var(--gray4);
+ transform: var(--toast-close-button-transform);
+ border-radius: 50%;
+ cursor: pointer;
+ z-index: 1;
+ transition: opacity 100ms, background 200ms, border-color 200ms;
+}
+
+:where([data-sonner-toast]) :where([data-close-button]):focus-visible {
+ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
+}
+
+:where([data-sonner-toast]) :where([data-disabled='true']) {
+ cursor: not-allowed;
+}
+
+:where([data-sonner-toast]):hover :where([data-close-button]):hover {
+ background: var(--gray2);
+ border-color: var(--gray5);
+}
+
+/* Leave a ghost div to avoid setting hover to false when swiping out */
+:where([data-sonner-toast][data-swiping='true'])::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 100%;
+ z-index: -1;
+}
+
+:where(
+ [data-sonner-toast][data-y-position='top'][data-swiping='true']
+ )::before {
+ /* y 50% needed to distribute height additional height evenly */
+ bottom: 50%;
+ transform: scaleY(3) translateY(50%);
+}
+
+:where(
+ [data-sonner-toast][data-y-position='bottom'][data-swiping='true']
+ )::before {
+ /* y -50% needed to distribute height additional height evenly */
+ top: 50%;
+ transform: scaleY(3) translateY(-50%);
+}
+
+/* Leave a ghost div to avoid setting hover to false when transitioning out */
+:where([data-sonner-toast][data-swiping='false'][data-removed='true'])::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ transform: scaleY(2);
+}
+
+/* Needed to avoid setting hover to false when inbetween toasts */
+:where([data-sonner-toast])::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ height: calc(var(--gap) + 1px);
+ bottom: 100%;
+ width: 100%;
+}
+
+:where([data-sonner-toast][data-mounted='true']) {
+ --y: translateY(0);
+ opacity: 1;
+}
+
+:where([data-sonner-toast][data-expanded='false'][data-front='false']) {
+ --scale: var(--toasts-before) * 0.05 + 1;
+ --y: translateY(calc(var(--lift-amount) * var(--toasts-before)))
+ scale(calc(-1 * var(--scale)));
+ height: var(--front-toast-height);
+}
+
+:where([data-sonner-toast]) > * {
+ transition: opacity 400ms;
+}
+
+:where(
+ [data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true']
+ )
+ > * {
+ opacity: 0;
+}
+
+:where([data-sonner-toast][data-visible='false']) {
+ opacity: 0;
+ pointer-events: none;
+}
+
+:where([data-sonner-toast][data-mounted='true'][data-expanded='true']) {
+ --y: translateY(calc(var(--lift) * var(--offset)));
+ height: var(--initial-height);
+}
+
+:where(
+ [data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false']
+ ) {
+ --y: translateY(calc(var(--lift) * -100%));
+ opacity: 0;
+}
+
+:where(
+ [data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true']
+ ) {
+ --y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));
+ opacity: 0;
+}
+
+:where(
+ [data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false']
+ ) {
+ --y: translateY(40%);
+ opacity: 0;
+ transition: transform 500ms, opacity 200ms;
+}
+
+/* Bump up the height to make sure hover state doesn't get set to false */
+:where([data-sonner-toast][data-removed='true'][data-front='false'])::before {
+ height: calc(var(--initial-height) + 20%);
+}
+
+[data-sonner-toast][data-swiping='true'] {
+ transform: var(--y) translateY(var(--swipe-amount, 0px));
+ transition: none;
+}
+
+[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'],
+[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] {
+ animation: swipe-out 200ms ease-out forwards;
+}
+
+@keyframes swipe-out {
+ from {
+ transform: translateY(
+ calc(var(--lift) * var(--offset) + var(--swipe-amount))
+ );
+ opacity: 1;
+ }
+
+ to {
+ transform: translateY(
+ calc(
+ var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%
+ )
+ );
+ opacity: 0;
+ }
+}
+
+@media (max-width: 600px) {
+ [data-sonner-toaster] {
+ position: fixed;
+ --mobile-offset: 16px;
+ right: var(--mobile-offset);
+ left: var(--mobile-offset);
+ width: 100%;
+ }
+
+ [data-sonner-toaster] [data-sonner-toast] {
+ left: 0;
+ right: 0;
+ width: calc(100% - var(--mobile-offset) * 2);
+ }
+
+ [data-sonner-toaster][data-x-position='left'] {
+ left: var(--mobile-offset);
+ }
+
+ [data-sonner-toaster][data-y-position='bottom'] {
+ bottom: 20px;
+ }
+
+ [data-sonner-toaster][data-y-position='top'] {
+ top: 20px;
+ }
+
+ [data-sonner-toaster][data-x-position='center'] {
+ left: var(--mobile-offset);
+ right: var(--mobile-offset);
+ transform: none;
+ }
+}
+
+[data-sonner-toaster][data-theme='light'] {
+ --normal-bg: #fff;
+ --normal-border: var(--gray4);
+ --normal-text: var(--gray12);
+
+ --success-bg: hsl(143, 85%, 96%);
+ --success-border: hsl(145, 92%, 91%);
+ --success-text: hsl(140, 100%, 27%);
+
+ --info-bg: hsl(208, 100%, 97%);
+ --info-border: hsl(221, 91%, 91%);
+ --info-text: hsl(210, 92%, 45%);
+
+ --warning-bg: hsl(49, 100%, 97%);
+ --warning-border: hsl(49, 91%, 91%);
+ --warning-text: hsl(31, 92%, 45%);
+
+ --error-bg: hsl(359, 100%, 97%);
+ --error-border: hsl(359, 100%, 94%);
+ --error-text: hsl(360, 100%, 45%);
+}
+
+[data-sonner-toaster][data-theme='light']
+ [data-sonner-toast][data-invert='true'] {
+ --normal-bg: #000;
+ --normal-border: hsl(0, 0%, 20%);
+ --normal-text: var(--gray1);
+}
+
+[data-sonner-toaster][data-theme='dark']
+ [data-sonner-toast][data-invert='true'] {
+ --normal-bg: #fff;
+ --normal-border: var(--gray3);
+ --normal-text: var(--gray12);
+}
+
+[data-sonner-toaster][data-theme='dark'] {
+ --normal-bg: #000;
+ --normal-border: hsl(0, 0%, 20%);
+ --normal-text: var(--gray1);
+
+ --success-bg: hsl(150, 100%, 6%);
+ --success-border: hsl(147, 100%, 12%);
+ --success-text: hsl(150, 86%, 65%);
+
+ --info-bg: hsl(215, 100%, 6%);
+ --info-border: hsl(223, 100%, 12%);
+ --info-text: hsl(216, 87%, 65%);
+
+ --warning-bg: hsl(64, 100%, 6%);
+ --warning-border: hsl(60, 100%, 12%);
+ --warning-text: hsl(46, 87%, 65%);
+
+ --error-bg: hsl(358, 76%, 10%);
+ --error-border: hsl(357, 89%, 16%);
+ --error-text: hsl(358, 100%, 81%);
+}
+
+[data-rich-colors='true'][data-sonner-toast][data-type='success'] {
+ background: var(--success-bg);
+ border-color: var(--success-border);
+ color: var(--success-text);
+}
+
+[data-rich-colors='true'][data-sonner-toast][data-type='success']
+ [data-close-button] {
+ background: var(--success-bg);
+ border-color: var(--success-border);
+ color: var(--success-text);
+}
+
+[data-rich-colors='true'][data-sonner-toast][data-type='info'] {
+ background: var(--info-bg);
+ border-color: var(--info-border);
+ color: var(--info-text);
+}
+
+[data-rich-colors='true'][data-sonner-toast][data-type='info']
+ [data-close-button] {
+ background: var(--info-bg);
+ border-color: var(--info-border);
+ color: var(--info-text);
+}
+
+[data-rich-colors='true'][data-sonner-toast][data-type='warning'] {
+ background: var(--warning-bg);
+ border-color: var(--warning-border);
+ color: var(--warning-text);
+}
+
+[data-rich-colors='true'][data-sonner-toast][data-type='warning']
+ [data-close-button] {
+ background: var(--warning-bg);
+ border-color: var(--warning-border);
+ color: var(--warning-text);
+}
+
+[data-rich-colors='true'][data-sonner-toast][data-type='error'] {
+ background: var(--error-bg);
+ border-color: var(--error-border);
+ color: var(--error-text);
+}
+
+[data-rich-colors='true'][data-sonner-toast][data-type='error']
+ [data-close-button] {
+ background: var(--error-bg);
+ border-color: var(--error-border);
+ color: var(--error-text);
+}
+
+.sonner-loading-wrapper {
+ --size: 16px;
+ height: var(--size);
+ width: var(--size);
+ position: absolute;
+ inset: 0;
+ z-index: 10;
+}
+
+.sonner-loading-wrapper[data-visible='false'] {
+ transform-origin: center;
+ animation: sonner-fade-out 0.2s ease forwards;
+}
+
+.sonner-spinner {
+ position: relative;
+ top: 50%;
+ left: 50%;
+ height: var(--size);
+ width: var(--size);
+}
+
+.sonner-loading-bar {
+ animation: sonner-spin 1.2s linear infinite;
+ background: var(--gray11);
+ border-radius: 6px;
+ height: 8%;
+ left: -10%;
+ position: absolute;
+ top: -3.9%;
+ width: 24%;
+}
+
+.sonner-loading-bar:nth-child(1) {
+ animation-delay: -1.2s;
+ transform: rotate(0.0001deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(2) {
+ animation-delay: -1.1s;
+ transform: rotate(30deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(3) {
+ animation-delay: -1s;
+ transform: rotate(60deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(4) {
+ animation-delay: -0.9s;
+ transform: rotate(90deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(5) {
+ animation-delay: -0.8s;
+ transform: rotate(120deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(6) {
+ animation-delay: -0.7s;
+ transform: rotate(150deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(7) {
+ animation-delay: -0.6s;
+ transform: rotate(180deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(8) {
+ animation-delay: -0.5s;
+ transform: rotate(210deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(9) {
+ animation-delay: -0.4s;
+ transform: rotate(240deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(10) {
+ animation-delay: -0.3s;
+ transform: rotate(270deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(11) {
+ animation-delay: -0.2s;
+ transform: rotate(300deg) translate(146%);
+}
+
+.sonner-loading-bar:nth-child(12) {
+ animation-delay: -0.1s;
+ transform: rotate(330deg) translate(146%);
+}
+
+@keyframes sonner-fade-in {
+ 0% {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes sonner-fade-out {
+ 0% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+}
+
+@keyframes sonner-spin {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.15;
+ }
+}
+
+@media (prefers-reduced-motion) {
+ [data-sonner-toast],
+ [data-sonner-toast] > *,
+ .sonner-loading-bar {
+ transition: none !important;
+ animation: none !important;
+ }
+}
+
+.sonner-loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ transform-origin: center;
+ transition: opacity 200ms, transform 200ms;
+}
+
+.sonner-loader[data-visible='false'] {
+ opacity: 0;
+ transform: scale(0.8) translate(-50%, -50%);
+}
diff --git a/libs/copilot/src/app.tsx b/libs/copilot/src/app.tsx
index b8fff3b8f5..c016bcad6b 100644
--- a/libs/copilot/src/app.tsx
+++ b/libs/copilot/src/app.tsx
@@ -22,6 +22,12 @@ interface Props {
config: IWidgetConfig;
}
+declare global {
+ interface Window {
+ cl_shadowRootElement: HTMLDivElement;
+ }
+}
+
export default function App({ config }: Props) {
const { apiClient, accessToken } = useContext(WidgetContext);
const { setAccessToken } = useAuth(apiClient);
@@ -45,11 +51,10 @@ export default function App({ config }: Props) {
.then((res) => res.json())
.then((data: IProjectSettings) => {
window.theme = data.ui.theme;
- data.ui.hide_cot = config.showCot ? data.ui.hide_cot : true;
+ config.theme = config.theme || data.ui.theme.default;
setSettings((old) => ({
...old,
- theme: config.theme ? config.theme : old.theme,
- hideCot: data.ui.hide_cot!
+ theme: config.theme!
}));
const _theme = overrideTheme(
@@ -64,6 +69,28 @@ export default function App({ config }: Props) {
}
})
);
+ if (!_theme.components) {
+ _theme.components = {};
+ }
+ _theme.components = {
+ ..._theme.components,
+ MuiPopover: {
+ defaultProps: {
+ container: window.cl_shadowRootElement
+ }
+ },
+ MuiPopper: {
+ defaultProps: {
+ container: window.cl_shadowRootElement
+ }
+ },
+ MuiModal: {
+ defaultProps: {
+ container: window.cl_shadowRootElement
+ }
+ }
+ };
+
setTheme(_theme);
setProjectSettings(data);
})
diff --git a/libs/copilot/src/chat/body.tsx b/libs/copilot/src/chat/body.tsx
index 2379ab5af7..3c7be16586 100644
--- a/libs/copilot/src/chat/body.tsx
+++ b/libs/copilot/src/chat/body.tsx
@@ -14,9 +14,11 @@ import { v4 as uuidv4 } from 'uuid';
import { Alert, Box } from '@mui/material';
import { ErrorBoundary } from '@chainlit/app/src/components/atoms/ErrorBoundary';
+import ScrollContainer from '@chainlit/app/src/components/molecules/messages/ScrollContainer';
import { TaskList } from '@chainlit/app/src/components/molecules/tasklist/TaskList';
import DropScreen from '@chainlit/app/src/components/organisms/chat/dropScreen';
import ChatSettingsModal from '@chainlit/app/src/components/organisms/chat/settings';
+import WelcomeScreen from '@chainlit/app/src/components/organisms/chat/welcomeScreen';
import { useUpload } from '@chainlit/app/src/hooks';
import { IAttachment, attachmentsState } from '@chainlit/app/src/state/chat';
import { projectSettingsState } from '@chainlit/app/src/state/project';
@@ -183,7 +185,7 @@ const Chat = () => {
height: '100%'
}}
>
- {error && (
+ {error ? (
{
Could not reach the server.
+ ) : (
+
)}
-
+ >
+
+
+
{
+ if (session?.socket?.connected) return;
connect({
client: apiClient,
userEnv: {},
diff --git a/libs/copilot/src/chat/messages/container.tsx b/libs/copilot/src/chat/messages/container.tsx
index d7f071cf66..dde64455bd 100644
--- a/libs/copilot/src/chat/messages/container.tsx
+++ b/libs/copilot/src/chat/messages/container.tsx
@@ -6,11 +6,9 @@ import { toast } from 'sonner';
import { MessageContainer as CMessageContainer } from '@chainlit/app/src/components/molecules/messages/MessageContainer';
import { highlightMessage } from '@chainlit/app/src/state/project';
import { projectSettingsState } from '@chainlit/app/src/state/project';
-import { settingsState } from '@chainlit/app/src/state/settings';
import {
IAction,
IAsk,
- IAvatarElement,
IFeedback,
IMessageElement,
IStep,
@@ -22,10 +20,8 @@ interface Props {
loading: boolean;
actions: IAction[];
elements: IMessageElement[];
- avatars: IAvatarElement[];
messages: IStep[];
askUser?: IAsk;
- autoScroll?: boolean;
onFeedbackUpdated: (
message: IStep,
onSuccess: () => void,
@@ -37,26 +33,21 @@ interface Props {
feedbackId: 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 { apiClient } = useContext(WidgetContext);
const projectSettings = useRecoilValue(projectSettingsState);
- const { hideCot } = useRecoilValue(settingsState);
const setSideView = useSetRecoilState(sideViewState);
const highlightedMessage = useRecoilValue(highlightMessage);
const { uploadFile: _uploadFile } = useChatInteract();
@@ -70,8 +61,6 @@ const MessageContainer = memo(
const enableFeedback = !!projectSettings?.dataPersistence;
- const onPlaygroundButtonClick = useCallback(() => null, []);
-
const onElementRefClick = useCallback(
(element: IMessageElement) => {
if (element.display === 'side') {
@@ -108,10 +97,7 @@ const MessageContainer = memo(
askUser,
allowHtml: projectSettings?.features?.unsafe_allow_html,
latex: projectSettings?.features?.latex,
- avatars,
defaultCollapseContent: true,
- expandAll: false,
- hideCot: hideCot,
highlightedMessage,
loading,
showFeedbackButtons: enableFeedback,
@@ -119,12 +105,10 @@ const MessageContainer = memo(
onElementRefClick,
onError,
onFeedbackUpdated,
- onFeedbackDeleted,
- onPlaygroundButtonClick
+ onFeedbackDeleted
};
}, [
askUser,
- avatars,
enableFeedback,
highlightedMessage,
loading,
@@ -132,8 +116,7 @@ const MessageContainer = memo(
projectSettings?.features?.unsafe_allow_html,
onElementRefClick,
onError,
- onFeedbackUpdated,
- onPlaygroundButtonClick
+ onFeedbackUpdated
]);
return (
@@ -141,8 +124,6 @@ const MessageContainer = memo(
actions={messageActions}
elements={elements}
messages={messages}
- autoScroll={autoScroll}
- setAutoScroll={setAutoScroll}
context={memoizedContext}
/>
);
diff --git a/libs/copilot/src/chat/messages/index.tsx b/libs/copilot/src/chat/messages/index.tsx
index af3f5ac367..66da0b9830 100644
--- a/libs/copilot/src/chat/messages/index.tsx
+++ b/libs/copilot/src/chat/messages/index.tsx
@@ -1,13 +1,8 @@
import { WidgetContext } from 'context';
import { useCallback, useContext } from 'react';
-import { useRecoilValue, useSetRecoilState } from 'recoil';
+import { useSetRecoilState } from 'recoil';
import { toast } from 'sonner';
-import WelcomeScreen from '@chainlit/app/src/components/organisms/chat/Messages/welcomeScreen';
-import {
- IProjectSettings,
- projectSettingsState
-} from '@chainlit/app/src/state/project';
import {
IAction,
IFeedback,
@@ -16,27 +11,15 @@ import {
updateMessageById,
useChatData,
useChatInteract,
- useChatMessages,
- useChatSession
+ useChatMessages
} from '@chainlit/react-client';
import MessageContainer from './container';
-interface MessagesProps {
- autoScroll: boolean;
- projectSettings?: IProjectSettings;
- setAutoScroll: (autoScroll: boolean) => void;
-}
-
-const Messages = ({
- autoScroll,
- setAutoScroll
-}: MessagesProps): JSX.Element => {
- const projectSettings = useRecoilValue(projectSettingsState);
+const Messages = (): JSX.Element => {
const { apiClient, accessToken } = useContext(WidgetContext);
- const { idToResume } = useChatSession();
- const { elements, askUser, avatars, loading, actions } = useChatData();
+ const { elements, askUser, loading, actions } = useChatData();
const { messages } = useChatMessages();
const { callAction } = useChatInteract();
const setMessages = useSetRecoilState(messagesState);
@@ -101,7 +84,7 @@ const Messages = ({
try {
toast.promise(apiClient.deleteFeedback(feedbackId, accessToken), {
loading: 'Updating',
- success: (res) => {
+ success: (_) => {
setMessages((prev) =>
updateMessageById(prev, message.id, {
...message,
@@ -122,35 +105,16 @@ const Messages = ({
[]
);
- const showWelcomeScreen =
- !idToResume &&
- !messages.length &&
- projectSettings?.ui.show_readme_as_default;
-
- if (showWelcomeScreen) {
- return (
-
- );
- }
-
return (
);
};
diff --git a/libs/copilot/src/components/Input.tsx b/libs/copilot/src/components/Input.tsx
index 5e6d83a90e..bcb5a5a070 100644
--- a/libs/copilot/src/components/Input.tsx
+++ b/libs/copilot/src/components/Input.tsx
@@ -156,7 +156,6 @@ const Input = memo(
void
- ) {
- const payload = { userEnv };
- if (generation.type === 'CHAT') {
- payload['chatGeneration'] = generation;
- } else {
- payload['completionGeneration'] = generation;
- }
- const response = await this.post(
- `/generation`,
- payload,
- accessToken,
- controller.signal
- );
-
- const reader = response?.body?.getReader();
-
- const stream = new ReadableStream({
- start(controller) {
- function push() {
- reader!
- .read()
- .then(({ done, value }) => {
- if (done) {
- controller.close();
- tokenCb && tokenCb(done, '');
- return;
- }
- const string = new TextDecoder('utf-8').decode(value);
- tokenCb && tokenCb(done, string);
- controller.enqueue(value);
- push();
- })
- .catch((err) => {
- controller.close();
- tokenCb && tokenCb(true, '');
- console.error(err);
- });
- }
- push();
- }
- });
-
- return stream;
- }
-
async setFeedback(
feedback: IFeedback,
accessToken?: string
diff --git a/libs/react-client/src/state.ts b/libs/react-client/src/state.ts
index 8a47104d35..d7d356d90a 100644
--- a/libs/react-client/src/state.ts
+++ b/libs/react-client/src/state.ts
@@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid';
import {
IAction,
IAsk,
- IAvatarElement,
ICallFn,
IMessageElement,
IStep,
@@ -108,11 +107,6 @@ export const elementState = atom({
default: []
});
-export const avatarState = atom({
- key: 'AvatarElements',
- default: []
-});
-
export const tasklistState = atom({
key: 'TasklistElements',
default: []
diff --git a/libs/react-client/src/types/element.ts b/libs/react-client/src/types/element.ts
index 42a2813614..1d8e25ccd2 100644
--- a/libs/react-client/src/types/element.ts
+++ b/libs/react-client/src/types/element.ts
@@ -2,7 +2,6 @@ export type IElement =
| IImageElement
| ITextElement
| IPdfElement
- | IAvatarElement
| ITasklistElement
| IAudioElement
| IVideoElement
@@ -40,10 +39,6 @@ export interface IImageElement extends TMessageElement<'image'> {
size?: IElementSize;
}
-export interface IAvatarElement extends TElement<'avatar'> {
- name: string;
-}
-
export interface ITextElement extends TMessageElement<'text'> {
language?: string;
}
diff --git a/libs/react-client/src/types/generation.ts b/libs/react-client/src/types/generation.ts
deleted file mode 100644
index 7e3f5161c2..0000000000
--- a/libs/react-client/src/types/generation.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-export type GenerationMessageRole =
- | 'system'
- | 'assistant'
- | 'user'
- | 'function'
- | 'tool';
-export type ILLMSettings = Record;
-
-export interface IGenerationMessage {
- content: string;
- role: GenerationMessageRole;
- name?: string;
- tool_calls?: any[];
-}
-
-export interface IFunction {
- name: string;
- description: string;
- parameters: {
- required: string[];
- properties: Record;
- };
-}
-
-export interface ITool {
- type: string;
- function: IFunction;
-}
-
-export interface IBaseGeneration {
- provider: string;
- model?: string;
- error?: string;
- id?: string;
- variables?: Record;
- tags?: string[];
- settings?: ILLMSettings;
- tools?: ITool[];
- tokenCount?: number;
-}
-
-export interface ICompletionGeneration extends IBaseGeneration {
- type: 'COMPLETION';
- prompt?: string;
- completion?: string;
-}
-
-export interface IChatGeneration extends IBaseGeneration {
- type: 'CHAT';
- messages?: IGenerationMessage[];
- messageCompletion?: IGenerationMessage;
-}
-
-export type IGeneration = ICompletionGeneration | IChatGeneration;
diff --git a/libs/react-client/src/types/index.ts b/libs/react-client/src/types/index.ts
index 378c223de0..c10cd2bc4e 100644
--- a/libs/react-client/src/types/index.ts
+++ b/libs/react-client/src/types/index.ts
@@ -5,5 +5,4 @@ export * from './feedback';
export * from './step';
export * from './user';
export * from './thread';
-export * from './generation';
export * from './history';
diff --git a/libs/react-client/src/types/step.ts b/libs/react-client/src/types/step.ts
index 87cde8723c..130e988ec2 100644
--- a/libs/react-client/src/types/step.ts
+++ b/libs/react-client/src/types/step.ts
@@ -1,5 +1,4 @@
import { IFeedback } from './feedback';
-import { IGeneration } from './generation';
type StepType =
| 'assistant_message'
@@ -31,7 +30,6 @@ export interface IStep {
feedback?: IFeedback;
language?: string;
streaming?: boolean;
- generation?: IGeneration;
steps?: IStep[];
//legacy
indent?: number;
diff --git a/libs/react-client/src/useChatData.ts b/libs/react-client/src/useChatData.ts
index 7a5b496395..533a35c65b 100644
--- a/libs/react-client/src/useChatData.ts
+++ b/libs/react-client/src/useChatData.ts
@@ -3,7 +3,6 @@ import { useRecoilValue } from 'recoil';
import {
actionState,
askUserState,
- avatarState,
chatSettingsDefaultValueSelector,
chatSettingsInputsState,
chatSettingsValueState,
@@ -22,7 +21,6 @@ export interface IToken {
const useChatData = () => {
const loading = useRecoilValue(loadingState);
const elements = useRecoilValue(elementState);
- const avatars = useRecoilValue(avatarState);
const tasklists = useRecoilValue(tasklistState);
const actions = useRecoilValue(actionState);
const session = useRecoilValue(sessionState);
@@ -43,7 +41,6 @@ const useChatData = () => {
return {
actions,
askUser,
- avatars,
chatSettingsDefaultValue,
chatSettingsInputs,
chatSettingsValue,
diff --git a/libs/react-client/src/useChatInteract.ts b/libs/react-client/src/useChatInteract.ts
index 76a320ab9c..1389f34284 100644
--- a/libs/react-client/src/useChatInteract.ts
+++ b/libs/react-client/src/useChatInteract.ts
@@ -4,7 +4,6 @@ import {
accessTokenState,
actionState,
askUserState,
- avatarState,
chatSettingsInputsState,
chatSettingsValueState,
currentThreadIdState,
@@ -38,7 +37,6 @@ const useChatInteract = () => {
const setLoading = useSetRecoilState(loadingState);
const setMessages = useSetRecoilState(messagesState);
const setElements = useSetRecoilState(elementState);
- const setAvatars = useSetRecoilState(avatarState);
const setTasklists = useSetRecoilState(tasklistState);
const setActions = useSetRecoilState(actionState);
const setTokenCount = useSetRecoilState(tokenCountState);
@@ -54,7 +52,6 @@ const useChatInteract = () => {
setFirstUserInteraction(undefined);
setMessages([]);
setElements([]);
- setAvatars([]);
setTasklists([]);
setActions([]);
setTokenCount(0);
@@ -110,7 +107,15 @@ const useChatInteract = () => {
);
const stopTask = useCallback(() => {
+ setMessages((oldMessages) =>
+ oldMessages.map((m) => {
+ m.streaming = false;
+ return m;
+ })
+ );
+
setLoading(false);
+
session?.socket.emit('stop');
}, [session?.socket]);
diff --git a/libs/react-client/src/useChatSession.ts b/libs/react-client/src/useChatSession.ts
index 51ef2278f5..fa325cbd03 100644
--- a/libs/react-client/src/useChatSession.ts
+++ b/libs/react-client/src/useChatSession.ts
@@ -10,7 +10,6 @@ import io from 'socket.io-client';
import {
actionState,
askUserState,
- avatarState,
callFnState,
chatProfileState,
chatSettingsInputsState,
@@ -28,7 +27,6 @@ import {
} from 'src/state';
import {
IAction,
- IAvatarElement,
IElement,
IMessageElement,
IStep,
@@ -58,7 +56,6 @@ const useChatSession = () => {
const setCallFn = useSetRecoilState(callFnState);
const setElements = useSetRecoilState(elementState);
- const setAvatars = useSetRecoilState(avatarState);
const setTasklists = useSetRecoilState(tasklistState);
const setActions = useSetRecoilState(actionState);
const setChatSettingsInputs = useSetRecoilState(chatSettingsInputsState);
@@ -132,9 +129,6 @@ const useChatSession = () => {
}
setMessages(messages);
const elements = thread.elements || [];
- setAvatars(
- (elements as IAvatarElement[]).filter((e) => e.type === 'avatar')
- );
setTasklists(
(elements as ITasklistElement[]).filter((e) => e.type === 'tasklist')
);
@@ -226,16 +220,7 @@ const useChatSession = () => {
element.url = client.getElementUrl(element.chainlitKey, sessionId);
}
- if (element.type === 'avatar') {
- setAvatars((old) => {
- const index = old.findIndex((e) => e.id === element.id);
- if (index === -1) {
- return [...old, element];
- } else {
- return [...old.slice(0, index), element, ...old.slice(index + 1)];
- }
- });
- } else if (element.type === 'tasklist') {
+ if (element.type === 'tasklist') {
setTasklists((old) => {
const index = old.findIndex((e) => e.id === element.id);
if (index === -1) {
@@ -263,9 +248,6 @@ const useChatSession = () => {
setTasklists((old) => {
return old.filter((e) => e.id !== remove.id);
});
- setAvatars((old) => {
- return old.filter((e) => e.id !== remove.id);
- });
});
socket.on('action', (action: IAction) => {
diff --git a/libs/react-client/src/utils/message.ts b/libs/react-client/src/utils/message.ts
index 92d1613363..41adeb0a79 100644
--- a/libs/react-client/src/utils/message.ts
+++ b/libs/react-client/src/utils/message.ts
@@ -31,8 +31,51 @@ const isLastMessage = (messages: IStep[], index: number) => {
// Nested messages utils
const addMessage = (messages: IStep[], message: IStep): IStep[] => {
+ const validRootTypes = ['assistant_message', 'user_message', 'tool'];
+ const isValidRootType = validRootTypes.includes(message.type);
+ const isRoot = !message.parentId;
+
+ if (isRoot && !isValidRootType) {
+ return messages;
+ }
+
+ const parentMessage = !isRoot
+ ? findMessageById(messages, message.parentId!)
+ : undefined;
+
+ const shouldWrap =
+ (isRoot || parentMessage?.type !== 'assistant_message') &&
+ message.type === 'tool';
+
if (hasMessageById(messages, message.id)) {
return updateMessageById(messages, message.id, message);
+ } else if (shouldWrap) {
+ const lastMessage =
+ messages.length > 0 ? messages[messages.length - 1] : undefined;
+ const collapseTool =
+ lastMessage?.type === 'assistant_message' &&
+ lastMessage?.id.startsWith('wrap_');
+ if (lastMessage && collapseTool) {
+ return [
+ ...messages.slice(0, messages.length - 1),
+ {
+ ...lastMessage,
+ steps: [...(lastMessage.steps || []), message]
+ }
+ ];
+ }
+ return [
+ ...messages,
+ {
+ ...message,
+ name: '',
+ input: '',
+ output: '',
+ id: 'wrap_' + message.id,
+ type: 'assistant_message',
+ steps: [message]
+ }
+ ];
} else if ('parentId' in message && message.parentId) {
return addMessageToParent(messages, message.parentId, message);
} else if ('indent' in message && message.indent && message.indent > 0) {
@@ -98,17 +141,25 @@ const addMessageToParent = (
return nextMessages;
};
-const hasMessageById = (messages: IStep[], messageId: string) => {
+const findMessageById = (
+ messages: IStep[],
+ messageId: string
+): IStep | undefined => {
for (const message of messages) {
if (isEqual(message.id, messageId)) {
- return true;
+ return message;
} else if (message.steps && message.steps.length > 0) {
- if (hasMessageById(message.steps, messageId)) {
- return true;
+ const foundMessage = findMessageById(message.steps, messageId);
+ if (foundMessage) {
+ return foundMessage;
}
}
}
- return false;
+ return undefined;
+};
+
+const hasMessageById = (messages: IStep[], messageId: string): boolean => {
+ return findMessageById(messages, messageId) !== undefined;
};
const updateMessageById = (