├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── actions
│ ├── pnpm-node-install
│ │ └── action.yaml
│ └── poetry-python-install
│ │ └── action.yaml
└── workflows
│ ├── ci.yaml
│ ├── e2e-tests.yaml
│ ├── lint-backend.yaml
│ ├── lint-ui.yaml
│ ├── publish.yaml
│ └── pytest.yaml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── .prettierrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── PRIVACY_POLICY.md
├── README.md
├── RELENG.md
├── backend
├── README.md
├── build.py
├── chainlit
│ ├── __init__.py
│ ├── __main__.py
│ ├── _utils.py
│ ├── action.py
│ ├── auth
│ │ ├── __init__.py
│ │ ├── cookie.py
│ │ └── jwt.py
│ ├── cache.py
│ ├── callbacks.py
│ ├── chat_context.py
│ ├── chat_settings.py
│ ├── cli
│ │ └── __init__.py
│ ├── config.py
│ ├── context.py
│ ├── data
│ │ ├── __init__.py
│ │ ├── acl.py
│ │ ├── base.py
│ │ ├── chainlit_data_layer.py
│ │ ├── dynamodb.py
│ │ ├── literalai.py
│ │ ├── sql_alchemy.py
│ │ ├── storage_clients
│ │ │ ├── __init__.py
│ │ │ ├── azure.py
│ │ │ ├── azure_blob.py
│ │ │ ├── base.py
│ │ │ ├── gcs.py
│ │ │ └── s3.py
│ │ └── utils.py
│ ├── discord
│ │ ├── __init__.py
│ │ └── app.py
│ ├── element.py
│ ├── emitter.py
│ ├── hello.py
│ ├── input_widget.py
│ ├── langchain
│ │ ├── __init__.py
│ │ └── callbacks.py
│ ├── langflow
│ │ └── __init__.py
│ ├── llama_index
│ │ ├── __init__.py
│ │ └── callbacks.py
│ ├── logger.py
│ ├── markdown.py
│ ├── mcp.py
│ ├── message.py
│ ├── mistralai
│ │ └── __init__.py
│ ├── oauth_providers.py
│ ├── openai
│ │ └── __init__.py
│ ├── py.typed
│ ├── secret.py
│ ├── semantic_kernel
│ │ └── __init__.py
│ ├── server.py
│ ├── session.py
│ ├── sidebar.py
│ ├── slack
│ │ ├── __init__.py
│ │ └── app.py
│ ├── socket.py
│ ├── step.py
│ ├── sync.py
│ ├── teams
│ │ ├── __init__.py
│ │ └── app.py
│ ├── telemetry.py
│ ├── translations.py
│ ├── translations
│ │ ├── bn.json
│ │ ├── en-US.json
│ │ ├── gu.json
│ │ ├── he-IL.json
│ │ ├── hi.json
│ │ ├── ja.json
│ │ ├── kn.json
│ │ ├── ml.json
│ │ ├── mr.json
│ │ ├── nl.json
│ │ ├── ta.json
│ │ ├── te.json
│ │ └── zh-CN.json
│ ├── types.py
│ ├── user.py
│ ├── user_session.py
│ ├── utils.py
│ └── version.py
├── poetry.lock
├── pyproject.toml
└── tests
│ ├── __init__.py
│ ├── auth
│ ├── __init__.py
│ └── test_cookie.py
│ ├── conftest.py
│ ├── data
│ ├── __init__.py
│ ├── conftest.py
│ ├── storage_clients
│ │ └── test_s3.py
│ ├── test_get_data_layer.py
│ ├── test_literalai.py
│ └── test_sql_alchemy.py
│ ├── llama_index
│ └── test_callbacks.py
│ ├── test_callbacks.py
│ ├── test_context.py
│ ├── test_emitter.py
│ ├── test_server.py
│ └── test_user_session.py
├── cypress.config.ts
├── cypress
├── e2e
│ ├── action
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── ask_file
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── ask_multiple_files
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── ask_user
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── audio_element
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── chat_context
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── chat_profiles
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── chat_settings
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── command
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── context
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── copilot
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── custom_build
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ ├── public
│ │ │ ├── .gitignore
│ │ │ └── build
│ │ │ │ ├── assets
│ │ │ │ └── .PLACEHOLDER
│ │ │ │ └── index.html
│ │ └── spec.cy.ts
│ ├── custom_data_layer
│ │ └── sql_alchemy.py
│ ├── custom_element
│ │ ├── main.py
│ │ ├── public
│ │ │ └── elements
│ │ │ │ └── Counter.jsx
│ │ └── spec.cy.ts
│ ├── custom_theme
│ │ ├── main.py
│ │ ├── public
│ │ │ └── theme.json
│ │ └── spec.cy.ts
│ ├── data_layer
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── dataframe
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── edit_message
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── elements
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── cat.jpeg
│ │ ├── dummy.pdf
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── error_handling
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── file_element
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── header_auth
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── llama_index_cb
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── on_chat_start
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── password_auth
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── plotly
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── pyplot
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── readme
│ │ ├── chainlit_pt-BR.md
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── remove_elements
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── remove_step
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── sidebar
│ │ ├── cat.jpeg
│ │ ├── dummy.pdf
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── starters
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── step
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ ├── main_async.py
│ │ └── spec.cy.ts
│ ├── stop_task
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main_async.py
│ │ ├── main_sync.py
│ │ └── spec.cy.ts
│ ├── streaming
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── tasklist
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── update_step
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── upload_attachments
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── user_env
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ ├── user_session
│ │ ├── .chainlit
│ │ │ └── config.toml
│ │ ├── main.py
│ │ └── spec.cy.ts
│ └── window_message
│ │ ├── main.py
│ │ ├── public
│ │ └── iframe.html
│ │ └── spec.cy.ts
├── fixtures
│ ├── cat.jpeg
│ ├── example.mp3
│ ├── example.mp4
│ ├── hello.cpp
│ ├── hello.py
│ └── state_of_the_union.txt
└── support
│ ├── e2e.ts
│ ├── run.ts
│ ├── testUtils.ts
│ └── utils.ts
├── frontend
├── .eslintignore
├── .gitignore
├── components.json
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
│ └── favicon.svg
├── src
│ ├── App.tsx
│ ├── AppWrapper.tsx
│ ├── api
│ │ └── index.ts
│ ├── assets
│ │ ├── logo_dark.svg
│ │ └── logo_light.svg
│ ├── components
│ │ ├── Alert.tsx
│ │ ├── AudioPresence.tsx
│ │ ├── AutoResizeTextarea.tsx
│ │ ├── AutoResumeThread.tsx
│ │ ├── BlinkingCursor.tsx
│ │ ├── ButtonLink.tsx
│ │ ├── ChatSettings
│ │ │ ├── FormInput.tsx
│ │ │ ├── InputLabel.tsx
│ │ │ ├── InputStateHandler.tsx
│ │ │ ├── NotificationCount.tsx
│ │ │ ├── SelectInput.tsx
│ │ │ ├── SliderInput.tsx
│ │ │ ├── SwitchInput.tsx
│ │ │ ├── TagsInput.tsx
│ │ │ ├── TextInput.tsx
│ │ │ └── index.tsx
│ │ ├── CodeSnippet.tsx
│ │ ├── CopyButton.tsx
│ │ ├── ElementSideView.tsx
│ │ ├── ElementView.tsx
│ │ ├── Elements
│ │ │ ├── Audio.tsx
│ │ │ ├── CustomElement
│ │ │ │ ├── Imports.ts
│ │ │ │ ├── Renderer.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Dataframe.tsx
│ │ │ ├── ElementRef.tsx
│ │ │ ├── File.tsx
│ │ │ ├── Image.tsx
│ │ │ ├── LazyDataframe.tsx
│ │ │ ├── PDF.tsx
│ │ │ ├── Plotly.tsx
│ │ │ ├── Text.tsx
│ │ │ ├── Video.tsx
│ │ │ └── index.tsx
│ │ ├── ErrorBoundary.tsx
│ │ ├── Icon.tsx
│ │ ├── Kbd.tsx
│ │ ├── LeftSidebar
│ │ │ ├── Search.tsx
│ │ │ ├── ThreadHistory.tsx
│ │ │ ├── ThreadList.tsx
│ │ │ ├── ThreadOptions.tsx
│ │ │ └── index.tsx
│ │ ├── Loader.tsx
│ │ ├── LoginForm.tsx
│ │ ├── Logo.tsx
│ │ ├── Markdown.tsx
│ │ ├── MarkdownAlert.tsx
│ │ ├── ProviderButton.tsx
│ │ ├── QuiltedGrid.tsx
│ │ ├── ReadOnlyThread.tsx
│ │ ├── Tasklist
│ │ │ ├── Task.tsx
│ │ │ ├── TaskStatusIcon.tsx
│ │ │ └── index.tsx
│ │ ├── ThemeProvider.tsx
│ │ ├── WaterMark.tsx
│ │ ├── chat
│ │ │ ├── Footer.tsx
│ │ │ ├── MessageComposer
│ │ │ │ ├── Attachment.tsx
│ │ │ │ ├── Attachments.tsx
│ │ │ │ ├── CommandButtons.tsx
│ │ │ │ ├── CommandPopoverButton.tsx
│ │ │ │ ├── Input.tsx
│ │ │ │ ├── Mcp
│ │ │ │ │ ├── AddForm.tsx
│ │ │ │ │ ├── AnimatedPlugIcon.tsx
│ │ │ │ │ ├── List.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── SubmitButton.tsx
│ │ │ │ ├── UploadButton.tsx
│ │ │ │ ├── VoiceButton.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Messages
│ │ │ │ ├── Message
│ │ │ │ │ ├── AskActionButtons.tsx
│ │ │ │ │ ├── AskFileButton.tsx
│ │ │ │ │ ├── Avatar.tsx
│ │ │ │ │ ├── Buttons
│ │ │ │ │ │ ├── Actions
│ │ │ │ │ │ │ ├── ActionButton.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── DebugButton.tsx
│ │ │ │ │ │ ├── FeedbackButtons.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── Content
│ │ │ │ │ │ ├── InlinedElements
│ │ │ │ │ │ │ ├── InlineCustomElementList.tsx
│ │ │ │ │ │ │ ├── InlinedAudioList.tsx
│ │ │ │ │ │ │ ├── InlinedDataframeList.tsx
│ │ │ │ │ │ │ ├── InlinedFileList.tsx
│ │ │ │ │ │ │ ├── InlinedImageList.tsx
│ │ │ │ │ │ │ ├── InlinedPDFList.tsx
│ │ │ │ │ │ │ ├── InlinedPlotlyList.tsx
│ │ │ │ │ │ │ ├── InlinedTextList.tsx
│ │ │ │ │ │ │ ├── InlinedVideoList.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── Step.tsx
│ │ │ │ │ ├── UserMessage.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── MessagesContainer
│ │ │ │ └── index.tsx
│ │ │ ├── ScrollContainer.tsx
│ │ │ ├── ScrollDownButton.tsx
│ │ │ ├── Starter.tsx
│ │ │ ├── Starters.tsx
│ │ │ ├── WelcomeScreen.tsx
│ │ │ └── index.tsx
│ │ ├── header
│ │ │ ├── ApiKeys.tsx
│ │ │ ├── ChatProfiles.tsx
│ │ │ ├── NewChat.tsx
│ │ │ ├── Readme.tsx
│ │ │ ├── SidebarTrigger.tsx
│ │ │ ├── ThemeToggle.tsx
│ │ │ ├── UserNav.tsx
│ │ │ └── index.tsx
│ │ ├── i18n
│ │ │ ├── Translator.tsx
│ │ │ └── index.ts
│ │ ├── icons
│ │ │ ├── Auth0.tsx
│ │ │ ├── Cognito.tsx
│ │ │ ├── Descope.tsx
│ │ │ ├── EditSquare.tsx
│ │ │ ├── Github.tsx
│ │ │ ├── Gitlab.tsx
│ │ │ ├── Google.tsx
│ │ │ ├── Microsoft.tsx
│ │ │ ├── Okta.tsx
│ │ │ ├── PaperClip.tsx
│ │ │ ├── Pencil.tsx
│ │ │ ├── Search.tsx
│ │ │ ├── Send.tsx
│ │ │ ├── Settings.tsx
│ │ │ ├── Sidebar.tsx
│ │ │ ├── Stop.tsx
│ │ │ ├── ToolBox.tsx
│ │ │ └── VoiceLines.tsx
│ │ └── ui
│ │ │ ├── accordion.tsx
│ │ │ ├── alert-dialog.tsx
│ │ │ ├── aspect-ratio.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── carousel.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── command.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── form.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── pagination.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── resizable.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── sidebar.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── table.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── textarea.tsx
│ │ │ └── tooltip.tsx
│ ├── contexts
│ │ └── MessageContext.tsx
│ ├── hooks
│ │ ├── query.ts
│ │ ├── use-mobile.tsx
│ │ ├── useFetch.tsx
│ │ ├── useLayoutMaxWidth.tsx
│ │ ├── usePlatform.ts
│ │ └── useUpload.tsx
│ ├── i18n
│ │ └── index.ts
│ ├── index.css
│ ├── index.d.ts
│ ├── lib
│ │ ├── message.ts
│ │ ├── router.ts
│ │ └── utils.ts
│ ├── main.tsx
│ ├── pages
│ │ ├── AuthCallback.tsx
│ │ ├── Element.tsx
│ │ ├── Env.tsx
│ │ ├── Home.tsx
│ │ ├── Login.tsx
│ │ ├── Page.tsx
│ │ └── Thread.tsx
│ ├── router.tsx
│ ├── state
│ │ ├── chat.ts
│ │ ├── project.ts
│ │ └── user.ts
│ ├── types
│ │ ├── Input.ts
│ │ ├── NotificationCount.tsx
│ │ ├── chat.ts
│ │ ├── index.ts
│ │ └── messageContext.ts
│ └── vite-env.d.ts
├── tailwind.config.js
├── tests
│ ├── content.spec.tsx
│ ├── setup-tests.ts
│ └── tsconfig.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts
├── images
└── quick-start.png
├── libs
├── copilot
│ ├── .storybook
│ │ ├── main.ts
│ │ └── preview.ts
│ ├── components.json
│ ├── index.tsx
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── postcss.config.js
│ ├── sonner.css
│ ├── src
│ │ ├── ThemeProvider.tsx
│ │ ├── api.ts
│ │ ├── app.tsx
│ │ ├── appWrapper.tsx
│ │ ├── chat
│ │ │ ├── body.tsx
│ │ │ └── index.tsx
│ │ ├── components
│ │ │ ├── ElementSideView.tsx
│ │ │ ├── Header.tsx
│ │ │ └── WelcomeScreen.tsx
│ │ ├── index.css
│ │ ├── lib
│ │ │ └── utils.ts
│ │ ├── types.ts
│ │ └── widget.tsx
│ ├── stories
│ │ └── App.stories.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
└── react-client
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── src
│ ├── api
│ │ ├── hooks
│ │ │ ├── api.ts
│ │ │ └── auth
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── sessionManagement.ts
│ │ │ │ ├── state.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── userManagement.ts
│ │ └── index.tsx
│ ├── context.ts
│ ├── index.ts
│ ├── state.ts
│ ├── types
│ │ ├── action.ts
│ │ ├── audio.ts
│ │ ├── command.ts
│ │ ├── config.ts
│ │ ├── element.ts
│ │ ├── feedback.ts
│ │ ├── file.ts
│ │ ├── history.ts
│ │ ├── index.ts
│ │ ├── mcp.ts
│ │ ├── step.ts
│ │ ├── thread.ts
│ │ └── user.ts
│ ├── useAudio.ts
│ ├── useChatData.ts
│ ├── useChatInteract.ts
│ ├── useChatMessages.ts
│ ├── useChatSession.ts
│ ├── useConfig.ts
│ ├── utils
│ │ ├── group.ts
│ │ └── message.ts
│ └── wavtools
│ │ ├── analysis
│ │ ├── audio_analysis.js
│ │ └── constants.js
│ │ ├── index.ts
│ │ ├── wav_packer.js
│ │ ├── wav_recorder.js
│ │ ├── wav_renderer.ts
│ │ ├── wav_stream_player.js
│ │ └── worklets
│ │ ├── audio_processor.js
│ │ └── stream_processor.js
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── lint-staged.config.js
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 |
6 | [*.{py,ts,tsx}]
7 | indent_style = space
8 | insert_final_newline = true
9 |
10 | [*.py]
11 | indent_size = 4
12 | trim_trailing_whitespace = true
13 |
14 | [*.{ts,tsx}]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "ignorePatterns": ["**/*.jsx"],
5 | "plugins": ["@typescript-eslint"],
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/eslint-recommended",
9 | "plugin:@typescript-eslint/recommended"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/no-non-null-assertion": "off",
13 | "@typescript-eslint/no-explicit-any": "off",
14 | "no-unused-vars": "off",
15 | "@typescript-eslint/no-unused-vars": [
16 | "error",
17 | {
18 | "argsIgnorePattern": "^_",
19 | "varsIgnorePattern": "^_",
20 | "caughtErrorsIgnorePattern": "^_",
21 | "ignoreRestSiblings": true
22 | }
23 | ]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: needs-triage
6 | assignees: ''
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 |
28 | - OS: [e.g. iOS]
29 | - Browser [e.g. chrome, safari]
30 | - Version [e.g. 22]
31 |
32 | **Smartphone (please complete the following information):**
33 |
34 | - Device: [e.g. iPhone6]
35 | - OS: [e.g. iOS8.1]
36 | - Browser [e.g. stock browser, safari]
37 | - Version [e.g. 22]
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: needs-triage
6 | assignees: ''
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/actions/pnpm-node-install/action.yaml:
--------------------------------------------------------------------------------
1 | name: Install Node, pnpm and dependencies.
2 | description: Install Node, pnpm and dependencies using cache.
3 |
4 | inputs:
5 | node-version:
6 | description: Node.js version
7 | required: true
8 | default: '23.3.0'
9 | pnpm-version:
10 | description: pnpm version
11 | required: true
12 | default: '9.7.0'
13 | pnpm-skip-install:
14 | description: Skip install.
15 | required: false
16 | default: 'false'
17 | pnpm-install-args:
18 | description: Extra arguments for pnpm install, e.g. --no-frozen-lockfile.
19 | default: '--frozen-lockfile'
20 |
21 | runs:
22 | using: composite
23 | steps:
24 | - uses: pnpm/action-setup@v4
25 | with:
26 | version: ${{ inputs.pnpm-version }}
27 | - name: Use Node.js
28 | uses: actions/setup-node@v4
29 | with:
30 | node-version: ${{ inputs.node-version }}
31 | cache: pnpm
32 | - name: Install JS dependencies
33 | run: pnpm install ${{ inputs.pnpm-install-args }}
34 | shell: bash
35 | # Skip install if pnpm-skip-install is true
36 | if: ${{ inputs.pnpm-skip-install != 'true' }}
37 |
--------------------------------------------------------------------------------
/.github/actions/poetry-python-install/action.yaml:
--------------------------------------------------------------------------------
1 | name: Install Python, poetry and dependencies.
2 | description: Install Python, Poetry and poetry dependencies using cache
3 |
4 | inputs:
5 | python-version:
6 | description: Python version
7 | required: true
8 | default: '3.10'
9 | poetry-version:
10 | description: Poetry version
11 | required: true
12 | default: '1.8.3'
13 | poetry-working-directory:
14 | description: Working directory for poetry command.
15 | required: false
16 | default: .
17 | poetry-install-args:
18 | description: Extra arguments for poetry install, e.g. --with tests.
19 | required: false
20 |
21 | runs:
22 | using: composite
23 | steps:
24 | - name: Cache poetry install
25 | uses: actions/cache@v4
26 | with:
27 | path: ~/.local
28 | key: poetry-${{ runner.os }}-${{ inputs.poetry-version }}-0
29 | - name: Install Poetry
30 | run: pipx install 'poetry==${{ inputs.poetry-version }}'
31 | shell: bash
32 | - name: Set up Python ${{ inputs.python-version }}
33 | id: setup_python
34 | uses: actions/setup-python@v5
35 | with:
36 | python-version: ${{ inputs.python-version }}
37 | cache: poetry
38 | cache-dependency-path: ${{ inputs.poetry-working-directory }}/poetry.lock
39 | - name: Set Poetry environment
40 | run: poetry -C '${{ inputs.poetry-working-directory }}' env use '${{ steps.setup_python.outputs.python-path }}'
41 | shell: bash
42 | - name: Install Python dependencies
43 | run: poetry -C '${{ inputs.poetry-working-directory }}' install ${{ inputs.poetry-install-args }}
44 | shell: bash
45 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_call:
5 | workflow_dispatch:
6 | merge_group:
7 | pull_request:
8 | branches: [main, dev, 'release/**']
9 | push:
10 | branches: [main, dev, 'release/**']
11 |
12 | permissions: read-all
13 |
14 | jobs:
15 | pytest:
16 | uses: ./.github/workflows/pytest.yaml
17 | secrets: inherit
18 | lint-backend:
19 | uses: ./.github/workflows/lint-backend.yaml
20 | secrets: inherit
21 | e2e-tests:
22 | uses: ./.github/workflows/e2e-tests.yaml
23 | secrets: inherit
24 | lint-ui:
25 | uses: ./.github/workflows/lint-ui.yaml
26 | secrets: inherit
27 | ci:
28 | runs-on: ubuntu-latest
29 | name: Run CI
30 | if: always() # This ensures the job always runs
31 | needs: [lint-backend, pytest, lint-ui, e2e-tests]
32 | steps:
33 | # Propagate failure
34 | - name: Check dependent jobs
35 | if: contains(needs.*.result, 'success') != true || contains(needs.*.result, 'skipped')
36 | run: |
37 | echo "Not all required jobs succeeded"
38 | exit 1
39 |
--------------------------------------------------------------------------------
/.github/workflows/e2e-tests.yaml:
--------------------------------------------------------------------------------
1 | name: E2ETests
2 |
3 | on: [workflow_call]
4 |
5 | permissions: read-all
6 |
7 | jobs:
8 | ci:
9 | runs-on: ${{ matrix.os }}
10 | strategy:
11 | matrix:
12 | os: [ubuntu-latest, windows-latest]
13 | env:
14 | BACKEND_DIR: ./backend
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: ./.github/actions/pnpm-node-install
18 | name: Install Node, pnpm and dependencies.
19 | with:
20 | pnpm-skip-install: true
21 | - name: Install depdendencies and Cypress
22 | uses: cypress-io/github-action@v6
23 | with:
24 | runTests: false
25 | - uses: ./.github/actions/poetry-python-install
26 | name: Install Python, poetry and Python dependencies
27 | with:
28 | poetry-working-directory: ${{ env.BACKEND_DIR }}
29 | poetry-install-args: --with tests
30 | - name: Run tests
31 | env:
32 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
33 | uses: nick-fields/retry@v3
34 | with:
35 | timeout_minutes: 20
36 | max_attempts: 3
37 | command: pnpm test
38 |
--------------------------------------------------------------------------------
/.github/workflows/lint-backend.yaml:
--------------------------------------------------------------------------------
1 | name: LintBackend
2 |
3 | on: [workflow_call]
4 |
5 | permissions: read-all
6 |
7 | jobs:
8 | lint-backend:
9 | runs-on: ubuntu-latest
10 | env:
11 | BACKEND_DIR: ./backend
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: ./.github/actions/poetry-python-install
15 | name: Install Python, poetry and Python dependencies
16 | with:
17 | poetry-install-args: --with tests --with mypy --with custom-data --no-root
18 | poetry-working-directory: ${{ env.BACKEND_DIR }}
19 | - name: Lint with ruff
20 | uses: astral-sh/ruff-action@v1
21 | with:
22 | version-file: "backend/pyproject.toml"
23 | src: ${{ env.BACKEND_DIR }}
24 | changed-files: "true"
25 | - name: Check formatting with ruff
26 | uses: astral-sh/ruff-action@v1
27 | with:
28 | version-file: "backend/pyproject.toml"
29 | src: ${{ env.BACKEND_DIR }}
30 | changed-files: "true"
31 | args: "format --check"
32 | - name: Run Mypy
33 | run: poetry run mypy chainlit/
34 | working-directory: ${{ env.BACKEND_DIR }}
35 |
--------------------------------------------------------------------------------
/.github/workflows/lint-ui.yaml:
--------------------------------------------------------------------------------
1 | name: LintUI
2 |
3 | on: [workflow_call]
4 |
5 | permissions: read-all
6 |
7 | jobs:
8 | ci:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: ./.github/actions/pnpm-node-install
13 | name: Install Node, pnpm and dependencies.
14 | - name: Build UI
15 | run: pnpm run buildUi
16 | - name: Lint UI
17 | run: pnpm run lintUi
18 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [published]
7 |
8 | permissions: read-all
9 |
10 | jobs:
11 | ci:
12 | uses: ./.github/workflows/ci.yaml
13 | secrets: inherit
14 | build-n-publish:
15 | name: Upload release to PyPI
16 | runs-on: ubuntu-latest
17 | needs: [ci]
18 | env:
19 | name: pypi
20 | url: https://pypi.org/p/chainlit
21 | BACKEND_DIR: ./backend
22 | permissions:
23 | contents: read
24 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
25 | steps:
26 | - uses: actions/checkout@v4
27 | with:
28 | ref: main
29 | - uses: ./.github/actions/pnpm-node-install
30 | name: Install Node, pnpm and dependencies.
31 | with:
32 | pnpm-install-args: --no-frozen-lockfile
33 | - uses: ./.github/actions/poetry-python-install
34 | name: Install Python, poetry and Python dependencies
35 | with:
36 | poetry-working-directory: ${{ env.BACKEND_DIR }}
37 | - name: Build Python distribution
38 | run: poetry self add poetry-plugin-ignore-build-script && poetry build --ignore-build-script
39 | working-directory: ${{ env.BACKEND_DIR }}
40 | - name: Publish package distributions to PyPI
41 | uses: pypa/gh-action-pypi-publish@release/v1
42 | with:
43 | packages-dir: backend/dist
44 | password: ${{ secrets.PYPI_API_TOKEN }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/pytest.yaml:
--------------------------------------------------------------------------------
1 | name: Pytest
2 |
3 | on: [workflow_call]
4 |
5 | permissions: read-all
6 |
7 | jobs:
8 | pytest:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | python-version: ['3.10', '3.11', '3.12']
13 | fastapi-version: ['0.115']
14 | env:
15 | BACKEND_DIR: ./backend
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: ./.github/actions/pnpm-node-install
19 | name: Install Node, pnpm and dependencies.
20 | - uses: ./.github/actions/poetry-python-install
21 | name: Install Python, poetry and Python dependencies
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | poetry-install-args: --with tests --with mypy --with custom-data
25 | poetry-working-directory: ${{ env.BACKEND_DIR }}
26 | - name: Install fastapi ${{ matrix.fastapi-version }}
27 | run: poetry add fastapi@^${{ matrix.fastapi-version}}
28 | working-directory: ${{ env.BACKEND_DIR }}
29 | - name: Run Pytest
30 | run: poetry run pytest --cov=chainlit/
31 | working-directory: ${{ env.BACKEND_DIR }}
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | dist
3 |
4 | *.egg-info
5 |
6 | .env
7 |
8 | *.files
9 |
10 | venv
11 | .venv
12 | .DS_Store
13 |
14 | .chainlit
15 | !cypress/e2e/**/*/.chainlit/*
16 | chainlit.md
17 |
18 | cypress/screenshots
19 | cypress/videos
20 | cypress/downloads
21 |
22 | __pycache__
23 |
24 | .ipynb_checkpoints
25 |
26 | *.db
27 |
28 | .mypy_cache
29 |
30 | chat_files
31 |
32 | .chroma
33 |
34 | # Logs
35 | logs
36 | *.log
37 | npm-debug.log*
38 | yarn-debug.log*
39 | yarn-error.log*
40 | pnpm-debug.log*
41 | lerna-debug.log*
42 |
43 | node_modules
44 | dist
45 | dist-ssr
46 | *.local
47 |
48 | # Editor directories and files
49 | .vscode/*
50 | !.vscode/extensions.json
51 | .idea
52 | .DS_Store
53 | *.suo
54 | *.ntvs*
55 | *.njsproj
56 | *.sln
57 | *.sw?
58 |
59 | .aider*
60 | .coverage
61 |
62 | backend/README.md
63 | backend/.dmypy.json
64 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm lint-staged
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shared-workspace-lockfile=false
2 | public-hoist-pattern[]=*eslint*
3 | public-hoist-pattern[]=*prettier*
4 | public-hoist-pattern[]=@types*
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "none",
4 | "singleQuote": true,
5 | "printWidth": 80,
6 | "plugins": ["@trivago/prettier-plugin-sort-imports"],
7 | "importOrder": [
8 | "pages/(.*)$",
9 | "@chainlit/(.*)$",
10 | "components/(.*)$",
11 | "assets/(.*)$",
12 | "hooks/(.*)$",
13 | "state/(.*)$",
14 | "types/(.*)$",
15 | "^./*.*.css",
16 | "^[./]"
17 | ],
18 | "importOrderSeparation": true,
19 | "importOrderSortSpecifiers": true
20 | }
21 |
--------------------------------------------------------------------------------
/PRIVACY_POLICY.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | ## 📏 Telemetry
4 |
5 | Chainlit collects specific metadata points by default to help us better understand and improve the package based on community usage. We greatly value your privacy and ensure that the metadata we collect [is limited](/backend/telemetry.py).
6 |
7 | ### 🕵️♀️ Scope
8 |
9 | Chainlit collects the following metadata points:
10 |
11 | - Count of SDK function calls
12 | - Duration of SDK function calls
13 |
14 | This information allows us to get an accurate representation of how the community uses Chainlit and make improvements accordingly.
15 |
16 | ### 🙅♀️ Opting Out of Telemetry
17 |
18 | If you prefer not to share this metadata, you can easily opt out by setting `enable_telemetry = false` in your `.chainlit/config.toml` file. This will disable the telemetry feature and prevent any data from being collected.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | backend/README.md
--------------------------------------------------------------------------------
/backend/chainlit/__main__.py:
--------------------------------------------------------------------------------
1 | from chainlit.cli import cli
2 |
3 | if __name__ == "__main__":
4 | cli(prog_name="chainlit")
5 |
--------------------------------------------------------------------------------
/backend/chainlit/_utils.py:
--------------------------------------------------------------------------------
1 | """Util functions which are explicitly not part of the public API."""
2 |
3 | from pathlib import Path
4 |
5 |
6 | def is_path_inside(child_path: Path, parent_path: Path) -> bool:
7 | """Check if the child path is inside the parent path."""
8 | return parent_path.resolve() in child_path.resolve().parents
9 |
--------------------------------------------------------------------------------
/backend/chainlit/action.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import Dict, Optional
3 |
4 | from dataclasses_json import DataClassJsonMixin
5 | from pydantic import Field
6 | from pydantic.dataclasses import dataclass
7 |
8 | from chainlit.context import context
9 | from chainlit.telemetry import trace_event
10 |
11 |
12 | @dataclass
13 | class Action(DataClassJsonMixin):
14 | # Name of the action, this should be used in the action_callback
15 | name: str
16 | # The parameters to call this action with.
17 | payload: Dict
18 | # The label of the action. This is what the user will see.
19 | label: str = ""
20 | # The tooltip of the action button. This is what the user will see when they hover the action.
21 | tooltip: str = ""
22 | # The lucid icon name for this action.
23 | icon: Optional[str] = None
24 | # This should not be set manually, only used internally.
25 | forId: Optional[str] = None
26 | # The ID of the action
27 | id: str = Field(default_factory=lambda: str(uuid.uuid4()))
28 |
29 | def __post_init__(self) -> None:
30 | trace_event(f"init {self.__class__.__name__}")
31 |
32 | async def send(self, for_id: str):
33 | trace_event(f"send {self.__class__.__name__}")
34 | self.forId = for_id
35 | await context.emitter.emit("action", self.to_dict())
36 |
37 | async def remove(self):
38 | trace_event(f"remove {self.__class__.__name__}")
39 | await context.emitter.emit("remove_action", self.to_dict())
40 |
--------------------------------------------------------------------------------
/backend/chainlit/auth/jwt.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime, timedelta, timezone
3 | from typing import Any, Dict, Optional
4 |
5 | import jwt as pyjwt
6 |
7 | from chainlit.config import config
8 | from chainlit.user import User
9 |
10 |
11 | def get_jwt_secret() -> Optional[str]:
12 | return os.environ.get("CHAINLIT_AUTH_SECRET")
13 |
14 |
15 | def create_jwt(data: User) -> str:
16 | to_encode: Dict[str, Any] = data.to_dict()
17 | to_encode.update(
18 | {
19 | "exp": datetime.now(timezone.utc)
20 | + timedelta(seconds=config.project.user_session_timeout),
21 | "iat": datetime.now(timezone.utc), # Add issued at time
22 | }
23 | )
24 |
25 | secret = get_jwt_secret()
26 | assert secret
27 | encoded_jwt = pyjwt.encode(to_encode, secret, algorithm="HS256")
28 | return encoded_jwt
29 |
30 |
31 | def decode_jwt(token: str) -> User:
32 | secret = get_jwt_secret()
33 | assert secret
34 |
35 | dict = pyjwt.decode(
36 | token,
37 | secret,
38 | algorithms=["HS256"],
39 | options={"verify_signature": True},
40 | )
41 | del dict["exp"]
42 | return User(**dict)
43 |
--------------------------------------------------------------------------------
/backend/chainlit/cache.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 | import os
3 | import threading
4 | from typing import Any
5 |
6 | from chainlit.config import config
7 | from chainlit.logger import logger
8 |
9 |
10 | def init_lc_cache():
11 | use_cache = config.project.cache is True and config.run.no_cache is False
12 |
13 | if use_cache and importlib.util.find_spec("langchain") is not None:
14 | from langchain.cache import SQLiteCache
15 | from langchain.globals import set_llm_cache
16 |
17 | if config.project.lc_cache_path is not None:
18 | set_llm_cache(SQLiteCache(database_path=config.project.lc_cache_path))
19 |
20 | if not os.path.exists(config.project.lc_cache_path):
21 | logger.info(
22 | f"LangChain cache created at: {config.project.lc_cache_path}"
23 | )
24 |
25 |
26 | _cache: dict[tuple, Any] = {}
27 | _cache_lock = threading.Lock()
28 |
29 |
30 | def cache(func):
31 | def wrapper(*args, **kwargs):
32 | # Create a cache key based on the function name, arguments, and keyword arguments
33 | cache_key = (
34 | (func.__name__,) + args + tuple((k, v) for k, v in sorted(kwargs.items()))
35 | )
36 |
37 | with _cache_lock:
38 | # Check if the result is already in the cache
39 | if cache_key not in _cache:
40 | # If not, call the function and store the result in the cache
41 | _cache[cache_key] = func(*args, **kwargs)
42 |
43 | return _cache[cache_key]
44 |
45 | return wrapper
46 |
--------------------------------------------------------------------------------
/backend/chainlit/chat_settings.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from pydantic import Field
4 | from pydantic.dataclasses import dataclass
5 |
6 | from chainlit.context import context
7 | from chainlit.input_widget import InputWidget
8 |
9 |
10 | @dataclass
11 | class ChatSettings:
12 | """Useful to create chat settings that the user can change."""
13 |
14 | inputs: List[InputWidget] = Field(default_factory=list, exclude=True)
15 |
16 | def __init__(
17 | self,
18 | inputs: List[InputWidget],
19 | ) -> None:
20 | self.inputs = inputs
21 |
22 | def settings(self):
23 | return dict(
24 | [(input_widget.id, input_widget.initial) for input_widget in self.inputs]
25 | )
26 |
27 | async def send(self):
28 | settings = self.settings()
29 | context.emitter.set_chat_settings(settings)
30 |
31 | inputs_content = [input_widget.to_dict() for input_widget in self.inputs]
32 | await context.emitter.emit("chat_settings", inputs_content)
33 |
34 | return settings
35 |
--------------------------------------------------------------------------------
/backend/chainlit/data/acl.py:
--------------------------------------------------------------------------------
1 | from fastapi import HTTPException
2 |
3 | from chainlit.data import get_data_layer
4 |
5 |
6 | async def is_thread_author(username: str, thread_id: str):
7 | data_layer = get_data_layer()
8 | if not data_layer:
9 | raise HTTPException(status_code=400, detail="Data layer not initialized")
10 |
11 | thread_author = await data_layer.get_thread_author(thread_id)
12 |
13 | if not thread_author:
14 | raise HTTPException(status_code=404, detail="Thread not found")
15 |
16 | if thread_author != username:
17 | raise HTTPException(status_code=401, detail="Unauthorized")
18 | else:
19 | return True
20 |
--------------------------------------------------------------------------------
/backend/chainlit/data/storage_clients/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/backend/chainlit/data/storage_clients/__init__.py
--------------------------------------------------------------------------------
/backend/chainlit/data/storage_clients/base.py:
--------------------------------------------------------------------------------
1 | import os
2 | from abc import ABC, abstractmethod
3 | from typing import Any, Dict, Union
4 |
5 | storage_expiry_time = int(os.getenv("STORAGE_EXPIRY_TIME", 3600))
6 |
7 |
8 | class BaseStorageClient(ABC):
9 | """Base class for non-text data persistence like Azure Data Lake, S3, Google Storage, etc."""
10 |
11 | @abstractmethod
12 | async def upload_file(
13 | self,
14 | object_key: str,
15 | data: Union[bytes, str],
16 | mime: str = "application/octet-stream",
17 | overwrite: bool = True,
18 | ) -> Dict[str, Any]:
19 | pass
20 |
21 | @abstractmethod
22 | async def delete_file(self, object_key: str) -> bool:
23 | pass
24 |
25 | @abstractmethod
26 | async def get_read_url(self, object_key: str) -> str:
27 | pass
28 |
--------------------------------------------------------------------------------
/backend/chainlit/data/utils.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from collections import deque
3 |
4 | from chainlit.context import context
5 | from chainlit.session import WebsocketSession
6 |
7 |
8 | def queue_until_user_message():
9 | def decorator(method):
10 | @functools.wraps(method)
11 | async def wrapper(self, *args, **kwargs):
12 | if (
13 | isinstance(context.session, WebsocketSession)
14 | and not context.session.has_first_interaction
15 | ):
16 | # Queue the method invocation waiting for the first user message
17 | queues = context.session.thread_queues
18 | method_name = method.__name__
19 | if method_name not in queues:
20 | queues[method_name] = deque()
21 | queues[method_name].append((method, self, args, kwargs))
22 |
23 | else:
24 | # Otherwise, Execute the method immediately
25 | return await method(self, *args, **kwargs)
26 |
27 | return wrapper
28 |
29 | return decorator
30 |
--------------------------------------------------------------------------------
/backend/chainlit/discord/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 |
3 | if importlib.util.find_spec("discord") is None:
4 | raise ValueError(
5 | "The discord package is required to integrate Chainlit with a Discord app. Run `pip install discord --upgrade`"
6 | )
7 |
--------------------------------------------------------------------------------
/backend/chainlit/hello.py:
--------------------------------------------------------------------------------
1 | # This is a simple example of a chainlit app.
2 |
3 | from chainlit import AskUserMessage, Message, on_chat_start
4 |
5 |
6 | @on_chat_start
7 | async def main():
8 | res = await AskUserMessage(content="What is your name?", timeout=30).send()
9 | if res:
10 | await Message(
11 | content=f"Your name is: {res['output']}.\nChainlit installation is working!\nYou can now start building your own chainlit apps!",
12 | ).send()
13 |
--------------------------------------------------------------------------------
/backend/chainlit/langchain/__init__.py:
--------------------------------------------------------------------------------
1 | from chainlit.utils import check_module_version
2 |
3 | if not check_module_version("langchain", "0.0.198"):
4 | raise ValueError(
5 | "Expected LangChain version >= 0.0.198. Run `pip install langchain --upgrade`"
6 | )
7 |
--------------------------------------------------------------------------------
/backend/chainlit/langflow/__init__.py:
--------------------------------------------------------------------------------
1 | from chainlit.utils import check_module_version
2 |
3 | if not check_module_version("langflow", "0.1.4"):
4 | raise ValueError(
5 | "Expected Langflow version >= 0.1.4. Run `pip install langflow --upgrade`"
6 | )
7 |
8 | from typing import Dict, Optional, Union
9 |
10 | import httpx
11 |
12 | from chainlit.telemetry import trace_event
13 |
14 |
15 | async def load_flow(schema: Union[Dict, str], tweaks: Optional[Dict] = None):
16 | from langflow import load_flow_from_json
17 |
18 | trace_event("load_langflow")
19 |
20 | if isinstance(schema, str):
21 | async with httpx.AsyncClient() as client:
22 | response = await client.get(schema)
23 | if response.status_code != 200:
24 | raise ValueError(f"Error: {response.text}")
25 | schema = response.json()
26 |
27 | flow = load_flow_from_json(flow=schema, tweaks=tweaks)
28 |
29 | return flow
30 |
--------------------------------------------------------------------------------
/backend/chainlit/llama_index/__init__.py:
--------------------------------------------------------------------------------
1 | from chainlit.utils import check_module_version
2 |
3 | if not check_module_version("llama_index.core", "0.10.15"):
4 | raise ValueError(
5 | "Expected LlamaIndex version >= 0.10.15. Run `pip install llama_index --upgrade`"
6 | )
7 |
--------------------------------------------------------------------------------
/backend/chainlit/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 |
4 | logging.basicConfig(
5 | level=logging.INFO,
6 | stream=sys.stdout,
7 | format="%(asctime)s - %(message)s",
8 | datefmt="%Y-%m-%d %H:%M:%S",
9 | )
10 |
11 | logging.getLogger("socketio").setLevel(logging.ERROR)
12 | logging.getLogger("engineio").setLevel(logging.ERROR)
13 | logging.getLogger("numexpr").setLevel(logging.ERROR)
14 |
15 |
16 | logger = logging.getLogger("chainlit")
17 |
--------------------------------------------------------------------------------
/backend/chainlit/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/backend/chainlit/py.typed
--------------------------------------------------------------------------------
/backend/chainlit/secret.py:
--------------------------------------------------------------------------------
1 | import secrets
2 | import string
3 |
4 | # Using punctuation, without chars that can break in the cli (quotes, backslash, backtick...)
5 | chars = string.ascii_letters + string.digits + "$%*,-./:=>?@^_~"
6 |
7 |
8 | def random_secret(length: int = 64):
9 | return "".join(secrets.choice(chars) for i in range(length))
10 |
--------------------------------------------------------------------------------
/backend/chainlit/slack/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 |
3 | if importlib.util.find_spec("slack_bolt") is None:
4 | raise ValueError(
5 | "The slack_bolt package is required to integrate Chainlit with a Slack app. Run `pip install slack_bolt --upgrade`"
6 | )
7 |
--------------------------------------------------------------------------------
/backend/chainlit/sync.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from typing import Any, Coroutine, TypeVar
3 |
4 | if sys.version_info >= (3, 10):
5 | from typing import ParamSpec
6 | else:
7 | from typing_extensions import ParamSpec
8 |
9 | import asyncio
10 | import threading
11 |
12 | from asyncer import asyncify
13 | from syncer import sync
14 |
15 | from chainlit.context import context_var
16 |
17 | make_async = asyncify
18 |
19 | T_Retval = TypeVar("T_Retval")
20 | T_ParamSpec = ParamSpec("T_ParamSpec")
21 | T = TypeVar("T")
22 |
23 |
24 | def run_sync(co: Coroutine[Any, Any, T_Retval]) -> T_Retval:
25 | """Run the coroutine synchronously."""
26 |
27 | # Copy the current context
28 | current_context = context_var.get()
29 |
30 | # Define a wrapper coroutine that sets the context before running the original coroutine
31 | async def context_preserving_coroutine():
32 | # Set the copied context to the coroutine
33 | context_var.set(current_context)
34 | return await co
35 |
36 | # Execute from the main thread in the main event loop
37 | if threading.current_thread() == threading.main_thread():
38 | return sync(context_preserving_coroutine())
39 | else: # Execute from a thread in the main event loop
40 | result = asyncio.run_coroutine_threadsafe(
41 | context_preserving_coroutine(), loop=current_context.loop
42 | )
43 | return result.result()
44 |
--------------------------------------------------------------------------------
/backend/chainlit/teams/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 |
3 | if importlib.util.find_spec("botbuilder") is None:
4 | raise ValueError(
5 | "The botbuilder-core package is required to integrate Chainlit with a Slack app. Run `pip install botbuilder-core --upgrade`"
6 | )
7 |
--------------------------------------------------------------------------------
/backend/chainlit/user.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Literal, Optional, TypedDict
2 |
3 | from dataclasses_json import DataClassJsonMixin
4 | from pydantic import Field
5 | from pydantic.dataclasses import dataclass
6 |
7 | Provider = Literal[
8 | "credentials",
9 | "header",
10 | "github",
11 | "google",
12 | "azure-ad",
13 | "azure-ad-hybrid",
14 | "okta",
15 | "auth0",
16 | "descope",
17 | ]
18 |
19 |
20 | class UserDict(TypedDict):
21 | id: str
22 | identifier: str
23 | display_name: Optional[str]
24 | metadata: Dict
25 |
26 |
27 | # Used when logging-in a user
28 | @dataclass
29 | class User(DataClassJsonMixin):
30 | identifier: str
31 | display_name: Optional[str] = None
32 | metadata: Dict = Field(default_factory=dict)
33 |
34 |
35 | @dataclass
36 | class PersistedUserFields:
37 | id: str
38 | createdAt: str
39 |
40 |
41 | @dataclass
42 | class PersistedUser(User, PersistedUserFields):
43 | pass
44 |
--------------------------------------------------------------------------------
/backend/chainlit/version.py:
--------------------------------------------------------------------------------
1 | from importlib import metadata
2 |
3 | try:
4 | __version__ = metadata.version(__package__)
5 | except metadata.PackageNotFoundError:
6 | # Case where package metadata is not available, default to a 'non-outdated' version.
7 | # Ref: config.py::load_settings()
8 | __version__ = "2.5.5"
9 |
--------------------------------------------------------------------------------
/backend/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/backend/tests/auth/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/backend/tests/auth/__init__.py
--------------------------------------------------------------------------------
/backend/tests/data/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/backend/tests/data/__init__.py
--------------------------------------------------------------------------------
/backend/tests/data/conftest.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import AsyncMock
2 |
3 | import pytest
4 |
5 | from chainlit.data.storage_clients.base import BaseStorageClient
6 | from chainlit.user import User
7 |
8 |
9 | @pytest.fixture
10 | def mock_storage_client():
11 | mock_client = AsyncMock(spec=BaseStorageClient)
12 | mock_client.upload_file.return_value = {
13 | "url": "https://example.com/test.txt",
14 | "object_key": "test_user/test_element/test.txt",
15 | }
16 | return mock_client
17 |
18 |
19 | @pytest.fixture
20 | def test_user() -> User:
21 | return User(identifier="test_user_identifier", metadata={})
22 |
--------------------------------------------------------------------------------
/backend/tests/data/test_get_data_layer.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import AsyncMock, Mock
2 |
3 | from chainlit.data import get_data_layer
4 |
5 |
6 | async def test_get_data_layer(
7 | mock_data_layer: AsyncMock,
8 | mock_get_data_layer: Mock,
9 | ):
10 | # Check whether the data layer is properly set
11 | assert mock_data_layer == get_data_layer()
12 |
13 | mock_get_data_layer.assert_called_once()
14 |
15 | # Getting the data layer again, should not result in additional call
16 | assert mock_data_layer == get_data_layer()
17 |
18 | mock_get_data_layer.assert_called_once()
19 |
--------------------------------------------------------------------------------
/backend/tests/test_user_session.py:
--------------------------------------------------------------------------------
1 | async def test_user_session_set_get(mock_chainlit_context, user_session):
2 | async with mock_chainlit_context as context:
3 | # Test setting a value
4 | user_session.set("test_key", "test_value")
5 |
6 | # Test getting the value
7 | assert user_session.get("test_key") == "test_value"
8 |
9 | # Test getting a default value for a non-existent key
10 | assert user_session.get("non_existent_key", "default") == "default"
11 |
12 | # Test getting session-related values
13 | assert user_session.get("id") == context.session.id
14 | assert user_session.get("env") == context.session.user_env
15 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | projectId: 'ij1tyk',
5 | component: {
6 | devServer: {
7 | framework: 'react',
8 | bundler: 'vite'
9 | }
10 | },
11 | viewportWidth: 1200,
12 |
13 | e2e: {
14 | supportFile: false,
15 | defaultCommandTimeout: 30000,
16 | video: false,
17 | baseUrl: 'http://127.0.0.1:8000',
18 | setupNodeEvents(on) {
19 | on('task', {
20 | log(message) {
21 | console.log(message);
22 | return null;
23 | }
24 | });
25 | }
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/cypress/e2e/ask_file/main.py:
--------------------------------------------------------------------------------
1 | import aiofiles
2 |
3 | import chainlit as cl
4 |
5 |
6 | @cl.on_chat_start
7 | async def start():
8 | files = await cl.AskFileMessage(
9 | content="Please upload a text file to begin!", accept=["text/plain"]
10 | ).send()
11 | txt_file = files[0]
12 |
13 | async with aiofiles.open(txt_file.path, "r", encoding="utf-8") as f:
14 | content = await f.read()
15 | await cl.Message(
16 | content=f"`Text file {txt_file.name}` uploaded, it contains {len(content)} characters!"
17 | ).send()
18 |
19 | files = await cl.AskFileMessage(
20 | content="Please upload a python file to begin!", accept={"text/plain": [".py"]}
21 | ).send()
22 | py_file = files[0]
23 |
24 | async with aiofiles.open(py_file.path, "r", encoding="utf-8") as f:
25 | content = await f.read()
26 | await cl.Message(
27 | content=f"`Python file {py_file.name}` uploaded, it contains {len(content)} characters!"
28 | ).send()
29 |
--------------------------------------------------------------------------------
/cypress/e2e/ask_file/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Upload file', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to receive and decode files', () => {
9 | cy.get('#ask-upload-button').should('exist');
10 |
11 | // Upload a text file
12 | cy.fixture('state_of_the_union.txt', 'utf-8').as('txtFile');
13 | cy.get('#ask-button-input').selectFile('@txtFile', { force: true });
14 |
15 | // Sometimes the loading indicator is not shown because the file upload is too fast
16 | // cy.get("#ask-upload-button-loading").should("exist");
17 | // cy.get("#ask-upload-button-loading").should("not.exist");
18 |
19 | cy.get('.step')
20 | .eq(1)
21 | .should(
22 | 'contain',
23 | 'Text file state_of_the_union.txt uploaded, it contains'
24 | );
25 |
26 | cy.get('#ask-upload-button').should('exist');
27 |
28 | // Expecting a python file, cpp file upload should be rejected
29 | cy.fixture('hello.cpp', 'utf-8').as('cppFile');
30 | cy.get('#ask-button-input').selectFile('@cppFile', { force: true });
31 |
32 | cy.get('.step').should('have.length', 3);
33 |
34 | // Upload a python file
35 | cy.fixture('hello.py', 'utf-8').as('pyFile');
36 | cy.get('#ask-button-input').selectFile('@pyFile', { force: true });
37 |
38 | cy.get('.step')
39 | .should('have.length', 4)
40 | .eq(3)
41 | .should('contain', 'Python file hello.py uploaded, it contains');
42 |
43 | cy.get('#ask-upload-button').should('not.exist');
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/cypress/e2e/ask_multiple_files/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def start():
6 | files = await cl.AskFileMessage(
7 | content="Please upload from one to two python files to begin!",
8 | max_files=2,
9 | accept={"text/plain": [".py"]},
10 | ).send()
11 |
12 | file_names = [file.name for file in files]
13 |
14 | await cl.Message(
15 | content=f"{len(files)} files uploaded: {','.join(file_names)}"
16 | ).send()
17 |
--------------------------------------------------------------------------------
/cypress/e2e/ask_multiple_files/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Upload multiple files', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to receive two files', () => {
9 | cy.get('#ask-upload-button').should('exist');
10 |
11 | cy.fixture('state_of_the_union.txt', 'utf-8').as('txtFile');
12 | cy.fixture('hello.py', 'utf-8').as('pyFile');
13 |
14 | cy.get('#ask-button-input').selectFile(['@txtFile', '@pyFile'], {
15 | force: true
16 | });
17 |
18 | // Sometimes the loading indicator is not shown because the file upload is too fast
19 | // cy.get("#ask-upload-button-loading").should("exist");
20 | // cy.get("#ask-upload-button-loading").should("not.exist");
21 |
22 | cy.get('.step')
23 | .eq(1)
24 | .should('contain', '2 files uploaded: state_of_the_union.txt,hello.py');
25 |
26 | cy.get('#ask-upload-button').should('not.exist');
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/cypress/e2e/ask_user/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def main():
6 | res = await cl.AskUserMessage(content="What is your name?", timeout=10).send()
7 | if res:
8 | await cl.Message(
9 | content=f"Your name is: {res['output']}",
10 | ).send()
11 |
--------------------------------------------------------------------------------
/cypress/e2e/ask_user/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer, submitMessage } from '../../support/testUtils';
2 |
3 | describe('Ask User', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should send a new message containing the user input', () => {
9 | cy.get('.step').should('have.length', 1);
10 | submitMessage('Jeeves');
11 |
12 | cy.get('.step').should('have.length', 3);
13 |
14 | cy.get('.step').eq(2).should('contain', 'Jeeves');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/cypress/e2e/audio_element/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def start():
6 | elements = [
7 | cl.Audio(
8 | name="example.mp3", path="../../fixtures/example.mp3", display="inline"
9 | )
10 | ]
11 |
12 | await cl.Message(content="This message has an audio", elements=elements).send()
13 |
--------------------------------------------------------------------------------
/cypress/e2e/audio_element/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('audio', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to display an audio element', () => {
9 | cy.get('.step').should('have.length', 1);
10 | cy.get('.step').eq(0).find('.inline-audio').should('have.length', 1);
11 |
12 | cy.get('.inline-audio audio')
13 | .then(($el) => {
14 | const audioElement = $el.get(0) as HTMLAudioElement;
15 | return audioElement.play().then(() => {
16 | return audioElement.duration;
17 | });
18 | })
19 | .should('be.greaterThan', 0);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/cypress/e2e/chat_context/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_message
5 | async def main():
6 | await cl.Message(
7 | content=f"Chat context length: {len(cl.chat_context.get())}"
8 | ).send()
9 |
--------------------------------------------------------------------------------
/cypress/e2e/chat_context/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer, submitMessage } from '../../support/testUtils';
2 |
3 | describe('Chat Context', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to current conversation chat history', () => {
9 | submitMessage('Hello 1');
10 |
11 | cy.get('.step').eq(1).should('contain', 'Chat context length: 1');
12 |
13 | submitMessage('Hello 2');
14 |
15 | cy.get('.step').eq(3).should('contain', 'Chat context length: 3');
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/cypress/e2e/command/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 | commands = [
4 | {"id": "Picture", "icon": "image", "description": "Use DALL-E"},
5 | {"id": "Search", "icon": "globe", "description": "Find on the web", "button": True},
6 | {
7 | "id": "Canvas",
8 | "icon": "pen-line",
9 | "description": "Collaborate on writing and code",
10 | },
11 | ]
12 |
13 |
14 | @cl.on_chat_start
15 | async def start():
16 | await cl.context.emitter.set_commands(commands)
17 |
18 |
19 | @cl.on_message
20 | async def message(msg: cl.Message):
21 | if msg.command == "Picture":
22 | await cl.context.emitter.set_commands([])
23 |
24 | await cl.Message(content=f"Command: {msg.command}").send()
25 |
--------------------------------------------------------------------------------
/cypress/e2e/command/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Command', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should correctly display commands', () => {
9 | cy.get(`#chat-input`).type("/sear")
10 | cy.get(".command-item").should('have.length', 1);
11 | cy.get(".command-item").eq(0).click()
12 |
13 | cy.get(`#chat-input`).type("Hello{enter}")
14 |
15 | cy.get(".step").should('have.length', 2);
16 | cy.get(".step").eq(0).find(".command-span").should("have.text", "Search")
17 |
18 | cy.get("#command-button").should("exist")
19 |
20 | cy.get(".step").eq(1).invoke('text').then((text) => {
21 | expect(text.trim()).to.equal("Command: Search")
22 | })
23 |
24 | cy.get(`#chat-input`).type("/pic")
25 | cy.get(".command-item").should('have.length', 1);
26 | cy.get(".command-item").eq(0).click()
27 |
28 | cy.get(`#chat-input`).type("Hello{enter}")
29 |
30 | cy.get(".step").should('have.length', 4);
31 | cy.get(".step").eq(2).find(".command-span").should("have.text", "Picture")
32 |
33 | cy.get("#command-button").should("not.exist")
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/cypress/e2e/context/main.py:
--------------------------------------------------------------------------------
1 | from chainlit.context import context
2 | from chainlit.sync import make_async, run_sync
3 |
4 | import chainlit as cl
5 |
6 |
7 | async def async_function_from_sync():
8 | await cl.sleep(2)
9 | return context.emitter
10 |
11 |
12 | def sync_function():
13 | emitter_from_make_async = context.emitter
14 | emitter_from_async_from_sync = run_sync(async_function_from_sync())
15 | return (emitter_from_make_async, emitter_from_async_from_sync)
16 |
17 |
18 | async def async_function():
19 | return await another_async_function()
20 |
21 |
22 | async def another_async_function():
23 | await cl.sleep(2)
24 | return context.emitter
25 |
26 |
27 | @cl.on_chat_start
28 | async def main():
29 | emitter_from_async = await async_function()
30 | if emitter_from_async:
31 | await cl.Message(content="emitter from async found!").send()
32 | else:
33 | await cl.ErrorMessage(content="emitter from async not found").send()
34 |
35 | emitter_from_make_async, emitter_from_async_from_sync = await make_async(
36 | sync_function
37 | )()
38 |
39 | if emitter_from_make_async:
40 | await cl.Message(content="emitter from make_async found!").send()
41 | else:
42 | await cl.ErrorMessage(content="emitter from make_async not found").send()
43 |
44 | if emitter_from_async_from_sync:
45 | await cl.Message(content="emitter from async_from_sync found!").send()
46 | else:
47 | await cl.ErrorMessage(content="emitter from async_from_sync not found").send()
48 |
--------------------------------------------------------------------------------
/cypress/e2e/context/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Context should be reachable', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should find the Emitter from async, make_async and async_from_sync contexts', () => {
9 | cy.get('.step').should('have.length', 3);
10 |
11 | cy.get('.step').eq(0).should('contain', 'emitter from async found!');
12 |
13 | cy.get('.step').eq(1).should('contain', 'emitter from make_async found!');
14 |
15 | cy.get('.step')
16 | .eq(2)
17 | .should('contain', 'emitter from async_from_sync found!');
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/cypress/e2e/copilot/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def on_chat_start():
6 | await cl.Message(content="Hi from copilot!").send()
7 |
8 |
9 | @cl.on_message
10 | async def on_message(msg: cl.Message):
11 | if cl.context.session.client_type == "copilot":
12 | if msg.type == "system_message":
13 | await cl.Message(content=f"System message received: {msg.content}").send()
14 | return
15 |
16 | fn = cl.CopilotFunction(name="test", args={"msg": msg.content})
17 | res = await fn.acall()
18 | await cl.Message(content=res).send()
19 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_build/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def main():
6 | await cl.Message(content="Hello!").send()
7 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_build/public/.gitignore:
--------------------------------------------------------------------------------
1 | !build
2 | !dist
--------------------------------------------------------------------------------
/cypress/e2e/custom_build/public/build/assets/.PLACEHOLDER:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/cypress/e2e/custom_build/public/build/assets/.PLACEHOLDER
--------------------------------------------------------------------------------
/cypress/e2e/custom_build/public/build/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Custom Build
4 |
5 |
6 | This is a test page for custom build configuration.
7 |
8 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_build/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from "../../support/testUtils";
2 |
3 | describe("Custom Build", () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it("should correctly serve the custom build page", () => {
9 | cy.get("body").contains("This is a test page for custom build configuration.");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_data_layer/sql_alchemy.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
4 | from chainlit.data.storage_clients.azure import AzureStorageClient
5 |
6 | import chainlit as cl
7 |
8 | storage_client = AzureStorageClient(
9 | account_url="", container=""
10 | )
11 |
12 |
13 | @cl.data_layer
14 | def data_layer():
15 | return SQLAlchemyDataLayer(
16 | conninfo="", storage_provider=storage_client
17 | )
18 |
19 |
20 | @cl.on_chat_start
21 | async def main():
22 | await cl.Message("Hello, send me a message!").send()
23 |
24 |
25 | @cl.on_message
26 | async def handle_message():
27 | await cl.sleep(2)
28 | await cl.Message("Ok!").send()
29 |
30 |
31 | @cl.password_auth_callback
32 | def auth_callback(username: str, password: str) -> Optional[cl.User]:
33 | if (username, password) == ("admin", "admin"):
34 | return cl.User(identifier="admin")
35 | else:
36 | return None
37 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_element/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.action_callback("test")
5 | async def on_test_action():
6 | await cl.sleep(1)
7 | await cl.Message(content="Executed test action!").send()
8 |
9 |
10 | @cl.on_chat_start
11 | async def on_start():
12 | custom_element = cl.CustomElement(
13 | name="Counter", display="inline", props={"count": 1}
14 | )
15 | await cl.Message(
16 | content="This message has a custom element!", elements=[custom_element]
17 | ).send()
18 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_element/public/elements/Counter.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { X } from 'lucide-react';
3 |
4 | export default function Counter() {
5 | return (
6 |
7 |
Count: {props.count}
8 | {props.loading ? "Loading..." : null}
9 |
10 |
15 |
16 |
17 | );
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_element/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Custom Element', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | function getCustomElement() {
9 | return cy.get('.step').eq(0).find('.inline-custom').first()
10 | }
11 |
12 | it('should be able to render an interactive custom element', () => {
13 | cy.get('.step').should('have.length', 1);
14 |
15 | cy.get('.step').eq(0).find('.inline-custom').should('have.length', 1);
16 |
17 | getCustomElement().should('contain', 'Count: 1');
18 |
19 | getCustomElement().find("#increment").click()
20 | getCustomElement().should('contain', 'Count: 2');
21 |
22 | getCustomElement().find("#increment").click()
23 | getCustomElement().should('contain', 'Count: 3');
24 |
25 | getCustomElement().find("#action").click()
26 |
27 | cy.get('.step').should('have.length', 2);
28 | cy.get('.step').eq(1).should('contain', 'Executed test action!');
29 |
30 | getCustomElement().find("#remove").click()
31 | cy.get('.step').eq(0).find('.inline-custom').should("not.exist")
32 |
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_theme/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def main():
6 | await cl.Message(content="Hello!").send()
7 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_theme/public/theme.json:
--------------------------------------------------------------------------------
1 | {
2 | "custom_fonts": ["https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto"],
3 | "variables": {
4 | "light": {
5 | "--font-sans": "'Poppins', sans-serif",
6 | "--background": "0 100% 50%"
7 | },
8 | "dark": {
9 | "--font-sans": "'Roboto', sans-serif",
10 | "--background": "100 100% 50%"
11 | }
12 | }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/cypress/e2e/custom_theme/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from "../../support/testUtils";
2 |
3 | describe("Custom Theme", () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it("should have the roboto font family and green bg in dark theme", () => {
9 | // The hsl value is converted to rgb
10 | cy.get('body').should('have.css', 'background-color', 'rgb(85, 255, 0)')
11 | cy.get('body').should('have.css', 'font-family', "Roboto, sans-serif")
12 | });
13 |
14 | it("should have the poppins font family and red bg in light theme", () => {
15 | cy.visit('/');
16 | cy.get("#theme-toggle").click()
17 | cy.contains('Light').click();
18 | // The hsl value is converted to rgb
19 | cy.get('body').should('have.css', 'background-color', 'rgb(255, 0, 0)')
20 | cy.get('body').should('have.css', 'font-family', "Poppins, sans-serif")
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/cypress/e2e/dataframe/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('dataframe', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to display an inline dataframe', () => {
9 | // Check if the DataFrame is rendered within the first step
10 | cy.get('.step').should('have.length', 1);
11 | cy.get('.step').first().find('.dataframe').should('have.length', 1);
12 | })
13 | });
14 |
--------------------------------------------------------------------------------
/cypress/e2e/edit_message/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_message
5 | async def main():
6 | await cl.Message(
7 | content=f"Chat context length: {len(cl.chat_context.get())}"
8 | ).send()
9 |
--------------------------------------------------------------------------------
/cypress/e2e/edit_message/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer, submitMessage } from '../../support/testUtils';
2 |
3 | describe('Edit Message', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to edit a message', () => {
9 | submitMessage('Hello 1');
10 | submitMessage('Hello 2');
11 |
12 | cy.get('.step').should('have.length', 4);
13 | cy.get('.step').eq(3).should('contain', 'Chat context length: 3');
14 |
15 | cy.get('.step').eq(0).trigger('mouseover').find('.edit-message').click({ force: true });
16 | cy.get('#edit-chat-input').type('Hello 3');
17 | cy.get('.step').eq(0).find('.confirm-edit').click({ force: true });
18 |
19 | cy.get('.step').should('have.length', 2);
20 | cy.get('.step').eq(1).should('contain', 'Chat context length: 1');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/cypress/e2e/elements/cat.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/cypress/e2e/elements/cat.jpeg
--------------------------------------------------------------------------------
/cypress/e2e/elements/dummy.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/cypress/e2e/elements/dummy.pdf
--------------------------------------------------------------------------------
/cypress/e2e/error_handling/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | def main():
6 | raise Exception("This is an error message")
7 |
--------------------------------------------------------------------------------
/cypress/e2e/error_handling/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Error Handling', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should correctly display errors', () => {
9 | cy.get('.step')
10 | .should('have.length', 1)
11 | .eq(0)
12 | .should('contain', 'This is an error message');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/cypress/e2e/file_element/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def start():
6 | elements = [
7 | cl.File(
8 | name="example.mp4",
9 | path="../../fixtures/example.mp4",
10 | display="inline",
11 | mime="video/mp4",
12 | ),
13 | cl.File(
14 | name="cat.jpeg",
15 | path="../../fixtures/cat.jpeg",
16 | display="inline",
17 | mime="image/jpg",
18 | ),
19 | cl.File(
20 | name="hello.py",
21 | path="../../fixtures/hello.py",
22 | display="inline",
23 | mime="plain/py",
24 | ),
25 | cl.File(
26 | name="example.mp3",
27 | path="../../fixtures/example.mp3",
28 | display="inline",
29 | mime="audio/mp3",
30 | ),
31 | ]
32 |
33 | await cl.Message(
34 | content="This message has a couple of file element", elements=elements
35 | ).send()
36 |
--------------------------------------------------------------------------------
/cypress/e2e/file_element/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('file', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to display a file element', () => {
9 | cy.get('.step').should('have.length', 1);
10 | cy.get('.step').eq(0).find('.inline-file').should('have.length', 4);
11 |
12 | cy.get('.inline-file').should(($files) => {
13 | const downloads = $files
14 | .map((i, el) => Cypress.$(el).attr('download'))
15 | .get();
16 |
17 | expect(downloads).to.have.members([
18 | 'example.mp4',
19 | 'cat.jpeg',
20 | 'hello.py',
21 | 'example.mp3'
22 | ]);
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/cypress/e2e/header_auth/main.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import chainlit as cl
4 |
5 |
6 | @cl.header_auth_callback
7 | async def header_auth_callback(headers) -> Optional[cl.User]:
8 | if headers.get("test-header"):
9 | return cl.User(identifier="admin")
10 | else:
11 | return None
12 |
13 |
14 | @cl.on_chat_start
15 | async def on_chat_start():
16 | user = cl.user_session.get("user")
17 | await cl.Message(f"Hello {user.identifier}").send()
18 |
--------------------------------------------------------------------------------
/cypress/e2e/llama_index_cb/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 | from llama_index.core.callbacks.schema import CBEventType, EventPayload
3 | from llama_index.core.llms import ChatMessage, ChatResponse
4 | from llama_index.core.schema import NodeWithScore, TextNode
5 |
6 |
7 | @cl.on_chat_start
8 | async def start():
9 | await cl.Message(content="LlamaIndexCb").send()
10 |
11 | cb = cl.LlamaIndexCallbackHandler()
12 |
13 | cb.on_event_start(CBEventType.RETRIEVE, payload={})
14 |
15 | await cl.sleep(0.2)
16 |
17 | cb.on_event_end(
18 | CBEventType.RETRIEVE,
19 | payload={
20 | EventPayload.NODES: [
21 | NodeWithScore(node=TextNode(text="This is text1"), score=1)
22 | ]
23 | },
24 | )
25 |
26 | cb.on_event_start(CBEventType.LLM)
27 |
28 | await cl.sleep(0.2)
29 |
30 | response = ChatResponse(message=ChatMessage(content="This is the LLM response"))
31 | cb.on_event_end(
32 | CBEventType.LLM,
33 | payload={
34 | EventPayload.RESPONSE: response,
35 | EventPayload.PROMPT: "This is the LLM prompt",
36 | },
37 | )
38 |
--------------------------------------------------------------------------------
/cypress/e2e/llama_index_cb/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Llama Index Callback', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to send messages to the UI with prompts and elements', () => {
9 | cy.get('.step').should('have.length', 3);
10 |
11 | const toolCall = cy.get('#step-retrieve');
12 |
13 | toolCall.should('exist').click();
14 |
15 | const toolCallContent = toolCall.get('.message-content').eq(0);
16 |
17 | toolCallContent
18 | .should('exist')
19 | .get('.element-link')
20 | .eq(0)
21 | .should('contain', 'Source 0');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/cypress/e2e/on_chat_start/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def start():
6 | await cl.Message(
7 | content="""Hello!
8 |
9 | ```python
10 | import chainlit as cl
11 |
12 | @cl.on_chat_start
13 | async def main():
14 | await cl.Message(
15 | content="Here is a simple message",
16 | ).send()
17 | ```"""
18 | ).send()
19 |
--------------------------------------------------------------------------------
/cypress/e2e/on_chat_start/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('on_chat_start', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should correctly run on_chat_start', () => {
9 | const messages = cy.get('.step');
10 | messages.should('have.length', 1);
11 |
12 | messages.eq(0).should('contain.text', 'Hello!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/cypress/e2e/password_auth/main.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import chainlit as cl
4 |
5 |
6 | @cl.password_auth_callback
7 | def auth_callback(username: str, password: str) -> Optional[cl.User]:
8 | if (username, password) == ("admin", "admin"):
9 | return cl.User(identifier="admin")
10 | else:
11 | return None
12 |
13 |
14 | @cl.on_chat_start
15 | async def on_chat_start():
16 | user = cl.user_session.get("user")
17 | await cl.Message(f"Hello {user.identifier}").send()
18 |
--------------------------------------------------------------------------------
/cypress/e2e/plotly/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 | import plotly.graph_objects as go
3 |
4 |
5 | @cl.on_chat_start
6 | async def start():
7 | fig = go.Figure(
8 | data=[go.Bar(y=[2, 1, 3])],
9 | layout_title_text="A Figure Displayed with fig.show()",
10 | )
11 | elements = [cl.Plotly(name="chart", figure=fig, display="inline")]
12 |
13 | await cl.Message(content="This message has a chart", elements=elements).send()
14 |
--------------------------------------------------------------------------------
/cypress/e2e/plotly/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('plotly', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to display an inline chart', () => {
9 | cy.get('.step').should('have.length', 1);
10 | cy.get('.step').eq(0).find('.inline-plotly').should('have.length', 1);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/cypress/e2e/pyplot/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 | import matplotlib.pyplot as plt
3 |
4 |
5 | @cl.on_chat_start
6 | async def start():
7 | fig, ax = plt.subplots()
8 | ax.plot([1, 2, 3, 4], [1, 4, 2, 3])
9 | elements = [cl.Pyplot(name="chart", figure=fig, display="inline")]
10 |
11 | await cl.Message(content="This message has a chart", elements=elements).send()
12 |
--------------------------------------------------------------------------------
/cypress/e2e/pyplot/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('pyplot', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to display an inline chart', () => {
9 | cy.get('.step').should('have.length', 1);
10 | cy.get('.step').eq(0).find('.inline-image').should('have.length', 1);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/cypress/e2e/readme/chainlit_pt-BR.md:
--------------------------------------------------------------------------------
1 | # Bem-vindo ao Chainlit! 🚀🤖
2 |
3 | Olá, desenvolvedor! 👋 Estamos empolgados em tê-lo a bordo. O Chainlit é uma ferramenta poderosa projetada para ajudá-lo a prototipar, depurar e compartilhar aplicativos construídos em cima de LLMs.
4 |
5 | ## Links úteis 🔗
6 |
7 | - **Documentação:** Comece com a nossa abrangente Documentação do Chainlit 📚
8 | - **Comunidade no Discord:** Junte-se ao nosso amigável Discord do Chainlit para fazer perguntas, compartilhar seus projetos e se conectar com outros desenvolvedores! 💬
9 |
10 | Mal podemos esperar para ver o que você cria com o Chainlit! Boa codificação! 💻😊
11 |
12 | ## Tela de boas-vindas
13 |
14 | Para modificar a tela de boas-vindas, edite o arquivo chainlit.md
15 | na raiz do seu projeto. Se você não quiser uma tela de boas-vindas, basta deixar este arquivo vazio.
--------------------------------------------------------------------------------
/cypress/e2e/readme/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl # noqa: F401
2 |
3 |
4 | @cl.on_message
5 | async def on_message(msg):
6 | pass
7 |
--------------------------------------------------------------------------------
/cypress/e2e/readme/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | function openReadme() {
4 | cy.get('#readme-button').click();
5 | }
6 |
7 | describe('readme_language', () => {
8 | before(() => {
9 | runTestServer();
10 | });
11 |
12 | it('should show default markdown on open', () => {
13 | openReadme();
14 | cy.contains('Welcome to Chainlit!');
15 | });
16 |
17 | it('should show Portguese markdown on pt-BR language', () => {
18 | cy.visit('/', {
19 | onBeforeLoad(win) {
20 | Object.defineProperty(win.navigator, 'language', {
21 | value: 'pt-BR'
22 | });
23 | }
24 | });
25 | openReadme();
26 | cy.contains('Bem-vindo ao Chainlit!');
27 | });
28 |
29 | it('should fallback to default markdown on Klingon language', () => {
30 | cy.visit('/', {
31 | onBeforeLoad(win) {
32 | Object.defineProperty(win.navigator, 'language', {
33 | value: 'tlh'
34 | });
35 | }
36 | });
37 | openReadme();
38 | cy.contains('Welcome to Chainlit!');
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/cypress/e2e/remove_elements/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def start():
6 | step_image = cl.Image(
7 | name="image1", display="inline", path="../../fixtures/cat.jpeg"
8 | )
9 | msg_image = cl.Image(
10 | name="image1", display="inline", path="../../fixtures/cat.jpeg"
11 | )
12 |
13 | async with cl.Step(type="tool", name="tool1") as step:
14 | step.elements = [
15 | step_image,
16 | cl.Image(name="image2", display="inline", path="../../fixtures/cat.jpeg"),
17 | ]
18 | step.output = "This step has an image"
19 |
20 | await cl.Message(
21 | content="This message has an image",
22 | elements=[
23 | msg_image,
24 | cl.Image(name="image2", display="inline", path="../../fixtures/cat.jpeg"),
25 | ],
26 | ).send()
27 | await msg_image.remove()
28 | await step_image.remove()
29 |
--------------------------------------------------------------------------------
/cypress/e2e/remove_elements/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('remove_elements', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to remove elements', () => {
9 | cy.get('#step-tool1').should('exist');
10 | cy.get('#step-tool1').click();
11 | cy.get('#step-tool1')
12 | .parent()
13 | .parent()
14 | .find('.inline-image')
15 | .should('have.length', 1);
16 |
17 | cy.get('.step').should('have.length', 2);
18 | cy.get('.step').eq(1).find('.inline-image').should('have.length', 1);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/cypress/e2e/remove_step/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def main():
6 | msg1 = cl.Message(content="Message 1")
7 | await msg1.send()
8 |
9 | await cl.sleep(1)
10 |
11 | async with cl.Step(type="tool", name="tool1") as child1:
12 | child1.output = "Child 1"
13 |
14 | await cl.sleep(1)
15 | await child1.remove()
16 |
17 | msg2 = cl.Message(content="Message 2")
18 | await msg2.send()
19 |
20 | await cl.sleep(1)
21 | await msg1.remove()
22 |
23 | await cl.sleep(1)
24 | await msg2.remove()
25 |
26 | await cl.sleep(1)
27 |
28 | ask_msg = cl.AskUserMessage("Message 3")
29 | await ask_msg.send()
30 |
31 | await cl.sleep(1)
32 | await ask_msg.remove()
33 |
--------------------------------------------------------------------------------
/cypress/e2e/remove_step/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer, submitMessage } from '../../support/testUtils';
2 |
3 | describe('Remove Step', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to remove a step', () => {
9 | cy.get('.step').should('have.length', 1);
10 | cy.get('.step').eq(0).should('contain', 'Message 1');
11 |
12 | cy.get('#step-tool1').should('exist');
13 | cy.get('#step-tool1').click();
14 | cy.get('.message-content').eq(1).should('contain', 'Child 1');
15 |
16 | cy.get('#step-tool1').should('not.exist');
17 |
18 | cy.get('.step').eq(1).should('contain', 'Message 2');
19 | cy.get('.step').should('have.length', 1);
20 | cy.get('.step').eq(0).should('contain', 'Message 2');
21 | cy.get('.step').should('have.length', 0);
22 |
23 | cy.get('.step').should('have.length', 1);
24 | cy.get('.step').eq(0).should('contain', 'Message 3');
25 |
26 | submitMessage('foo');
27 |
28 | cy.get('.step').should('have.length', 1);
29 | cy.get('.step').eq(0).should('contain', 'foo');
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/cypress/e2e/sidebar/cat.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/cypress/e2e/sidebar/cat.jpeg
--------------------------------------------------------------------------------
/cypress/e2e/sidebar/dummy.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/cypress/e2e/sidebar/dummy.pdf
--------------------------------------------------------------------------------
/cypress/e2e/sidebar/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def start():
6 | await cl.Message(content="Hello").send()
7 |
8 | elements = [
9 | cl.Image(path="./cat.jpeg", name="image1"),
10 | cl.Pdf(path="./dummy.pdf", name="pdf1"),
11 | cl.Text(content="Here is a side text document", name="text1"),
12 | cl.Text(content="Here is a page text document", name="text2"),
13 | ]
14 |
15 | await cl.ElementSidebar.set_elements(elements)
16 | await cl.ElementSidebar.set_title("Test title")
17 |
18 |
19 | @cl.on_message
20 | async def message(msg: cl.Message):
21 | await cl.ElementSidebar.set_elements([cl.Text(content="Text changed!")])
22 | await cl.ElementSidebar.set_title("Title changed!")
23 |
24 | await cl.sleep(2)
25 |
26 | await cl.ElementSidebar.set_elements([])
27 |
--------------------------------------------------------------------------------
/cypress/e2e/sidebar/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer, submitMessage } from '../../support/testUtils';
2 |
3 | describe('Elements', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to open the sidebar from python', () => {
9 | cy.get('#side-view-content').find('.inline-image').should('have.length', 1);
10 | cy.get('#side-view-content').find('.inline-pdf').should('have.length', 1);
11 | cy.get('#side-view-content').find('.inline-text').should('have.length', 2);
12 |
13 | cy.get('#side-view-title').should("have.text", "Test title")
14 |
15 | submitMessage('Hello');
16 |
17 | cy.get('#side-view-content').find('.inline-text').should('have.length', 1).should("have.text", "Text changed!");
18 | cy.get('#side-view-content').find('.inline-image').should('have.length', 0);
19 | cy.get('#side-view-content').find('.inline-pdf').should('have.length', 0);
20 |
21 | cy.get('#side-view-title').should("have.text", "Title changed!")
22 |
23 | cy.get('#side-view-content').should("not.exist")
24 |
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/cypress/e2e/starters/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.set_starters
5 | async def starters():
6 | return [
7 | cl.Starter(label="test1", message="Running starter 1"),
8 | cl.Starter(label="test2", message="Running starter 2"),
9 | cl.Starter(label="test3", message="Running starter 3"),
10 | ]
11 |
12 |
13 | @cl.on_message
14 | async def on_message(msg: cl.Message):
15 | await cl.Message(msg.content).send()
16 |
--------------------------------------------------------------------------------
/cypress/e2e/starters/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Starters', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to use a starter', () => {
9 | cy.wait(1000);
10 | cy.get('#starter-test1').should('exist').click();
11 | cy.get('.step').should('have.length', 2);
12 |
13 | cy.get('.step').eq(0).contains('Running starter 1');
14 | cy.get('.step').eq(1).contains('Running starter 1');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/cypress/e2e/step/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | def tool_3():
5 | with cl.Step(name="tool3", type="tool") as s:
6 | cl.run_sync(cl.sleep(2))
7 | s.output = "Response from tool 3"
8 |
9 |
10 | @cl.step(name="tool2", type="tool")
11 | def tool_2():
12 | tool_3()
13 | cl.run_sync(cl.Message(content="Message from tool 2").send())
14 | return "Response from tool 2"
15 |
16 |
17 | @cl.step(name="tool1", type="tool")
18 | def tool_1():
19 | tool_2()
20 | return "Response from tool 1"
21 |
22 |
23 | @cl.on_message
24 | async def main(message: cl.Message):
25 | tool_1()
26 |
--------------------------------------------------------------------------------
/cypress/e2e/step/main_async.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | async def tool_3():
5 | async with cl.Step(name="tool3", type="tool") as s:
6 | await cl.sleep(2)
7 | s.output = "Response from tool 3"
8 |
9 |
10 | @cl.step(name="tool2", type="tool")
11 | async def tool_2():
12 | await tool_3()
13 | await cl.Message(content="Message from tool 2").send()
14 | return "Response from tool 2"
15 |
16 |
17 | @cl.step(name="tool1", type="tool")
18 | async def tool_1():
19 | await tool_2()
20 | return "Response from tool 1"
21 |
22 |
23 | @cl.on_message
24 | async def main(message: cl.Message):
25 | await tool_1()
26 |
--------------------------------------------------------------------------------
/cypress/e2e/step/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import {
2 | describeSyncAsync,
3 | runTestServer,
4 | submitMessage
5 | } from '../../support/testUtils';
6 |
7 | describeSyncAsync('Step', () => {
8 | before(() => {
9 | runTestServer();
10 | });
11 |
12 | it('should be able to nest steps', () => {
13 | submitMessage('Hello');
14 |
15 | cy.get('#step-tool1').should('exist').click();
16 |
17 | cy.get('#step-tool2').should('exist').click();
18 |
19 | cy.get('#step-tool3').should('exist');
20 |
21 | cy.get('.step').should('have.length', 5);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/cypress/e2e/stop_task/main_async.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_message
5 | async def message(message: cl.Message):
6 | await cl.Message(content="Message 1").send()
7 | await cl.sleep(1)
8 | await cl.Message(content="Message 2").send()
9 |
--------------------------------------------------------------------------------
/cypress/e2e/stop_task/main_sync.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | import chainlit as cl
4 |
5 |
6 | def sync_function():
7 | time.sleep(1)
8 |
9 |
10 | @cl.on_message
11 | async def message(message: cl.Message):
12 | await cl.Message(content="Message 1").send()
13 | await cl.make_async(sync_function)()
14 | await cl.Message(content="Message 2").send()
15 |
--------------------------------------------------------------------------------
/cypress/e2e/stop_task/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import {
2 | describeSyncAsync,
3 | runTestServer,
4 | submitMessage
5 | } from '../../support/testUtils';
6 |
7 | describeSyncAsync('Stop task', (mode) => {
8 | before(() => {
9 | runTestServer(mode);
10 | });
11 |
12 | it('should be able to stop a task', () => {
13 | submitMessage('Hello');
14 | cy.get('#stop-button').should('exist').click();
15 | cy.get('#stop-button').should('not.exist');
16 |
17 | cy.get('.step').should('have.length', 3);
18 | cy.get('.step').last().should('contain.text', 'Task manually stopped.');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/cypress/e2e/streaming/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 | token_list = ["the ", "quick ", "brown ", "fox"]
4 |
5 | sequence_list = ["the", "the quick", "the quick brown", "the quick brown fox"]
6 |
7 |
8 | @cl.on_chat_start
9 | async def main():
10 | msg = cl.Message(content="")
11 | for token in token_list:
12 | await msg.stream_token(token)
13 | await cl.sleep(0.2)
14 |
15 | await msg.send()
16 |
17 | msg = cl.Message(content="")
18 | for seq in sequence_list:
19 | await msg.stream_token(token=seq, is_sequence=True)
20 | await cl.sleep(0.2)
21 |
22 | await msg.send()
23 |
24 | step = cl.Step(type="tool", name="tool1")
25 | for token in token_list:
26 | await step.stream_token(token)
27 | await cl.sleep(0.2)
28 |
29 | await step.send()
30 |
--------------------------------------------------------------------------------
/cypress/e2e/streaming/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | const tokenList = ['the', 'quick', 'brown', 'fox'];
4 |
5 | function messageStream(index: number) {
6 | for (const token of tokenList) {
7 | cy.get('.step').eq(index).should('contain', token);
8 | }
9 | cy.get('.step').eq(index).should('contain', tokenList.join(' '));
10 | }
11 |
12 | function toolStream(tool: string) {
13 | const toolCall = cy.get(`#step-${tool}`);
14 | toolCall.click();
15 | for (const token of tokenList) {
16 | toolCall.parent().parent().should('contain', token);
17 | }
18 | toolCall.parent().parent().should('contain', tokenList.join(' '));
19 | }
20 |
21 | describe('Streaming', () => {
22 | before(() => {
23 | runTestServer();
24 | });
25 |
26 | it('should be able to stream a message', () => {
27 | cy.get('.step').should('have.length', 1);
28 |
29 | messageStream(0);
30 |
31 | cy.get('.step').should('have.length', 1);
32 |
33 | messageStream(1);
34 |
35 | cy.get('.step').should('have.length', 2);
36 |
37 | toolStream('tool1');
38 |
39 | cy.get('.step').should('have.length', 3);
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/cypress/e2e/tasklist/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer, submitMessage } from '../../support/testUtils';
2 |
3 | describe('tasklist', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should display the tasklist ', () => {
9 | cy.get('.step').should('have.length', 0);
10 | cy.get('.tasklist').should('have.length', 1);
11 | cy.get('.tasklist.tasklist-mobile').should('not.exist');
12 |
13 | cy.get('.tasklist').should('be.visible');
14 | cy.get('.tasklist .task').should('have.length', 17);
15 |
16 | cy.get('.tasklist .task.task-status-ready').should(
17 | 'have.length',
18 | 7
19 | );
20 | cy.get('.tasklist .task.task-status-running').should(
21 | 'have.length',
22 | 0
23 | );
24 | cy.get('.tasklist .task.task-status-failed').should(
25 | 'have.length',
26 | 1
27 | );
28 | cy.get('.tasklist .task.task-status-done').should(
29 | 'have.length',
30 | 9
31 | );
32 |
33 | submitMessage('ok');
34 |
35 | cy.get('.tasklist').should('not.exist');
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/cypress/e2e/update_step/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_chat_start
5 | async def main():
6 | msg = cl.Message(content="Hello!")
7 | await msg.send()
8 |
9 | async with cl.Step(type="tool", name="tool1") as step:
10 | step.output = "Foo"
11 |
12 | await cl.sleep(1)
13 | msg.content = "Hello again!"
14 | await msg.update()
15 |
16 | step.output += " Bar"
17 | await step.update()
18 |
--------------------------------------------------------------------------------
/cypress/e2e/update_step/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | describe('Update Step', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to update a step', () => {
9 | cy.get(`#step-tool1`).click();
10 | cy.get('.step').should('have.length', 2);
11 | cy.get('.step').eq(0).should('contain', 'Hello!');
12 | cy.get(`#step-tool1`).parent().parent().should('contain', 'Foo');
13 |
14 | cy.get('.step').eq(0).should('contain', 'Hello again!');
15 | cy.get(`#step-tool1`).parent().parent().should('contain', 'Foo Bar');
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/cypress/e2e/upload_attachments/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_message
5 | async def main(message: cl.Message):
6 | await cl.Message(content=f"Content: {message.content}").send()
7 | # Check if message.elements is not empty and is a list
8 | for index, item in enumerate(message.elements):
9 | # Send a response for each element
10 | await cl.Message(content=f"Received element {index}: {item.name}").send()
11 |
--------------------------------------------------------------------------------
/cypress/e2e/user_env/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_message
5 | async def main():
6 | key = "TEST_KEY"
7 | user_env = cl.user_session.get("env")
8 | provided_key = user_env.get(key)
9 | await cl.Message(content=f"Key {key} has value {provided_key}").send()
10 |
--------------------------------------------------------------------------------
/cypress/e2e/user_env/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer, submitMessage } from '../../support/testUtils';
2 |
3 | describe('User Env', () => {
4 | before(() => {
5 | runTestServer();
6 | });
7 |
8 | it('should be able to ask a user for required keys', () => {
9 | const key = 'TEST_KEY';
10 | const keyValue = 'TEST_VALUE';
11 |
12 | cy.get(`#${key}`).should('exist').type(keyValue);
13 |
14 | cy.get('#submit-env').should('exist').click();
15 |
16 | submitMessage('Hello');
17 |
18 | cy.get('.step').should('have.length', 2);
19 | cy.get('.step').eq(1).should('contain', keyValue);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/cypress/e2e/user_session/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_message
5 | async def main(message: cl.Message):
6 | prev_msg = cl.user_session.get("prev_msg")
7 | await cl.Message(content=f"Prev message: {prev_msg}").send()
8 | cl.user_session.set("prev_msg", message.content)
9 |
--------------------------------------------------------------------------------
/cypress/e2e/user_session/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer, submitMessage } from '../../support/testUtils';
2 |
3 | function newSession() {
4 | cy.get('#header')
5 | .get('#new-chat-button')
6 | .should('exist')
7 | .click({ force: true });
8 | cy.get('#new-chat-dialog').should('exist');
9 | cy.get('#confirm').should('exist').click();
10 |
11 | cy.get('#new-chat-dialog').should('not.exist');
12 | }
13 |
14 | describe('User Session', () => {
15 | before(() => {
16 | runTestServer();
17 | });
18 |
19 | it('should be able to store data related per user session', () => {
20 | submitMessage('Hello 1');
21 |
22 | cy.get('.step').should('have.length', 2);
23 | cy.get('.step').eq(1).should('contain', 'Prev message: None');
24 |
25 | submitMessage('Hello 2');
26 |
27 | cy.get('.step').should('have.length', 4);
28 | cy.get('.step').eq(3).should('contain', 'Prev message: Hello 1');
29 |
30 | newSession();
31 |
32 | submitMessage('Hello 3');
33 |
34 | cy.get('.step').should('have.length', 2);
35 | cy.get('.step').eq(1).should('contain', 'Prev message: None');
36 |
37 | submitMessage('Hello 4');
38 |
39 | cy.get('.step').should('have.length', 4);
40 | cy.get('.step').eq(3).should('contain', 'Prev message: Hello 3');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/cypress/e2e/window_message/main.py:
--------------------------------------------------------------------------------
1 | import chainlit as cl
2 |
3 |
4 | @cl.on_window_message
5 | async def window_message(message: str):
6 | if message.startswith("Client: "):
7 | await cl.send_window_message("Server: World")
8 |
9 |
10 | @cl.on_message
11 | async def message(message: str):
12 | await cl.Message(content="ok").send()
13 |
--------------------------------------------------------------------------------
/cypress/e2e/window_message/public/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chainlit iframe
5 |
6 |
7 | Chainlit iframe
8 |
9 | No message received
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/cypress/e2e/window_message/spec.cy.ts:
--------------------------------------------------------------------------------
1 | import { runTestServer } from '../../support/testUtils';
2 |
3 | const getIframeWindow = () => {
4 | return cy
5 | .get('iframe[data-cy="the-frame"]')
6 | .its('0.contentWindow')
7 | .should('exist');
8 | };
9 |
10 | describe('Window Message', () => {
11 | before(() => {
12 | runTestServer();
13 | });
14 |
15 | it('should be able to send and receive window messages', () => {
16 | cy.visit('/public/iframe.html');
17 |
18 | cy.get('div#message').should('contain', 'No message received');
19 |
20 | getIframeWindow().then((win) => {
21 | cy.wait(1000).then(() => {
22 | win.postMessage('Client: Hello', '*');
23 | });
24 | });
25 |
26 | cy.get('div#message').should('contain', 'Server: World');
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/cypress/fixtures/cat.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/cypress/fixtures/cat.jpeg
--------------------------------------------------------------------------------
/cypress/fixtures/example.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/cypress/fixtures/example.mp3
--------------------------------------------------------------------------------
/cypress/fixtures/example.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/cypress/fixtures/example.mp4
--------------------------------------------------------------------------------
/cypress/fixtures/hello.cpp:
--------------------------------------------------------------------------------
1 | // Your First C++ Program
2 |
3 | #include
4 |
5 | int main() {
6 | std::cout << "Hello World!";
7 | return 0;
8 | }
9 |
--------------------------------------------------------------------------------
/cypress/fixtures/hello.py:
--------------------------------------------------------------------------------
1 | print("hello world")
2 |
--------------------------------------------------------------------------------
/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from 'dotenv';
2 |
3 | import { runTests } from './utils';
4 |
5 | dotenv.config();
6 |
7 | async function main() {
8 | const matchName = process.env.SINGLE_TEST || '*';
9 |
10 | await runTests(matchName);
11 | }
12 |
13 | main()
14 | .then(() => {
15 | console.log('Done!');
16 | process.exit(0);
17 | })
18 | .catch((error) => {
19 | console.error(error);
20 | process.exit(1);
21 | });
22 |
--------------------------------------------------------------------------------
/cypress/support/utils.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'child_process';
2 | import { join } from 'path';
3 |
4 | const ROOT = process.cwd();
5 | export const E2E_DIR = join(ROOT, 'cypress/e2e');
6 | export const BACKEND_DIR = join(ROOT, 'backend');
7 | export const CHAINLIT_PORT = 8000;
8 |
9 | export enum ExecutionMode {
10 | Async = 'async',
11 | Sync = 'sync'
12 | }
13 |
14 | export async function runTests(matchName) {
15 | // Cypress requires a healthcheck on the server at startup so let's run
16 | // Chainlit before running tests to pass the healthcheck
17 | runCommand('pnpm exec ts-node ./cypress/support/run.ts action');
18 |
19 | // Recording the cypress run is time consuming. Disabled by default.
20 | // const recordOptions = ` --record --key ${process.env.CYPRESS_RECORD_KEY} `;
21 | return runCommand(
22 | `pnpm exec cypress run --record false ${
23 | process.env.CYPRESS_OPTIONS || ''
24 | } --spec "cypress/e2e/${matchName}/spec.cy.ts"`
25 | );
26 | }
27 |
28 | export function runCommand(command: string, cwd = ROOT) {
29 | return execSync(command, {
30 | encoding: 'utf-8',
31 | cwd,
32 | env: process.env,
33 | stdio: 'inherit'
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | tsconfig.tsbuildinfo
11 |
12 | node_modules
13 | dist
14 | dist_embed
15 | dist-ssr
16 | *.local
17 |
18 | # Editor directories and files
19 | .vscode/*
20 | !.vscode/extensions.json
21 | .idea
22 | .DS_Store
23 | *.suo
24 | *.ntvs*
25 | *.njsproj
26 | *.sln
27 | *.sw?
28 |
--------------------------------------------------------------------------------
/frontend/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import getRouterBasename from '@/lib/router';
2 | import { toast } from 'sonner';
3 |
4 | import { ChainlitAPI, ClientError } from '@chainlit/react-client';
5 |
6 | const devServer = 'http://localhost:8000' + getRouterBasename();
7 | const url = import.meta.env.DEV
8 | ? devServer
9 | : window.origin + getRouterBasename();
10 | const serverUrl = new URL(url);
11 |
12 | const httpEndpoint = serverUrl.toString();
13 |
14 | const on401 = () => {
15 | if (window.location.pathname !== getRouterBasename() + '/login') {
16 | // The credentials aren't correct, remove the token and redirect to login
17 | window.location.href = getRouterBasename() + '/login';
18 | }
19 | };
20 |
21 | const onError = (error: ClientError) => {
22 | toast.error(error.toString());
23 | };
24 |
25 | export const apiClient = new ChainlitAPI(
26 | httpEndpoint,
27 | 'webapp',
28 | {}, // Optional - additionalQueryParams property.
29 | on401,
30 | onError
31 | );
32 |
--------------------------------------------------------------------------------
/frontend/src/components/AutoResumeThread.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { useRecoilState } from 'recoil';
4 | import { toast } from 'sonner';
5 |
6 | import {
7 | resumeThreadErrorState,
8 | useChatInteract,
9 | useChatSession,
10 | useConfig
11 | } from '@chainlit/react-client';
12 |
13 | interface Props {
14 | id: string;
15 | }
16 |
17 | export default function AutoResumeThread({ id }: Props) {
18 | const navigate = useNavigate();
19 | const { config } = useConfig();
20 | const { clear, setIdToResume } = useChatInteract();
21 | const { session, idToResume } = useChatSession();
22 | const [resumeThreadError, setResumeThreadError] = useRecoilState(
23 | resumeThreadErrorState
24 | );
25 |
26 | useEffect(() => {
27 | if (!config?.threadResumable) return;
28 | clear();
29 | setIdToResume(id);
30 | if (!config?.dataPersistence) {
31 | navigate('/');
32 | }
33 | }, [config?.threadResumable, id]);
34 |
35 | useEffect(() => {
36 | if (id !== idToResume) {
37 | return;
38 | }
39 | if (session?.error) {
40 | toast.error("Couldn't resume chat");
41 | navigate('/');
42 | }
43 | }, [session, idToResume, id]);
44 |
45 | useEffect(() => {
46 | if (resumeThreadError) {
47 | toast.error("Couldn't resume chat: " + resumeThreadError);
48 | navigate('/');
49 | setResumeThreadError(undefined);
50 | }
51 | }, [resumeThreadError]);
52 |
53 | return null;
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/src/components/BlinkingCursor.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 |
3 | export const CURSOR_PLACEHOLDER = '\u200B';
4 |
5 | interface Props {
6 | whitespace?: boolean;
7 | }
8 |
9 | export default function BlinkingCursor({ whitespace }: Props) {
10 | return (
11 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/src/components/ChatSettings/InputLabel.tsx:
--------------------------------------------------------------------------------
1 | import { InfoIcon } from 'lucide-react';
2 |
3 | import { Label } from '@/components/ui/label';
4 | import {
5 | Tooltip,
6 | TooltipContent,
7 | TooltipProvider,
8 | TooltipTrigger
9 | } from '@/components/ui/tooltip';
10 |
11 | import { NotificationCount, NotificationCountProps } from './NotificationCount';
12 |
13 | interface InputLabelProps {
14 | id?: string;
15 | label: string | number;
16 | tooltip?: string;
17 | notificationsProps?: NotificationCountProps;
18 | }
19 |
20 | const InputLabel = ({
21 | id,
22 | label,
23 | tooltip,
24 | notificationsProps
25 | }: InputLabelProps): JSX.Element => {
26 | return (
27 |
28 |
29 |
32 | {tooltip && (
33 |
34 |
35 |
36 |
37 |
38 |
39 | {tooltip}
40 |
41 |
42 |
43 | )}
44 |
45 | {notificationsProps &&
}
46 |
47 | );
48 | };
49 |
50 | export { InputLabel };
51 |
--------------------------------------------------------------------------------
/frontend/src/components/ChatSettings/SwitchInput.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as React from 'react';
3 |
4 | import { Switch } from '@/components/ui/switch';
5 |
6 | import { InputStateHandler } from './InputStateHandler';
7 |
8 | interface InputStateProps {
9 | description?: string;
10 | hasError?: boolean;
11 | id: string;
12 | label?: string;
13 | tooltip?: string;
14 | children: React.ReactNode;
15 | }
16 |
17 | interface SwitchInputProps extends InputStateProps {
18 | checked: boolean;
19 | disabled?: boolean;
20 | onChange: (checked: boolean) => void;
21 | setField?: (field: string, value: boolean, shouldValidate?: boolean) => void;
22 | }
23 |
24 | const SwitchInput = ({
25 | checked,
26 | description,
27 | disabled,
28 | hasError,
29 | id,
30 | label,
31 | setField,
32 | tooltip
33 | }: SwitchInputProps): JSX.Element => {
34 | return (
35 |
42 | {
47 | setField?.(id, checked);
48 | }}
49 | className={cn(
50 | 'data-[state=checked]:bg-primary',
51 | hasError && 'border-destructive'
52 | )}
53 | />
54 |
55 | );
56 | };
57 |
58 | export { SwitchInput };
59 | export type { SwitchInputProps };
60 |
--------------------------------------------------------------------------------
/frontend/src/components/ChatSettings/TextInput.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from '@/components/ui/input';
2 | import { Textarea } from '@/components/ui/textarea';
3 |
4 | import { IInput } from 'types/Input';
5 |
6 | import { InputStateHandler } from './InputStateHandler';
7 |
8 | interface TextInputProps
9 | extends IInput,
10 | Omit, 'id' | 'size'> {
11 | setField?: (field: string, value: string, shouldValidate?: boolean) => void;
12 | value?: string;
13 | placeholder?: string;
14 | multiline?: boolean;
15 | }
16 |
17 | const TextInput = ({
18 | description,
19 | disabled,
20 | hasError,
21 | id,
22 | label,
23 | tooltip,
24 | multiline,
25 | className,
26 | setField,
27 | ...rest
28 | }: TextInputProps): JSX.Element => {
29 | const InputComponent = multiline ? Textarea : Input;
30 |
31 | return (
32 |
39 | setField?.(id, e.target.value)}
45 | className={`text-sm font-normal my-0.5 ${className ?? ''}`}
46 | />
47 |
48 | );
49 | };
50 |
51 | export { TextInput };
52 | export type { TextInputProps };
53 |
--------------------------------------------------------------------------------
/frontend/src/components/ElementView.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowLeft } from 'lucide-react';
2 |
3 | import type { IMessageElement } from '@chainlit/react-client';
4 |
5 | import { useLayoutMaxWidth } from 'hooks/useLayoutMaxWidth';
6 |
7 | import { Element } from './Elements';
8 | import { Button } from './ui/button';
9 |
10 | interface ElementViewProps {
11 | element: IMessageElement;
12 | onGoBack?: () => void;
13 | }
14 |
15 | const ElementView = ({ element, onGoBack }: ElementViewProps) => {
16 | const layoutMaxWidth = useLayoutMaxWidth();
17 |
18 | return (
19 |
26 |
27 | {onGoBack ? (
28 |
31 | ) : null}
32 |
33 | {element.name}
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export { ElementView };
43 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/Audio.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 |
3 | import { IAudioElement } from '@chainlit/react-client';
4 |
5 | const AudioElement = ({ element }: { element: IAudioElement }) => {
6 | if (!element.url) {
7 | return null;
8 | }
9 |
10 | return (
11 |
12 |
{element.name}
13 |
14 |
15 | );
16 | };
17 |
18 | export { AudioElement };
19 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/ElementRef.tsx:
--------------------------------------------------------------------------------
1 | import { MessageContext } from '@/contexts/MessageContext';
2 | import { useContext } from 'react';
3 |
4 | import type { IMessageElement } from '@chainlit/react-client';
5 |
6 | interface ElementRefProps {
7 | element: IMessageElement;
8 | }
9 |
10 | const ElementRef = ({ element }: ElementRefProps) => {
11 | const { onElementRefClick } = useContext(MessageContext);
12 |
13 | // For inline elements, return a styled span
14 | if (element.display === 'inline') {
15 | return {element.name};
16 | }
17 |
18 | // For other elements, return a clickable link
19 | return (
20 | onElementRefClick?.(element)}
24 | >
25 | {element.name}
26 |
27 | );
28 | };
29 |
30 | export { ElementRef };
31 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/File.tsx:
--------------------------------------------------------------------------------
1 | import { type IFileElement } from '@chainlit/react-client';
2 |
3 | import { Attachment } from '@/components/chat/MessageComposer/Attachment';
4 |
5 | const FileElement = ({ element }: { element: IFileElement }) => {
6 | if (!element.url) {
7 | return null;
8 | }
9 |
10 | return (
11 |
17 |
18 |
19 | );
20 | };
21 |
22 | export { FileElement };
23 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/LazyDataframe.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, lazy } from 'react';
2 |
3 | import { IDataframeElement } from '@chainlit/react-client';
4 |
5 | import { Skeleton } from '@/components/ui/skeleton';
6 |
7 | interface Props {
8 | element: IDataframeElement;
9 | }
10 | const DataframeElement = lazy(() => import('@/components/Elements/Dataframe'));
11 |
12 | const LazyDataframe = ({ element }: Props) => {
13 | return (
14 | }>
15 |
16 |
17 | );
18 | };
19 |
20 | export { LazyDataframe };
21 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/PDF.tsx:
--------------------------------------------------------------------------------
1 | import { type IPdfElement } from 'client-types/';
2 |
3 | interface Props {
4 | element: IPdfElement;
5 | }
6 |
7 | const PDFElement = ({ element }: Props) => {
8 | if (!element.url) {
9 | return null;
10 | }
11 | const url = element.page
12 | ? `${element.url}#page=${element.page}`
13 | : element.url;
14 | return (
15 |
19 | );
20 | };
21 |
22 | export { PDFElement };
23 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/Plotly.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, lazy } from 'react';
2 |
3 | import { ErrorBoundary } from '@/components/ErrorBoundary';
4 | import { Skeleton } from '@/components/ui/skeleton';
5 |
6 | import { useFetch } from 'hooks/useFetch';
7 |
8 | import { type IPlotlyElement } from 'client-types/';
9 |
10 | const Plot = lazy(() => import('react-plotly.js'));
11 |
12 | interface Props {
13 | element: IPlotlyElement;
14 | }
15 |
16 | const _PlotlyElement = ({ element }: Props) => {
17 | const { data, error, isLoading } = useFetch(element.url || null);
18 |
19 | if (isLoading) {
20 | return Loading...
;
21 | } else if (error) {
22 | return An error occurred
;
23 | }
24 |
25 | let state;
26 |
27 | if (data) {
28 | state = data;
29 | } else {
30 | return null;
31 | }
32 |
33 | return (
34 | }>
35 |
49 |
50 | );
51 | };
52 |
53 | const PlotlyElement = (props: Props) => {
54 | return (
55 |
56 | <_PlotlyElement {...props} />
57 |
58 | );
59 | };
60 |
61 | export { PlotlyElement };
62 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/Text.tsx:
--------------------------------------------------------------------------------
1 | import { type ITextElement, useConfig } from '@chainlit/react-client';
2 |
3 | import Alert from '@/components/Alert';
4 | import { Markdown } from '@/components/Markdown';
5 | import { Skeleton } from '@/components/ui/skeleton';
6 |
7 | import { useFetch } from 'hooks/useFetch';
8 |
9 | interface TextElementProps {
10 | element: ITextElement;
11 | }
12 |
13 | const TextElement = ({ element }: TextElementProps) => {
14 | const { data, error, isLoading } = useFetch(element.url || null);
15 | const { config } = useConfig();
16 | const allowHtml = config?.features?.unsafe_allow_html;
17 | const latex = config?.features?.latex;
18 |
19 | let content = '';
20 |
21 | if (isLoading) {
22 | return ;
23 | }
24 |
25 | if (error) {
26 | return (
27 | An error occurred while loading the content
28 | );
29 | }
30 |
31 | if (data) {
32 | content = data;
33 | }
34 |
35 | if (element.language) {
36 | content = `\`\`\`${element.language}\n${content}\n\`\`\``;
37 | }
38 |
39 | return (
40 |
45 | {content}
46 |
47 | );
48 | };
49 |
50 | export { TextElement };
51 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/Video.tsx:
--------------------------------------------------------------------------------
1 | import ReactPlayer from 'react-player';
2 |
3 | import { type IVideoElement } from '@chainlit/react-client';
4 |
5 | const VideoElement = ({ element }: { element: IVideoElement }) => {
6 | if (!element.url) {
7 | return null;
8 | }
9 |
10 | return (
11 |
18 | );
19 | };
20 |
21 | export { VideoElement };
22 |
--------------------------------------------------------------------------------
/frontend/src/components/Elements/index.tsx:
--------------------------------------------------------------------------------
1 | import type { IMessageElement } from '@chainlit/react-client';
2 |
3 | import { AudioElement } from './Audio';
4 | import CustomElement from './CustomElement';
5 | import { FileElement } from './File';
6 | import { ImageElement } from './Image';
7 | import { LazyDataframe } from './LazyDataframe';
8 | import { PDFElement } from './PDF';
9 | import { PlotlyElement } from './Plotly';
10 | import { TextElement } from './Text';
11 | import { VideoElement } from './Video';
12 |
13 | interface ElementProps {
14 | element?: IMessageElement;
15 | }
16 |
17 | const Element = ({ element }: ElementProps): JSX.Element | null => {
18 | switch (element?.type) {
19 | case 'file':
20 | return ;
21 | case 'image':
22 | return ;
23 | case 'text':
24 | return ;
25 | case 'pdf':
26 | return ;
27 | case 'audio':
28 | return ;
29 | case 'video':
30 | return ;
31 | case 'plotly':
32 | return ;
33 | case 'dataframe':
34 | return ;
35 | case 'custom':
36 | return ;
37 | default:
38 | return null;
39 | }
40 | };
41 |
42 | export { Element };
43 |
--------------------------------------------------------------------------------
/frontend/src/components/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import { Component, ErrorInfo, ReactNode } from 'react';
2 |
3 | import Alert from './Alert';
4 |
5 | interface Props {
6 | prefix?: string;
7 | children?: ReactNode;
8 | }
9 |
10 | interface State {
11 | hasError: boolean;
12 | error?: string;
13 | }
14 |
15 | class ErrorBoundary extends Component {
16 | public state: State = {
17 | hasError: false,
18 | error: undefined
19 | };
20 |
21 | public static getDerivedStateFromError(err: Error): State {
22 | // Update state so the next render will show the fallback UI.
23 | return { hasError: true, error: err.message };
24 | }
25 |
26 | public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
27 | console.error('Uncaught error:', error, errorInfo);
28 | }
29 |
30 | public render() {
31 | if (this.state.hasError) {
32 | const msg = this.props.prefix
33 | ? `${this.props.prefix}: ${this.state.error}`
34 | : this.state.error;
35 | return (
36 |
39 | );
40 | }
41 |
42 | return this.props.children;
43 | }
44 | }
45 |
46 | export { ErrorBoundary };
47 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon.tsx:
--------------------------------------------------------------------------------
1 | import * as LucideIcons from 'lucide-react';
2 |
3 | interface Props {
4 | name: string;
5 | className?: string;
6 | size?: number;
7 | color?: string;
8 | }
9 |
10 | const Icon = ({ name, ...props }: Props) => {
11 | // Convert the name to proper case (e.g., "plus" -> "Plus", "chevron-right" -> "ChevronRight")
12 | const formatIconName = (str: string): string => {
13 | return str
14 | .split('-')
15 | .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
16 | .join('');
17 | };
18 |
19 | // Try to get the icon component using the formatted name
20 | const formattedName = formatIconName(name);
21 | const IconComponent = LucideIcons[
22 | formattedName as keyof typeof LucideIcons
23 | ] as any;
24 |
25 | if (!IconComponent) {
26 | console.warn(`Icon "${name}" not found in Lucide icons`);
27 | return null;
28 | }
29 |
30 | return ;
31 | };
32 |
33 | export default Icon;
34 |
--------------------------------------------------------------------------------
/frontend/src/components/LeftSidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 |
3 | import SidebarTrigger from '@/components/header/SidebarTrigger';
4 | import { Sidebar, SidebarHeader, SidebarRail } from '@/components/ui/sidebar';
5 |
6 | import NewChatButton from '../header/NewChat';
7 | import SearchChats from './Search';
8 | import { ThreadHistory } from './ThreadHistory';
9 |
10 | export default function LeftSidebar({
11 | ...props
12 | }: React.ComponentProps) {
13 | const navigate = useNavigate();
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/components/Loader.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { LoaderIcon } from 'lucide-react';
3 |
4 | interface LoaderProps {
5 | className?: string;
6 | }
7 |
8 | const Loader = ({ className }: LoaderProps): JSX.Element => {
9 | return (
10 |
13 | );
14 | };
15 |
16 | export { Loader };
17 |
--------------------------------------------------------------------------------
/frontend/src/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { useContext } from 'react';
3 |
4 | import { ChainlitContext, useConfig } from '@chainlit/react-client';
5 |
6 | import { useTheme } from './ThemeProvider';
7 |
8 | interface Props {
9 | className?: string;
10 | }
11 |
12 | export const Logo = ({ className }: Props) => {
13 | const { variant } = useTheme();
14 | const { config } = useConfig();
15 | const apiClient = useContext(ChainlitContext);
16 |
17 | return (
18 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/frontend/src/components/Tasklist/Task.tsx:
--------------------------------------------------------------------------------
1 | import { TaskStatusIcon } from './TaskStatusIcon';
2 |
3 | export interface ITask {
4 | title: string;
5 | status: 'ready' | 'running' | 'done' | 'failed';
6 | forId?: string;
7 | }
8 |
9 | export interface ITaskList {
10 | status: 'ready' | 'running' | 'done';
11 | tasks: ITask[];
12 | }
13 |
14 | interface TaskProps {
15 | index: number;
16 | task: ITask;
17 | }
18 |
19 | export const Task = ({ index, task }: TaskProps) => {
20 | const statusStyles = {
21 | ready: '',
22 | running: 'font-semibold',
23 | done: 'text-muted-foreground',
24 | failed: 'text-muted-foreground'
25 | };
26 |
27 | return (
28 |
29 |
34 | {index}
35 |
36 | {task.title}
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/frontend/src/components/Tasklist/TaskStatusIcon.tsx:
--------------------------------------------------------------------------------
1 | import { Check, Dot, X } from 'lucide-react';
2 |
3 | import { Loader } from '@/components/Loader';
4 |
5 | import type { ITask } from './Task';
6 |
7 | export const TaskStatusIcon = ({ status }: { status: ITask['status'] }) => {
8 | if (status === 'running') {
9 | return ;
10 | }
11 |
12 | return (
13 | <>
14 | {status === 'done' && (
15 |
16 | )}
17 | {status === 'ready' && }
18 | {status === 'failed' && }
19 | >
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/frontend/src/components/WaterMark.tsx:
--------------------------------------------------------------------------------
1 | import { Translator } from 'components/i18n';
2 |
3 | import 'assets/logo_dark.svg';
4 | import LogoDark from 'assets/logo_dark.svg?react';
5 | import 'assets/logo_light.svg';
6 | import LogoLight from 'assets/logo_light.svg?react';
7 |
8 | import { useTheme } from './ThemeProvider';
9 |
10 | export default function WaterMark() {
11 | const { variant } = useTheme();
12 | const Logo = variant === 'light' ? LogoLight : LogoDark;
13 |
14 | return (
15 |
25 |
26 |
27 |
28 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { cn, hasMessage } from '@/lib/utils';
2 | import { MutableRefObject } from 'react';
3 |
4 | import { FileSpec, useChatMessages } from '@chainlit/react-client';
5 |
6 | import WaterMark from '@/components/WaterMark';
7 |
8 | import MessageComposer from './MessageComposer';
9 |
10 | interface Props {
11 | fileSpec: FileSpec;
12 | onFileUpload: (payload: File[]) => void;
13 | onFileUploadError: (error: string) => void;
14 | autoScrollRef: MutableRefObject;
15 | showIfEmptyThread?: boolean;
16 | }
17 |
18 | export default function ChatFooter({ showIfEmptyThread, ...props }: Props) {
19 | const { messages } = useChatMessages();
20 | if (!hasMessage(messages) && !showIfEmptyThread) return null;
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/MessageComposer/Mcp/AnimatedPlugIcon.tsx:
--------------------------------------------------------------------------------
1 | import { Plug } from 'lucide-react';
2 | import { useEffect, useRef } from 'react';
3 |
4 | interface AnimatedPlugIconProps {
5 | duration?: number;
6 | strokeWidth?: number;
7 | className?: string;
8 | }
9 |
10 | const AnimatedPlugIcon: React.FC = ({
11 | duration = 1500,
12 | strokeWidth = 2,
13 | className = ''
14 | }) => {
15 | const iconRef = useRef(null);
16 |
17 | useEffect(() => {
18 | if (iconRef.current) {
19 | // Get all SVG paths inside the icon
20 | const paths = iconRef.current.querySelectorAll('path');
21 |
22 | paths.forEach((path: SVGPathElement) => {
23 | // Get the total length of the path
24 | const length = path.getTotalLength();
25 |
26 | // Set up the starting position
27 | path.style.strokeDasharray = `${length}`;
28 | path.style.strokeDashoffset = `${length}`;
29 |
30 | // Create the animation
31 | path.animate([{ strokeDashoffset: length }, { strokeDashoffset: 0 }], {
32 | duration: duration,
33 | easing: 'ease-in-out',
34 | iterations: Infinity,
35 | direction: 'alternate'
36 | });
37 | });
38 | }
39 | }, [duration]);
40 |
41 | return (
42 |
45 | );
46 | };
47 |
48 | export default AnimatedPlugIcon;
49 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Buttons/Actions/index.tsx:
--------------------------------------------------------------------------------
1 | import { IAction } from '@chainlit/react-client';
2 |
3 | import { ActionButton } from './ActionButton';
4 |
5 | interface Props {
6 | actions: IAction[];
7 | }
8 |
9 | export default function MessageActions({ actions }: Props) {
10 | return (
11 | <>
12 | {actions.map((a) => (
13 |
14 | ))}
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Buttons/DebugButton.tsx:
--------------------------------------------------------------------------------
1 | import { BugIcon } from 'lucide-react';
2 |
3 | import { IStep } from '@chainlit/react-client';
4 |
5 | import { Button } from '@/components/ui/button';
6 | import {
7 | Tooltip,
8 | TooltipContent,
9 | TooltipProvider,
10 | TooltipTrigger
11 | } from '@/components/ui/tooltip';
12 |
13 | interface DebugButtonProps {
14 | debugUrl: string;
15 | step: IStep;
16 | }
17 |
18 | const DebugButton = ({ step, debugUrl }: DebugButtonProps) => {
19 | let stepId = step.id;
20 | if (stepId.startsWith('wrap_')) {
21 | stepId = stepId.replace('wrap_', '');
22 | }
23 |
24 | const href = debugUrl
25 | .replace('[thread_id]', step.threadId ?? '')
26 | .replace('[step_id]', stepId);
27 |
28 | return (
29 |
30 |
31 |
32 |
37 |
38 |
39 | Debug in Literal AI
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export { DebugButton };
47 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlineCustomElementList.tsx:
--------------------------------------------------------------------------------
1 | import type { ICustomElement } from '@chainlit/react-client';
2 |
3 | import CustomElement from '@/components/Elements/CustomElement';
4 |
5 | interface Props {
6 | items: ICustomElement[];
7 | }
8 |
9 | const InlinedCustomElementList = ({ items }: Props) => (
10 |
11 | {items.map((customElement) => {
12 | return ;
13 | })}
14 |
15 | );
16 |
17 | export { InlinedCustomElementList };
18 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlinedAudioList.tsx:
--------------------------------------------------------------------------------
1 | import type { IAudioElement } from '@chainlit/react-client';
2 |
3 | import { AudioElement } from '@/components/Elements/Audio';
4 |
5 | interface InlinedAudioListProps {
6 | items: IAudioElement[];
7 | }
8 |
9 | const InlinedAudioList = ({ items }: InlinedAudioListProps) => {
10 | return (
11 |
12 | {items.map((audio, i) => (
13 |
16 | ))}
17 |
18 | );
19 | };
20 |
21 | export { InlinedAudioList };
22 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlinedDataframeList.tsx:
--------------------------------------------------------------------------------
1 | import type { IDataframeElement } from '@chainlit/react-client';
2 |
3 | import { LazyDataframe } from '@/components/Elements/LazyDataframe';
4 |
5 | interface Props {
6 | items: IDataframeElement[];
7 | }
8 |
9 | const InlinedDataframeList = ({ items }: Props) => (
10 |
11 | {items.map((element, i) => {
12 | return (
13 |
14 |
15 |
16 | );
17 | })}
18 |
19 | );
20 |
21 | export { InlinedDataframeList };
22 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlinedFileList.tsx:
--------------------------------------------------------------------------------
1 | import type { IFileElement } from '@chainlit/react-client';
2 |
3 | import { FileElement } from '@/components/Elements/File';
4 |
5 | interface Props {
6 | items: IFileElement[];
7 | }
8 |
9 | const InlinedFileList = ({ items }: Props) => {
10 | return (
11 |
12 | {items.map((file, i) => {
13 | return (
14 |
15 |
16 |
17 | );
18 | })}
19 |
20 | );
21 | };
22 |
23 | export { InlinedFileList };
24 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlinedImageList.tsx:
--------------------------------------------------------------------------------
1 | import { ImageElement } from '@/components/Elements/Image';
2 | import { QuiltedGrid } from '@/components/QuiltedGrid';
3 |
4 | import type { IImageElement } from 'client-types/';
5 |
6 | interface Props {
7 | items: IImageElement[];
8 | }
9 |
10 | const InlinedImageList = ({ items }: Props) => (
11 | }
14 | />
15 | );
16 |
17 | export { InlinedImageList };
18 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlinedPDFList.tsx:
--------------------------------------------------------------------------------
1 | import type { IPdfElement } from '@chainlit/react-client';
2 |
3 | import { PDFElement } from '@/components/Elements/PDF';
4 |
5 | interface Props {
6 | items: IPdfElement[];
7 | }
8 |
9 | const InlinedPDFList = ({ items }: Props) => (
10 |
11 | {items.map((pdf, i) => {
12 | return (
13 |
16 | );
17 | })}
18 |
19 | );
20 |
21 | export { InlinedPDFList };
22 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlinedPlotlyList.tsx:
--------------------------------------------------------------------------------
1 | import type { IPlotlyElement } from '@chainlit/react-client';
2 |
3 | import { PlotlyElement } from '@/components/Elements/Plotly';
4 |
5 | interface Props {
6 | items: IPlotlyElement[];
7 | }
8 |
9 | const InlinedPlotlyList = ({ items }: Props) => (
10 |
11 | {items.map((element, i) => {
12 | return (
13 |
23 | );
24 | })}
25 |
26 | );
27 |
28 | export { InlinedPlotlyList };
29 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlinedTextList.tsx:
--------------------------------------------------------------------------------
1 | import type { ITextElement } from '@chainlit/react-client';
2 |
3 | import { TextElement } from '@/components/Elements/Text';
4 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5 |
6 | interface Props {
7 | items: ITextElement[];
8 | }
9 |
10 | const InlinedTextList = ({ items }: Props) => (
11 |
12 | {items.map((el) => {
13 | return (
14 |
15 |
16 | {el.name}
17 |
18 |
19 |
20 |
21 |
22 | );
23 | })}
24 |
25 | );
26 |
27 | export { InlinedTextList };
28 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Messages/Message/Content/InlinedElements/InlinedVideoList.tsx:
--------------------------------------------------------------------------------
1 | import { VideoElement } from '@/components/Elements/Video';
2 |
3 | import type { IVideoElement } from 'client-types/';
4 |
5 | interface Props {
6 | items: IVideoElement[];
7 | }
8 |
9 | const InlinedVideoList = ({ items }: Props) => (
10 |
11 | {items.map((i) => (
12 |
13 | ))}
14 |
15 | );
16 |
17 | export { InlinedVideoList };
18 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/ScrollDownButton.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowDown } from 'lucide-react';
2 |
3 | import { Button } from '@/components/ui/button';
4 |
5 | interface Props {
6 | onClick?: () => void;
7 | }
8 |
9 | export default function ScrollDownButton({ onClick }: Props) {
10 | return (
11 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Starters.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { useMemo } from 'react';
3 |
4 | import { useChatSession, useConfig } from '@chainlit/react-client';
5 |
6 | import Starter from './Starter';
7 |
8 | interface Props {
9 | className?: string;
10 | }
11 |
12 | export default function Starters({ className }: Props) {
13 | const { chatProfile } = useChatSession();
14 | const { config } = useConfig();
15 |
16 | const starters = useMemo(() => {
17 | if (chatProfile) {
18 | const selectedChatProfile = config?.chatProfiles.find(
19 | (profile) => profile.name === chatProfile
20 | );
21 | if (selectedChatProfile?.starters) {
22 | return selectedChatProfile.starters.slice(0, 4);
23 | }
24 | }
25 | return config?.starters;
26 | }, [config, chatProfile]);
27 |
28 | if (!starters?.length) return null;
29 |
30 | return (
31 |
35 | {starters?.map((starter, i) => (
36 |
37 | ))}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/frontend/src/components/header/ApiKeys.tsx:
--------------------------------------------------------------------------------
1 | import { KeyRound } from 'lucide-react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import { useConfig } from '@chainlit/react-client';
5 |
6 | import { Button } from '@/components/ui/button';
7 | import {
8 | Tooltip,
9 | TooltipContent,
10 | TooltipProvider,
11 | TooltipTrigger
12 | } from '@/components/ui/tooltip';
13 | import { Translator } from 'components/i18n';
14 |
15 | export default function ApiKeys() {
16 | const { config } = useConfig();
17 | const requiredKeys = !!config?.userEnv?.length;
18 |
19 | if (!requiredKeys) return null;
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/src/components/header/SidebarTrigger.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger
7 | } from '@/components/ui/tooltip';
8 | import { Translator } from 'components/i18n';
9 |
10 | import { Sidebar } from '../icons/Sidebar';
11 | import { useSidebar } from '../ui/sidebar';
12 |
13 | export default function SidebarTrigger() {
14 | const { setOpen, open, openMobile, setOpenMobile, isMobile } = useSidebar();
15 |
16 | return (
17 |
18 |
19 |
20 |
31 |
32 |
33 |
34 | {open ? (
35 |
36 | ) : (
37 |
38 | )}
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/src/components/i18n/Translator.tsx:
--------------------------------------------------------------------------------
1 | import { TOptions } from 'i18next';
2 | import { $Dictionary } from 'i18next/typescript/helpers';
3 | import { useTranslation as usei18nextTranslation } from 'react-i18next';
4 |
5 | import { Skeleton } from '@/components/ui/skeleton';
6 |
7 | type options = TOptions<$Dictionary>;
8 |
9 | type TranslatorProps = {
10 | path: string | string[];
11 | suffix?: string;
12 | options?: options;
13 | };
14 |
15 | const Translator = ({ path, options, suffix }: TranslatorProps) => {
16 | const { t, i18n } = usei18nextTranslation();
17 |
18 | if (!i18n.exists(path, options)) {
19 | return ;
20 | }
21 |
22 | return (
23 |
24 | {t(path, options)}
25 | {suffix}
26 |
27 | );
28 | };
29 |
30 | export const useTranslation = () => {
31 | const { t, ready, i18n } = usei18nextTranslation();
32 |
33 | return {
34 | t: (path: string | string[], options?: options) => {
35 | if (!i18n.exists(path, options)) {
36 | return '...';
37 | }
38 |
39 | return t(path, options);
40 | },
41 | ready,
42 | i18n
43 | };
44 | };
45 |
46 | export default Translator;
47 |
--------------------------------------------------------------------------------
/frontend/src/components/i18n/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Translator } from './Translator';
2 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Auth0.tsx:
--------------------------------------------------------------------------------
1 | export const Auth0 = () => {
2 | return (
3 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Github.tsx:
--------------------------------------------------------------------------------
1 | export const GitHub = () => {
2 | return (
3 |
17 | );
18 | };
19 |
20 | export default GitHub;
21 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Gitlab.tsx:
--------------------------------------------------------------------------------
1 | export const Gitlab = () => {
2 | return (
3 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Google.tsx:
--------------------------------------------------------------------------------
1 | export const Google = () => {
2 | return (
3 |
27 | );
28 | };
29 | export default Google;
30 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Microsoft.tsx:
--------------------------------------------------------------------------------
1 | export const Microsoft = () => {
2 | return (
3 |
10 | );
11 | };
12 | export default Microsoft;
13 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/PaperClip.tsx:
--------------------------------------------------------------------------------
1 | export const PaperClip = ({ className }: { className?: string }) => {
2 | return (
3 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Pencil.tsx:
--------------------------------------------------------------------------------
1 | export const Pencil = ({ className }: { className?: string }) => {
2 | return (
3 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Search.tsx:
--------------------------------------------------------------------------------
1 | export const Search = ({ className }: { className?: string }) => {
2 | return (
3 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Send.tsx:
--------------------------------------------------------------------------------
1 | export const Send = ({ className }: { className?: string }) => {
2 | return (
3 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Stop.tsx:
--------------------------------------------------------------------------------
1 | export const Stop = ({ className }: { className?: string }) => {
2 | return (
3 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/VoiceLines.tsx:
--------------------------------------------------------------------------------
1 | export const VoiceLines = ({ className }: { className?: string }) => {
2 | return (
3 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
2 |
3 | const AspectRatio = AspectRatioPrimitive.Root;
4 |
5 | export { AspectRatio };
6 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as AvatarPrimitive from '@radix-ui/react-avatar';
3 | import * as React from 'react';
4 |
5 | const Avatar = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Avatar.displayName = AvatarPrimitive.Root.displayName;
19 |
20 | const AvatarImage = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
31 |
32 | const AvatarFallback = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
46 |
47 | export { Avatar, AvatarImage, AvatarFallback };
48 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { type VariantProps, cva } from 'class-variance-authority';
3 | import * as React from 'react';
4 |
5 | const badgeVariants = cva(
6 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7 | {
8 | variants: {
9 | variant: {
10 | default:
11 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
12 | secondary:
13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
14 | destructive:
15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
16 | outline: 'text-foreground'
17 | }
18 | },
19 | defaultVariants: {
20 | variant: 'default'
21 | }
22 | }
23 | );
24 |
25 | export interface BadgeProps
26 | extends React.HTMLAttributes,
27 | VariantProps {}
28 |
29 | function Badge({ className, variant, ...props }: BadgeProps) {
30 | return (
31 |
32 | );
33 | }
34 |
35 | export { Badge, badgeVariants };
36 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
3 | import { Check } from 'lucide-react';
4 | import * as React from 'react';
5 |
6 | const Checkbox = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
21 |
22 |
23 |
24 | ));
25 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
26 |
27 | export { Checkbox };
28 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
3 | import * as React from 'react';
4 |
5 | const HoverCard = HoverCardPrimitive.Root;
6 |
7 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
8 |
9 | const HoverCardContent = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
13 |
23 | ));
24 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
25 |
26 | export { HoverCard, HoverCardTrigger, HoverCardContent };
27 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as React from 'react';
3 |
4 | const Input = React.forwardRef>(
5 | ({ className, type, ...props }, ref) => {
6 | return (
7 |
16 | );
17 | }
18 | );
19 | Input.displayName = 'Input';
20 |
21 | export { Input };
22 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as LabelPrimitive from '@radix-ui/react-label';
3 | import { type VariantProps, cva } from 'class-variance-authority';
4 | import * as React from 'react';
5 |
6 | const labelVariants = cva(
7 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
8 | );
9 |
10 | const Label = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef &
13 | VariantProps
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | Label.displayName = LabelPrimitive.Root.displayName;
22 |
23 | export { Label };
24 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as PopoverPrimitive from '@radix-ui/react-popover';
3 | import * as React from 'react';
4 |
5 | const Popover = PopoverPrimitive.Root;
6 |
7 | const PopoverTrigger = PopoverPrimitive.Trigger;
8 |
9 | const PopoverContent = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
13 |
18 |
28 |
29 | ));
30 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
31 |
32 | export { Popover, PopoverTrigger, PopoverContent };
33 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as ProgressPrimitive from '@radix-ui/react-progress';
3 | import * as React from 'react';
4 |
5 | const Progress = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, value, ...props }, ref) => (
9 |
17 |
21 |
22 | ));
23 | Progress.displayName = ProgressPrimitive.Root.displayName;
24 |
25 | export { Progress };
26 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as SeparatorPrimitive from '@radix-ui/react-separator';
3 | import * as React from 'react';
4 |
5 | const Separator = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(
9 | (
10 | { className, orientation = 'horizontal', decorative = true, ...props },
11 | ref
12 | ) => (
13 |
24 | )
25 | );
26 | Separator.displayName = SeparatorPrimitive.Root.displayName;
27 |
28 | export { Separator };
29 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as SliderPrimitive from '@radix-ui/react-slider';
3 | import * as React from 'react';
4 |
5 | const Slider = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 |
18 |
19 |
20 |
21 |
22 | ));
23 | Slider.displayName = SliderPrimitive.Root.displayName;
24 |
25 | export { Slider };
26 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster as Sonner } from 'sonner';
2 |
3 | import { useTheme } from '../ThemeProvider';
4 |
5 | type ToasterProps = React.ComponentProps;
6 |
7 | const Toaster = ({ ...props }: ToasterProps) => {
8 | const { variant } = useTheme();
9 |
10 | return (
11 |
27 | );
28 | };
29 |
30 | export { Toaster };
31 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as SwitchPrimitives from '@radix-ui/react-switch';
3 | import * as React from 'react';
4 |
5 | const Switch = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 |
22 |
23 | ));
24 | Switch.displayName = SwitchPrimitives.Root.displayName;
25 |
26 | export { Switch };
27 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as React from 'react';
3 |
4 | const Textarea = React.forwardRef<
5 | HTMLTextAreaElement,
6 | React.ComponentProps<'textarea'>
7 | >(({ className, ...props }, ref) => {
8 | return (
9 |
17 | );
18 | });
19 | Textarea.displayName = 'Textarea';
20 |
21 | export { Textarea };
22 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
3 | import * as React from 'react';
4 |
5 | const TooltipProvider = TooltipPrimitive.Provider;
6 |
7 | const Tooltip = TooltipPrimitive.Root;
8 |
9 | const TooltipTrigger = TooltipPrimitive.Trigger;
10 |
11 | const TooltipContent = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, sideOffset = 4, ...props }, ref) => (
15 |
24 | ));
25 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
26 |
27 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
28 |
--------------------------------------------------------------------------------
/frontend/src/contexts/MessageContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | import { IMessageContext } from 'types/messageContext';
4 |
5 | const defaultMessageContext = {
6 | highlightedMessage: null,
7 | loading: false,
8 | editable: false,
9 | onElementRefClick: undefined,
10 | onFeedbackUpdated: undefined,
11 | showFeedbackButtons: true,
12 | onError: () => undefined,
13 | uiName: '',
14 | cot: 'hidden' as const
15 | };
16 |
17 | const MessageContext = createContext(defaultMessageContext);
18 |
19 | export { MessageContext, defaultMessageContext };
20 |
--------------------------------------------------------------------------------
/frontend/src/hooks/query.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 |
4 | export function useQuery() {
5 | const { search } = useLocation();
6 |
7 | return useMemo(() => new URLSearchParams(search), [search]);
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const MOBILE_BREAKPOINT = 768;
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(
7 | undefined
8 | );
9 |
10 | React.useEffect(() => {
11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12 | const onChange = () => {
13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14 | };
15 | mql.addEventListener('change', onChange);
16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17 | return () => mql.removeEventListener('change', onChange);
18 | }, []);
19 |
20 | return !!isMobile;
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useFetch.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import useSWR, { SWRResponse } from 'swr';
3 |
4 | import { ChainlitContext } from '@chainlit/react-client';
5 |
6 | const fetcher =
7 | (isChainlitRequest: boolean) =>
8 | async (url: string): Promise => {
9 | const fetchOptions: RequestInit = {
10 | ...(isChainlitRequest && { credentials: 'include' })
11 | };
12 |
13 | const response = await fetch(url, fetchOptions);
14 |
15 | if (!response.ok) {
16 | throw new Error('Network response was not ok');
17 | }
18 |
19 | const contentType = response.headers.get('content-type');
20 | return contentType?.includes('application/json')
21 | ? response.json()
22 | : response.text();
23 | };
24 |
25 | const useFetch = (endpoint: string | null): SWRResponse => {
26 | const apiClient = useContext(ChainlitContext);
27 | const isChainlitRequest = endpoint?.startsWith(apiClient.httpEndpoint);
28 |
29 | return useSWR(endpoint, fetcher(!!isChainlitRequest));
30 | };
31 |
32 | export { useFetch };
33 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useLayoutMaxWidth.tsx:
--------------------------------------------------------------------------------
1 | import { useConfig } from '@chainlit/react-client';
2 |
3 | const useLayoutMaxWidth = () => {
4 | const { config } = useConfig();
5 | return config?.ui.layout === 'wide'
6 | ? 'min(60rem, 100vw)'
7 | : 'min(48rem, 100vw)';
8 | };
9 |
10 | export { useLayoutMaxWidth };
11 |
--------------------------------------------------------------------------------
/frontend/src/hooks/usePlatform.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | const MOBILE_REGEX =
4 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
5 |
6 | type PlatformPayload = {
7 | isSSR: boolean;
8 | isMobile: boolean;
9 | isDesktop: boolean;
10 | isMac: boolean;
11 | };
12 |
13 | export const usePlatform = (): PlatformPayload => {
14 | const platforms = useMemo(() => {
15 | if (navigator == null) {
16 | return {
17 | isSSR: true,
18 | isMobile: false,
19 | isDesktop: false,
20 | isMac: false
21 | };
22 | }
23 | const isMobile = navigator.userAgent.match(MOBILE_REGEX) != null;
24 | const isMac = navigator.userAgent.toUpperCase().match(/MAC/) != null;
25 |
26 | return {
27 | isSSR: false,
28 | isMobile,
29 | isDesktop: !isMobile,
30 | isMac
31 | };
32 | }, []);
33 |
34 | return platforms;
35 | };
36 |
--------------------------------------------------------------------------------
/frontend/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import { initReactI18next } from 'react-i18next';
3 |
4 | const i18nConfig = {
5 | fallbackLng: 'en-US',
6 | defaultNS: 'translation'
7 | };
8 |
9 | export function i18nSetupLocalization(): void {
10 | i18n
11 | .use(initReactI18next)
12 | .init(i18nConfig)
13 | .catch((err) => console.error('[i18n] Failed to setup localization.', err));
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/index.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | /**
4 | * Enables using the findLast method on arrays.
5 | */
6 | declare global {
7 | interface Array {
8 | findLast(
9 | predicate: (value: T, index: number, array: T[]) => unknown,
10 | thisArg?: any
11 | ): T | undefined;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/lib/router.ts:
--------------------------------------------------------------------------------
1 | const getRouterBasename = () => {
2 | const ogTitleMeta = document.querySelector('meta[property="og:root_path"]');
3 | if (ogTitleMeta && typeof ogTitleMeta.getAttribute('content') === 'string') {
4 | return ogTitleMeta.getAttribute('content')!;
5 | } else {
6 | return '';
7 | }
8 | };
9 |
10 | export default getRouterBasename;
11 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import AppWrapper from 'AppWrapper';
2 | import { apiClient } from 'api';
3 | import React from 'react';
4 | import ReactDOM from 'react-dom/client';
5 | import { RecoilRoot } from 'recoil';
6 |
7 | import { ChainlitContext } from '@chainlit/react-client';
8 |
9 | import './index.css';
10 |
11 | import { i18nSetupLocalization } from './i18n';
12 |
13 | i18nSetupLocalization();
14 |
15 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/frontend/src/pages/AuthCallback.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | import { useAuth } from '@chainlit/react-client';
5 |
6 | export default function AuthCallback() {
7 | const { user, setUserFromAPI } = useAuth();
8 | const navigate = useNavigate();
9 |
10 | // Fetch user in cookie-based oauth.
11 | useEffect(() => {
12 | if (!user) setUserFromAPI();
13 | }, []);
14 |
15 | useEffect(() => {
16 | if (user) {
17 | navigate('/');
18 | }
19 | }, [user]);
20 |
21 | return null;
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import Page from 'pages/Page';
2 |
3 | import Chat from '@/components/chat';
4 |
5 | export default function Home() {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/router.tsx:
--------------------------------------------------------------------------------
1 | import getRouterBasename from '@/lib/router';
2 | import { Navigate, createBrowserRouter } from 'react-router-dom';
3 |
4 | import AuthCallback from 'pages/AuthCallback';
5 | import Element from 'pages/Element';
6 | import Env from 'pages/Env';
7 | import Home from 'pages/Home';
8 | import Login from 'pages/Login';
9 | import Thread from 'pages/Thread';
10 |
11 | export const router = createBrowserRouter(
12 | [
13 | {
14 | path: '/',
15 | element:
16 | },
17 | {
18 | path: '/env',
19 | element:
20 | },
21 | {
22 | path: '/thread/:id?',
23 | element:
24 | },
25 | {
26 | path: '/element/:id',
27 | element:
28 | },
29 | {
30 | path: '/login',
31 | element:
32 | },
33 | {
34 | path: '/login/callback',
35 | element:
36 | },
37 | {
38 | path: '*',
39 | element:
40 | }
41 | ],
42 | { basename: getRouterBasename() }
43 | );
44 |
--------------------------------------------------------------------------------
/frontend/src/state/chat.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'recoil';
2 |
3 | import { ICommand } from 'client-types/*';
4 |
5 | export interface IAttachment {
6 | id: string;
7 | serverId?: string;
8 | name: string;
9 | size: number;
10 | type: string;
11 | uploadProgress?: number;
12 | uploaded?: boolean;
13 | cancel?: () => void;
14 | remove?: () => void;
15 | }
16 |
17 | export const attachmentsState = atom({
18 | key: 'Attachments',
19 | default: []
20 | });
21 |
22 | export const persistentCommandState = atom({
23 | key: 'PersistentCommand',
24 | default: undefined
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/state/project.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'recoil';
2 |
3 | export const chatSettingsOpenState = atom({
4 | key: 'chatSettingsOpen',
5 | default: false
6 | });
7 |
--------------------------------------------------------------------------------
/frontend/src/state/user.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'recoil';
2 |
3 | const localUserEnv = localStorage.getItem('userEnv');
4 |
5 | export const userEnvState = atom>({
6 | key: 'UserEnv',
7 | default: localUserEnv ? JSON.parse(localUserEnv) : {}
8 | });
9 |
--------------------------------------------------------------------------------
/frontend/src/types/Input.ts:
--------------------------------------------------------------------------------
1 | import { NotificationCountProps } from './NotificationCount';
2 |
3 | interface IInput {
4 | className?: string;
5 | description?: string;
6 | disabled?: boolean;
7 | hasError?: boolean;
8 | id: string;
9 | label?: string;
10 | notificationsProps?: NotificationCountProps;
11 | tooltip?: string;
12 | }
13 |
14 | export type { IInput };
15 |
--------------------------------------------------------------------------------
/frontend/src/types/NotificationCount.tsx:
--------------------------------------------------------------------------------
1 | export type NotificationCountProps = {
2 | count?: number | string;
3 | inputProps?: {
4 | id: string;
5 | max?: number;
6 | min?: number;
7 | onChange: (event: any) => void;
8 | step?: number;
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/frontend/src/types/chat.ts:
--------------------------------------------------------------------------------
1 | import { Socket } from '@chainlit/react-client';
2 |
3 | export interface IToken {
4 | id: number | string;
5 | token: string;
6 | isSequence: boolean;
7 | }
8 |
9 | export interface ISession {
10 | socket: Socket;
11 | error?: boolean;
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Input';
2 | export * from './messageContext';
3 | export * from './NotificationCount';
4 |
--------------------------------------------------------------------------------
/frontend/src/types/messageContext.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | IAsk,
3 | IFeedback,
4 | IFileRef,
5 | IMessageElement,
6 | IStep
7 | } from '@chainlit/react-client';
8 |
9 | interface IMessageContext {
10 | uploadFile?: (
11 | file: File,
12 | onProgress: (progress: number) => void
13 | ) => { xhr: XMLHttpRequest; promise: Promise };
14 | cot: 'hidden' | 'tool_call' | 'full';
15 | askUser?: IAsk;
16 | editable: boolean;
17 | loading: boolean;
18 | showFeedbackButtons: boolean;
19 | uiName: string;
20 | allowHtml?: boolean;
21 | latex?: boolean;
22 | onElementRefClick?: (element: IMessageElement) => void;
23 | onFeedbackUpdated?: (
24 | message: IStep,
25 | onSuccess: () => void,
26 | feedback: IFeedback
27 | ) => void;
28 | onFeedbackDeleted?: (
29 | message: IStep,
30 | onSuccess: () => void,
31 | feedbackId: string
32 | ) => void;
33 | onError: (error: string) => void;
34 | }
35 |
36 | export type { IMessageContext };
37 |
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | declare module 'react-mentions-continued';
5 |
--------------------------------------------------------------------------------
/frontend/tests/setup-tests.ts:
--------------------------------------------------------------------------------
1 | import matchers from '@testing-library/jest-dom/matchers';
2 | import { cleanup } from '@testing-library/react';
3 | import { afterEach, expect, vi } from 'vitest';
4 |
5 | expect.extend(matchers);
6 |
7 | // Mock URL.createObjectURL
8 | global.URL.createObjectURL = vi.fn();
9 |
10 | afterEach(() => {
11 | cleanup();
12 | });
13 |
--------------------------------------------------------------------------------
/frontend/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["jest", "testing-library__jest-dom"]
5 | },
6 | "include": ["**/*.spec.tsx"]
7 | }
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "target": "ESNext",
5 | "useDefineForClassFields": true,
6 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
7 | "baseUrl": "./src",
8 | "allowJs": false,
9 | "skipLibCheck": true,
10 | "esModuleInterop": false,
11 | "allowSyntheticDefaultImports": true,
12 | "strict": true,
13 | "strictNullChecks": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "ESNext",
16 | "moduleResolution": "Node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react-jsx",
21 | "types": ["node"],
22 | "paths": {
23 | "client-types/*": ["../../libs/react-client/dist"],
24 | "@/*": ["./*"]
25 | }
26 | },
27 | "include": ["./src"],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc';
2 | import path from 'path';
3 | import { defineConfig } from 'vite';
4 | import svgr from 'vite-plugin-svgr';
5 | import tsconfigPaths from 'vite-tsconfig-paths';
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | build: {
10 | sourcemap: true
11 | },
12 | plugins: [react(), tsconfigPaths(), svgr()],
13 | resolve: {
14 | alias: {
15 | '@': path.resolve(__dirname, './src'),
16 | // To prevent conflicts with packages in @chainlit/react-client, we need to specify the resolution paths for these dependencies.
17 | react: path.resolve(__dirname, './node_modules/react'),
18 | 'usehooks-ts': path.resolve(__dirname, './node_modules/usehooks-ts'),
19 | sonner: path.resolve(__dirname, './node_modules/sonner'),
20 | lodash: path.resolve(__dirname, './node_modules/lodash'),
21 | recoil: path.resolve(__dirname, './node_modules/recoil')
22 | }
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/frontend/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import react from '@vitejs/plugin-react-swc';
3 | import { defineConfig } from 'vite';
4 | import tsconfigPaths from 'vite-tsconfig-paths';
5 |
6 | export default defineConfig({
7 | plugins: [tsconfigPaths(), react()],
8 | test: {
9 | environment: 'jsdom',
10 | setupFiles: './tests/setup-tests.ts',
11 | include: ['./**/*.{test,spec}.?(c|m)[jt]s?(x)']
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/images/quick-start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chainlit/chainlit/203b6821ef2e9697f1da8cb8ebda7ba5a9dd5be8/images/quick-start.png
--------------------------------------------------------------------------------
/libs/copilot/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from '@storybook/react-vite';
2 |
3 | const config: StorybookConfig = {
4 | stories: [
5 | '../stories/**/*.mdx',
6 | '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'
7 | ],
8 | addons: [
9 | '@storybook/addon-links',
10 | '@storybook/addon-essentials',
11 | '@storybook/addon-onboarding',
12 | '@storybook/addon-interactions'
13 | ],
14 | framework: {
15 | name: '@storybook/react-vite',
16 | options: {}
17 | },
18 | docs: {
19 | autodocs: 'tag'
20 | }
21 | };
22 | export default config;
23 |
--------------------------------------------------------------------------------
/libs/copilot/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import '../src/index.css'
2 |
3 | import type { Preview } from '@storybook/react';
4 |
5 | const preview: Preview = {
6 | parameters: {
7 | actions: { argTypesRegex: '^on[A-Z].*' },
8 | controls: {
9 | matchers: {
10 | color: /(background|color)$/i,
11 | date: /Date$/i
12 | }
13 | }
14 | }
15 | };
16 |
17 | export default preview;
18 |
--------------------------------------------------------------------------------
/libs/copilot/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@chainlit/app/src/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@chainlit/app/src/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/libs/copilot/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/libs/copilot/src/api.ts:
--------------------------------------------------------------------------------
1 | import { toast } from 'sonner';
2 |
3 | import { ChainlitAPI, ClientError } from '@chainlit/react-client';
4 |
5 | export function makeApiClient(
6 | chainlitServer: string,
7 | additionalQueryParams: Record
8 | ) {
9 | const httpEndpoint = chainlitServer;
10 |
11 | const on401 = () => {
12 | toast.error('Unauthorized');
13 | };
14 |
15 | const onError = (error: ClientError) => {
16 | toast.error(error.toString());
17 | };
18 |
19 | return new ChainlitAPI(
20 | httpEndpoint,
21 | 'copilot',
22 | additionalQueryParams,
23 | on401,
24 | onError
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/libs/copilot/src/chat/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | import { useChatInteract, useChatSession } from '@chainlit/react-client';
4 |
5 | import ChatBody from './body';
6 |
7 | export default function ChatWrapper() {
8 | const { connect, session } = useChatSession();
9 | const { sendMessage } = useChatInteract();
10 | useEffect(() => {
11 | if (session?.socket?.connected) return;
12 | connect({
13 | // @ts-expect-error window typing
14 | transports: window.transports,
15 | userEnv: {}
16 | });
17 | }, [connect]);
18 |
19 | useEffect(() => {
20 | // @ts-expect-error is not a valid prop
21 | window.sendChainlitMessage = sendMessage;
22 | }, [sendMessage]);
23 |
24 | return ;
25 | }
26 |
--------------------------------------------------------------------------------
/libs/copilot/src/components/ElementSideView.tsx:
--------------------------------------------------------------------------------
1 | import { useRecoilState } from 'recoil';
2 |
3 | import { Element } from '@chainlit/app/src/components/Elements';
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogHeader,
8 | DialogTitle
9 | } from '@chainlit/app/src/components/ui/dialog';
10 | import { sideViewState } from '@chainlit/react-client';
11 |
12 | export default function ElementSideView() {
13 | const [sideView, setSideView] = useRecoilState(sideViewState);
14 |
15 | if (!sideView || sideView.title === 'canvas') return null;
16 |
17 | return (
18 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/libs/copilot/src/components/WelcomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import Starters from '@chainlit/app/src/components/chat/Starters';
4 | import { cn, hasMessage } from '@chainlit/app/src/lib/utils';
5 | import { useChatMessages } from '@chainlit/react-client';
6 |
7 | export default function WelcomeScreen() {
8 | const { messages } = useChatMessages();
9 | const [isVisible, setIsVisible] = useState(false);
10 |
11 | useEffect(() => {
12 | setIsVisible(true);
13 | }, []);
14 |
15 | if (hasMessage(messages)) return null;
16 |
17 | return (
18 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/libs/copilot/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/libs/copilot/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface IWidgetConfig {
2 | chainlitServer: string;
3 | showCot?: boolean;
4 | accessToken?: string;
5 | theme?: 'light' | 'dark';
6 | button?: {
7 | containerId?: string;
8 | imageUrl?: string;
9 | className?: string;
10 | };
11 | customCssUrl?: string;
12 | additionalQueryParamsForAPI?: Record;
13 | }
14 |
--------------------------------------------------------------------------------
/libs/copilot/stories/App.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import AppWrapper from 'appWrapper';
3 |
4 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
5 | const meta = {
6 | title: 'Example/App',
7 | component: AppWrapper,
8 | parameters: {
9 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
10 | layout: 'centered'
11 | },
12 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
13 | tags: ['autodocs'],
14 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
15 | argTypes: {}
16 | } satisfies Meta;
17 |
18 | export default meta;
19 | type Story = StoryObj;
20 |
21 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
22 | export const Primary: Story = {
23 | args: {
24 | widgetConfig: {
25 | chainlitServer: 'http://localhost:8000'
26 | }
27 | }
28 | };
29 |
30 | export const Secondary: Story = {
31 | args: {
32 | widgetConfig: {
33 | chainlitServer: 'http://localhost:8000',
34 | theme: 'dark',
35 | button: {
36 | imageUrl:
37 | 'https://steelbluemedia.com/wp-content/uploads/2019/06/new-google-favicon-512.png',
38 | }
39 | }
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/libs/copilot/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "baseUrl": "./src",
7 | "paths": {
8 | "@/*": ["./*"]
9 | },
10 | "allowJs": false,
11 | "skipLibCheck": true,
12 | "esModuleInterop": false,
13 | "allowSyntheticDefaultImports": true,
14 | "sourceMap": true,
15 | "strict": true,
16 | "strictNullChecks": true,
17 | "forceConsistentCasingInFileNames": true,
18 | "module": "ESNext",
19 | "moduleResolution": "Node",
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "noEmit": true,
23 | "jsx": "react-jsx",
24 | "types": ["node"]
25 | },
26 | "include": ["./src", "./stories"],
27 | "references": [{ "path": "./tsconfig.node.json" }]
28 | }
29 |
--------------------------------------------------------------------------------
/libs/copilot/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/libs/react-client/src/api/hooks/auth/config.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { IAuthConfig } from 'src/index';
3 |
4 | import { useApi } from '../api';
5 | import { useAuthState } from './state';
6 |
7 | export const useAuthConfig = () => {
8 | const { authConfig, setAuthConfig } = useAuthState();
9 | const { data: authConfigData, isLoading } = useApi(
10 | authConfig ? null : '/auth/config'
11 | );
12 |
13 | useEffect(() => {
14 | if (authConfigData) {
15 | setAuthConfig(authConfigData);
16 | }
17 | }, [authConfigData, setAuthConfig]);
18 |
19 | return { authConfig, isLoading };
20 | };
21 |
--------------------------------------------------------------------------------
/libs/react-client/src/api/hooks/auth/index.ts:
--------------------------------------------------------------------------------
1 | import { IAuthConfig, IUser } from 'src/types';
2 |
3 | import { useAuthConfig } from './config';
4 | import { useSessionManagement } from './sessionManagement';
5 | import { useUserManagement } from './userManagement';
6 |
7 | export const useAuth = () => {
8 | const { authConfig } = useAuthConfig();
9 | const { logout } = useSessionManagement();
10 | const { user, setUserFromAPI } = useUserManagement();
11 |
12 | const isReady =
13 | !!authConfig && (!authConfig.requireLogin || user !== undefined);
14 |
15 | if (authConfig && !authConfig.requireLogin) {
16 | return {
17 | data: authConfig,
18 | user: null,
19 | isReady,
20 | isAuthenticated: true,
21 | logout: () => Promise.resolve(),
22 | setUserFromAPI: () => Promise.resolve()
23 | };
24 | }
25 |
26 | return {
27 | data: authConfig,
28 | user,
29 | isReady,
30 | isAuthenticated: !!user,
31 | logout,
32 | setUserFromAPI
33 | };
34 | };
35 |
36 | export type { IAuthConfig, IUser };
37 |
--------------------------------------------------------------------------------
/libs/react-client/src/api/hooks/auth/sessionManagement.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { ChainlitContext } from 'src/index';
3 |
4 | import { useAuthState } from './state';
5 |
6 | export const useSessionManagement = () => {
7 | const apiClient = useContext(ChainlitContext);
8 | const { setUser, setThreadHistory } = useAuthState();
9 |
10 | const logout = async (reload = false): Promise => {
11 | await apiClient.logout();
12 | setUser(undefined);
13 | setThreadHistory(undefined);
14 |
15 | if (reload) {
16 | window.location.reload();
17 | }
18 | };
19 |
20 | return { logout };
21 | };
22 |
--------------------------------------------------------------------------------
/libs/react-client/src/api/hooks/auth/state.ts:
--------------------------------------------------------------------------------
1 | import { useRecoilState, useSetRecoilState } from 'recoil';
2 | import { authState, threadHistoryState, userState } from 'src/state';
3 |
4 | export const useAuthState = () => {
5 | const [authConfig, setAuthConfig] = useRecoilState(authState);
6 | const [user, setUser] = useRecoilState(userState);
7 | const setThreadHistory = useSetRecoilState(threadHistoryState);
8 |
9 | return {
10 | authConfig,
11 | setAuthConfig,
12 | user,
13 | setUser,
14 | setThreadHistory
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/libs/react-client/src/api/hooks/auth/types.ts:
--------------------------------------------------------------------------------
1 | import { IAuthConfig, IUser } from 'src/types';
2 |
3 | export interface JWTPayload extends IUser {
4 | exp: number;
5 | }
6 |
7 | export interface AuthState {
8 | data: IAuthConfig | undefined;
9 | user: IUser | null;
10 | isAuthenticated: boolean;
11 | isReady: boolean;
12 | }
13 |
14 | export interface AuthActions {
15 | logout: (reload?: boolean) => Promise;
16 | setUserFromAPI: () => Promise;
17 | }
18 |
19 | export type IUseAuth = AuthState & AuthActions;
20 |
--------------------------------------------------------------------------------
/libs/react-client/src/api/hooks/auth/userManagement.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { IUser } from 'src/types';
3 |
4 | import { useApi } from '../api';
5 | import { useAuthState } from './state';
6 |
7 | export const useUserManagement = () => {
8 | const { user, setUser } = useAuthState();
9 |
10 | const {
11 | data: userData,
12 | error,
13 | isLoading,
14 | mutate: setUserFromAPI
15 | } = useApi('/user');
16 |
17 | useEffect(() => {
18 | if (userData) {
19 | setUser(userData);
20 | } else if (isLoading) {
21 | setUser(undefined);
22 | }
23 | }, [userData, isLoading, setUser]);
24 |
25 | useEffect(() => {
26 | if (error) {
27 | setUser(null);
28 | }
29 | }, [error]);
30 |
31 | return { user, setUserFromAPI };
32 | };
33 |
--------------------------------------------------------------------------------
/libs/react-client/src/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | import { ChainlitAPI } from './api';
4 |
5 | const defaultChainlitContext = undefined;
6 |
7 | const ChainlitContext = createContext(
8 | new ChainlitAPI('http://localhost:8000', 'webapp')
9 | );
10 |
11 | export { ChainlitContext, defaultChainlitContext };
12 |
--------------------------------------------------------------------------------
/libs/react-client/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useChatData';
2 | export * from './useChatInteract';
3 | export * from './useChatMessages';
4 | export * from './useChatSession';
5 | export * from './useAudio';
6 | export * from './useConfig';
7 | export * from './api';
8 | export * from './types';
9 | export * from './context';
10 | export * from './state';
11 | export * from './utils/message';
12 |
13 | export { Socket } from 'socket.io-client';
14 |
15 | export { WavRenderer } from './wavtools/wav_renderer';
16 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/action.ts:
--------------------------------------------------------------------------------
1 | export interface IAction {
2 | label: string;
3 | forId: string;
4 | id: string;
5 | payload: Record;
6 | name: string;
7 | onClick: () => void;
8 | tooltip: string;
9 | icon?: string;
10 | }
11 |
12 | export interface ICallFn {
13 | callback: (payload: Record) => void;
14 | name: string;
15 | args: Record;
16 | }
17 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/audio.ts:
--------------------------------------------------------------------------------
1 | export interface OutputAudioChunk {
2 | track: string;
3 | mimeType: string;
4 | data: Int16Array;
5 | }
6 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/command.ts:
--------------------------------------------------------------------------------
1 | export interface ICommand {
2 | id: string;
3 | icon: string;
4 | description: string;
5 | button?: boolean;
6 | persistent?: boolean;
7 | }
8 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/feedback.ts:
--------------------------------------------------------------------------------
1 | export interface IFeedback {
2 | id?: string;
3 | forId?: string;
4 | threadId?: string;
5 | comment?: string;
6 | value: number;
7 | }
8 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/file.ts:
--------------------------------------------------------------------------------
1 | import { IAction } from './action';
2 | import { IStep } from './step';
3 |
4 | export interface FileSpec {
5 | accept?: string[] | Record;
6 | max_size_mb?: number;
7 | max_files?: number;
8 | }
9 |
10 | export interface ActionSpec {
11 | keys?: string[];
12 | }
13 |
14 | export interface IFileRef {
15 | id: string;
16 | }
17 |
18 | export interface IAsk {
19 | callback: (payload: IStep | IFileRef[] | IAction) => void;
20 | spec: {
21 | type: 'text' | 'file' | 'action';
22 | step_id: string;
23 | timeout: number;
24 | } & FileSpec &
25 | ActionSpec;
26 | parentId?: string;
27 | }
28 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/history.ts:
--------------------------------------------------------------------------------
1 | import { IThread } from 'src/types';
2 |
3 | import { IPageInfo } from '..';
4 |
5 | export type UserInput = {
6 | content: string;
7 | createdAt: number;
8 | };
9 |
10 | export type ThreadHistory = {
11 | threads?: IThread[];
12 | currentThreadId?: string;
13 | timeGroupedThreads?: { [key: string]: IThread[] };
14 | pageInfo?: IPageInfo;
15 | };
16 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './action';
2 | export * from './element';
3 | export * from './command';
4 | export * from './file';
5 | export * from './feedback';
6 | export * from './step';
7 | export * from './user';
8 | export * from './thread';
9 | export * from './history';
10 | export * from './config';
11 | export * from './mcp';
12 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/mcp.ts:
--------------------------------------------------------------------------------
1 | export interface IMcp {
2 | name: string;
3 | tools: [{ name: string }];
4 | status: 'connected' | 'connecting' | 'failed';
5 | clientType: 'sse' | 'stdio';
6 | command?: string;
7 | url?: string;
8 | }
9 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/step.ts:
--------------------------------------------------------------------------------
1 | import { IFeedback } from './feedback';
2 |
3 | type StepType =
4 | | 'assistant_message'
5 | | 'user_message'
6 | | 'system_message'
7 | | 'run'
8 | | 'tool'
9 | | 'llm'
10 | | 'embedding'
11 | | 'retrieval'
12 | | 'rerank'
13 | | 'undefined';
14 |
15 | export interface IStep {
16 | id: string;
17 | name: string;
18 | type: StepType;
19 | threadId?: string;
20 | parentId?: string;
21 | isError?: boolean;
22 | command?: string;
23 | showInput?: boolean | string;
24 | waitForAnswer?: boolean;
25 | input?: string;
26 | output: string;
27 | createdAt: number | string;
28 | start?: number | string;
29 | end?: number | string;
30 | feedback?: IFeedback;
31 | language?: string;
32 | defaultOpen?: boolean;
33 | streaming?: boolean;
34 | steps?: IStep[];
35 | metadata?: Record;
36 | //legacy
37 | indent?: number;
38 | }
39 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/thread.ts:
--------------------------------------------------------------------------------
1 | import { IElement } from './element';
2 | import { IStep } from './step';
3 |
4 | export interface IThread {
5 | id: string;
6 | createdAt: number | string;
7 | name?: string;
8 | userId?: string;
9 | userIdentifier?: string;
10 | metadata?: Record;
11 | steps: IStep[];
12 | elements?: IElement[];
13 | }
14 |
--------------------------------------------------------------------------------
/libs/react-client/src/types/user.ts:
--------------------------------------------------------------------------------
1 | export type AuthProvider =
2 | | 'credentials'
3 | | 'header'
4 | | 'github'
5 | | 'google'
6 | | 'azure-ad'
7 | | 'azure-ad-hybrid';
8 |
9 | export interface IUserMetadata extends Record {
10 | tags?: string[];
11 | image?: string;
12 | provider?: AuthProvider;
13 | }
14 |
15 | export interface IUser {
16 | id: string;
17 | identifier: string;
18 | display_name?: string;
19 | metadata: IUserMetadata;
20 | }
21 |
--------------------------------------------------------------------------------
/libs/react-client/src/useAudio.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useRecoilState, useRecoilValue } from 'recoil';
3 |
4 | import {
5 | audioConnectionState,
6 | isAiSpeakingState,
7 | wavRecorderState,
8 | wavStreamPlayerState
9 | } from './state';
10 | import { useChatInteract } from './useChatInteract';
11 |
12 | const useAudio = () => {
13 | const [audioConnection, setAudioConnection] =
14 | useRecoilState(audioConnectionState);
15 | const wavRecorder = useRecoilValue(wavRecorderState);
16 | const wavStreamPlayer = useRecoilValue(wavStreamPlayerState);
17 | const isAiSpeaking = useRecoilValue(isAiSpeakingState);
18 |
19 | const { startAudioStream, endAudioStream } = useChatInteract();
20 |
21 | const startConversation = useCallback(async () => {
22 | setAudioConnection('connecting');
23 | await startAudioStream();
24 | }, [startAudioStream]);
25 |
26 | const endConversation = useCallback(async () => {
27 | setAudioConnection('off');
28 | await wavRecorder.end();
29 | await wavStreamPlayer.interrupt();
30 | await endAudioStream();
31 | }, [endAudioStream, wavRecorder, wavStreamPlayer]);
32 |
33 | return {
34 | startConversation,
35 | endConversation,
36 | audioConnection,
37 | isAiSpeaking,
38 | wavRecorder,
39 | wavStreamPlayer
40 | };
41 | };
42 |
43 | export { useAudio };
44 |
--------------------------------------------------------------------------------
/libs/react-client/src/useChatMessages.ts:
--------------------------------------------------------------------------------
1 | import { useRecoilValue } from 'recoil';
2 |
3 | import {
4 | currentThreadIdState,
5 | firstUserInteraction,
6 | messagesState
7 | } from './state';
8 |
9 | const useChatMessages = () => {
10 | const messages = useRecoilValue(messagesState);
11 | const firstInteraction = useRecoilValue(firstUserInteraction);
12 | const threadId = useRecoilValue(currentThreadIdState);
13 |
14 | return {
15 | threadId,
16 | messages,
17 | firstInteraction
18 | };
19 | };
20 |
21 | export { useChatMessages };
22 |
--------------------------------------------------------------------------------
/libs/react-client/src/useConfig.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useRecoilState } from 'recoil';
3 |
4 | import { useApi, useAuth } from './api';
5 | import { configState } from './state';
6 | import { IChainlitConfig } from './types';
7 |
8 | const useConfig = () => {
9 | const [config, setConfig] = useRecoilState(configState);
10 | const { isAuthenticated } = useAuth();
11 | const language = navigator.language || 'en-US';
12 |
13 | const { data, error, isLoading } = useApi(
14 | !config && isAuthenticated ? `/project/settings?language=${language}` : null
15 | );
16 |
17 | useEffect(() => {
18 | if (!data) return;
19 | setConfig(data);
20 | }, [data, setConfig]);
21 |
22 | return { config, error, isLoading, language };
23 | };
24 |
25 | export { useConfig };
26 |
--------------------------------------------------------------------------------
/libs/react-client/src/utils/group.ts:
--------------------------------------------------------------------------------
1 | import { IThread } from 'src/types';
2 |
3 | export const groupByDate = (data: IThread[]) => {
4 | const groupedData: { [key: string]: IThread[] } = {};
5 |
6 | const today = new Date();
7 | today.setHours(0, 0, 0, 0);
8 |
9 | [...data]
10 | .sort(
11 | (a, b) =>
12 | new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
13 | )
14 | .forEach((item) => {
15 | const threadDate = new Date(item.createdAt);
16 | threadDate.setHours(0, 0, 0, 0);
17 |
18 | const daysDiff = Math.floor(
19 | (today.getTime() - threadDate.getTime()) / 86400000
20 | );
21 |
22 | let category: string;
23 | if (daysDiff === 0) {
24 | category = 'Today';
25 | } else if (daysDiff === 1) {
26 | category = 'Yesterday';
27 | } else if (daysDiff <= 7) {
28 | category = 'Previous 7 days';
29 | } else if (daysDiff <= 30) {
30 | category = 'Previous 30 days';
31 | } else {
32 | category = threadDate.toLocaleString('default', {
33 | month: 'long',
34 | year: 'numeric'
35 | });
36 | }
37 |
38 | groupedData[category] ??= [];
39 | groupedData[category].push(item);
40 | });
41 |
42 | return groupedData;
43 | };
44 |
--------------------------------------------------------------------------------
/libs/react-client/src/wavtools/index.ts:
--------------------------------------------------------------------------------
1 | // Courtesy of https://github.com/openai/openai-realtime-console
2 | import { AudioAnalysis } from './analysis/audio_analysis.js';
3 | import { WavPacker } from './wav_packer.js';
4 | import { WavRecorder } from './wav_recorder.js';
5 | import { WavStreamPlayer } from './wav_stream_player.js';
6 |
7 | export { AudioAnalysis, WavPacker, WavStreamPlayer, WavRecorder };
8 |
--------------------------------------------------------------------------------
/libs/react-client/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | // This is for tsup build since it's not supporting composite
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "composite": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/libs/react-client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // THIS MUST BE AT ROOT, if you set baseurl in sub-package it breaks intellisense jump to
4 | "composite": true,
5 | "baseUrl": ".",
6 | "rootDir": ".",
7 | "outDir": "dist",
8 | "importHelpers": true,
9 | "allowJs": false,
10 | "allowSyntheticDefaultImports": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "declaration": true,
13 | "downlevelIteration": true,
14 | "strict": true,
15 | "esModuleInterop": true,
16 | "jsx": "react-jsx",
17 | "module": "system",
18 | "moduleResolution": "node",
19 | "noEmitOnError": false,
20 | "noImplicitAny": false,
21 | "noImplicitReturns": false,
22 | "noUnusedLocals": false,
23 | "noUnusedParameters": false,
24 | "preserveConstEnums": true,
25 | "removeComments": true,
26 | "skipLibCheck": true,
27 | "sourceMap": true,
28 | "strictNullChecks": true,
29 | "target": "es5",
30 | "types": ["node", "react"],
31 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
32 | "paths": {
33 | "src/*": ["./src/*"]
34 | }
35 | },
36 | "exclude": ["**/test", "**/dist", "**/__tests__"],
37 | "include": ["src/**/*"],
38 | "types": ["@testing-library/jest-dom", "node"]
39 | }
40 |
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | module.exports = {
3 | '**/*.{js,jsx,ts,tsx}': ['npx prettier --write', 'npx eslint --fix'],
4 | '**/*.{ts,tsx}': [() => 'tsc --skipLibCheck --noEmit'],
5 | '**/*.py': [
6 | 'poetry run -C backend ruff check --fix',
7 | 'poetry run -C backend ruff format',
8 | () => 'pnpm run lintPython'
9 | ],
10 | '.github/{workflows,actions}/**': ['actionlint']
11 | };
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
4 | "@types/react": "^18.2.15",
5 | "@types/react-dom": "^18.2.7",
6 | "@typescript-eslint/eslint-plugin": "^8.15.0",
7 | "@typescript-eslint/parser": "^8.15.0",
8 | "cypress": "12.9.0",
9 | "dotenv": "^16.3.1",
10 | "eslint": "^8.57.1",
11 | "husky": "^9.1.6",
12 | "kill-port": "^2.0.1",
13 | "lint-staged": "^13.3.0",
14 | "prettier": "^2.8.8",
15 | "shell-exec": "^1.1.2",
16 | "ts-node": "^10.9.1",
17 | "typescript": "^5.2.2"
18 | },
19 | "scripts": {
20 | "preinstall": "npx only-allow pnpm",
21 | "test": "pnpm exec ts-node ./cypress/support/e2e.ts",
22 | "test:ui": "cd frontend && pnpm test",
23 | "prepare": "husky",
24 | "lint": "pnpm run lintUi && pnpm run lintPython",
25 | "lintUi": "pnpm run --parallel lint",
26 | "formatUi": "pnpm run --parallel format",
27 | "lintPython": "cd backend && poetry run dmypy run --timeout 600 -- chainlit/ tests/",
28 | "formatPython": "black `git ls-files | grep '.py$'` && isort --profile=black .",
29 | "buildUi": "cd libs/react-client && pnpm run build && cd ../copilot && pnpm run build && cd ../../frontend && pnpm run build"
30 | },
31 | "pnpm": {
32 | "overrides": {
33 | "@cypress/request@<=2.88.12": ">=3.0.0",
34 | "braces@<3.0.3": ">=3.0.3",
35 | "micromatch@<4.0.8": ">=4.0.8"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - frontend/
3 | - libs/react-components/
4 | - libs/react-client/
5 | - libs/copilot/
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext", "dom"],
4 | "types": ["cypress", "node"]
5 | },
6 | "include": ["cypress/**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------