From 651268f756ad46325a34ea14062a8a1472ff3dde Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Tue, 30 Jun 2026 21:27:30 -0700 Subject: [PATCH 01/10] feat(ui): restore Chat page behind admin-gated enable_chat_ui flag Reintroduces the /ui/chat page and its components (removed in #30178 as unreachable dead code) now that it has a real entry point: an enable_chat_ui UI setting, off by default, that shows a Chat nav item in the sidebar when turned on. The page lets users chat with an LLM and connect their own MCP server credentials via OAuth (used automatically for their subsequent tool calls) rather than a shared credential. (cherry picked from commit ec6d71b76a96b60c67940200e9299c52b65fbc60) --- .../proxy_setting_endpoints.py | 6 + ui/litellm-dashboard/eslint-metrics.json | 2 +- ui/litellm-dashboard/eslint-suppressions.json | 932 +++++----- .../components/SidebarProvider.tsx | 6 + ui/litellm-dashboard/src/app/chat/page.tsx | 27 + .../AdminSettings/UISettings/UISettings.tsx | 33 + .../src/components/chat/ChatMessages.tsx | 590 +++++++ .../src/components/chat/ChatPage.tsx | 1512 +++++++++++++++++ .../src/components/chat/ConversationList.tsx | 450 +++++ .../src/components/chat/MCPAppsPanel.tsx | 726 ++++++++ .../src/components/chat/MCPConnectPicker.tsx | 171 ++ .../src/components/chat/MCPCredentialsTab.tsx | 166 ++ .../src/components/chat/types.ts | 25 + .../src/components/chat/useChatHistory.ts | 213 +++ .../src/components/leftnav.test.tsx | 10 + .../src/components/leftnav.tsx | 16 + .../src/components/page_metadata.ts | 1 + .../src/utils/migratedPages.ts | 1 + 18 files changed, 4441 insertions(+), 446 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/chat/page.tsx create mode 100644 ui/litellm-dashboard/src/components/chat/ChatMessages.tsx create mode 100644 ui/litellm-dashboard/src/components/chat/ChatPage.tsx create mode 100644 ui/litellm-dashboard/src/components/chat/ConversationList.tsx create mode 100644 ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx create mode 100644 ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx create mode 100644 ui/litellm-dashboard/src/components/chat/MCPCredentialsTab.tsx create mode 100644 ui/litellm-dashboard/src/components/chat/types.ts create mode 100644 ui/litellm-dashboard/src/components/chat/useChatHistory.ts diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index e4f68a1e9dbb..97a1e280b34b 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -176,6 +176,11 @@ class UISettings(BaseModel): description="If true, org admins cannot generate API keys via /key/generate.", ) + enable_chat_ui: bool = Field( + default=False, + description="If true, shows the Chat page in the UI sidebar, letting users chat with an LLM and connect their own MCP server credentials via OAuth.", + ) + class UISettingsResponse(SettingsResponse): """Response model for UI settings""" @@ -199,6 +204,7 @@ class UISettingsResponse(SettingsResponse): "scope_user_search_to_org", "disable_custom_api_keys", "disable_key_generate_for_org_admin", + "enable_chat_ui", } # Flags that must be synced from the persisted UISettings into diff --git a/ui/litellm-dashboard/eslint-metrics.json b/ui/litellm-dashboard/eslint-metrics.json index 09ad247391b9..8ba28795c6ff 100644 --- a/ui/litellm-dashboard/eslint-metrics.json +++ b/ui/litellm-dashboard/eslint-metrics.json @@ -1,5 +1,5 @@ { "@typescript-eslint/no-explicit-any": 2014, - "complexity": 126, + "complexity": 128, "max-depth": 61 } diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 7a3c8f4a42c0..181ad324894b 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -4,6 +4,155 @@ "count": 1 } }, + "src/app/(dashboard)/budgets/components/budget_modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/budgets/components/budget_panel.test.tsx": { + "unused-imports/no-unused-imports": { + "count": 2 + } + }, + "src/app/(dashboard)/budgets/components/budget_panel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/budgets/components/edit_budget_modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/caching/components/cache_dashboard.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/app/(dashboard)/caching/components/cache_health.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/caching/components/cache_settings/CacheFieldRenderer.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/caching/components/cache_settings/RedisTypeSelector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/caching/components/cache_settings/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/add_margin_form.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/add_provider_form.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/cost_tracking_settings.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/how_it_works.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/pricing_calculator/multi_cost_results.test.tsx": { + "unused-imports/no-unused-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/pricing_calculator/multi_cost_results.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/pricing_calculator/multi_export_dropdown.test.tsx": { + "unused-imports/no-unused-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/pricing_calculator/multi_export_dropdown.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/pricing_calculator/use_multi_cost_estimate.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/provider_discount_table.test.tsx": { + "unused-imports/no-unused-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/provider_discount_table.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/provider_display_helpers.test.ts": { + "unused-imports/no-unused-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/provider_margin_table.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/cost-tracking/components/use_discount_config.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "src/app/(dashboard)/cost-tracking/components/use_margin_config.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "src/app/(dashboard)/guardrails-monitor/components/EvaluationSettingsModal.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails-monitor/components/GuardrailsMonitorView.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails-monitor/components/ScoreChart.test.tsx": { + "react/display-name": { + "count": 1 + } + }, + "src/app/(dashboard)/guardrails-monitor/components/ScoreChart.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, "src/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails.ts": { "no-restricted-syntax": { "count": 1 @@ -144,68 +293,348 @@ "count": 2 } }, - "src/app/(dashboard)/hooks/router/useRouterFields.ts": { - "no-restricted-syntax": { + "src/app/(dashboard)/hooks/router/useRouterFields.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "src/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "src/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "src/app/(dashboard)/hooks/teams/useTeams.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "src/app/(dashboard)/memory/components/MemoryView.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/preserve-manual-memoization": { + "count": 4 + } + }, + "src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx": { + "max-params": { + "count": 1 + }, + "unused-imports/no-unused-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 3 + } + }, + "src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.test.tsx": { + "react/display-name": { + "count": 1 + } + }, + "src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/app/(dashboard)/playground/components/chat_ui/AgentBuilderView.tsx": { + "react-hooks/set-state-in-effect": { + "count": 5 + } + }, + "src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.test.tsx": { + "max-nested-callbacks": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 4 + }, + "unused-imports/no-unused-imports": { + "count": 13 + } + }, + "src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx": { + "no-restricted-syntax": { + "count": 2 + } + }, + "src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterTool.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx": { + "react-hooks/immutability": { + "count": 2 + }, + "react-hooks/preserve-manual-memoization": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx": { + "react-hooks/preserve-manual-memoization": { + "count": 3 + } + }, + "src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx": { + "max-params": { + "count": 2 + }, + "no-restricted-syntax": { + "count": 2 + } + }, + "src/app/(dashboard)/playground/llm_calls/anthropic_messages.tsx": { + "max-params": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/llm_calls/audio_speech.tsx": { + "max-params": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/llm_calls/audio_transcriptions.tsx": { + "max-params": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx": { + "max-params": { + "count": 1 + }, + "no-restricted-syntax": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/llm_calls/fetch_agents.tsx": { + "no-restricted-syntax": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/llm_calls/image_edits.tsx": { + "max-params": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/llm_calls/image_generation.tsx": { + "max-params": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/llm_calls/interactions_api.tsx": { + "max-params": { + "count": 1 + }, + "no-restricted-syntax": { + "count": 1 + } + }, + "src/app/(dashboard)/playground/page.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/projects/components/ProjectDetailsPage.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/projects/components/ProjectKeysSection.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/projects/components/ProjectModals/ProjectBaseForm.tsx": { + "react-hooks/set-state-in-effect": { + "count": 2 + } + }, + "src/app/(dashboard)/projects/components/ProjectsPage.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/add_prompt_form.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/DeveloperMessageCard.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/ModelConfigCard.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/PromptCodeSnippets.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/PromptEditorHeader.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/PromptMessagesCard.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/PublishModal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/ToolsCard.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/VersionHistorySidePanel.test.tsx": { + "max-nested-callbacks": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/VersionHistorySidePanel.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/app/(dashboard)/prompts/components/prompt_editor_view/conversation_panel/MessageInput.tsx": { + "no-restricted-imports": { "count": 1 } }, - "src/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB.ts": { - "no-restricted-syntax": { + "src/app/(dashboard)/prompts/components/prompt_editor_view/conversation_panel/index.tsx": { + "no-restricted-imports": { "count": 1 } }, - "src/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs.ts": { + "src/app/(dashboard)/prompts/components/prompt_editor_view/conversation_panel/useConversation.ts": { "no-restricted-syntax": { "count": 1 } }, - "src/app/(dashboard)/hooks/teams/useTeams.ts": { - "no-restricted-syntax": { + "src/app/(dashboard)/prompts/components/prompt_info.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { "count": 2 } }, - "src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx": { + "src/app/(dashboard)/prompts/components/prompt_table.tsx": { "no-restricted-imports": { "count": 1 - }, - "react-hooks/preserve-manual-memoization": { - "count": 4 } }, - "src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx": { - "max-params": { + "src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx": { + "no-restricted-imports": { "count": 1 }, - "unused-imports/no-unused-imports": { + "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx": { + "src/app/(dashboard)/search-tools/_components/SearchToolTester.tsx": { "no-restricted-imports": { "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 3 } }, - "src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.test.tsx": { - "react/display-name": { + "src/app/(dashboard)/search-tools/_components/SearchToolView.tsx": { + "no-restricted-imports": { "count": 1 } }, - "src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.tsx": { + "src/app/(dashboard)/search-tools/_components/SearchTools.tsx": { "no-restricted-imports": { "count": 1 + }, + "react-hooks/static-components": { + "count": 1 } }, - "src/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab.tsx": { + "src/app/(dashboard)/transform-request/TransformRequestPanel.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/app/(dashboard)/playground/page.tsx": { + "src/app/(dashboard)/ui-theme/UIThemeSettings.tsx": { "no-restricted-imports": { "count": 1 + }, + "no-restricted-syntax": { + "count": 3 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/app/(dashboard)/workflows/WorkflowRuns.tsx": { + "no-restricted-syntax": { + "count": 3 + }, + "react-hooks/set-state-in-effect": { + "count": 1 } }, "src/app/login/LoginPage.tsx": { @@ -293,81 +722,6 @@ "count": 1 } }, - "src/app/(dashboard)/cost-tracking/components/add_margin_form.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/add_provider_form.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/cost_tracking_settings.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/how_it_works.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/pricing_calculator/multi_cost_results.test.tsx": { - "unused-imports/no-unused-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/pricing_calculator/multi_cost_results.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/pricing_calculator/multi_export_dropdown.test.tsx": { - "unused-imports/no-unused-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/pricing_calculator/multi_export_dropdown.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/pricing_calculator/use_multi_cost_estimate.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/provider_discount_table.test.tsx": { - "unused-imports/no-unused-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/provider_discount_table.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/provider_display_helpers.test.ts": { - "unused-imports/no-unused-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/provider_margin_table.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/cost-tracking/components/use_discount_config.ts": { - "no-restricted-syntax": { - "count": 2 - } - }, - "src/app/(dashboard)/cost-tracking/components/use_margin_config.ts": { - "no-restricted-syntax": { - "count": 2 - } - }, "src/components/CreateUserButton.tsx": { "no-restricted-imports": { "count": 1 @@ -419,36 +773,11 @@ "count": 1 } }, - "src/app/(dashboard)/guardrails-monitor/components/EvaluationSettingsModal.tsx": { - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails-monitor/components/GuardrailsMonitorView.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails-monitor/components/ScoreChart.test.tsx": { - "react/display-name": { - "count": 1 - } - }, - "src/app/(dashboard)/guardrails-monitor/components/ScoreChart.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/HelpLink.test.tsx": { "unused-imports/no-unused-imports": { "count": 1 } }, - "src/app/(dashboard)/memory/components/MemoryView.tsx": { - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/Navbar/BlogDropdown/BlogDropdown.test.tsx": { "max-nested-callbacks": { "count": 12 @@ -472,26 +801,6 @@ "count": 4 } }, - "src/app/(dashboard)/projects/components/ProjectDetailsPage.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/projects/components/ProjectKeysSection.tsx": { - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/projects/components/ProjectModals/ProjectBaseForm.tsx": { - "react-hooks/set-state-in-effect": { - "count": 2 - } - }, - "src/app/(dashboard)/projects/components/ProjectsPage.tsx": { - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/SCIM.tsx": { "no-restricted-imports": { "count": 1 @@ -505,32 +814,6 @@ "count": 1 } }, - "src/app/(dashboard)/search-tools/_components/CreateSearchTools.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/search-tools/_components/SearchToolTester.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/search-tools/_components/SearchToolView.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/search-tools/_components/SearchTools.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/static-components": { - "count": 1 - } - }, "src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx": { "react-hooks/set-state-in-effect": { "count": 1 @@ -750,7 +1033,7 @@ "count": 1 }, "react-hooks/set-state-in-effect": { - "count": 2 + "count": 1 } }, "src/components/agents/add_agent_form.tsx": { @@ -790,26 +1073,6 @@ "count": 1 } }, - "src/app/(dashboard)/budgets/components/budget_modal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/budgets/components/budget_panel.test.tsx": { - "unused-imports/no-unused-imports": { - "count": 2 - } - }, - "src/app/(dashboard)/budgets/components/budget_panel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/budgets/components/edit_budget_modal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/bulk_create_users_button.tsx": { "no-restricted-imports": { "count": 1 @@ -818,33 +1081,36 @@ "count": 1 } }, - "src/app/(dashboard)/caching/components/cache_dashboard.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/purity": { + "src/components/chat/ChatMessages.tsx": { + "react-hooks/refs": { "count": 1 + } + }, + "src/components/chat/ChatPage.tsx": { + "max-params": { + "count": 2 }, "react-hooks/set-state-in-effect": { "count": 2 - } - }, - "src/app/(dashboard)/caching/components/cache_health.tsx": { - "no-restricted-imports": { + }, + "unused-imports/no-unused-imports": { "count": 1 } }, - "src/app/(dashboard)/caching/components/cache_settings/CacheFieldRenderer.tsx": { - "no-restricted-imports": { + "src/components/chat/ConversationList.tsx": { + "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/app/(dashboard)/caching/components/cache_settings/RedisTypeSelector.tsx": { - "no-restricted-imports": { - "count": 1 + "src/components/chat/MCPAppsPanel.tsx": { + "max-nested-callbacks": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 2 } }, - "src/app/(dashboard)/caching/components/cache_settings/index.tsx": { + "src/components/chat/MCPCredentialsTab.tsx": { "no-restricted-imports": { "count": 1 }, @@ -852,6 +1118,11 @@ "count": 1 } }, + "src/components/chat/useChatHistory.ts": { + "react-hooks/set-state-in-effect": { + "count": 3 + } + }, "src/components/claude_code_plugins.tsx": { "no-restricted-imports": { "count": 1 @@ -1138,6 +1409,16 @@ "count": 1 } }, + "src/components/llm_calls/chat_completion.tsx": { + "max-params": { + "count": 1 + } + }, + "src/components/llm_calls/responses_api.tsx": { + "max-params": { + "count": 1 + } + }, "src/components/mcp_hub_table_columns.tsx": { "no-restricted-imports": { "count": 1 @@ -1464,132 +1745,6 @@ "count": 1 } }, - "src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 2 - } - }, - "src/app/(dashboard)/playground/components/chat_ui/AgentBuilderView.tsx": { - "react-hooks/set-state-in-effect": { - "count": 5 - } - }, - "src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.test.tsx": { - "max-nested-callbacks": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 4 - }, - "unused-imports/no-unused-imports": { - "count": 13 - } - }, - "src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx": { - "no-restricted-syntax": { - "count": 2 - } - }, - "src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterTool.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx": { - "react-hooks/immutability": { - "count": 2 - }, - "react-hooks/preserve-manual-memoization": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx": { - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx": { - "react-hooks/preserve-manual-memoization": { - "count": 3 - } - }, - "src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx": { - "max-params": { - "count": 2 - }, - "no-restricted-syntax": { - "count": 2 - } - }, - "src/app/(dashboard)/playground/llm_calls/anthropic_messages.tsx": { - "max-params": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/llm_calls/audio_speech.tsx": { - "max-params": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/llm_calls/audio_transcriptions.tsx": { - "max-params": { - "count": 1 - } - }, - "src/components/llm_calls/chat_completion.tsx": { - "max-params": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx": { - "max-params": { - "count": 1 - }, - "no-restricted-syntax": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/llm_calls/fetch_agents.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/llm_calls/image_edits.tsx": { - "max-params": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/llm_calls/image_generation.tsx": { - "max-params": { - "count": 1 - } - }, - "src/app/(dashboard)/playground/llm_calls/interactions_api.tsx": { - "max-params": { - "count": 1 - }, - "no-restricted-syntax": { - "count": 1 - } - }, - "src/components/llm_calls/responses_api.tsx": { - "max-params": { - "count": 1 - } - }, "src/components/policies/add_attachment_form.tsx": { "no-restricted-imports": { "count": 1 @@ -1705,87 +1860,6 @@ "count": 2 } }, - "src/app/(dashboard)/prompts/components/add_prompt_form.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/DeveloperMessageCard.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/ModelConfigCard.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/PromptCodeSnippets.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/PromptEditorHeader.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/PromptMessagesCard.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/PublishModal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/ToolsCard.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/VersionHistorySidePanel.test.tsx": { - "max-nested-callbacks": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/VersionHistorySidePanel.tsx": { - "react-hooks/immutability": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/conversation_panel/MessageInput.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/conversation_panel/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_editor_view/conversation_panel/useConversation.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, - "src/app/(dashboard)/prompts/components/prompt_info.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 2 - } - }, - "src/app/(dashboard)/prompts/components/prompt_table.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/public_model_hub.tsx": { "no-restricted-imports": { "count": 1 @@ -1914,22 +1988,6 @@ "count": 1 } }, - "src/app/(dashboard)/transform-request/TransformRequestPanel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/ui-theme/UIThemeSettings.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "no-restricted-syntax": { - "count": 3 - }, - "react-hooks/immutability": { - "count": 1 - } - }, "src/components/usage.tsx": { "no-restricted-imports": { "count": 2 @@ -2076,14 +2134,6 @@ "count": 1 } }, - "src/app/(dashboard)/workflows/WorkflowRuns.tsx": { - "no-restricted-syntax": { - "count": 3 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/contexts/AuthContext.tsx": { "react-hooks/set-state-in-effect": { "count": 1 @@ -2162,13 +2212,5 @@ "react/display-name": { "count": 1 } - }, - "src/app/(dashboard)/prompts/components/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } } -} +} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx index 1e091314ecdc..4629def86f35 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx @@ -15,6 +15,7 @@ const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: Side const { accessToken } = useAuthorized(); const [enabledPagesInternalUsers, setEnabledPagesInternalUsers] = useState(null); const [enableProjectsUI, setEnableProjectsUI] = useState(false); + const [enableChatUI, setEnableChatUI] = useState(false); const [disableAgentsForInternalUsers, setDisableAgentsForInternalUsers] = useState(false); const [allowAgentsForTeamAdmins, setAllowAgentsForTeamAdmins] = useState(false); const [disableVectorStoresForInternalUsers, setDisableVectorStoresForInternalUsers] = useState(false); @@ -46,6 +47,10 @@ const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: Side setEnableProjectsUI(Boolean(settings.values.enable_projects_ui)); } + if (settings?.values?.enable_chat_ui !== undefined) { + setEnableChatUI(Boolean(settings.values.enable_chat_ui)); + } + if (settings?.values?.disable_agents_for_internal_users !== undefined) { setDisableAgentsForInternalUsers(Boolean(settings.values.disable_agents_for_internal_users)); } @@ -76,6 +81,7 @@ const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: Side collapsed={sidebarCollapsed} enabledPagesInternalUsers={enabledPagesInternalUsers} enableProjectsUI={enableProjectsUI} + enableChatUI={enableChatUI} disableAgentsForInternalUsers={disableAgentsForInternalUsers} allowAgentsForTeamAdmins={allowAgentsForTeamAdmins} disableVectorStoresForInternalUsers={disableVectorStoresForInternalUsers} diff --git a/ui/litellm-dashboard/src/app/chat/page.tsx b/ui/litellm-dashboard/src/app/chat/page.tsx new file mode 100644 index 000000000000..5046f1628778 --- /dev/null +++ b/ui/litellm-dashboard/src/app/chat/page.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Suspense } from "react"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import ChatPage from "@/components/chat/ChatPage"; + +// ChatPage uses useSearchParams() which requires a Suspense boundary for static export. +const ChatPageContent = () => { + const { accessToken, userRole, userId, userEmail } = useAuthorized(); + + return ( + + ); +}; + +const ChatPageRoute = () => ( + + + +); + +export default ChatPageRoute; diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx index 9ce0d9088382..ec970c348738 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx @@ -19,6 +19,7 @@ export default function UISettings() { const forwardClientHeadersProperty = schema?.properties?.forward_client_headers_to_llm_api; const forwardLLMProviderAuthHeadersProperty = schema?.properties?.forward_llm_provider_auth_headers; const enableProjectsUIProperty = schema?.properties?.enable_projects_ui; + const enableChatUIProperty = schema?.properties?.enable_chat_ui; const enabledPagesProperty = schema?.properties?.enabled_ui_pages_internal_users; const disableAgentsProperty = schema?.properties?.disable_agents_for_internal_users; const allowAgentsTeamAdminsProperty = schema?.properties?.allow_agents_for_team_admins; @@ -114,6 +115,21 @@ export default function UISettings() { ); }; + const handleToggleEnableChatUI = (checked: boolean) => { + updateSettings( + { enable_chat_ui: checked }, + { + onSuccess: () => { + NotificationManager.success("UI settings updated successfully. Refreshing page..."); + setTimeout(() => window.location.reload(), 1000); + }, + onError: (error) => { + NotificationManager.fromBackend(error); + }, + }, + ); + }; + const handleToggleRequireAuthForPublicAIHub = (checked: boolean) => { updateSettings( { require_auth_for_public_ai_hub: checked }, @@ -335,6 +351,23 @@ export default function UISettings() { )} + + + + [BETA] Enable Chat page (page will refresh) + + {enableChatUIProperty?.description ?? + "If enabled, shows the Chat page in the UI sidebar, letting users chat with an LLM and connect their own MCP server credentials via OAuth."} + + + + {/* Agents access control */} diff --git a/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx b/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx new file mode 100644 index 000000000000..53877be1737b --- /dev/null +++ b/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx @@ -0,0 +1,590 @@ +"use client"; + +import { ToolOutlined, CopyOutlined, CheckOutlined, EditOutlined } from "@ant-design/icons"; +import { Collapse, Tooltip } from "antd"; +import React, { useEffect, useRef, useState } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; +import ReasoningContent from "@/components/chat_ui/ReasoningContent"; +import MCPEventsDisplay from "@/components/chat_ui/MCPEventsDisplay"; +import { ChatMessage } from "./types"; + +const { Panel } = Collapse; + +// Keys whose values must be redacted in tool args display +const REDACTED_KEY_PATTERNS = /token|key|secret|password|auth/i; + +function redactSensitiveValues(obj: Record): Record { + const result: Record = {}; + for (const [k, v] of Object.entries(obj)) { + if (REDACTED_KEY_PATTERNS.test(k)) { + result[k] = "[redacted]"; + } else if (Array.isArray(v)) { + result[k] = v.map((item) => + item !== null && typeof item === "object" && !Array.isArray(item) + ? redactSensitiveValues(item as Record) + : item, + ); + } else if (v !== null && typeof v === "object") { + result[k] = redactSensitiveValues(v as Record); + } else { + result[k] = v; + } + } + return result; +} + +function formatTimestamp(ts: number): string { + const d = new Date(ts); + const hh = String(d.getHours()).padStart(2, "0"); + const mm = String(d.getMinutes()).padStart(2, "0"); + return `${hh}:${mm}`; +} + +// Shared markdown code renderer matching ReasoningContent style. +// react-markdown v9 removed the `inline` prop; detect fenced blocks via language className. +function MarkdownCodeRenderer({ + node, + className, + children, + ...props +}: React.ComponentPropsWithoutRef<"code"> & { node?: unknown }) { + const match = /language-(\w+)/.exec(className || ""); + return match ? ( + } + language={match[1]} + PreTag="div" + className="rounded-md my-2" + {...(props as Record)} + > + {String(children).replace(/\n$/, "")} + + ) : ( + + {children} + + ); +} + +// ------- Sub-components ------- + +interface UserBubbleProps { + message: ChatMessage; + onEdit?: (messageId: string, newContent: string) => void; + isStreaming?: boolean; +} + +function UserBubble({ message, onEdit, isStreaming }: UserBubbleProps) { + const [hovered, setHovered] = useState(false); + const [editing, setEditing] = useState(false); + const [editValue, setEditValue] = useState(message.content); + const textareaRef = useRef(null); + + useEffect(() => { + if (editing && textareaRef.current) { + textareaRef.current.focus(); + textareaRef.current.selectionStart = textareaRef.current.value.length; + } + }, [editing]); + + // Auto-resize textarea + useEffect(() => { + const ta = textareaRef.current; + if (!ta) return; + ta.style.height = "auto"; + ta.style.height = `${ta.scrollHeight}px`; + }, [editValue, editing]); + + const handleSave = () => { + const trimmed = editValue.trim(); + if (trimmed && trimmed !== message.content && onEdit) { + onEdit(message.id, trimmed); + } + setEditing(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSave(); + } + if (e.key === "Escape") { + setEditValue(message.content); + setEditing(false); + } + }; + + if (editing) { + return ( +
+
+