From 9468afe3d07631cd4838ab118e6d63424f1b3d64 Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Tue, 21 May 2024 18:40:55 +0200 Subject: [PATCH] Wd/1.1.200 (#1008) * loader rework * update icons * fix auto scroll * fix github button --- CHANGELOG.md | 14 +++ backend/chainlit/discord/app.py | 8 +- backend/chainlit/message.py | 4 +- backend/chainlit/slack/app.py | 8 +- backend/chainlit/step.py | 23 +++-- backend/pyproject.toml | 2 +- cypress/e2e/step/.chainlit/config.toml | 58 +++++++++++- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 3 + frontend/src/assets/attachment.tsx | 21 +++++ frontend/src/assets/chevronDown.tsx | 21 +++++ frontend/src/assets/chevronUp.tsx | 3 +- frontend/src/assets/copy.tsx | 22 +++++ frontend/src/assets/messageBubble.tsx | 21 +++++ frontend/src/assets/microphone.tsx | 23 +++++ frontend/src/assets/thumbs.tsx | 73 +++++++++++++++ frontend/src/components/atoms/Attachment.tsx | 2 +- .../src/components/atoms/ClipboardCopy.tsx | 5 +- frontend/src/components/atoms/Collapse.tsx | 7 +- .../components/atoms/buttons/githubButton.tsx | 15 +-- .../atoms/buttons/scrollDownButton.tsx | 13 ++- .../components/molecules/BlinkingCursor.tsx | 33 +++++++ frontend/src/components/molecules/Code.tsx | 23 +++-- .../src/components/molecules/Markdown.tsx | 57 +++++++++++- .../components/molecules/messages/Message.tsx | 91 +++++++++++++++---- .../molecules/messages/MessageContainer.tsx | 10 +- .../messages/components/DetailsButton.tsx | 41 +++------ .../messages/components/FeedbackButtons.tsx | 19 ++-- .../messages/components/MessageButtons.tsx | 2 +- .../messages/components/MessageContent.tsx | 14 +-- .../playground/editor/completion.tsx | 7 +- .../chat/inputBox/MicButton/RecordScreen.tsx | 36 ++------ .../chat/inputBox/MicButton/index.tsx | 5 +- .../organisms/chat/inputBox/UploadButton.tsx | 5 +- .../organisms/chat/inputBox/input.tsx | 7 +- frontend/src/state/settings.ts | 2 +- 36 files changed, 549 insertions(+), 150 deletions(-) create mode 100644 frontend/src/assets/attachment.tsx create mode 100644 frontend/src/assets/chevronDown.tsx create mode 100644 frontend/src/assets/copy.tsx create mode 100644 frontend/src/assets/messageBubble.tsx create mode 100644 frontend/src/assets/microphone.tsx create mode 100644 frontend/src/assets/thumbs.tsx create mode 100644 frontend/src/components/molecules/BlinkingCursor.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 815bbc1574..7593704752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Nothing unreleased! +## [1.1.200] - 2024-05-21 + +### Changed + +- User message UI has been updated +- Loading indicator has been improved and visually updated +- Icons have been updated +- Dark theme is now the default + +### Fixed + +- Scroll issues on mobile browsers +- Github button now showing + ## [1.1.101] - 2024-05-14 ### Added diff --git a/backend/chainlit/discord/app.py b/backend/chainlit/discord/app.py index e5604bd8bd..5e9f4a192b 100644 --- a/backend/chainlit/discord/app.py +++ b/backend/chainlit/discord/app.py @@ -100,10 +100,16 @@ async def send_step(self, step_dict: StepDict): if not self.enabled: return + step_type = step_dict.get("type") + 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") - if is_chain_of_thought or is_empty_output: + if is_chain_of_thought or is_empty_output or not is_message: return else: enable_feedback = not step_dict.get("disableFeedback") and get_data_layer() diff --git a/backend/chainlit/message.py b/backend/chainlit/message.py index 688a52f223..474f4cb33c 100644 --- a/backend/chainlit/message.py +++ b/backend/chainlit/message.py @@ -206,7 +206,7 @@ class Message(MessageBase): def __init__( self, content: Union[str, Dict], - author: str = config.ui.name, + author: Optional[str] = None, language: Optional[str] = None, actions: Optional[List[Action]] = None, elements: Optional[List[ElementBased]] = None, @@ -243,7 +243,7 @@ def __init__( self.metadata = metadata self.tags = tags - self.author = author + self.author = author or config.ui.name self.type = type self.actions = actions if actions is not None else [] self.elements = elements if elements is not None else [] diff --git a/backend/chainlit/slack/app.py b/backend/chainlit/slack/app.py index 38059e66b9..8fd82f6449 100644 --- a/backend/chainlit/slack/app.py +++ b/backend/chainlit/slack/app.py @@ -67,10 +67,16 @@ async def send_step(self, step_dict: StepDict): if not self.enabled: return + step_type = step_dict.get("type") + 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") - if is_chain_of_thought or is_empty_output: + if is_chain_of_thought or is_empty_output or not is_message: return enable_feedback = not step_dict.get("disableFeedback") and get_data_layer() diff --git a/backend/chainlit/step.py b/backend/chainlit/step.py index 777f249895..debc2dcf3b 100644 --- a/backend/chainlit/step.py +++ b/backend/chainlit/step.py @@ -299,7 +299,7 @@ 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: + 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: @@ -332,7 +332,7 @@ async def remove(self): async def send(self): if self.persisted: - return + return self if config.code.author_rename: self.name = await config.code.author_rename(self.name) @@ -356,27 +356,21 @@ 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: - return self.id + if config.ui.hide_cot and (self.parent_id or not self.root): + 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.id + return self async def stream_token(self, token: str, is_sequence=False): """ Sends a token to the UI. Once all tokens have been streamed, call .send() to end the stream and persist the step 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.output = token else: @@ -384,9 +378,14 @@ async def stream_token(self, token: str, is_sequence=False): assert self.id - if config.ui.hide_cot and self.parent_id: + if config.ui.hide_cot and (self.parent_id or not self.root): 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 ) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c3e2ebc725..20e6b75600 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chainlit" -version = "1.1.101" +version = "1.1.200" keywords = ['LLM', 'Agents', 'gen ai', 'chat ui', 'chatbot ui', 'openai', 'copilot', 'langchain', 'conversational ai'] description = "Build Conversational AI." authors = ["Chainlit"] diff --git a/cypress/e2e/step/.chainlit/config.toml b/cypress/e2e/step/.chainlit/config.toml index 0c509af72c..041f4b6013 100644 --- a/cypress/e2e/step/.chainlit/config.toml +++ b/cypress/e2e/step/.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,6 +12,9 @@ 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 @@ -18,10 +22,43 @@ cache = false # 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. 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 = "" @@ -37,6 +74,25 @@ hide_cot = false # Link to your github repo. This will add a github button in the UI's header. # github = "" +# Specify a CSS file that can be used to customize the user interface. +# 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 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] + #layout = "wide" + #font_family = "Inter, sans-serif" # Override default MUI light theme. (Check theme.ts) [UI.theme.light] #background = "#FAFAFA" @@ -59,4 +115,4 @@ hide_cot = false [meta] -generated_by = "0.6.402" +generated_by = "1.1.0rc1" diff --git a/frontend/package.json b/frontend/package.json index 743516d19f..52b81a9c99 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,7 @@ "remark-math": "^6.0.0", "sonner": "^1.2.3", "swr": "^2.2.2", + "unist-util-visit": "^5.0.0", "usehooks-ts": "^2.9.1", "uuid": "^9.0.0", "yup": "^1.2.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5f16d4f018..d6d1d09bf5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: swr: specifier: ^2.2.2 version: 2.2.5(react@18.2.0) + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 usehooks-ts: specifier: ^2.9.1 version: 2.9.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) diff --git a/frontend/src/assets/attachment.tsx b/frontend/src/assets/attachment.tsx new file mode 100644 index 0000000000..4f245fc2fe --- /dev/null +++ b/frontend/src/assets/attachment.tsx @@ -0,0 +1,21 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const AttachmentIcon = (props: SvgIconProps) => { + return ( + + + + ); +}; + +export default AttachmentIcon; diff --git a/frontend/src/assets/chevronDown.tsx b/frontend/src/assets/chevronDown.tsx new file mode 100644 index 0000000000..437662d6cc --- /dev/null +++ b/frontend/src/assets/chevronDown.tsx @@ -0,0 +1,21 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const ChevronDownIcon = (props: SvgIconProps) => { + return ( + + + + + + ); +}; + +export default ChevronDownIcon; diff --git a/frontend/src/assets/chevronUp.tsx b/frontend/src/assets/chevronUp.tsx index 31eea42b75..8653d3fc2d 100644 --- a/frontend/src/assets/chevronUp.tsx +++ b/frontend/src/assets/chevronUp.tsx @@ -12,8 +12,7 @@ const ChevronUpIcon = (props: SvgIconProps) => { strokeLinecap="round" strokeLinejoin="round" > - - + ); diff --git a/frontend/src/assets/copy.tsx b/frontend/src/assets/copy.tsx new file mode 100644 index 0000000000..893de06cc1 --- /dev/null +++ b/frontend/src/assets/copy.tsx @@ -0,0 +1,22 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const CopyIcon = (props: SvgIconProps) => { + return ( + + + + + ); +}; + +export default CopyIcon; diff --git a/frontend/src/assets/messageBubble.tsx b/frontend/src/assets/messageBubble.tsx new file mode 100644 index 0000000000..e61bdfc879 --- /dev/null +++ b/frontend/src/assets/messageBubble.tsx @@ -0,0 +1,21 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const MessageBubbleIcon = (props: SvgIconProps) => { + return ( + + + {' '} + + + ); +}; + +export default MessageBubbleIcon; diff --git a/frontend/src/assets/microphone.tsx b/frontend/src/assets/microphone.tsx new file mode 100644 index 0000000000..226b649e07 --- /dev/null +++ b/frontend/src/assets/microphone.tsx @@ -0,0 +1,23 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +const MicrophoneIcon = (props: SvgIconProps) => { + return ( + + + + {' '} + + ); +}; + +export default MicrophoneIcon; diff --git a/frontend/src/assets/thumbs.tsx b/frontend/src/assets/thumbs.tsx new file mode 100644 index 0000000000..bde1412f19 --- /dev/null +++ b/frontend/src/assets/thumbs.tsx @@ -0,0 +1,73 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +export const ThumbDownIcon = (props: SvgIconProps) => { + return ( + + + + + + + ); +}; + +export const ThumbUpIcon = (props: SvgIconProps) => { + return ( + + + + {' '} + + + ); +}; + +export const ThumbDownFilledIcon = (props: SvgIconProps) => { + return ( + + + + + + + ); +}; + +export const ThumbUpFilledIcon = (props: SvgIconProps) => { + return ( + + + + {' '} + + + ); +}; diff --git a/frontend/src/components/atoms/Attachment.tsx b/frontend/src/components/atoms/Attachment.tsx index 84d3ead521..189596d536 100644 --- a/frontend/src/components/atoms/Attachment.tsx +++ b/frontend/src/components/atoms/Attachment.tsx @@ -32,7 +32,7 @@ const Attachment = ({ name, mime, children }: Props) => { flexDirection: 'row', alignItems: 'center', gap: 1.2, - borderRadius: 1, + borderRadius: '0.7rem', px: 1.2, border: (theme) => `1px solid ${theme.palette.primary.main}`, color: (theme) => diff --git a/frontend/src/components/atoms/ClipboardCopy.tsx b/frontend/src/components/atoms/ClipboardCopy.tsx index e085dff207..cb1d2e80e9 100644 --- a/frontend/src/components/atoms/ClipboardCopy.tsx +++ b/frontend/src/components/atoms/ClipboardCopy.tsx @@ -1,10 +1,11 @@ import { useState } from 'react'; import { useCopyToClipboard } from 'usehooks-ts'; -import ContentPaste from '@mui/icons-material/ContentPaste'; import IconButton, { IconButtonProps } from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; +import CopyIcon from 'assets/copy'; + interface ClipboardCopyProps { value: string; theme?: 'dark' | 'light'; @@ -34,7 +35,7 @@ const ClipboardCopy = ({ value, edge }: ClipboardCopyProps): JSX.Element => { sx={{ zIndex: 2 }} > - + ); diff --git a/frontend/src/components/atoms/Collapse.tsx b/frontend/src/components/atoms/Collapse.tsx index bc999e6651..41daf7de3f 100644 --- a/frontend/src/components/atoms/Collapse.tsx +++ b/frontend/src/components/atoms/Collapse.tsx @@ -1,13 +1,14 @@ import { useToggle } from 'usehooks-ts'; -import ExpandLess from '@mui/icons-material/ExpandLess'; -import ExpandMore from '@mui/icons-material/ExpandMore'; import Box from '@mui/material/Box'; import MCollapse from '@mui/material/Collapse'; import IconButton from '@mui/material/IconButton'; import Stack from '@mui/material/Stack'; import Tooltip from '@mui/material/Tooltip'; +import ChevronDownIcon from 'assets/chevronDown'; +import ChevronUpIcon from 'assets/chevronUp'; + interface CollapseProps { children: React.ReactNode; defaultExpandAll?: boolean; @@ -41,7 +42,7 @@ const Collapse = ({ - {expandAll ? : } + {expandAll ? : } diff --git a/frontend/src/components/atoms/buttons/githubButton.tsx b/frontend/src/components/atoms/buttons/githubButton.tsx index 121b75a408..db3f6ad228 100644 --- a/frontend/src/components/atoms/buttons/githubButton.tsx +++ b/frontend/src/components/atoms/buttons/githubButton.tsx @@ -1,16 +1,17 @@ +import { useRecoilValue } from 'recoil'; + import { IconButton, IconButtonProps, Tooltip } from '@mui/material'; import GithubIcon from 'assets/github'; -interface Props extends IconButtonProps { - href?: string; -} +import { projectSettingsState } from 'state/project'; -export default function GithubButton({ href, ...props }: Props) { - if (!href) { - return null; - } +interface Props extends IconButtonProps {} +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 */} diff --git a/frontend/src/components/atoms/buttons/scrollDownButton.tsx b/frontend/src/components/atoms/buttons/scrollDownButton.tsx index a1b3fe8220..8f6c852d69 100644 --- a/frontend/src/components/atoms/buttons/scrollDownButton.tsx +++ b/frontend/src/components/atoms/buttons/scrollDownButton.tsx @@ -1,6 +1,7 @@ -import ExpandCircleDown from '@mui/icons-material/ExpandCircleDown'; import IconButton from '@mui/material/IconButton'; +import ChevronDownIcon from 'assets/chevronDown'; + interface Props { onClick?: () => void; } @@ -8,19 +9,25 @@ interface Props { export default function ScrollDownButton({ onClick }: Props) { return ( - + ); } diff --git a/frontend/src/components/molecules/BlinkingCursor.tsx b/frontend/src/components/molecules/BlinkingCursor.tsx new file mode 100644 index 0000000000..9fb7aa2bed --- /dev/null +++ b/frontend/src/components/molecules/BlinkingCursor.tsx @@ -0,0 +1,33 @@ +import { keyframes } from '@emotion/react'; + +import Box from '@mui/material/Box'; + +const blink = keyframes` + from { + opacity: 1; + } + 50% { + opacity: 0; + } + to { + opacity: 1; + } +`; + +export const CURSOR_PLACEHOLDER = '\u200B'; + +export default function BlinkingCursor() { + return ( + + ); +} diff --git a/frontend/src/components/molecules/Code.tsx b/frontend/src/components/molecules/Code.tsx index 58e73707b3..9936f92dc7 100644 --- a/frontend/src/components/molecules/Code.tsx +++ b/frontend/src/components/molecules/Code.tsx @@ -1,5 +1,5 @@ import hljs from 'highlight.js'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { grey } from 'theme/palette'; import Box from '@mui/material/Box'; @@ -12,14 +12,17 @@ import { useIsDarkMode } from 'hooks/useIsDarkMode'; import 'highlight.js/styles/monokai-sublime.css'; +import BlinkingCursor, { CURSOR_PLACEHOLDER } from './BlinkingCursor'; + const CodeSnippet = ({ language, children }: { language: string; - children: React.ReactNode; + children: string; }) => { const codeRef = useRef(null); + const [highlighted, setHighlighted] = useState(false); useEffect(() => { if (codeRef.current) { @@ -27,23 +30,27 @@ const CodeSnippet = ({ codeRef.current.getAttribute('data-highlighted') === 'yes'; if (!highlighted) { hljs.highlightElement(codeRef.current); + setHighlighted(true); } } }, []); + const streaming = highlighted && children.includes(CURSOR_PLACEHOLDER); + return (
       
-        {children}
+        {children.replace(CURSOR_PLACEHOLDER, '')}
+        {streaming ?  : null}
       
     
); @@ -66,7 +73,7 @@ const Code = ({ children, ...props }: any) => { theme.spacing(1), paddingRight: '2.5em', minHeight: '20px', @@ -97,8 +104,8 @@ const Code = ({ children, ...props }: any) => { sx={{ justifyContent: 'space-between', alignItems: 'center', - borderTopLeftRadius: '4px', - borderTopRightRadius: '4px', + borderTopLeftRadius: '0.7rem', + borderTopRightRadius: '0.7rem', color: 'text.secondary', background: isDarkMode ? grey[900] : grey[200] }} diff --git a/frontend/src/components/molecules/Markdown.tsx b/frontend/src/components/molecules/Markdown.tsx index e1c461f2e9..77d6db4451 100644 --- a/frontend/src/components/molecules/Markdown.tsx +++ b/frontend/src/components/molecules/Markdown.tsx @@ -6,6 +6,7 @@ import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; +import { visit } from 'unist-util-visit'; import Link from '@mui/material/Link'; import Paper from '@mui/material/Paper'; @@ -22,6 +23,8 @@ import { ElementRef } from 'components/molecules/messages/components/ElementRef' import type { IMessageElement } from 'client-types/'; +import BlinkingCursor from './BlinkingCursor'; + interface Props { allowHtml?: boolean; latex?: boolean; @@ -29,6 +32,52 @@ interface Props { children: string; } +const cursorPlugin = () => { + return (tree: any) => { + visit(tree, 'text', (node: any, index, parent) => { + const placeholderPattern = /\u200B/g; + const matches = [...(node.value?.matchAll(placeholderPattern) || [])]; + + if (matches.length > 0) { + const newNodes: any[] = []; + let lastIndex = 0; + + matches.forEach((match) => { + const [fullMatch] = match; + const startIndex = match.index!; + const endIndex = startIndex + fullMatch.length; + + if (startIndex > lastIndex) { + newNodes.push({ + type: 'text', + value: node.value!.slice(lastIndex, startIndex) + }); + } + + newNodes.push({ + type: 'blinkingCursor', + data: { + hName: 'blinkingCursor', + hProperties: { text: 'Blinking Cursor' } + } + }); + + lastIndex = endIndex; + }); + + if (lastIndex < node.value!.length) { + newNodes.push({ + type: 'text', + value: node.value!.slice(lastIndex) + }); + } + + parent!.children.splice(index, 1, ...newNodes); + } + }); + }; +}; + function Markdown({ refElements, allowHtml, latex, children }: Props) { const rehypePlugins = useMemo(() => { let rehypePlugins: PluggableList = []; @@ -42,10 +91,10 @@ function Markdown({ refElements, allowHtml, latex, children }: Props) { }, [allowHtml, latex]); const remarkPlugins = useMemo(() => { - let remarkPlugins: PluggableList = [remarkGfm as any]; + let remarkPlugins: PluggableList = [cursorPlugin, remarkGfm as any]; if (latex) { - remarkPlugins = [remarkMath as any, ...remarkPlugins]; + remarkPlugins = [...remarkPlugins, remarkMath as any]; } return remarkPlugins; }, [latex]); @@ -119,7 +168,9 @@ function Markdown({ refElements, allowHtml, latex, children }: Props) { tbody({ children, ...props }) { // @ts-ignore return {children}; - } + }, + // @ts-expect-error custom plugin + blinkingCursor: () => }} > {children} diff --git a/frontend/src/components/molecules/messages/Message.tsx b/frontend/src/components/molecules/messages/Message.tsx index b2bf79d86f..89e748cd6b 100644 --- a/frontend/src/components/molecules/messages/Message.tsx +++ b/frontend/src/components/molecules/messages/Message.tsx @@ -16,6 +16,7 @@ import { useLayoutMaxWidth } from 'hooks/useLayoutMaxWidth'; import type { IAction, IMessageElement, IStep } from 'client-types/'; +import BlinkingCursor from '../BlinkingCursor'; import { Messages } from './Messages'; interface Props { @@ -62,11 +63,13 @@ const Message = memo( } const isAsk = message.waitForAnswer; + const isUserMessage = message.type === 'user_message'; return ( @@ -96,31 +99,81 @@ const Message = memo( overflowX: 'auto' }} > - - - + {isUserMessage ? ( + + + + setShowDetails(!showDetails)} loading={isRunning && isLast} /> - {!isRunning && isLast && isAsk && ( - - )} - {actions?.length ? ( - - ) : null} - - - + + ) : ( + + + + setShowDetails(!showDetails)} + loading={isRunning && isLast} + /> + {!isRunning && isLast && isAsk && ( + + )} + {actions?.length ? ( + + ) : null} + + + + )} + {isLast && isRunning && !message.streaming && ( + + + + )} {message.steps && showDetails && ( (); useEffect(() => { - if (!ref.current || !autoScroll) { - return; - } - ref.current.scrollTop = ref.current.scrollHeight; + setTimeout(() => { + if (!ref.current || !autoScroll) { + return; + } + ref.current.scrollTop = ref.current.scrollHeight; + }, 0); }, [messages, autoScroll]); const handleScroll = () => { diff --git a/frontend/src/components/molecules/messages/components/DetailsButton.tsx b/frontend/src/components/molecules/messages/components/DetailsButton.tsx index 5f58d30874..8ae15c5be8 100644 --- a/frontend/src/components/molecules/messages/components/DetailsButton.tsx +++ b/frontend/src/components/molecules/messages/components/DetailsButton.tsx @@ -1,13 +1,12 @@ import { MessageContext } from 'contexts/MessageContext'; import { useContext } from 'react'; -import ExpandLess from '@mui/icons-material/ExpandLess'; -import ExpandMore from '@mui/icons-material/ExpandMore'; -import CircularProgress from '@mui/material/CircularProgress'; - 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 { @@ -19,7 +18,6 @@ interface Props { const DetailsButton = ({ message, opened, onClick, loading }: Props) => { const messageContext = useContext(MessageContext); - const nestedCount = message.steps?.length; const nested = !!nestedCount && !messageContext.hideCot; @@ -27,14 +25,7 @@ const DetailsButton = ({ message, opened, onClick, loading }: Props) => { const tool = lastStep ? lastStep.name : undefined; - const content = message.output; - - const showDefaultLoader = - loading && (!content || (messageContext.hideCot && !message.streaming)); - - const show = tool || showDefaultLoader; - - if (!show) { + if (!tool) { return null; } @@ -46,14 +37,9 @@ const DetailsButton = ({ message, opened, onClick, loading }: Props) => { const text = ( {loading ? ( - tool ? ( - <> - {' '} - {tool} - - ) : ( - - ) + <> + {tool} + ) : ( <> { : undefined - } variant="contained" endIcon={ - nested && tool ? opened ? : : undefined + nested && tool ? ( + opened ? ( + + ) : ( + + ) + ) : undefined } onClick={tool ? onClick : undefined} > diff --git a/frontend/src/components/molecules/messages/components/FeedbackButtons.tsx b/frontend/src/components/molecules/messages/components/FeedbackButtons.tsx index b13a23ec63..d9ccc16f7f 100644 --- a/frontend/src/components/molecules/messages/components/FeedbackButtons.tsx +++ b/frontend/src/components/molecules/messages/components/FeedbackButtons.tsx @@ -3,11 +3,6 @@ import { useContext, useState } from 'react'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; -import StickyNote2Outlined from '@mui/icons-material/StickyNote2Outlined'; -import ThumbDownAlt from '@mui/icons-material/ThumbDownAlt'; -import ThumbDownAltOutlined from '@mui/icons-material/ThumbDownAltOutlined'; -import ThumbUpAlt from '@mui/icons-material/ThumbUpAlt'; -import ThumbUpAltOutlined from '@mui/icons-material/ThumbUpAltOutlined'; import IconButton from '@mui/material/IconButton'; import Stack from '@mui/material/Stack'; import Tooltip from '@mui/material/Tooltip'; @@ -18,6 +13,14 @@ import Dialog from 'components/atoms/Dialog'; import { AccentButton } from 'components/atoms/buttons/AccentButton'; import { TextInput } from 'components/atoms/inputs'; +import MessageBubbleIcon from 'assets/messageBubble'; +import { + ThumbDownFilledIcon, + ThumbDownIcon, + ThumbUpFilledIcon, + ThumbUpIcon +} from 'assets/thumbs'; + import type { IStep } from 'client-types/'; const ICON_SIZE = '16px'; @@ -36,8 +39,8 @@ const FeedbackButtons = ({ message }: Props) => { const [feedback, setFeedback] = useState(message.feedback?.value); const [comment, setComment] = useState(message.feedback?.comment); - const DownIcon = feedback === 0 ? ThumbDownAlt : ThumbDownAltOutlined; - const UpIcon = feedback === 1 ? ThumbUpAlt : ThumbUpAltOutlined; + const DownIcon = feedback === 0 ? ThumbDownFilledIcon : ThumbDownIcon; + const UpIcon = feedback === 1 ? ThumbUpFilledIcon : ThumbUpIcon; const handleFeedbackChanged = (feedback?: number, comment?: string) => { if (feedback === undefined) { @@ -136,7 +139,7 @@ const FeedbackButtons = ({ message }: Props) => { }} className="feedback-comment-edit" > - +
diff --git a/frontend/src/components/molecules/messages/components/MessageButtons.tsx b/frontend/src/components/molecules/messages/components/MessageButtons.tsx index 96d984e97c..cf3fdf365c 100644 --- a/frontend/src/components/molecules/messages/components/MessageButtons.tsx +++ b/frontend/src/components/molecules/messages/components/MessageButtons.tsx @@ -43,7 +43,7 @@ const MessageButtons = ({ message }: Props) => { return ( { - const isUser = 'role' in message && message.role === 'user'; - let lineCount = 0; let contentLength = 0; + const content = message.streaming + ? message.output + CURSOR_PLACEHOLDER + : message.output; + const { preparedContent: output, inlinedElements: outputInlinedElements, @@ -37,7 +40,7 @@ const MessageContent = memo( } = prepareContent({ elements, id: message.id, - content: message.output, + content: content, language: message.language }); @@ -88,8 +91,7 @@ const MessageContent = memo( width: '100%', minHeight: '20px', fontSize: '1rem', - fontFamily: (theme) => theme.typography.fontFamily, - fontWeight: isUser ? 500 : 300 + fontFamily: (theme) => theme.typography.fontFamily }} component="div" > @@ -110,7 +112,7 @@ const MessageContent = memo( return ( - + {output ? messageContent : null} diff --git a/frontend/src/components/molecules/playground/editor/completion.tsx b/frontend/src/components/molecules/playground/editor/completion.tsx index f72a1800dc..56223870f5 100644 --- a/frontend/src/components/molecules/playground/editor/completion.tsx +++ b/frontend/src/components/molecules/playground/editor/completion.tsx @@ -3,13 +3,14 @@ import { OrderedSet } from 'immutable'; import { useEffect, useState } from 'react'; import { grey } from 'theme'; -import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 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'; @@ -97,7 +98,7 @@ export default function Completion({ completion, chatMode }: Props) { Completion setCompletionOpen(!isCompletionOpen)}> - {isCompletionOpen ? : } + {isCompletionOpen ? : } - - - + - { size={size} onClick={startRecording} > - + diff --git a/frontend/src/components/organisms/chat/inputBox/UploadButton.tsx b/frontend/src/components/organisms/chat/inputBox/UploadButton.tsx index c1b40d6425..9ac6b5a05f 100644 --- a/frontend/src/components/organisms/chat/inputBox/UploadButton.tsx +++ b/frontend/src/components/organisms/chat/inputBox/UploadButton.tsx @@ -1,13 +1,14 @@ import { useUpload } from 'hooks'; import { useRecoilValue } from 'recoil'; -import AttachFile from '@mui/icons-material/AttachFile'; import { IconButton, Theme, Tooltip, useMediaQuery } from '@mui/material'; import { FileSpec } from '@chainlit/react-client'; import { Translator } from 'components/i18n'; +import AttachmentIcon from 'assets/attachment'; + import { projectSettingsState } from 'state/project'; type Props = { @@ -55,7 +56,7 @@ const UploadButton = ({ size={size} {...getRootProps({ className: 'dropzone' })} > - + diff --git a/frontend/src/components/organisms/chat/inputBox/input.tsx b/frontend/src/components/organisms/chat/inputBox/input.tsx index 89b39c33a4..78f64c2e0b 100644 --- a/frontend/src/components/organisms/chat/inputBox/input.tsx +++ b/frontend/src/components/organisms/chat/inputBox/input.tsx @@ -172,16 +172,15 @@ const Input = memo( `1px solid ${theme.palette.divider}`, + borderRadius: '1.5rem', boxShadow: 'box-shadow: 0px 2px 4px 0px #0000000D', textarea: { height: '34px', maxHeight: '30vh', overflowY: 'auto !important', resize: 'none', - paddingBottom: '0.75rem', - paddingTop: '0.75rem', + paddingBottom: '0.7rem', + paddingTop: '0.7rem', color: 'text.primary', lineHeight: '24px' } diff --git a/frontend/src/state/settings.ts b/frontend/src/state/settings.ts index 34dfc92169..774979caa3 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 = 'light'; +const defaultTheme = 'dark'; const preferredTheme = localStorage.getItem( 'themeVariant'