@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,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<SessionTitleModal />` — the modal used for both new-session creation
|
|
3
|
+
* and edit-title flow on existing sessions.
|
|
4
|
+
*
|
|
5
|
+
* Single text input ("Chat title"), `[Cancel]` / `[Create]` (new) or
|
|
6
|
+
* `[Cancel]` / `[Save]` (edit). Title is required and 1..80 chars —
|
|
7
|
+
* validated client-side before submit (the server's Zod schema enforces
|
|
8
|
+
* the same bounds for defense-in-depth).
|
|
9
|
+
*
|
|
10
|
+
* The modal is uncontrolled state-wise — `onSubmit` resolves with the
|
|
11
|
+
* server's returned `Session` on success; the caller (`SessionsPanel`)
|
|
12
|
+
* decides whether to close. We close on a successful submit by tying
|
|
13
|
+
* `open` back to the parent's state.
|
|
14
|
+
*
|
|
15
|
+
* @see SPECIFICATIONS.md §6.3.1 — sessions
|
|
16
|
+
* @see ui-sketches/03-chat.md — new_session_modal / edit_session_title_modal
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
useEffect,
|
|
21
|
+
useRef,
|
|
22
|
+
useState,
|
|
23
|
+
type FormEvent,
|
|
24
|
+
type ReactNode,
|
|
25
|
+
} from "react";
|
|
26
|
+
|
|
27
|
+
import type { AIHubChatTexts } from "../../auth/texts.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* BEM class names emitted by `<SessionTitleModal />`. Shared with the
|
|
31
|
+
* generic-modal styles in the default stylesheet so admin / settings
|
|
32
|
+
* modals layered in Steps 11+ can reuse them.
|
|
33
|
+
*/
|
|
34
|
+
export interface SessionTitleModalClassNames {
|
|
35
|
+
/** Overlay backdrop. */
|
|
36
|
+
overlay: string;
|
|
37
|
+
/** The modal card. */
|
|
38
|
+
modal: string;
|
|
39
|
+
/** Title bar. */
|
|
40
|
+
title: string;
|
|
41
|
+
/** Form. */
|
|
42
|
+
form: string;
|
|
43
|
+
/** Field group (label + input). */
|
|
44
|
+
fieldGroup: string;
|
|
45
|
+
/** Field label. */
|
|
46
|
+
fieldLabel: string;
|
|
47
|
+
/** Text input. */
|
|
48
|
+
input: string;
|
|
49
|
+
/** Inline-validation error under the input. */
|
|
50
|
+
fieldError: string;
|
|
51
|
+
/** Button row at the bottom of the modal. */
|
|
52
|
+
actions: string;
|
|
53
|
+
/** Cancel button. */
|
|
54
|
+
cancelButton: string;
|
|
55
|
+
/** Submit button. */
|
|
56
|
+
submitButton: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DEFAULT_CLASS_NAMES: SessionTitleModalClassNames = Object.freeze({
|
|
60
|
+
overlay: "data-club-ai-hub__modal-overlay",
|
|
61
|
+
modal: "data-club-ai-hub__modal",
|
|
62
|
+
title: "data-club-ai-hub__modal__title",
|
|
63
|
+
form: "data-club-ai-hub__modal__form",
|
|
64
|
+
fieldGroup: "data-club-ai-hub__modal__field",
|
|
65
|
+
fieldLabel: "data-club-ai-hub__modal__label",
|
|
66
|
+
input: "data-club-ai-hub__modal__input",
|
|
67
|
+
fieldError: "data-club-ai-hub__modal__field-error",
|
|
68
|
+
actions: "data-club-ai-hub__modal__actions",
|
|
69
|
+
cancelButton: "data-club-ai-hub__modal__cancel",
|
|
70
|
+
submitButton: "data-club-ai-hub__modal__confirm",
|
|
71
|
+
}) as SessionTitleModalClassNames;
|
|
72
|
+
|
|
73
|
+
/** The maximum title length enforced both server- and client-side. */
|
|
74
|
+
const TITLE_MAX = 80;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Props for `<SessionTitleModal />`.
|
|
78
|
+
*/
|
|
79
|
+
export interface SessionTitleModalProps {
|
|
80
|
+
/** Discriminates the title + submit-button label. */
|
|
81
|
+
mode: "create" | "edit";
|
|
82
|
+
/** Pre-filled value for edit-mode; empty string for create-mode. */
|
|
83
|
+
initialTitle?: string;
|
|
84
|
+
/** Whether the modal is currently visible. */
|
|
85
|
+
open: boolean;
|
|
86
|
+
/** Called when the user dismisses without saving. */
|
|
87
|
+
onCancel: () => void;
|
|
88
|
+
/**
|
|
89
|
+
* Submission handler. Should `await` the server call and throw on
|
|
90
|
+
* failure — the modal renders the error inline and stays open.
|
|
91
|
+
*/
|
|
92
|
+
onSubmit: (title: string) => Promise<void>;
|
|
93
|
+
/** Resolved chat texts. */
|
|
94
|
+
texts: AIHubChatTexts;
|
|
95
|
+
/** Per-element class overrides. */
|
|
96
|
+
classNames?: Partial<SessionTitleModalClassNames>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Renders the modal. Validates the input client-side and surfaces server
|
|
101
|
+
* errors inline.
|
|
102
|
+
*
|
|
103
|
+
* @param props - see `SessionTitleModalProps`.
|
|
104
|
+
* @returns the rendered modal, or `null` when `open === false`.
|
|
105
|
+
*/
|
|
106
|
+
export function SessionTitleModal(props: SessionTitleModalProps): ReactNode {
|
|
107
|
+
const {
|
|
108
|
+
mode,
|
|
109
|
+
initialTitle = "",
|
|
110
|
+
open,
|
|
111
|
+
onCancel,
|
|
112
|
+
onSubmit,
|
|
113
|
+
texts,
|
|
114
|
+
} = props;
|
|
115
|
+
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
116
|
+
|
|
117
|
+
const [title, setTitle] = useState<string>(initialTitle);
|
|
118
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
119
|
+
const [serverError, setServerError] = useState<string | null>(null);
|
|
120
|
+
const [submitting, setSubmitting] = useState(false);
|
|
121
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
122
|
+
|
|
123
|
+
// Re-seed on open / initialTitle change so reopening for a different
|
|
124
|
+
// session doesn't carry stale text.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (open) {
|
|
127
|
+
setTitle(initialTitle);
|
|
128
|
+
setValidationError(null);
|
|
129
|
+
setServerError(null);
|
|
130
|
+
setSubmitting(false);
|
|
131
|
+
// Defer focus to next tick so the input is mounted.
|
|
132
|
+
queueMicrotask(() => inputRef.current?.focus());
|
|
133
|
+
}
|
|
134
|
+
}, [open, initialTitle]);
|
|
135
|
+
|
|
136
|
+
if (!open) return null;
|
|
137
|
+
|
|
138
|
+
const trimmed = title.trim();
|
|
139
|
+
const isEmpty = trimmed.length === 0;
|
|
140
|
+
const tooLong = trimmed.length > TITLE_MAX;
|
|
141
|
+
const submitDisabled = isEmpty || tooLong || submitting;
|
|
142
|
+
|
|
143
|
+
function handleSubmit(e: FormEvent<HTMLFormElement>): void {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
if (isEmpty) {
|
|
146
|
+
setValidationError(texts.sessionTitleRequiredError);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (tooLong) {
|
|
150
|
+
setValidationError(texts.sessionTitleTooLongError);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
setValidationError(null);
|
|
154
|
+
setServerError(null);
|
|
155
|
+
setSubmitting(true);
|
|
156
|
+
onSubmit(trimmed)
|
|
157
|
+
.catch((err: unknown) => {
|
|
158
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
159
|
+
setServerError(msg || texts.genericRequestError);
|
|
160
|
+
})
|
|
161
|
+
.finally(() => setSubmitting(false));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const modalTitle =
|
|
165
|
+
mode === "create"
|
|
166
|
+
? texts.newSessionModalTitle
|
|
167
|
+
: texts.editSessionModalTitle;
|
|
168
|
+
const submitLabel =
|
|
169
|
+
mode === "create" ? texts.modalCreateButton : texts.modalSaveButton;
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div
|
|
173
|
+
className={cls.overlay}
|
|
174
|
+
role="presentation"
|
|
175
|
+
data-testid="session-title-modal-overlay"
|
|
176
|
+
onClick={(e) => {
|
|
177
|
+
// Click on the backdrop (not the modal itself) cancels.
|
|
178
|
+
if (e.target === e.currentTarget && !submitting) onCancel();
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<div
|
|
182
|
+
className={cls.modal}
|
|
183
|
+
role="dialog"
|
|
184
|
+
aria-modal="true"
|
|
185
|
+
aria-labelledby="data-club-ai-hub__session-title-modal__heading"
|
|
186
|
+
data-testid="session-title-modal"
|
|
187
|
+
data-mode={mode}
|
|
188
|
+
>
|
|
189
|
+
<h2
|
|
190
|
+
id="data-club-ai-hub__session-title-modal__heading"
|
|
191
|
+
className={cls.title}
|
|
192
|
+
>
|
|
193
|
+
{modalTitle}
|
|
194
|
+
</h2>
|
|
195
|
+
<form className={cls.form} onSubmit={handleSubmit} noValidate>
|
|
196
|
+
<div className={cls.fieldGroup}>
|
|
197
|
+
<label className={cls.fieldLabel} htmlFor="session-title-input">
|
|
198
|
+
{texts.sessionTitleLabel}
|
|
199
|
+
</label>
|
|
200
|
+
<input
|
|
201
|
+
id="session-title-input"
|
|
202
|
+
ref={inputRef}
|
|
203
|
+
type="text"
|
|
204
|
+
className={cls.input}
|
|
205
|
+
value={title}
|
|
206
|
+
onChange={(e) => {
|
|
207
|
+
setTitle(e.target.value);
|
|
208
|
+
if (validationError) setValidationError(null);
|
|
209
|
+
}}
|
|
210
|
+
placeholder={texts.sessionTitlePlaceholder}
|
|
211
|
+
maxLength={TITLE_MAX + 16 /* let the user paste over, validate on submit */}
|
|
212
|
+
autoComplete="off"
|
|
213
|
+
disabled={submitting}
|
|
214
|
+
data-testid="session-title-input"
|
|
215
|
+
/>
|
|
216
|
+
{validationError ? (
|
|
217
|
+
<p
|
|
218
|
+
className={cls.fieldError}
|
|
219
|
+
data-testid="session-title-validation-error"
|
|
220
|
+
>
|
|
221
|
+
{validationError}
|
|
222
|
+
</p>
|
|
223
|
+
) : null}
|
|
224
|
+
{serverError ? (
|
|
225
|
+
<p
|
|
226
|
+
className={cls.fieldError}
|
|
227
|
+
data-testid="session-title-server-error"
|
|
228
|
+
>
|
|
229
|
+
{serverError}
|
|
230
|
+
</p>
|
|
231
|
+
) : null}
|
|
232
|
+
</div>
|
|
233
|
+
<div className={cls.actions}>
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
className={cls.cancelButton}
|
|
237
|
+
onClick={onCancel}
|
|
238
|
+
disabled={submitting}
|
|
239
|
+
data-testid="session-title-cancel"
|
|
240
|
+
>
|
|
241
|
+
{texts.modalCancelButton}
|
|
242
|
+
</button>
|
|
243
|
+
<button
|
|
244
|
+
type="submit"
|
|
245
|
+
className={cls.submitButton}
|
|
246
|
+
disabled={submitDisabled}
|
|
247
|
+
data-testid="session-title-submit"
|
|
248
|
+
>
|
|
249
|
+
{submitLabel}
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</form>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<SessionsPanel />` — the left-side list of the caller's sessions for
|
|
3
|
+
* the currently-opened agent.
|
|
4
|
+
*
|
|
5
|
+
* Includes:
|
|
6
|
+
*
|
|
7
|
+
* - `new_session_button` → opens `SessionTitleModal` in create mode.
|
|
8
|
+
* - One `session_item` per row with hover-revealed `edit_title_button` +
|
|
9
|
+
* `archive_session_button` actions. Edit opens the same modal in
|
|
10
|
+
* `edit` mode pre-filled with the row's title. Archive uses a native
|
|
11
|
+
* `confirm()` (v1; future polish is a styled modal).
|
|
12
|
+
* - `archived_filter_toggle` at the bottom of the panel — flipping it
|
|
13
|
+
* re-fetches with `include_archived=true`. Archived rows render with
|
|
14
|
+
* the `--archived` modifier and their hover-actions swap to an
|
|
15
|
+
* Unarchive button.
|
|
16
|
+
* - Collapsible / resizable variants — Step 9 ships the collapsible
|
|
17
|
+
* reveal strip; resizable drag is layout-only and uses a CSS gutter
|
|
18
|
+
* the parent layout owns.
|
|
19
|
+
*
|
|
20
|
+
* **No delete action exists anywhere on this panel by design** — v1
|
|
21
|
+
* sessions are archive-only (§6.3.1).
|
|
22
|
+
*
|
|
23
|
+
* Exported under `AIHub.SessionsPanel`.
|
|
24
|
+
*
|
|
25
|
+
* @see SPECIFICATIONS.md §7.1 — exports
|
|
26
|
+
* @see SPECIFICATIONS.md §6.3.1 — sessions
|
|
27
|
+
* @see ui-sketches/03-chat.md — sessions_panel
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
resolveAIHubTexts,
|
|
34
|
+
type AIHubTexts,
|
|
35
|
+
type DeepPartial,
|
|
36
|
+
} from "../../auth/texts.js";
|
|
37
|
+
import { useAIHub } from "../../context/useAIHub.js";
|
|
38
|
+
import type { Session } from "../../types.js";
|
|
39
|
+
|
|
40
|
+
import { SessionTitleModal } from "./SessionTitleModal.js";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* BEM class names emitted by `<SessionsPanel />`.
|
|
44
|
+
*/
|
|
45
|
+
export interface SessionsPanelClassNames {
|
|
46
|
+
/** Outer panel container (the column). */
|
|
47
|
+
root: string;
|
|
48
|
+
/** Modifier applied when the panel is collapsed. */
|
|
49
|
+
rootCollapsed: string;
|
|
50
|
+
/** Optional custom-header slot wrapper at the top of the panel. */
|
|
51
|
+
customHeader: string;
|
|
52
|
+
/** "+ New chat" button. */
|
|
53
|
+
newSessionButton: string;
|
|
54
|
+
/** Reveal-strip rendered when collapsed. */
|
|
55
|
+
reveal: string;
|
|
56
|
+
/** Collapse / expand toggle. */
|
|
57
|
+
collapseToggle: string;
|
|
58
|
+
/** Scrollable list region. */
|
|
59
|
+
list: string;
|
|
60
|
+
/** Empty-state line ("No chats yet."). */
|
|
61
|
+
empty: string;
|
|
62
|
+
/** Loading state. */
|
|
63
|
+
loading: string;
|
|
64
|
+
/** A session-list row. */
|
|
65
|
+
item: string;
|
|
66
|
+
/** Modifier applied to the currently-selected row. */
|
|
67
|
+
itemActive: string;
|
|
68
|
+
/** Modifier applied to archived rows. */
|
|
69
|
+
itemArchived: string;
|
|
70
|
+
/** Row title text. */
|
|
71
|
+
itemTitle: string;
|
|
72
|
+
/** Row meta (relative timestamp). */
|
|
73
|
+
itemMeta: string;
|
|
74
|
+
/** Row hover-action group wrapper. */
|
|
75
|
+
itemActions: string;
|
|
76
|
+
/** A single action button (edit / archive / unarchive). */
|
|
77
|
+
actionButton: string;
|
|
78
|
+
/** Footer wrapper holding the archived-filter toggle + custom footer. */
|
|
79
|
+
footer: string;
|
|
80
|
+
/** Show-archived toggle. */
|
|
81
|
+
archivedToggle: string;
|
|
82
|
+
/** Optional custom-footer slot wrapper. */
|
|
83
|
+
customFooter: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const DEFAULT_CLASS_NAMES: SessionsPanelClassNames = Object.freeze({
|
|
87
|
+
root: "data-club-ai-hub__sessions-panel",
|
|
88
|
+
rootCollapsed: "data-club-ai-hub__sessions-panel--collapsed",
|
|
89
|
+
customHeader: "data-club-ai-hub__sessions-panel__custom-header",
|
|
90
|
+
newSessionButton: "data-club-ai-hub__sessions-panel__new-button",
|
|
91
|
+
reveal: "data-club-ai-hub__sessions-panel__reveal",
|
|
92
|
+
collapseToggle: "data-club-ai-hub__sessions-panel__collapse-toggle",
|
|
93
|
+
list: "data-club-ai-hub__sessions-panel__list",
|
|
94
|
+
empty: "data-club-ai-hub__sessions-panel__empty",
|
|
95
|
+
loading: "data-club-ai-hub__sessions-panel__loading",
|
|
96
|
+
item: "data-club-ai-hub__sessions-panel__item",
|
|
97
|
+
itemActive: "data-club-ai-hub__sessions-panel__item--active",
|
|
98
|
+
itemArchived: "data-club-ai-hub__sessions-panel__item--archived",
|
|
99
|
+
itemTitle: "data-club-ai-hub__sessions-panel__item-title",
|
|
100
|
+
itemMeta: "data-club-ai-hub__sessions-panel__item-meta",
|
|
101
|
+
itemActions: "data-club-ai-hub__sessions-panel__item-actions",
|
|
102
|
+
actionButton: "data-club-ai-hub__sessions-panel__action-button",
|
|
103
|
+
footer: "data-club-ai-hub__sessions-panel__footer",
|
|
104
|
+
archivedToggle: "data-club-ai-hub__sessions-panel__archived-toggle",
|
|
105
|
+
customFooter: "data-club-ai-hub__sessions-panel__custom-footer",
|
|
106
|
+
}) as SessionsPanelClassNames;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Props for `<SessionsPanel />`.
|
|
110
|
+
*/
|
|
111
|
+
export interface SessionsPanelProps {
|
|
112
|
+
/** Whether to render the "+ New chat" button. Defaults to true. */
|
|
113
|
+
showCreateButton?: boolean;
|
|
114
|
+
/** Whether the panel can be collapsed. Defaults to true. */
|
|
115
|
+
collapsible?: boolean;
|
|
116
|
+
/** Top-of-panel slot. */
|
|
117
|
+
customHeader?: ReactNode;
|
|
118
|
+
/** Bottom-of-panel slot, rendered above the archived-filter toggle. */
|
|
119
|
+
customFooter?: ReactNode;
|
|
120
|
+
/** Extra class merged onto the root. */
|
|
121
|
+
className?: string;
|
|
122
|
+
/** Per-element class overrides. */
|
|
123
|
+
classNames?: Partial<SessionsPanelClassNames>;
|
|
124
|
+
/** Wrapper-supplied i18n. */
|
|
125
|
+
texts?: DeepPartial<AIHubTexts>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Formats an ISO-8601 timestamp into a short "N min ago" / "yesterday" /
|
|
130
|
+
* "N days ago" line. Intentionally minimal — i18n libraries can take this
|
|
131
|
+
* over later via the `texts.*` map if needed.
|
|
132
|
+
*
|
|
133
|
+
* @param iso - ISO timestamp.
|
|
134
|
+
* @returns the short relative-time string.
|
|
135
|
+
*/
|
|
136
|
+
function formatRelative(iso: string): string {
|
|
137
|
+
const then = Date.parse(iso);
|
|
138
|
+
if (Number.isNaN(then)) return "";
|
|
139
|
+
const diffMs = Date.now() - then;
|
|
140
|
+
const minutes = Math.round(diffMs / 60_000);
|
|
141
|
+
if (minutes < 1) return "just now";
|
|
142
|
+
if (minutes === 1) return "1 min ago";
|
|
143
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
144
|
+
const hours = Math.round(minutes / 60);
|
|
145
|
+
if (hours === 1) return "1 hr ago";
|
|
146
|
+
if (hours < 24) return `${hours} hr ago`;
|
|
147
|
+
const days = Math.round(hours / 24);
|
|
148
|
+
if (days === 1) return "yesterday";
|
|
149
|
+
return `${days} days ago`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Renders the sessions panel.
|
|
154
|
+
*
|
|
155
|
+
* @param props - see `SessionsPanelProps`.
|
|
156
|
+
* @returns the rendered panel.
|
|
157
|
+
*/
|
|
158
|
+
export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
159
|
+
const {
|
|
160
|
+
sessions,
|
|
161
|
+
sessionsLoading,
|
|
162
|
+
activeSessionId,
|
|
163
|
+
setActiveSession,
|
|
164
|
+
createSession,
|
|
165
|
+
patchSession,
|
|
166
|
+
includeArchivedSessions,
|
|
167
|
+
setIncludeArchivedSessions,
|
|
168
|
+
} = useAIHub();
|
|
169
|
+
const texts = resolveAIHubTexts(props.texts);
|
|
170
|
+
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
171
|
+
const showCreate = props.showCreateButton ?? true;
|
|
172
|
+
const collapsible = props.collapsible ?? true;
|
|
173
|
+
|
|
174
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
175
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
176
|
+
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
|
177
|
+
|
|
178
|
+
const editingSession = useMemo<Session | null>(() => {
|
|
179
|
+
if (!editingSessionId) return null;
|
|
180
|
+
return sessions.find((s) => s.id === editingSessionId) ?? null;
|
|
181
|
+
}, [editingSessionId, sessions]);
|
|
182
|
+
|
|
183
|
+
// Stale-modal guard: if the row being edited gets archived/removed under
|
|
184
|
+
// us, close the modal so we don't submit edits against a stale id.
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (editingSessionId && !editingSession) {
|
|
187
|
+
setEditingSessionId(null);
|
|
188
|
+
}
|
|
189
|
+
}, [editingSessionId, editingSession]);
|
|
190
|
+
|
|
191
|
+
if (collapsed) {
|
|
192
|
+
return (
|
|
193
|
+
<div
|
|
194
|
+
className={`${cls.root} ${cls.rootCollapsed}`}
|
|
195
|
+
data-testid="sessions-panel"
|
|
196
|
+
data-collapsed="true"
|
|
197
|
+
>
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
className={cls.reveal}
|
|
201
|
+
onClick={() => setCollapsed(false)}
|
|
202
|
+
aria-label={texts.chat.expandSessionsPanel}
|
|
203
|
+
data-testid="sessions-panel-reveal"
|
|
204
|
+
>
|
|
205
|
+
{/* "≫" — chevron-right glyph. SVG would be more polished, the
|
|
206
|
+
text glyph keeps the bundle small and is accessible via
|
|
207
|
+
aria-label. */}
|
|
208
|
+
≫
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const rootClass = [cls.root, props.className].filter(Boolean).join(" ");
|
|
215
|
+
|
|
216
|
+
async function handleArchive(session: Session): Promise<void> {
|
|
217
|
+
const prompt = texts.chat.archiveConfirmPrompt.replace(
|
|
218
|
+
"{title}",
|
|
219
|
+
session.title,
|
|
220
|
+
);
|
|
221
|
+
// eslint-disable-next-line no-alert
|
|
222
|
+
if (typeof window !== "undefined" && !window.confirm(prompt)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
await patchSession(session.id, { isArchived: true });
|
|
227
|
+
} catch {
|
|
228
|
+
/* surfaced via provider's onError; row stays */
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function handleUnarchive(session: Session): Promise<void> {
|
|
233
|
+
try {
|
|
234
|
+
await patchSession(session.id, { isArchived: false });
|
|
235
|
+
} catch {
|
|
236
|
+
/* surfaced via provider's onError */
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<aside
|
|
242
|
+
className={rootClass}
|
|
243
|
+
data-testid="sessions-panel"
|
|
244
|
+
aria-label="Sessions"
|
|
245
|
+
>
|
|
246
|
+
{props.customHeader ? (
|
|
247
|
+
<div className={cls.customHeader} data-testid="sessions-panel-custom-header">
|
|
248
|
+
{props.customHeader}
|
|
249
|
+
</div>
|
|
250
|
+
) : null}
|
|
251
|
+
{showCreate ? (
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
className={cls.newSessionButton}
|
|
255
|
+
onClick={() => {
|
|
256
|
+
setEditingSessionId(null);
|
|
257
|
+
setModalOpen(true);
|
|
258
|
+
}}
|
|
259
|
+
data-testid="sessions-panel-new-button"
|
|
260
|
+
>
|
|
261
|
+
{texts.chat.newSessionButton}
|
|
262
|
+
</button>
|
|
263
|
+
) : null}
|
|
264
|
+
{collapsible ? (
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
className={cls.collapseToggle}
|
|
268
|
+
onClick={() => setCollapsed(true)}
|
|
269
|
+
aria-label={texts.chat.collapseSessionsPanel}
|
|
270
|
+
data-testid="sessions-panel-collapse"
|
|
271
|
+
>
|
|
272
|
+
≪
|
|
273
|
+
</button>
|
|
274
|
+
) : null}
|
|
275
|
+
{sessionsLoading && sessions.length === 0 ? (
|
|
276
|
+
<p className={cls.loading} data-testid="sessions-panel-loading">
|
|
277
|
+
{texts.chat.sessionsLoading}
|
|
278
|
+
</p>
|
|
279
|
+
) : null}
|
|
280
|
+
{!sessionsLoading && sessions.length === 0 ? (
|
|
281
|
+
<p className={cls.empty} data-testid="sessions-panel-empty">
|
|
282
|
+
{texts.chat.sessionsEmpty}
|
|
283
|
+
</p>
|
|
284
|
+
) : null}
|
|
285
|
+
<ul className={cls.list} data-testid="sessions-panel-list">
|
|
286
|
+
{sessions.map((session) => {
|
|
287
|
+
const isActive = session.id === activeSessionId;
|
|
288
|
+
const itemClasses = [
|
|
289
|
+
cls.item,
|
|
290
|
+
isActive ? cls.itemActive : "",
|
|
291
|
+
session.is_archived ? cls.itemArchived : "",
|
|
292
|
+
]
|
|
293
|
+
.filter(Boolean)
|
|
294
|
+
.join(" ");
|
|
295
|
+
const displayTitle =
|
|
296
|
+
session.title.trim().length > 0
|
|
297
|
+
? session.title
|
|
298
|
+
: texts.chat.untitledSessionFallback;
|
|
299
|
+
return (
|
|
300
|
+
<li
|
|
301
|
+
key={session.id}
|
|
302
|
+
className={itemClasses}
|
|
303
|
+
data-testid="session-item"
|
|
304
|
+
data-active={isActive ? "true" : "false"}
|
|
305
|
+
data-archived={session.is_archived ? "true" : "false"}
|
|
306
|
+
data-session-id={session.id}
|
|
307
|
+
>
|
|
308
|
+
<button
|
|
309
|
+
type="button"
|
|
310
|
+
className={cls.itemTitle}
|
|
311
|
+
onClick={() => setActiveSession(session.id)}
|
|
312
|
+
data-testid="session-item-open"
|
|
313
|
+
>
|
|
314
|
+
<span>{displayTitle}</span>
|
|
315
|
+
<span className={cls.itemMeta}>
|
|
316
|
+
{formatRelative(session.updated_at)}
|
|
317
|
+
</span>
|
|
318
|
+
</button>
|
|
319
|
+
<div className={cls.itemActions} data-testid="session-item-actions">
|
|
320
|
+
{!session.is_archived ? (
|
|
321
|
+
<>
|
|
322
|
+
<button
|
|
323
|
+
type="button"
|
|
324
|
+
className={cls.actionButton}
|
|
325
|
+
aria-label={texts.chat.editSessionTitleAriaLabel}
|
|
326
|
+
onClick={() => {
|
|
327
|
+
setEditingSessionId(session.id);
|
|
328
|
+
setModalOpen(true);
|
|
329
|
+
}}
|
|
330
|
+
data-testid="session-item-edit"
|
|
331
|
+
>
|
|
332
|
+
✎
|
|
333
|
+
</button>
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
className={cls.actionButton}
|
|
337
|
+
aria-label={texts.chat.archiveSessionAriaLabel}
|
|
338
|
+
onClick={() => {
|
|
339
|
+
void handleArchive(session);
|
|
340
|
+
}}
|
|
341
|
+
data-testid="session-item-archive"
|
|
342
|
+
>
|
|
343
|
+
🗄
|
|
344
|
+
</button>
|
|
345
|
+
</>
|
|
346
|
+
) : (
|
|
347
|
+
<button
|
|
348
|
+
type="button"
|
|
349
|
+
className={cls.actionButton}
|
|
350
|
+
aria-label={texts.chat.unarchiveSessionAriaLabel}
|
|
351
|
+
onClick={() => {
|
|
352
|
+
void handleUnarchive(session);
|
|
353
|
+
}}
|
|
354
|
+
data-testid="session-item-unarchive"
|
|
355
|
+
>
|
|
356
|
+
⤴
|
|
357
|
+
</button>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
</li>
|
|
361
|
+
);
|
|
362
|
+
})}
|
|
363
|
+
</ul>
|
|
364
|
+
<div className={cls.footer}>
|
|
365
|
+
{props.customFooter ? (
|
|
366
|
+
<div
|
|
367
|
+
className={cls.customFooter}
|
|
368
|
+
data-testid="sessions-panel-custom-footer"
|
|
369
|
+
>
|
|
370
|
+
{props.customFooter}
|
|
371
|
+
</div>
|
|
372
|
+
) : null}
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
className={cls.archivedToggle}
|
|
376
|
+
onClick={() => setIncludeArchivedSessions(!includeArchivedSessions)}
|
|
377
|
+
aria-pressed={includeArchivedSessions}
|
|
378
|
+
data-testid="sessions-panel-archived-toggle"
|
|
379
|
+
>
|
|
380
|
+
{includeArchivedSessions
|
|
381
|
+
? texts.chat.hideArchivedToggle
|
|
382
|
+
: texts.chat.showArchivedToggle}
|
|
383
|
+
</button>
|
|
384
|
+
</div>
|
|
385
|
+
<SessionTitleModal
|
|
386
|
+
mode={editingSession ? "edit" : "create"}
|
|
387
|
+
open={modalOpen}
|
|
388
|
+
initialTitle={editingSession?.title ?? ""}
|
|
389
|
+
onCancel={() => {
|
|
390
|
+
setModalOpen(false);
|
|
391
|
+
setEditingSessionId(null);
|
|
392
|
+
}}
|
|
393
|
+
onSubmit={async (title) => {
|
|
394
|
+
if (editingSession) {
|
|
395
|
+
await patchSession(editingSession.id, { title });
|
|
396
|
+
} else {
|
|
397
|
+
await createSession(title);
|
|
398
|
+
}
|
|
399
|
+
setModalOpen(false);
|
|
400
|
+
setEditingSessionId(null);
|
|
401
|
+
}}
|
|
402
|
+
texts={texts.chat}
|
|
403
|
+
/>
|
|
404
|
+
</aside>
|
|
405
|
+
);
|
|
406
|
+
}
|