@data-club/ai-hub 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/passwordPolicy.d.ts +55 -0
- package/dist/auth/passwordPolicy.d.ts.map +1 -0
- package/dist/auth/passwordPolicy.js +63 -0
- package/dist/auth/passwordPolicy.js.map +1 -0
- package/dist/auth/texts.d.ts +815 -0
- package/dist/auth/texts.d.ts.map +1 -0
- package/dist/auth/texts.js +429 -0
- package/dist/auth/texts.js.map +1 -0
- package/dist/client/AIHubClient.d.ts +1058 -0
- package/dist/client/AIHubClient.d.ts.map +1 -0
- package/dist/client/AIHubClient.js +950 -0
- package/dist/client/AIHubClient.js.map +1 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +9 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/sseParser.d.ts +41 -0
- package/dist/client/sseParser.d.ts.map +1 -0
- package/dist/client/sseParser.js +153 -0
- package/dist/client/sseParser.js.map +1 -0
- package/dist/client/tokenStorage.d.ts +56 -0
- package/dist/client/tokenStorage.d.ts.map +1 -0
- package/dist/client/tokenStorage.js +116 -0
- package/dist/client/tokenStorage.js.map +1 -0
- package/dist/components/AgentsPicker/index.d.ts +70 -0
- package/dist/components/AgentsPicker/index.d.ts.map +1 -0
- package/dist/components/AgentsPicker/index.js +63 -0
- package/dist/components/AgentsPicker/index.js.map +1 -0
- package/dist/components/AuthGate/index.d.ts +79 -0
- package/dist/components/AuthGate/index.d.ts.map +1 -0
- package/dist/components/AuthGate/index.js +61 -0
- package/dist/components/AuthGate/index.js.map +1 -0
- package/dist/components/CacheBadge/index.d.ts +88 -0
- package/dist/components/CacheBadge/index.d.ts.map +1 -0
- package/dist/components/CacheBadge/index.js +127 -0
- package/dist/components/CacheBadge/index.js.map +1 -0
- package/dist/components/ChangePassword/index.d.ts +109 -0
- package/dist/components/ChangePassword/index.d.ts.map +1 -0
- package/dist/components/ChangePassword/index.js +142 -0
- package/dist/components/ChangePassword/index.js.map +1 -0
- package/dist/components/ChatShell/ShellHeader.d.ts +44 -0
- package/dist/components/ChatShell/ShellHeader.d.ts.map +1 -0
- package/dist/components/ChatShell/ShellHeader.js +14 -0
- package/dist/components/ChatShell/ShellHeader.js.map +1 -0
- package/dist/components/ChatShell/UserMenu.d.ts +48 -0
- package/dist/components/ChatShell/UserMenu.d.ts.map +1 -0
- package/dist/components/ChatShell/UserMenu.js +86 -0
- package/dist/components/ChatShell/UserMenu.js.map +1 -0
- package/dist/components/ChatShell/index.d.ts +98 -0
- package/dist/components/ChatShell/index.d.ts.map +1 -0
- package/dist/components/ChatShell/index.js +124 -0
- package/dist/components/ChatShell/index.js.map +1 -0
- package/dist/components/DataClubAIHub/index.d.ts +132 -0
- package/dist/components/DataClubAIHub/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHub/index.js +124 -0
- package/dist/components/DataClubAIHub/index.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts +89 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js +199 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts +33 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.js +137 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts +50 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.js +93 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts +49 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js +94 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts +37 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js +44 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts +56 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js +158 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts +29 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js +183 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts +38 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js +294 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts +36 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js +373 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts +47 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.js +233 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/index.d.ts +87 -0
- package/dist/components/DataClubAIHubAdmin/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/index.js +137 -0
- package/dist/components/DataClubAIHubAdmin/index.js.map +1 -0
- package/dist/components/DataClubAIHubLogin/index.d.ts +113 -0
- package/dist/components/DataClubAIHubLogin/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHubLogin/index.js +123 -0
- package/dist/components/DataClubAIHubLogin/index.js.map +1 -0
- package/dist/components/EffortControl/index.d.ts +57 -0
- package/dist/components/EffortControl/index.d.ts.map +1 -0
- package/dist/components/EffortControl/index.js +84 -0
- package/dist/components/EffortControl/index.js.map +1 -0
- package/dist/components/Layout/index.d.ts +63 -0
- package/dist/components/Layout/index.d.ts.map +1 -0
- package/dist/components/Layout/index.js +45 -0
- package/dist/components/Layout/index.js.map +1 -0
- package/dist/components/MainColumn/index.d.ts +69 -0
- package/dist/components/MainColumn/index.d.ts.map +1 -0
- package/dist/components/MainColumn/index.js +47 -0
- package/dist/components/MainColumn/index.js.map +1 -0
- package/dist/components/MarkdownRenderer/index.d.ts +66 -0
- package/dist/components/MarkdownRenderer/index.d.ts.map +1 -0
- package/dist/components/MarkdownRenderer/index.js +153 -0
- package/dist/components/MarkdownRenderer/index.js.map +1 -0
- package/dist/components/MessageInput/index.d.ts +85 -0
- package/dist/components/MessageInput/index.d.ts.map +1 -0
- package/dist/components/MessageInput/index.js +139 -0
- package/dist/components/MessageInput/index.js.map +1 -0
- package/dist/components/MessageList/index.d.ts +156 -0
- package/dist/components/MessageList/index.d.ts.map +1 -0
- package/dist/components/MessageList/index.js +352 -0
- package/dist/components/MessageList/index.js.map +1 -0
- package/dist/components/ReasoningSelector/index.d.ts +77 -0
- package/dist/components/ReasoningSelector/index.d.ts.map +1 -0
- package/dist/components/ReasoningSelector/index.js +156 -0
- package/dist/components/ReasoningSelector/index.js.map +1 -0
- package/dist/components/ReasoningView/index.d.ts +94 -0
- package/dist/components/ReasoningView/index.d.ts.map +1 -0
- package/dist/components/ReasoningView/index.js +83 -0
- package/dist/components/ReasoningView/index.js.map +1 -0
- package/dist/components/ScrollToPresentButton/index.d.ts +54 -0
- package/dist/components/ScrollToPresentButton/index.d.ts.map +1 -0
- package/dist/components/ScrollToPresentButton/index.js +44 -0
- package/dist/components/ScrollToPresentButton/index.js.map +1 -0
- package/dist/components/SessionsPanel/SessionTitleModal.d.ts +79 -0
- package/dist/components/SessionsPanel/SessionTitleModal.d.ts.map +1 -0
- package/dist/components/SessionsPanel/SessionTitleModal.js +102 -0
- package/dist/components/SessionsPanel/SessionTitleModal.js.map +1 -0
- package/dist/components/SessionsPanel/index.d.ts +100 -0
- package/dist/components/SessionsPanel/index.d.ts.map +1 -0
- package/dist/components/SessionsPanel/index.js +176 -0
- package/dist/components/SessionsPanel/index.js.map +1 -0
- package/dist/components/SystemNotice/index.d.ts +66 -0
- package/dist/components/SystemNotice/index.d.ts.map +1 -0
- package/dist/components/SystemNotice/index.js +81 -0
- package/dist/components/SystemNotice/index.js.map +1 -0
- package/dist/components/ThemeSwitcher/index.d.ts +66 -0
- package/dist/components/ThemeSwitcher/index.d.ts.map +1 -0
- package/dist/components/ThemeSwitcher/index.js +73 -0
- package/dist/components/ThemeSwitcher/index.js.map +1 -0
- package/dist/components/ToolCallView/formatContent.d.ts +36 -0
- package/dist/components/ToolCallView/formatContent.d.ts.map +1 -0
- package/dist/components/ToolCallView/formatContent.js +53 -0
- package/dist/components/ToolCallView/formatContent.js.map +1 -0
- package/dist/components/ToolCallView/index.d.ts +120 -0
- package/dist/components/ToolCallView/index.d.ts.map +1 -0
- package/dist/components/ToolCallView/index.js +104 -0
- package/dist/components/ToolCallView/index.js.map +1 -0
- package/dist/components/TpmBanner/index.d.ts +53 -0
- package/dist/components/TpmBanner/index.d.ts.map +1 -0
- package/dist/components/TpmBanner/index.js +116 -0
- package/dist/components/TpmBanner/index.js.map +1 -0
- package/dist/components/TurnStatusIndicator/index.d.ts +77 -0
- package/dist/components/TurnStatusIndicator/index.d.ts.map +1 -0
- package/dist/components/TurnStatusIndicator/index.js +122 -0
- package/dist/components/TurnStatusIndicator/index.js.map +1 -0
- package/dist/components/internal/PasswordInput.d.ts +62 -0
- package/dist/components/internal/PasswordInput.d.ts.map +1 -0
- package/dist/components/internal/PasswordInput.js +42 -0
- package/dist/components/internal/PasswordInput.js.map +1 -0
- package/dist/context/AIHubContext.d.ts +220 -0
- package/dist/context/AIHubContext.d.ts.map +1 -0
- package/dist/context/AIHubContext.js +19 -0
- package/dist/context/AIHubContext.js.map +1 -0
- package/dist/context/AIHubProvider.d.ts +75 -0
- package/dist/context/AIHubProvider.d.ts.map +1 -0
- package/dist/context/AIHubProvider.js +954 -0
- package/dist/context/AIHubProvider.js.map +1 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +9 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/theme.d.ts +43 -0
- package/dist/context/theme.d.ts.map +1 -0
- package/dist/context/theme.js +70 -0
- package/dist/context/theme.js.map +1 -0
- package/dist/context/useAIHub.d.ts +104 -0
- package/dist/context/useAIHub.d.ts.map +1 -0
- package/dist/context/useAIHub.js +63 -0
- package/dist/context/useAIHub.js.map +1 -0
- package/dist/context/useAIHubAuth.d.ts +44 -0
- package/dist/context/useAIHubAuth.d.ts.map +1 -0
- package/dist/context/useAIHubAuth.js +35 -0
- package/dist/context/useAIHubAuth.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/index.d.ts +99 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +97 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/state/affordances.d.ts +182 -0
- package/dist/state/affordances.d.ts.map +1 -0
- package/dist/state/affordances.js +189 -0
- package/dist/state/affordances.js.map +1 -0
- package/dist/state/stateReducer.d.ts +72 -0
- package/dist/state/stateReducer.d.ts.map +1 -0
- package/dist/state/stateReducer.js +122 -0
- package/dist/state/stateReducer.js.map +1 -0
- package/dist/stories/EffortControl.stories.d.ts +14 -0
- package/dist/stories/EffortControl.stories.d.ts.map +1 -0
- package/dist/stories/EffortControl.stories.js +43 -0
- package/dist/stories/EffortControl.stories.js.map +1 -0
- package/dist/stories/MarkdownRenderer.stories.d.ts +25 -0
- package/dist/stories/MarkdownRenderer.stories.d.ts.map +1 -0
- package/dist/stories/MarkdownRenderer.stories.js +178 -0
- package/dist/stories/MarkdownRenderer.stories.js.map +1 -0
- package/dist/stories/MessageBubble.stories.d.ts +21 -0
- package/dist/stories/MessageBubble.stories.d.ts.map +1 -0
- package/dist/stories/MessageBubble.stories.js +59 -0
- package/dist/stories/MessageBubble.stories.js.map +1 -0
- package/dist/stories/ReasoningView.stories.d.ts +18 -0
- package/dist/stories/ReasoningView.stories.d.ts.map +1 -0
- package/dist/stories/ReasoningView.stories.js +62 -0
- package/dist/stories/ReasoningView.stories.js.map +1 -0
- package/dist/stories/ScrollToPresentButton.stories.d.ts +18 -0
- package/dist/stories/ScrollToPresentButton.stories.d.ts.map +1 -0
- package/dist/stories/ScrollToPresentButton.stories.js +34 -0
- package/dist/stories/ScrollToPresentButton.stories.js.map +1 -0
- package/dist/stories/SystemNotice.stories.d.ts +17 -0
- package/dist/stories/SystemNotice.stories.d.ts.map +1 -0
- package/dist/stories/SystemNotice.stories.js +42 -0
- package/dist/stories/SystemNotice.stories.js.map +1 -0
- package/dist/stories/ThemeSwitcher.stories.d.ts +15 -0
- package/dist/stories/ThemeSwitcher.stories.d.ts.map +1 -0
- package/dist/stories/ThemeSwitcher.stories.js +35 -0
- package/dist/stories/ThemeSwitcher.stories.js.map +1 -0
- package/dist/stories/ToolCallView.stories.d.ts +20 -0
- package/dist/stories/ToolCallView.stories.d.ts.map +1 -0
- package/dist/stories/ToolCallView.stories.js +65 -0
- package/dist/stories/ToolCallView.stories.js.map +1 -0
- package/dist/stories/TpmBanner.stories.d.ts +24 -0
- package/dist/stories/TpmBanner.stories.d.ts.map +1 -0
- package/dist/stories/TpmBanner.stories.js +63 -0
- package/dist/stories/TpmBanner.stories.js.map +1 -0
- package/dist/stories/TurnStatusIndicator.stories.d.ts +25 -0
- package/dist/stories/TurnStatusIndicator.stories.d.ts.map +1 -0
- package/dist/stories/TurnStatusIndicator.stories.js +56 -0
- package/dist/stories/TurnStatusIndicator.stories.js.map +1 -0
- package/dist/stories/_mockProvider.d.ts +44 -0
- package/dist/stories/_mockProvider.d.ts.map +1 -0
- package/dist/stories/_mockProvider.js +84 -0
- package/dist/stories/_mockProvider.js.map +1 -0
- package/dist/types.d.ts +787 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/webcomponent/defineWebComponent.d.ts +106 -0
- package/dist/webcomponent/defineWebComponent.d.ts.map +1 -0
- package/dist/webcomponent/defineWebComponent.js +126 -0
- package/dist/webcomponent/defineWebComponent.js.map +1 -0
- package/dist/webcomponent/index.d.ts +7 -0
- package/dist/webcomponent/index.d.ts.map +1 -0
- package/dist/webcomponent/index.js +7 -0
- package/dist/webcomponent/index.js.map +1 -0
- package/package.json +63 -0
- package/src/__lint-canary__/step1-lint-canary.ts.disabled +28 -0
- package/src/auth/passwordPolicy.ts +75 -0
- package/src/auth/texts.ts +1294 -0
- package/src/client/AIHubClient.ts +1718 -0
- package/src/client/index.ts +30 -0
- package/src/client/sseParser.ts +168 -0
- package/src/client/tokenStorage.ts +131 -0
- package/src/components/AgentsPicker/index.tsx +147 -0
- package/src/components/AuthGate/index.tsx +132 -0
- package/src/components/CacheBadge/index.tsx +245 -0
- package/src/components/ChangePassword/index.tsx +375 -0
- package/src/components/ChatShell/ShellHeader.tsx +80 -0
- package/src/components/ChatShell/UserMenu.tsx +196 -0
- package/src/components/ChatShell/index.tsx +307 -0
- package/src/components/DataClubAIHub/index.tsx +300 -0
- package/src/components/DataClubAIHubAdmin/AgentEditorModal.tsx +505 -0
- package/src/components/DataClubAIHubAdmin/AgentsPage.tsx +342 -0
- package/src/components/DataClubAIHubAdmin/CreateUserModal.tsx +214 -0
- package/src/components/DataClubAIHubAdmin/PasswordRevealModal.tsx +204 -0
- package/src/components/DataClubAIHubAdmin/ResetPasswordModal.tsx +122 -0
- package/src/components/DataClubAIHubAdmin/RotatingMessageEditorModal.tsx +408 -0
- package/src/components/DataClubAIHubAdmin/RotatingMessagesPage.tsx +373 -0
- package/src/components/DataClubAIHubAdmin/SessionsOverviewPage.tsx +639 -0
- package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +979 -0
- package/src/components/DataClubAIHubAdmin/UsersPage.tsx +495 -0
- package/src/components/DataClubAIHubAdmin/index.tsx +277 -0
- package/src/components/DataClubAIHubLogin/index.tsx +282 -0
- package/src/components/EffortControl/index.tsx +138 -0
- package/src/components/Layout/index.tsx +92 -0
- package/src/components/MainColumn/index.tsx +131 -0
- package/src/components/MarkdownRenderer/index.tsx +256 -0
- package/src/components/MessageInput/index.tsx +283 -0
- package/src/components/MessageList/index.tsx +753 -0
- package/src/components/ReasoningSelector/index.tsx +268 -0
- package/src/components/ReasoningView/index.tsx +183 -0
- package/src/components/ScrollToPresentButton/index.tsx +85 -0
- package/src/components/SessionsPanel/SessionTitleModal.tsx +256 -0
- package/src/components/SessionsPanel/index.tsx +406 -0
- package/src/components/SystemNotice/index.tsx +151 -0
- package/src/components/ThemeSwitcher/index.tsx +136 -0
- package/src/components/ToolCallView/formatContent.ts +50 -0
- package/src/components/ToolCallView/index.tsx +274 -0
- package/src/components/TpmBanner/index.tsx +172 -0
- package/src/components/TurnStatusIndicator/index.tsx +203 -0
- package/src/components/internal/PasswordInput.tsx +145 -0
- package/src/context/AIHubContext.ts +249 -0
- package/src/context/AIHubProvider.tsx +1084 -0
- package/src/context/index.ts +9 -0
- package/src/context/theme.ts +76 -0
- package/src/context/useAIHub.ts +151 -0
- package/src/context/useAIHubAuth.ts +65 -0
- package/src/index.ts +275 -0
- package/src/primitives/index.ts +101 -0
- package/src/state/affordances.ts +321 -0
- package/src/state/stateReducer.ts +131 -0
- package/src/stories/EffortControl.stories.tsx +63 -0
- package/src/stories/MarkdownRenderer.stories.tsx +201 -0
- package/src/stories/MessageBubble.stories.tsx +101 -0
- package/src/stories/ReasoningView.stories.tsx +81 -0
- package/src/stories/ScrollToPresentButton.stories.tsx +52 -0
- package/src/stories/SystemNotice.stories.tsx +59 -0
- package/src/stories/ThemeSwitcher.stories.tsx +55 -0
- package/src/stories/ToolCallView.stories.tsx +95 -0
- package/src/stories/TpmBanner.stories.tsx +93 -0
- package/src/stories/TurnStatusIndicator.stories.tsx +86 -0
- package/src/stories/_mockProvider.tsx +116 -0
- package/src/styles/CLASSES.md +356 -0
- package/src/styles/default.css +2937 -0
- package/src/types.ts +867 -0
- package/src/webcomponent/defineWebComponent.tsx +193 -0
- package/src/webcomponent/index.ts +11 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<MessageList />` — the scrollable list of message bubbles in the
|
|
3
|
+
* active session.
|
|
4
|
+
*
|
|
5
|
+
* Step 10 layers in:
|
|
6
|
+
*
|
|
7
|
+
* - `--system` / `--tool` / `--failed` bubble modifiers (Step 9 shipped
|
|
8
|
+
* `--user` / `--assistant`).
|
|
9
|
+
* - The `customMessageActions` slot — Tier-2 / Tier-3 wrappers render
|
|
10
|
+
* their own action bar inside every assistant bubble (e.g. 👍 / 👎).
|
|
11
|
+
* - The `customMessageBubble` slot — Tier-3 escape hatch that replaces
|
|
12
|
+
* the entire bubble for a message.
|
|
13
|
+
* - The `<TpmBanner />` — sits above the streaming bubble during a
|
|
14
|
+
* `waiting_provider` phase + the brief resumed-flash.
|
|
15
|
+
* - The `<ScrollToPresentButton />` — appears when the user has scrolled
|
|
16
|
+
* away from the bottom during a running turn (auto-scroll is
|
|
17
|
+
* inhibited while they're scrolled up).
|
|
18
|
+
* - Per-row Retry button on the failed-turn `--failed` assistant bubble.
|
|
19
|
+
*
|
|
20
|
+
* Server toggles (§8.4) gate via the resolved prop stack the monolith
|
|
21
|
+
* passes in:
|
|
22
|
+
*
|
|
23
|
+
* - `systemMessagesVisible` — when false, `system` and `tool`-role
|
|
24
|
+
* messages are filtered out of the list.
|
|
25
|
+
* - `tpmNoticeVisible` — when false, `<TpmBanner />` is suppressed.
|
|
26
|
+
* - `scrollToPresentVisible` — when false, the scroll-to-present
|
|
27
|
+
* button is suppressed.
|
|
28
|
+
*
|
|
29
|
+
* Exported under `AIHub.MessageList`.
|
|
30
|
+
*
|
|
31
|
+
* @see SPECIFICATIONS.md §7.1, §8.4, §10.4
|
|
32
|
+
* @see ui-sketches/03-chat.md — message_bubble + system_notice
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
useCallback,
|
|
37
|
+
useEffect,
|
|
38
|
+
useMemo,
|
|
39
|
+
useRef,
|
|
40
|
+
useState,
|
|
41
|
+
type ReactNode,
|
|
42
|
+
} from "react";
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
resolveAIHubTexts,
|
|
46
|
+
type AIHubTexts,
|
|
47
|
+
type DeepPartial,
|
|
48
|
+
} from "../../auth/texts.js";
|
|
49
|
+
import { useAIHub } from "../../context/useAIHub.js";
|
|
50
|
+
import { chatAffordances } from "../../state/affordances.js";
|
|
51
|
+
import type {
|
|
52
|
+
AIHubMessage,
|
|
53
|
+
AIHubMessageActionRenderer,
|
|
54
|
+
AIHubMessageBubbleRenderer,
|
|
55
|
+
} from "../../types.js";
|
|
56
|
+
|
|
57
|
+
import { CacheBadge } from "../CacheBadge/index.js";
|
|
58
|
+
import { MarkdownRenderer } from "../MarkdownRenderer/index.js";
|
|
59
|
+
import { ReasoningView } from "../ReasoningView/index.js";
|
|
60
|
+
import { ScrollToPresentButton } from "../ScrollToPresentButton/index.js";
|
|
61
|
+
import { ToolCallView } from "../ToolCallView/index.js";
|
|
62
|
+
import { TpmBanner } from "../TpmBanner/index.js";
|
|
63
|
+
|
|
64
|
+
/** Distance from bottom (in pixels) that still counts as "at the latest". */
|
|
65
|
+
const BOTTOM_EPSILON_PX = 32;
|
|
66
|
+
|
|
67
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
68
|
+
/* Block-expansion stable IDs */
|
|
69
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Stable id for the reasoning sub-block of a *historical* assistant
|
|
73
|
+
* message. Used as the controlled-mode key into the user-expanded
|
|
74
|
+
* accordion state owned by `<MessageList />`.
|
|
75
|
+
*/
|
|
76
|
+
function reasoningBlockId(messageId: string): string {
|
|
77
|
+
return `reasoning:${messageId}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Stable id for the reasoning sub-block of the *currently-streaming* bubble. */
|
|
81
|
+
const STREAMING_REASONING_BLOCK_ID = "reasoning:streaming";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Stable id for a tool-call card. The accordion key is namespaced by the
|
|
85
|
+
* **owning bubble** (`tool:<parent>:<tool_call_id>`) — empirically Letta
|
|
86
|
+
* has been observed to reuse `tool_call_id` values across logical
|
|
87
|
+
* messages within a single multi-step run, and even when it doesn't, the
|
|
88
|
+
* historical projector's per-step attribution could surface a record in
|
|
89
|
+
* two bubbles if it misfires. Without the namespace, two cards sharing
|
|
90
|
+
* an id would expand together because the accordion state lives in
|
|
91
|
+
* `<MessageList />` (cross-bubble). The namespace makes each bubble's
|
|
92
|
+
* card an independent accordion slot regardless of upstream id quirks.
|
|
93
|
+
*
|
|
94
|
+
* Pass `"streaming"` as `parentId` for the currently-streaming bubble's
|
|
95
|
+
* tool cards; pass the historical assistant message's id otherwise.
|
|
96
|
+
*/
|
|
97
|
+
function toolCallBlockId(parentId: string, toolCallId: string): string {
|
|
98
|
+
return `tool:${parentId}:${toolCallId}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* BEM class names emitted by `<MessageList />`.
|
|
103
|
+
*/
|
|
104
|
+
export interface MessageListClassNames {
|
|
105
|
+
/**
|
|
106
|
+
* Outer wrapper. NOT the scroll container — that's `root` (a child of
|
|
107
|
+
* this element). The wrapper exists so `<ScrollToPresentButton />`
|
|
108
|
+
* can sit as a sibling of the scroll container and stay anchored to
|
|
109
|
+
* the visible chat area as the user scrolls.
|
|
110
|
+
*/
|
|
111
|
+
wrapper: string;
|
|
112
|
+
/** Scroll container — the actual element that overflows + scrolls. */
|
|
113
|
+
root: string;
|
|
114
|
+
/** Inner block; targets `max-width` for readable line lengths. */
|
|
115
|
+
inner: string;
|
|
116
|
+
/** Empty-state placeholder (zero messages in the active session). */
|
|
117
|
+
empty: string;
|
|
118
|
+
/** Loading state. */
|
|
119
|
+
loading: string;
|
|
120
|
+
/** Placeholder when no session is selected. */
|
|
121
|
+
noSession: string;
|
|
122
|
+
/**
|
|
123
|
+
* Error banner shown above the streaming-assistant placeholder when
|
|
124
|
+
* the active turn failed without producing any assistant output.
|
|
125
|
+
*/
|
|
126
|
+
errorBanner: string;
|
|
127
|
+
/** Per-bubble base class. */
|
|
128
|
+
bubble: string;
|
|
129
|
+
/** Bubble modifier — `--user`. */
|
|
130
|
+
bubbleUser: string;
|
|
131
|
+
/** Bubble modifier — `--assistant`. */
|
|
132
|
+
bubbleAssistant: string;
|
|
133
|
+
/** Bubble modifier — `--system`. */
|
|
134
|
+
bubbleSystem: string;
|
|
135
|
+
/** Bubble modifier — `--tool`. */
|
|
136
|
+
bubbleTool: string;
|
|
137
|
+
/** Bubble modifier — `--failed` (applied to the assistant bubble of a failed turn). */
|
|
138
|
+
bubbleFailed: string;
|
|
139
|
+
/** Bubble modifier — `--cancelled` (for the "Stopped" stamp). */
|
|
140
|
+
bubbleCancelled: string;
|
|
141
|
+
/** Wrapper for the role label inside the bubble. */
|
|
142
|
+
bubbleRole: string;
|
|
143
|
+
/** The text body inside the bubble. */
|
|
144
|
+
bubbleBody: string;
|
|
145
|
+
/** The action bar inside the bubble (slot for custom actions). */
|
|
146
|
+
bubbleActions: string;
|
|
147
|
+
/** The retry-button on `--failed` bubbles. */
|
|
148
|
+
bubbleRetryButton: string;
|
|
149
|
+
/** The "Stopped" stamp on `--cancelled` bubbles. */
|
|
150
|
+
bubbleCancelledStamp: string;
|
|
151
|
+
/** Streaming caret while a turn is in flight. */
|
|
152
|
+
bubbleCaret: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const DEFAULT_CLASS_NAMES: MessageListClassNames = Object.freeze({
|
|
156
|
+
wrapper: "data-club-ai-hub__messages-wrapper",
|
|
157
|
+
root: "data-club-ai-hub__messages",
|
|
158
|
+
inner: "data-club-ai-hub__messages__inner",
|
|
159
|
+
empty: "data-club-ai-hub__messages__empty",
|
|
160
|
+
loading: "data-club-ai-hub__messages__loading",
|
|
161
|
+
noSession: "data-club-ai-hub__messages__no-session",
|
|
162
|
+
errorBanner: "data-club-ai-hub__messages__error",
|
|
163
|
+
bubble: "data-club-ai-hub__message-bubble",
|
|
164
|
+
bubbleUser: "data-club-ai-hub__message-bubble--user",
|
|
165
|
+
bubbleAssistant: "data-club-ai-hub__message-bubble--assistant",
|
|
166
|
+
bubbleSystem: "data-club-ai-hub__message-bubble--system",
|
|
167
|
+
bubbleTool: "data-club-ai-hub__message-bubble--tool",
|
|
168
|
+
bubbleFailed: "data-club-ai-hub__message-bubble--failed",
|
|
169
|
+
bubbleCancelled: "data-club-ai-hub__message-bubble--cancelled",
|
|
170
|
+
bubbleRole: "data-club-ai-hub__message-bubble__role",
|
|
171
|
+
bubbleBody: "data-club-ai-hub__message-bubble__body",
|
|
172
|
+
bubbleActions: "data-club-ai-hub__message-bubble__actions",
|
|
173
|
+
bubbleRetryButton: "data-club-ai-hub__message-bubble__retry",
|
|
174
|
+
bubbleCancelledStamp: "data-club-ai-hub__message-bubble__cancelled-stamp",
|
|
175
|
+
bubbleCaret: "data-club-ai-hub__message-bubble__caret",
|
|
176
|
+
}) as MessageListClassNames;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Props for `<MessageList />`.
|
|
180
|
+
*/
|
|
181
|
+
export interface MessageListProps {
|
|
182
|
+
/**
|
|
183
|
+
* Show system / tool-role bubbles. Default true. When false, those
|
|
184
|
+
* messages are filtered out (the user-facing list shows user +
|
|
185
|
+
* assistant only).
|
|
186
|
+
*/
|
|
187
|
+
showSystemMessages?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Render `<TpmBanner />` during `waiting_provider` and on
|
|
190
|
+
* tpm_exhausted. Default true. The monolith passes the resolved
|
|
191
|
+
* §8.4 flag here.
|
|
192
|
+
*/
|
|
193
|
+
showTpmNotice?: boolean;
|
|
194
|
+
/**
|
|
195
|
+
* Render `<ReasoningView />` inside assistant bubbles when the
|
|
196
|
+
* message carries `reasoning`. Default true. Monolith forwards the
|
|
197
|
+
* resolved `reasoning_visible` flag.
|
|
198
|
+
*/
|
|
199
|
+
showReasoning?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Render `<ToolCallView />` cards inside assistant bubbles when the
|
|
202
|
+
* message carries `tool_calls`. Default true. Monolith forwards the
|
|
203
|
+
* resolved `tool_calls_visible` flag.
|
|
204
|
+
*/
|
|
205
|
+
showToolCalls?: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Render `<ScrollToPresentButton />` when the user has scrolled away
|
|
208
|
+
* from the bottom of the list during a running turn. Default true.
|
|
209
|
+
*/
|
|
210
|
+
showScrollToPresentButton?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Render a "Retry" button on `--failed` assistant bubbles. Default
|
|
213
|
+
* true. When false the bubble still renders with the failed modifier
|
|
214
|
+
* but no action is offered.
|
|
215
|
+
*/
|
|
216
|
+
showRetryButton?: boolean;
|
|
217
|
+
/**
|
|
218
|
+
* Tier 2 / Tier 3 slot — render the action bar of every assistant
|
|
219
|
+
* message bubble. The renderer receives the full `AIHubMessage` and
|
|
220
|
+
* returns React-renderable content (or null to suppress the bar
|
|
221
|
+
* for that message).
|
|
222
|
+
*/
|
|
223
|
+
renderMessageActions?: AIHubMessageActionRenderer;
|
|
224
|
+
/**
|
|
225
|
+
* Tier 3 escape hatch — fully replace the message bubble for any
|
|
226
|
+
* message. The renderer is wrapped in the list's flex container; the
|
|
227
|
+
* caller is responsible for choosing variant modifiers.
|
|
228
|
+
*/
|
|
229
|
+
renderMessageBubble?: AIHubMessageBubbleRenderer;
|
|
230
|
+
/** Extra class merged onto the root. */
|
|
231
|
+
className?: string;
|
|
232
|
+
/** Per-element class overrides. */
|
|
233
|
+
classNames?: Partial<MessageListClassNames>;
|
|
234
|
+
/** Wrapper-supplied i18n. */
|
|
235
|
+
texts?: DeepPartial<AIHubTexts>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Renders the message list.
|
|
240
|
+
*
|
|
241
|
+
* @param props - see `MessageListProps`.
|
|
242
|
+
* @returns the rendered list.
|
|
243
|
+
*/
|
|
244
|
+
export function MessageList(props: MessageListProps): ReactNode {
|
|
245
|
+
const {
|
|
246
|
+
messages,
|
|
247
|
+
messagesLoading,
|
|
248
|
+
activeSessionId,
|
|
249
|
+
streamingAssistantText,
|
|
250
|
+
streamingReasoning,
|
|
251
|
+
streamingToolCalls,
|
|
252
|
+
lastTurnError,
|
|
253
|
+
state,
|
|
254
|
+
sendMessage,
|
|
255
|
+
} = useAIHub();
|
|
256
|
+
const texts = resolveAIHubTexts(props.texts);
|
|
257
|
+
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
258
|
+
const showSystemMessages = props.showSystemMessages ?? true;
|
|
259
|
+
const showTpmNotice = props.showTpmNotice ?? true;
|
|
260
|
+
const showScrollToPresent = props.showScrollToPresentButton ?? true;
|
|
261
|
+
const showRetryButton = props.showRetryButton ?? true;
|
|
262
|
+
const showReasoning = props.showReasoning ?? true;
|
|
263
|
+
const showToolCalls = props.showToolCalls ?? true;
|
|
264
|
+
const rootClass = props.className
|
|
265
|
+
? `${cls.root} ${props.className}`
|
|
266
|
+
: cls.root;
|
|
267
|
+
const affordances = chatAffordances(state);
|
|
268
|
+
|
|
269
|
+
// Block-expansion accordion (reasoning + tool-call blocks). At rest,
|
|
270
|
+
// at most one block in the entire conversation is user-expanded —
|
|
271
|
+
// clicking a second block collapses the first. During generation,
|
|
272
|
+
// the currently-streaming block is additionally auto-expanded, and
|
|
273
|
+
// the user-accordion ignores it (so up to TWO blocks can be visible:
|
|
274
|
+
// the auto-expanded streaming one + one user-expanded). When
|
|
275
|
+
// generation finishes, the streaming auto-expand falls away and the
|
|
276
|
+
// user's manual accordion remains.
|
|
277
|
+
const [userExpandedBlockId, setUserExpandedBlockId] = useState<string | null>(null);
|
|
278
|
+
// The currently-streaming block (reasoning while phase ==
|
|
279
|
+
// model_reasoning; the most recent still-running tool call while
|
|
280
|
+
// phase == model_tool_input or tool_executing). Null otherwise.
|
|
281
|
+
const currentlyStreamingBlockId = useMemo<string | null>(() => {
|
|
282
|
+
if (state.status !== "running") return null;
|
|
283
|
+
if (state.phase === "model_reasoning") return STREAMING_REASONING_BLOCK_ID;
|
|
284
|
+
if (state.phase === "model_tool_input" || state.phase === "tool_executing") {
|
|
285
|
+
// Walk newest-first; "running" = not yet finished. The translator
|
|
286
|
+
// computes `status === "running"` when neither output nor error is
|
|
287
|
+
// set yet, but that derivation happens in the streaming-bubble
|
|
288
|
+
// render below — here we mirror it.
|
|
289
|
+
for (let i = streamingToolCalls.length - 1; i >= 0; i--) {
|
|
290
|
+
const c = streamingToolCalls[i];
|
|
291
|
+
if (!c) continue;
|
|
292
|
+
const isRunning = c.status !== "error" && c.output == null;
|
|
293
|
+
if (isRunning) return toolCallBlockId("streaming", c.tool_call_id);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}, [state.status, state.phase, streamingToolCalls]);
|
|
298
|
+
// `expanded` resolver: auto-expand wins (it's the visible-during-stream
|
|
299
|
+
// contract); user-accordion is the fallback.
|
|
300
|
+
const isBlockExpanded = useCallback(
|
|
301
|
+
(blockId: string): boolean =>
|
|
302
|
+
blockId === currentlyStreamingBlockId || blockId === userExpandedBlockId,
|
|
303
|
+
[currentlyStreamingBlockId, userExpandedBlockId],
|
|
304
|
+
);
|
|
305
|
+
// Toggle: clicking the streaming-locked block is a no-op (the rule is
|
|
306
|
+
// "the latest in-progress block is always visible"). For every other
|
|
307
|
+
// block, the accordion replaces: clicking an already-user-expanded
|
|
308
|
+
// block collapses it to null; clicking any other expands it.
|
|
309
|
+
const handleToggleBlock = useCallback(
|
|
310
|
+
(blockId: string): void => {
|
|
311
|
+
if (blockId === currentlyStreamingBlockId) return;
|
|
312
|
+
setUserExpandedBlockId((current) => (current === blockId ? null : blockId));
|
|
313
|
+
},
|
|
314
|
+
[currentlyStreamingBlockId],
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Filter messages: assistant + user always, system + tool gated by
|
|
318
|
+
// showSystemMessages. Dedupe by id defensively even though the
|
|
319
|
+
// provider already does it on its own. Empty/whitespace-only messages
|
|
320
|
+
// are skipped — they appear when the messages-list route applies an
|
|
321
|
+
// empty `user_override` row (the rendered chip would otherwise be a
|
|
322
|
+
// tiny empty bubble), or when Letta's `<memory_metadata>`-stripped
|
|
323
|
+
// system message leaves nothing behind. Assistant bubbles keep
|
|
324
|
+
// rendering when they carry `reasoning` or `tool_calls` even with
|
|
325
|
+
// empty text, since those affordances are the message body.
|
|
326
|
+
const visibleMessages = useMemo(() => {
|
|
327
|
+
const seen = new Set<string>();
|
|
328
|
+
const out: AIHubMessage[] = [];
|
|
329
|
+
for (const msg of messages) {
|
|
330
|
+
if (seen.has(msg.id)) continue;
|
|
331
|
+
if ((msg.role === "system" || msg.role === "tool") && !showSystemMessages) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (isMessageRenderEmpty(msg)) continue;
|
|
335
|
+
seen.add(msg.id);
|
|
336
|
+
out.push(msg);
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}, [messages, showSystemMessages]);
|
|
340
|
+
|
|
341
|
+
// Track whether the user is currently at-the-bottom (within
|
|
342
|
+
// BOTTOM_EPSILON_PX). When at-bottom, new content auto-scrolls. When
|
|
343
|
+
// scrolled up, auto-scroll is suppressed and the
|
|
344
|
+
// ScrollToPresentButton renders. Recomputed on every scroll event
|
|
345
|
+
// through the scroll container.
|
|
346
|
+
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
347
|
+
const [atBottom, setAtBottom] = useState(true);
|
|
348
|
+
|
|
349
|
+
const checkAtBottom = useCallback((): boolean => {
|
|
350
|
+
const el = scrollRef.current;
|
|
351
|
+
if (!el) return true;
|
|
352
|
+
return el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_EPSILON_PX;
|
|
353
|
+
}, []);
|
|
354
|
+
|
|
355
|
+
function handleScroll(): void {
|
|
356
|
+
setAtBottom(checkAtBottom());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Auto-scroll only when the user is at-the-bottom. On session change
|
|
360
|
+
// we always scroll to the bottom (loading a new session shouldn't
|
|
361
|
+
// strand the user mid-history).
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
const el = scrollRef.current;
|
|
364
|
+
if (!el) return;
|
|
365
|
+
if (atBottom) {
|
|
366
|
+
el.scrollTop = el.scrollHeight;
|
|
367
|
+
}
|
|
368
|
+
}, [messages, streamingAssistantText, atBottom]);
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const el = scrollRef.current;
|
|
371
|
+
if (!el) return;
|
|
372
|
+
el.scrollTop = el.scrollHeight;
|
|
373
|
+
setAtBottom(true);
|
|
374
|
+
}, [activeSessionId]);
|
|
375
|
+
|
|
376
|
+
const handleScrollToPresent = useCallback((): void => {
|
|
377
|
+
const el = scrollRef.current;
|
|
378
|
+
if (!el) return;
|
|
379
|
+
el.scrollTop = el.scrollHeight;
|
|
380
|
+
setAtBottom(true);
|
|
381
|
+
}, []);
|
|
382
|
+
|
|
383
|
+
const handleRetry = useCallback((): void => {
|
|
384
|
+
// Find the last user message in the visible list (the failed
|
|
385
|
+
// turn's input). Sending it re-issues the turn.
|
|
386
|
+
for (let i = visibleMessages.length - 1; i >= 0; i--) {
|
|
387
|
+
const msg = visibleMessages[i];
|
|
388
|
+
if (msg && msg.role === "user") {
|
|
389
|
+
void sendMessage(msg.content);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}, [visibleMessages, sendMessage]);
|
|
394
|
+
|
|
395
|
+
if (!activeSessionId) {
|
|
396
|
+
return (
|
|
397
|
+
<div className={cls.wrapper} data-testid="message-list-wrapper">
|
|
398
|
+
<div className={rootClass} data-testid="message-list">
|
|
399
|
+
<div className={cls.noSession} data-testid="message-list-no-session">
|
|
400
|
+
{texts.chat.noActiveSessionHint}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<div className={cls.wrapper} data-testid="message-list-wrapper">
|
|
409
|
+
<div
|
|
410
|
+
className={rootClass}
|
|
411
|
+
ref={scrollRef}
|
|
412
|
+
onScroll={handleScroll}
|
|
413
|
+
data-testid="message-list"
|
|
414
|
+
>
|
|
415
|
+
<div className={cls.inner} aria-live="polite">
|
|
416
|
+
{showTpmNotice ? (
|
|
417
|
+
<TpmBanner
|
|
418
|
+
onRetry={handleRetry}
|
|
419
|
+
{...(props.texts !== undefined ? { texts: props.texts } : {})}
|
|
420
|
+
/>
|
|
421
|
+
) : null}
|
|
422
|
+
{messagesLoading ? (
|
|
423
|
+
<p className={cls.loading} data-testid="message-list-loading">
|
|
424
|
+
{texts.chat.sessionsLoading}
|
|
425
|
+
</p>
|
|
426
|
+
) : null}
|
|
427
|
+
{!messagesLoading &&
|
|
428
|
+
visibleMessages.length === 0 &&
|
|
429
|
+
streamingAssistantText === null &&
|
|
430
|
+
lastTurnError === null ? (
|
|
431
|
+
<p className={cls.empty} data-testid="message-list-empty">
|
|
432
|
+
{texts.chat.emptyConversationHint}
|
|
433
|
+
</p>
|
|
434
|
+
) : null}
|
|
435
|
+
{visibleMessages.map((msg) =>
|
|
436
|
+
props.renderMessageBubble ? (
|
|
437
|
+
// The Tier-3 escape hatch returns arbitrary content; we
|
|
438
|
+
// wrap with a stable React key.
|
|
439
|
+
<div key={msg.id} data-testid="message-bubble-custom">
|
|
440
|
+
{props.renderMessageBubble(msg) as ReactNode}
|
|
441
|
+
</div>
|
|
442
|
+
) : (
|
|
443
|
+
<MessageBubble
|
|
444
|
+
key={msg.id}
|
|
445
|
+
message={msg}
|
|
446
|
+
classNames={cls}
|
|
447
|
+
isFailedAssistant={
|
|
448
|
+
state.status === "failed" &&
|
|
449
|
+
msg.role === "assistant" &&
|
|
450
|
+
isLastAssistant(visibleMessages, msg)
|
|
451
|
+
}
|
|
452
|
+
isCancelledAssistant={
|
|
453
|
+
state.status === "cancelled" &&
|
|
454
|
+
msg.role === "assistant" &&
|
|
455
|
+
isLastAssistant(visibleMessages, msg)
|
|
456
|
+
}
|
|
457
|
+
showRetryButton={showRetryButton}
|
|
458
|
+
showReasoning={showReasoning}
|
|
459
|
+
showToolCalls={showToolCalls}
|
|
460
|
+
retryLabel={texts.chat.retryFailedButton}
|
|
461
|
+
failedLabel={texts.chat.streamFailedText}
|
|
462
|
+
cancelledStampLabel={texts.chat.cancelledStampText}
|
|
463
|
+
onRetry={handleRetry}
|
|
464
|
+
isBlockExpanded={isBlockExpanded}
|
|
465
|
+
onToggleBlock={handleToggleBlock}
|
|
466
|
+
{...(props.texts !== undefined ? { texts: props.texts } : {})}
|
|
467
|
+
{...(props.renderMessageActions !== undefined
|
|
468
|
+
? { renderMessageActions: props.renderMessageActions }
|
|
469
|
+
: {})}
|
|
470
|
+
/>
|
|
471
|
+
),
|
|
472
|
+
)}
|
|
473
|
+
{lastTurnError !== null && state.status === "failed" ? (
|
|
474
|
+
<div
|
|
475
|
+
className={`${cls.bubble} ${cls.bubbleAssistant} ${cls.bubbleFailed}`}
|
|
476
|
+
data-testid="message-bubble-failed-error"
|
|
477
|
+
data-role="assistant"
|
|
478
|
+
>
|
|
479
|
+
<span className={cls.bubbleBody}>
|
|
480
|
+
{texts.chat.streamFailedText}
|
|
481
|
+
</span>
|
|
482
|
+
{showRetryButton ? (
|
|
483
|
+
<button
|
|
484
|
+
type="button"
|
|
485
|
+
className={cls.bubbleRetryButton}
|
|
486
|
+
onClick={handleRetry}
|
|
487
|
+
data-testid="message-list-retry"
|
|
488
|
+
>
|
|
489
|
+
{texts.chat.retryFailedButton}
|
|
490
|
+
</button>
|
|
491
|
+
) : null}
|
|
492
|
+
</div>
|
|
493
|
+
) : lastTurnError !== null ? (
|
|
494
|
+
<div
|
|
495
|
+
className={cls.errorBanner}
|
|
496
|
+
data-testid="message-list-error"
|
|
497
|
+
role="alert"
|
|
498
|
+
>
|
|
499
|
+
{lastTurnError}
|
|
500
|
+
</div>
|
|
501
|
+
) : null}
|
|
502
|
+
{streamingAssistantText !== null ||
|
|
503
|
+
(streamingReasoning !== null && showReasoning) ||
|
|
504
|
+
(streamingToolCalls.length > 0 && showToolCalls) ? (
|
|
505
|
+
<div
|
|
506
|
+
className={`${cls.bubble} ${cls.bubbleAssistant}`}
|
|
507
|
+
data-testid="message-bubble"
|
|
508
|
+
data-role="assistant"
|
|
509
|
+
data-streaming="true"
|
|
510
|
+
>
|
|
511
|
+
{streamingReasoning !== null && showReasoning ? (
|
|
512
|
+
<ReasoningView
|
|
513
|
+
content={streamingReasoning}
|
|
514
|
+
streaming={state.phase === "model_reasoning"}
|
|
515
|
+
expanded={isBlockExpanded(STREAMING_REASONING_BLOCK_ID)}
|
|
516
|
+
onToggle={() => handleToggleBlock(STREAMING_REASONING_BLOCK_ID)}
|
|
517
|
+
{...(props.texts !== undefined ? { texts: props.texts } : {})}
|
|
518
|
+
/>
|
|
519
|
+
) : null}
|
|
520
|
+
{showToolCalls
|
|
521
|
+
? streamingToolCalls.map((call) => {
|
|
522
|
+
const blockId = toolCallBlockId("streaming", call.tool_call_id);
|
|
523
|
+
return (
|
|
524
|
+
<ToolCallView
|
|
525
|
+
key={call.tool_call_id}
|
|
526
|
+
toolName={call.tool_name}
|
|
527
|
+
status={
|
|
528
|
+
call.status === "error"
|
|
529
|
+
? "error"
|
|
530
|
+
: call.output != null
|
|
531
|
+
? "done"
|
|
532
|
+
: "running"
|
|
533
|
+
}
|
|
534
|
+
input={call.input}
|
|
535
|
+
expanded={isBlockExpanded(blockId)}
|
|
536
|
+
onToggle={() => handleToggleBlock(blockId)}
|
|
537
|
+
{...(call.output != null ? { output: call.output } : {})}
|
|
538
|
+
{...(call.error != null ? { error: call.error } : {})}
|
|
539
|
+
{...(props.texts !== undefined ? { texts: props.texts } : {})}
|
|
540
|
+
/>
|
|
541
|
+
);
|
|
542
|
+
})
|
|
543
|
+
: null}
|
|
544
|
+
{streamingAssistantText !== null ? (
|
|
545
|
+
<span className={cls.bubbleBody}>
|
|
546
|
+
<MarkdownRenderer
|
|
547
|
+
content={streamingAssistantText.replace(/^\s+|\s+$/g, "")}
|
|
548
|
+
/>
|
|
549
|
+
{affordances.streamingDots ? (
|
|
550
|
+
<span className={cls.bubbleCaret} aria-hidden="true" />
|
|
551
|
+
) : null}
|
|
552
|
+
</span>
|
|
553
|
+
) : null}
|
|
554
|
+
</div>
|
|
555
|
+
) : null}
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
{showScrollToPresent ? (
|
|
559
|
+
<ScrollToPresentButton
|
|
560
|
+
visible={affordances.scrollToPresentVisible && !atBottom}
|
|
561
|
+
onClick={handleScrollToPresent}
|
|
562
|
+
{...(props.texts !== undefined ? { texts: props.texts } : {})}
|
|
563
|
+
/>
|
|
564
|
+
) : null}
|
|
565
|
+
</div>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Returns true when `msg` is the last assistant-role entry in `list`.
|
|
571
|
+
* Used to scope the failed/cancelled modifiers to the latest assistant
|
|
572
|
+
* turn only (mid-history assistant messages are not retroactively
|
|
573
|
+
* failed when a *new* turn fails).
|
|
574
|
+
*/
|
|
575
|
+
function isLastAssistant(
|
|
576
|
+
list: ReadonlyArray<AIHubMessage>,
|
|
577
|
+
msg: AIHubMessage,
|
|
578
|
+
): boolean {
|
|
579
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
580
|
+
const item = list[i];
|
|
581
|
+
if (!item) continue;
|
|
582
|
+
if (item.role !== "assistant") continue;
|
|
583
|
+
return item.id === msg.id;
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* True when `msg` has nothing meaningful to render — its `content` is
|
|
590
|
+
* empty or whitespace-only AND it carries no `reasoning` text or
|
|
591
|
+
* `tool_calls`. Used by the visibility filter so messages overridden to
|
|
592
|
+
* empty (e.g. an empty `user_override` row applied to the persisted
|
|
593
|
+
* `system_message`, or Letta's `<memory_metadata>`-stripped system row
|
|
594
|
+
* after the strip leaves the slot empty) don't render as tiny empty
|
|
595
|
+
* bubbles.
|
|
596
|
+
*
|
|
597
|
+
* Assistant bubbles with `reasoning` or `tool_calls` are NEVER empty for
|
|
598
|
+
* this purpose — those affordances are the body. A regular text-only
|
|
599
|
+
* assistant message with whitespace content is still hidden.
|
|
600
|
+
*/
|
|
601
|
+
function isMessageRenderEmpty(msg: AIHubMessage): boolean {
|
|
602
|
+
const text = (msg.content ?? "").trim();
|
|
603
|
+
if (text !== "") return false;
|
|
604
|
+
if (msg.role === "assistant") {
|
|
605
|
+
if (msg.reasoning && msg.reasoning.trim() !== "") return false;
|
|
606
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) return false;
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Single message bubble. Variant modifier is driven by `message.role`,
|
|
613
|
+
* with `--failed` / `--cancelled` overrides for the most-recent
|
|
614
|
+
* assistant bubble of the corresponding terminal status.
|
|
615
|
+
*
|
|
616
|
+
* For assistant bubbles, renders nested `<ReasoningView />` (when the
|
|
617
|
+
* message carries `reasoning`) and `<ToolCallView />` cards (one per
|
|
618
|
+
* `tool_calls` entry) before the visible text — matching the
|
|
619
|
+
* ui-sketches/03-chat.md layout.
|
|
620
|
+
*/
|
|
621
|
+
function MessageBubble(props: {
|
|
622
|
+
message: AIHubMessage;
|
|
623
|
+
classNames: MessageListClassNames;
|
|
624
|
+
isFailedAssistant: boolean;
|
|
625
|
+
isCancelledAssistant: boolean;
|
|
626
|
+
showRetryButton: boolean;
|
|
627
|
+
showReasoning: boolean;
|
|
628
|
+
showToolCalls: boolean;
|
|
629
|
+
retryLabel: string;
|
|
630
|
+
failedLabel: string;
|
|
631
|
+
cancelledStampLabel: string;
|
|
632
|
+
onRetry: () => void;
|
|
633
|
+
/**
|
|
634
|
+
* Resolver for the accordion + auto-expand state owned by
|
|
635
|
+
* `<MessageList />`. Returns whether a given block (`reasoning:<id>`
|
|
636
|
+
* or `tool:<tool_call_id>`) should render expanded.
|
|
637
|
+
*/
|
|
638
|
+
isBlockExpanded: (blockId: string) => boolean;
|
|
639
|
+
/** Toggle handler for the accordion. */
|
|
640
|
+
onToggleBlock: (blockId: string) => void;
|
|
641
|
+
texts?: DeepPartial<AIHubTexts>;
|
|
642
|
+
renderMessageActions?: AIHubMessageActionRenderer;
|
|
643
|
+
}): ReactNode {
|
|
644
|
+
const { message, classNames: cls } = props;
|
|
645
|
+
const baseModifier =
|
|
646
|
+
message.role === "user"
|
|
647
|
+
? cls.bubbleUser
|
|
648
|
+
: message.role === "assistant"
|
|
649
|
+
? cls.bubbleAssistant
|
|
650
|
+
: message.role === "system"
|
|
651
|
+
? cls.bubbleSystem
|
|
652
|
+
: cls.bubbleTool;
|
|
653
|
+
const stateModifier = props.isFailedAssistant
|
|
654
|
+
? cls.bubbleFailed
|
|
655
|
+
: props.isCancelledAssistant
|
|
656
|
+
? cls.bubbleCancelled
|
|
657
|
+
: null;
|
|
658
|
+
const klass = [cls.bubble, baseModifier, stateModifier]
|
|
659
|
+
.filter(Boolean)
|
|
660
|
+
.join(" ");
|
|
661
|
+
|
|
662
|
+
const actions =
|
|
663
|
+
props.renderMessageActions && message.role === "assistant"
|
|
664
|
+
? (props.renderMessageActions(message) as ReactNode)
|
|
665
|
+
: null;
|
|
666
|
+
|
|
667
|
+
const isAssistant = message.role === "assistant";
|
|
668
|
+
const reasoning = isAssistant && props.showReasoning ? message.reasoning : null;
|
|
669
|
+
const toolCalls = isAssistant && props.showToolCalls ? message.tool_calls : null;
|
|
670
|
+
|
|
671
|
+
return (
|
|
672
|
+
<div
|
|
673
|
+
className={klass}
|
|
674
|
+
data-testid="message-bubble"
|
|
675
|
+
data-role={message.role}
|
|
676
|
+
data-message-id={message.id}
|
|
677
|
+
data-failed={props.isFailedAssistant ? "true" : "false"}
|
|
678
|
+
data-cancelled={props.isCancelledAssistant ? "true" : "false"}
|
|
679
|
+
>
|
|
680
|
+
{reasoning ? (
|
|
681
|
+
(() => {
|
|
682
|
+
const blockId = reasoningBlockId(message.id);
|
|
683
|
+
return (
|
|
684
|
+
<ReasoningView
|
|
685
|
+
content={reasoning}
|
|
686
|
+
expanded={props.isBlockExpanded(blockId)}
|
|
687
|
+
onToggle={() => props.onToggleBlock(blockId)}
|
|
688
|
+
{...(props.texts !== undefined ? { texts: props.texts } : {})}
|
|
689
|
+
/>
|
|
690
|
+
);
|
|
691
|
+
})()
|
|
692
|
+
) : null}
|
|
693
|
+
{toolCalls?.map((call) => {
|
|
694
|
+
const blockId = toolCallBlockId(message.id, call.tool_call_id);
|
|
695
|
+
return (
|
|
696
|
+
<ToolCallView
|
|
697
|
+
key={call.tool_call_id}
|
|
698
|
+
toolName={call.tool_name}
|
|
699
|
+
status={call.status === "error" ? "error" : "done"}
|
|
700
|
+
input={call.input}
|
|
701
|
+
expanded={props.isBlockExpanded(blockId)}
|
|
702
|
+
onToggle={() => props.onToggleBlock(blockId)}
|
|
703
|
+
{...(call.output != null ? { output: call.output } : {})}
|
|
704
|
+
{...(call.error != null ? { error: call.error } : {})}
|
|
705
|
+
{...(props.texts !== undefined ? { texts: props.texts } : {})}
|
|
706
|
+
/>
|
|
707
|
+
);
|
|
708
|
+
})}
|
|
709
|
+
<span className={cls.bubbleBody}>
|
|
710
|
+
{isAssistant ? (
|
|
711
|
+
<MarkdownRenderer content={message.content} />
|
|
712
|
+
) : (
|
|
713
|
+
message.content.trim()
|
|
714
|
+
)}
|
|
715
|
+
</span>
|
|
716
|
+
{isAssistant && message.usage ? (
|
|
717
|
+
<CacheBadge usage={message.usage} />
|
|
718
|
+
) : null}
|
|
719
|
+
{props.isFailedAssistant ? (
|
|
720
|
+
<div className={cls.bubbleActions}>
|
|
721
|
+
<span>{props.failedLabel}</span>
|
|
722
|
+
{props.showRetryButton ? (
|
|
723
|
+
<button
|
|
724
|
+
type="button"
|
|
725
|
+
className={cls.bubbleRetryButton}
|
|
726
|
+
onClick={props.onRetry}
|
|
727
|
+
data-testid="message-bubble-retry"
|
|
728
|
+
>
|
|
729
|
+
{props.retryLabel}
|
|
730
|
+
</button>
|
|
731
|
+
) : null}
|
|
732
|
+
</div>
|
|
733
|
+
) : props.isCancelledAssistant ? (
|
|
734
|
+
<div className={cls.bubbleActions}>
|
|
735
|
+
<span
|
|
736
|
+
className={cls.bubbleCancelledStamp}
|
|
737
|
+
data-testid="message-bubble-cancelled-stamp"
|
|
738
|
+
>
|
|
739
|
+
{props.cancelledStampLabel}
|
|
740
|
+
</span>
|
|
741
|
+
</div>
|
|
742
|
+
) : null}
|
|
743
|
+
{actions ? (
|
|
744
|
+
<div
|
|
745
|
+
className={cls.bubbleActions}
|
|
746
|
+
data-testid="message-bubble-actions"
|
|
747
|
+
>
|
|
748
|
+
{actions}
|
|
749
|
+
</div>
|
|
750
|
+
) : null}
|
|
751
|
+
</div>
|
|
752
|
+
);
|
|
753
|
+
}
|