├── .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 |
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 |
37 | {msg} 38 |
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 | logo 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 |
43 | 44 |
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 |
14 | 15 |
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 |
14 | 15 |
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 |
21 | 22 |
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 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Github.tsx: -------------------------------------------------------------------------------- 1 | export const GitHub = () => { 2 | return ( 3 | 11 | 16 | 17 | ); 18 | }; 19 | 20 | export default GitHub; 21 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Gitlab.tsx: -------------------------------------------------------------------------------- 1 | export const Gitlab = () => { 2 | return ( 3 | 9 | 13 | 17 | 21 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Google.tsx: -------------------------------------------------------------------------------- 1 | export const Google = () => { 2 | return ( 3 | 9 | 13 | 17 | 21 | 25 | 26 | 27 | ); 28 | }; 29 | export default Google; 30 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Microsoft.tsx: -------------------------------------------------------------------------------- 1 | export const Microsoft = () => { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | export default Microsoft; 13 | -------------------------------------------------------------------------------- /frontend/src/components/icons/PaperClip.tsx: -------------------------------------------------------------------------------- 1 | export const PaperClip = ({ className }: { className?: string }) => { 2 | return ( 3 | 11 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Pencil.tsx: -------------------------------------------------------------------------------- 1 | export const Pencil = ({ className }: { className?: string }) => { 2 | return ( 3 | 11 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Search.tsx: -------------------------------------------------------------------------------- 1 | export const Search = ({ className }: { className?: string }) => { 2 | return ( 3 | 11 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Send.tsx: -------------------------------------------------------------------------------- 1 | export const Send = ({ className }: { className?: string }) => { 2 | return ( 3 | 11 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Stop.tsx: -------------------------------------------------------------------------------- 1 | export const Stop = ({ className }: { className?: string }) => { 2 | return ( 3 | 11 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/components/icons/VoiceLines.tsx: -------------------------------------------------------------------------------- 1 | export const VoiceLines = ({ className }: { className?: string }) => { 2 | return ( 3 | 11 | 15 | 19 | 23 | 27 | 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 |