@micha.bigler/ui-core-micha 3.13.2 → 3.14.4

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.
Files changed (77) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/.github/workflows/publish.yml +4 -4
  3. package/CHANGELOG.md +35 -0
  4. package/WORK_ORDERS.md +7 -1
  5. package/dist/auth/UserMenu.js +1 -1
  6. package/dist/auth/apiClient.js +7 -6
  7. package/dist/chat/ChatComposer.js +23 -0
  8. package/dist/chat/ChatList.js +30 -0
  9. package/dist/chat/ChatThread.js +70 -0
  10. package/dist/chat/ChatWorkspace.js +218 -0
  11. package/dist/chat/MarkdownReply.js +21 -0
  12. package/dist/chat/ModelPicker.js +35 -0
  13. package/dist/chat/api.js +17 -0
  14. package/dist/chat/useChatSocket.js +68 -0
  15. package/dist/chat/wire.js +7 -0
  16. package/dist/components/LoginForm.js +1 -1
  17. package/dist/components/SecurityComponent.js +1 -1
  18. package/dist/components/UserListComponent.js +2 -8
  19. package/dist/components/charts/ChartFrame.js +1 -1
  20. package/dist/components/charts/ScatterChart.js +2 -1
  21. package/dist/i18n/chatTranslations.js +55 -0
  22. package/dist/i18n/uiCoreTranslations.js +2 -1
  23. package/dist/index.js +10 -0
  24. package/dist/layout/SectionNav.js +1 -1
  25. package/dist/messaging/AttachmentList.js +2 -2
  26. package/dist/messaging/Composer.js +2 -1
  27. package/dist/messaging/ConversationLaunchers.js +1 -4
  28. package/dist/messaging/ConversationList.js +1 -1
  29. package/dist/messaging/DirectMessageLauncher.js +1 -1
  30. package/dist/messaging/MessagingProvider.js +0 -0
  31. package/dist/messaging/PollCard.js +7 -5
  32. package/dist/messaging/ReadTicks.js +2 -2
  33. package/dist/messaging/Thread.js +5 -3
  34. package/dist/notifications/NotificationBell.js +4 -7
  35. package/dist/notifications/NotificationSettings.js +1 -1
  36. package/dist/onboarding/OnboardingProvider.js +3 -6
  37. package/dist/pages/LoginPage.js +1 -1
  38. package/dist/pages/PasswordChangePage.js +1 -1
  39. package/dist/pages/PasswordInvitePage.js +1 -1
  40. package/dist/pages/PasswordResetRequestPage.js +1 -1
  41. package/dist/pages/SignUpPage.js +1 -1
  42. package/dist/pages/SignupConfirmPage.js +1 -1
  43. package/dist/theme/createAppTheme.js +5 -4
  44. package/dist/theme/kitSxRegistry.js +10 -0
  45. package/dist/theme/themeCompleteness.js +3 -12
  46. package/dist/utils/auth-errors.js +4 -3
  47. package/package.json +13 -11
  48. package/src/chat/ChatComposer.jsx +36 -0
  49. package/src/chat/ChatList.jsx +87 -0
  50. package/src/chat/ChatThread.jsx +128 -0
  51. package/src/chat/ChatWorkspace.jsx +244 -0
  52. package/src/chat/MarkdownReply.jsx +22 -0
  53. package/src/chat/ModelPicker.jsx +85 -0
  54. package/src/chat/api.js +18 -0
  55. package/src/chat/useChatSocket.js +32 -0
  56. package/src/chat/wire.js +7 -0
  57. package/src/i18n/chatTranslations.ts +55 -0
  58. package/src/i18n/uiCoreTranslations.js +2 -0
  59. package/src/index.js +11 -0
  60. package/src/theme/kitSxRegistry.js +10 -0
  61. package/tests/chatApi.test.js +48 -0
  62. package/tests/chatDriverNames.test.jsx +71 -0
  63. package/tests/chatExports.test.js +3 -0
  64. package/tests/chatList.test.jsx +84 -0
  65. package/tests/chatPicker.test.jsx +82 -0
  66. package/tests/chatSocket.test.jsx +26 -0
  67. package/tests/chatThread.test.jsx +116 -0
  68. package/tests/chatTranslations.test.js +47 -0
  69. package/tests/chatWorkspace.test.jsx +234 -0
  70. package/tests/i18nAggregate.test.js +2 -0
  71. package/tsconfig.build.json +1 -1
  72. package/work-orders/UCM-AUTH-13.md +2 -0
  73. package/work-orders/UCM-CHAT-1.md +190 -0
  74. package/work-orders/UCM-CHAT-2.md +75 -0
  75. package/work-orders/UCM-CI-1.md +65 -0
  76. package/work-orders/UCM-DEP-1.md +63 -0
  77. package/work-orders/assets/UCM-CHAT-1-ai-chat.html +447 -0
@@ -0,0 +1,35 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useMemo, useRef, useState } from 'react';
3
+ import { Box, Button, Chip, Drawer, List, ListItemButton, ListItemText, Popover, Stack, ToggleButton, ToggleButtonGroup, Typography, useMediaQuery, useTheme } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ export const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'];
6
+ export const driverName = (t, id) => t(`Chat.driver.${id}`, { defaultValue: id });
7
+ const groupByDriver = (entries) => entries.reduce((groups, entry) => {
8
+ (groups[entry.driver] = groups[entry.driver] || []).push(entry);
9
+ return groups;
10
+ }, {});
11
+ /**
12
+ * Model and effort of one chat. A popover at wide widths, a bottom sheet at narrow widths. `onChange` receives
13
+ * `{ entry, effort }`; the caller persists it (the change applies from the next turn). Can be controlled with `open`.
14
+ */
15
+ export function ModelPicker({ entries = [], selection, onChange, disabled = false, open, onOpenChange }) {
16
+ const { t } = useTranslation();
17
+ const theme = useTheme();
18
+ const narrow = useMediaQuery(theme.breakpoints.down('md'));
19
+ const anchor = useRef(null);
20
+ const [innerOpen, setInnerOpen] = useState(false);
21
+ const isOpen = open !== null && open !== void 0 ? open : innerOpen;
22
+ const setOpen = (value) => { setInnerOpen(value); onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(value); };
23
+ const groups = useMemo(() => groupByDriver(entries), [entries]);
24
+ const active = entries.find((entry) => entry.id === (selection === null || selection === void 0 ? void 0 : selection.entry));
25
+ const pickEntry = (entry) => {
26
+ var _a, _b;
27
+ const effort = ((_a = entry.efforts) === null || _a === void 0 ? void 0 : _a.includes(selection === null || selection === void 0 ? void 0 : selection.effort)) ? selection.effort : (_b = entry.efforts) === null || _b === void 0 ? void 0 : _b[0];
28
+ onChange === null || onChange === void 0 ? void 0 : onChange({ entry: entry.id, effort });
29
+ };
30
+ const content = (_jsx(Box, { sx: { p: 2, minWidth: narrow ? 0 : 320, maxWidth: '100%' }, children: _jsxs(Stack, { spacing: 1.5, children: [Object.entries(groups).map(([driver, items]) => (_jsxs(Box, { role: "group", "aria-label": driverName(t, driver), children: [_jsx(Typography, { variant: "overline", color: "text.secondary", children: driverName(t, driver) }), _jsx(List, { dense: true, disablePadding: true, children: items.map((entry) => (_jsxs(ListItemButton, { selected: entry.id === (selection === null || selection === void 0 ? void 0 : selection.entry), onClick: () => pickEntry(entry), sx: { minHeight: 40, borderRadius: 1 }, children: [_jsx(ListItemText, { primary: entry.label || entry.model }), entry.billing === 'pay_per_use' ? _jsx(Chip, { size: "small", variant: "outlined", label: t('Chat.payPerUse') }) : null] }, entry.id))) })] }, driver))), _jsxs(Box, { children: [_jsx(Typography, { variant: "overline", color: "text.secondary", children: t('Chat.effortLabel') }), _jsx(ToggleButtonGroup, { exclusive: true, fullWidth: true, size: "small", "aria-label": t('Chat.effortLabel'), value: (selection === null || selection === void 0 ? void 0 : selection.effort) || null, onChange: (_event, effort) => { if (effort && active)
31
+ onChange === null || onChange === void 0 ? void 0 : onChange({ entry: active.id, effort }); }, children: EFFORT_LEVELS.map((effort) => { var _a; return (_jsx(ToggleButton, { value: effort, disabled: !((_a = active === null || active === void 0 ? void 0 : active.efforts) === null || _a === void 0 ? void 0 : _a.includes(effort)), sx: { minHeight: 40 }, children: t(`Chat.effort.${effort}`) }, effort)); }) })] })] }) }));
32
+ const label = active ? [active.label || active.model, (selection === null || selection === void 0 ? void 0 : selection.effort) && t(`Chat.effort.${selection.effort}`)].filter(Boolean).join(' · ') : t('Chat.chooseModel');
33
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { ref: anchor, size: "small", color: "inherit", disabled: disabled || entries.length === 0, onClick: () => setOpen(true), "aria-haspopup": "dialog", children: label }), narrow ? (_jsx(Drawer, { anchor: "bottom", open: isOpen, onClose: () => setOpen(false), children: content })) : (_jsx(Popover, { open: isOpen, anchorEl: anchor.current, onClose: () => setOpen(false), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, children: content }))] }));
34
+ }
35
+ export default ModelPicker;
@@ -0,0 +1,17 @@
1
+ import apiClient from '../auth/apiClient';
2
+ import { chatApiUrls } from './wire';
3
+ export function createChatApi(overrides = {}, { apiBase = '/api/assistant/' } = {}) {
4
+ const urls = chatApiUrls(apiBase);
5
+ const data = (request) => request.then((response) => response.data);
6
+ const defaults = {
7
+ getCatalogue: () => data(apiClient.get(urls.catalogue)),
8
+ listChats: ({ archived = false, cursor } = {}) => data(apiClient.get(urls.chats, { params: Object.assign({ archived }, (cursor ? { cursor } : {})) })),
9
+ createChat: (payload = {}) => data(apiClient.post(urls.chats, payload)),
10
+ updateChat: (id, patch) => data(apiClient.patch(urls.chat(id), patch)),
11
+ deleteChat: (id) => data(apiClient.delete(urls.chat(id))),
12
+ listTurns: (id, { cursor } = {}) => data(apiClient.get(urls.turns(id), { params: cursor ? { cursor } : {} })),
13
+ getStatus: () => data(apiClient.get(urls.status)),
14
+ };
15
+ return Object.assign(Object.assign({}, defaults), overrides);
16
+ }
17
+ export const defaultChatApi = createChatApi();
@@ -0,0 +1,68 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { CHAT_CLOSE_CODES, defaultChatWsUrl } from './wire';
3
+ const delays = [1000, 2000, 4000];
4
+ export function useChatSocket(chatId, { wsUrl = defaultChatWsUrl, onFrame, onOpen, onClose } = {}) {
5
+ const socket = useRef(null);
6
+ const callbacks = useRef({ onFrame, onOpen, onClose });
7
+ callbacks.current = { onFrame, onOpen, onClose };
8
+ // The URL builder is read at connect time only: an inline arrow from the caller must not reopen the socket on every render.
9
+ const urlBuilder = useRef(wsUrl);
10
+ urlBuilder.current = wsUrl;
11
+ const [status, setStatus] = useState('idle');
12
+ const [generation, setGeneration] = useState(0);
13
+ const reconnect = useCallback(() => setGeneration((n) => n + 1), []);
14
+ useEffect(() => {
15
+ if (!chatId || typeof WebSocket === 'undefined') {
16
+ setStatus('idle');
17
+ return undefined;
18
+ }
19
+ let alive = true;
20
+ let tries = 0;
21
+ let timer;
22
+ let wasOpened = false;
23
+ const connect = () => {
24
+ setStatus('connecting');
25
+ const ws = new WebSocket(typeof urlBuilder.current === 'function' ? urlBuilder.current(chatId) : urlBuilder.current);
26
+ socket.current = ws;
27
+ ws.onopen = () => { var _a, _b; if (!alive)
28
+ return; tries = 0; setStatus('open'); (_b = (_a = callbacks.current).onOpen) === null || _b === void 0 ? void 0 : _b.call(_a, chatId, wasOpened); wasOpened = true; };
29
+ ws.onmessage = (event) => { var _a, _b; if (!alive)
30
+ return; try {
31
+ const frame = JSON.parse(event.data);
32
+ if (frame && typeof frame === 'object')
33
+ (_b = (_a = callbacks.current).onFrame) === null || _b === void 0 ? void 0 : _b.call(_a, frame, chatId);
34
+ }
35
+ catch (_c) { /* Ignore malformed frames. */ } };
36
+ ws.onclose = (event) => {
37
+ var _a, _b, _c, _d, _e, _f;
38
+ if (!alive || socket.current !== ws)
39
+ return;
40
+ socket.current = null;
41
+ const reason = CHAT_CLOSE_CODES[event.code];
42
+ if (reason) {
43
+ setStatus('closed');
44
+ (_b = (_a = callbacks.current).onClose) === null || _b === void 0 ? void 0 : _b.call(_a, { reason, chatId, retrying: false });
45
+ return;
46
+ }
47
+ if (tries < delays.length) {
48
+ setStatus('connecting');
49
+ (_d = (_c = callbacks.current).onClose) === null || _d === void 0 ? void 0 : _d.call(_c, { reason: 'connection_lost', chatId, retrying: true });
50
+ timer = setTimeout(connect, delays[tries++]);
51
+ }
52
+ else {
53
+ setStatus('closed');
54
+ (_f = (_e = callbacks.current).onClose) === null || _f === void 0 ? void 0 : _f.call(_e, { reason: 'connection_lost', chatId, retrying: false });
55
+ }
56
+ };
57
+ };
58
+ connect();
59
+ return () => { alive = false; clearTimeout(timer); const ws = socket.current; socket.current = null; if (ws) {
60
+ ws.onopen = ws.onmessage = ws.onclose = null;
61
+ ws.close();
62
+ } };
63
+ }, [chatId, generation]);
64
+ const send = useCallback((frame) => { const ws = socket.current; if (!ws || ws.readyState !== WebSocket.OPEN)
65
+ return false; ws.send(JSON.stringify(frame)); return true; }, []);
66
+ return { status, connected: status === 'open', send, reconnect };
67
+ }
68
+ export default useChatSocket;
@@ -0,0 +1,7 @@
1
+ export const CHAT_CLOSE_CODES = { 4401: 'session_ended', 4403: 'access_lost', 4429: 'rate_limited' };
2
+ export const CHAT_ERROR_CODES = ['quota_exceeded', 'provider_error', 'timeout', 'prompt_too_long', 'invalid_output', 'security_violation', 'not_configured', 'rate_limited', 'invalid_selection', 'archived', 'turn_active', 'invalid_frame'];
3
+ export const chatApiUrls = (base = '/api/assistant/') => {
4
+ const root = base.replace(/\/$/, '') + '/';
5
+ return { catalogue: root + 'catalogue/', chats: root + 'chats/', chat: (id) => root + 'chats/' + encodeURIComponent(id) + '/', turns: (id) => root + 'chats/' + encodeURIComponent(id) + '/turns/', status: root + 'status/' };
6
+ };
7
+ export const defaultChatWsUrl = (chatId) => (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + '/ws/assistant/chats/' + encodeURIComponent(chatId) + '/';
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useState, useEffect } from 'react';
3
3
  import { Box, TextField, Button, Typography, Divider, } from '@mui/material';
4
4
  import { useTranslation } from 'react-i18next';
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/auth/components/SecurityComponent.jsx
3
3
  import React, { useContext, useMemo, useState } from 'react';
4
4
  import { Box, Typography, Divider, Alert, } from '@mui/material';
@@ -265,10 +265,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
265
265
  align: 'center',
266
266
  headerAlign: 'center',
267
267
  valueGetter: (row) => { var _a; return Boolean(((_a = row === null || row === void 0 ? void 0 : row.profile) === null || _a === void 0 ? void 0 : _a.is_new) || (row === null || row === void 0 ? void 0 : row.is_new)); },
268
- renderCell: (row) => {
269
- var _a;
270
- return renderBooleanStatusIcon(Boolean(((_a = row === null || row === void 0 ? void 0 : row.profile) === null || _a === void 0 ? void 0 : _a.is_new) || (row === null || row === void 0 ? void 0 : row.is_new)), t('Common.YES', 'Yes'), t('Common.NO', 'No'));
271
- },
268
+ renderCell: (row) => { var _a; return renderBooleanStatusIcon(Boolean(((_a = row === null || row === void 0 ? void 0 : row.profile) === null || _a === void 0 ? void 0 : _a.is_new) || (row === null || row === void 0 ? void 0 : row.is_new)), t('Common.YES', 'Yes'), t('Common.NO', 'No')); },
272
269
  });
273
270
  }
274
271
  if (showSuccessfulLoginColumn) {
@@ -281,10 +278,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
281
278
  align: 'center',
282
279
  headerAlign: 'center',
283
280
  valueGetter: (row) => { var _a; return Boolean((_a = row === null || row === void 0 ? void 0 : row.successful_login) !== null && _a !== void 0 ? _a : row === null || row === void 0 ? void 0 : row.last_login); },
284
- renderCell: (row) => {
285
- var _a;
286
- return renderBooleanStatusIcon(Boolean((_a = row === null || row === void 0 ? void 0 : row.successful_login) !== null && _a !== void 0 ? _a : row === null || row === void 0 ? void 0 : row.last_login), t('Common.YES', 'Yes'), t('Common.NO', 'No'));
287
- },
281
+ renderCell: (row) => { var _a; return renderBooleanStatusIcon(Boolean((_a = row === null || row === void 0 ? void 0 : row.successful_login) !== null && _a !== void 0 ? _a : row === null || row === void 0 ? void 0 : row.last_login), t('Common.YES', 'Yes'), t('Common.NO', 'No')); },
288
282
  });
289
283
  }
290
284
  if (showRoleColumn) {
@@ -9,7 +9,7 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
12
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import React, { useId, useRef, useState } from 'react';
14
14
  import { Alert, Box, Button, CircularProgress, Paper, Stack, Tooltip, Typography, } from '@mui/material';
15
15
  import { useTranslation } from 'react-i18next';
@@ -40,7 +40,8 @@ export function scaleBubbleRadius(value, maxValue, { minRadius = BUBBLE_MIN_RADI
40
40
  // continuous-scale colour) can only be expressed by replacing the marker itself.
41
41
  function makeMarkerSlot(seriesById, { sizeAccessor, chartMax, getPointStyle }) {
42
42
  return function PresetScatterMarker(_a) {
43
- var _b, _c, _d, _e, _f, _g;
43
+ var _b, _c;
44
+ var _d, _e, _f, _g;
44
45
  var { seriesId, dataIndex, color, x, y, isHighlighted, isFaded, size, onClick } = _a, rest = __rest(_a, ["seriesId", "dataIndex", "color", "x", "y", "isHighlighted", "isFaded", "size", "onClick"]);
45
46
  const point = (_c = (_b = seriesById[seriesId]) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c[dataIndex];
46
47
  const style = point && getPointStyle ? (getPointStyle(point, { seriesId, dataIndex }) || {}) : {};
@@ -0,0 +1,55 @@
1
+ export const chatTranslations = {
2
+ 'Chat.newChat': { de: 'Neuer Chat', en: 'New chat', fr: 'Nouveau chat', sw: 'Mazungumzo mapya' },
3
+ 'Chat.chats': { de: 'Chats', en: 'Chats', fr: 'Discussions', sw: 'Mazungumzo' },
4
+ 'Chat.archived': { de: 'Archiviert', en: 'Archived', fr: 'Archivé', sw: 'Imewekwa kwenye kumbukumbu' },
5
+ 'Chat.restore': { de: 'Wiederherstellen', en: 'Restore', fr: 'Restaurer', sw: 'Rejesha' },
6
+ 'Chat.rename': { de: 'Umbenennen', en: 'Rename', fr: 'Renommer', sw: 'Badilisha jina' },
7
+ 'Chat.archive': { de: 'Archivieren', en: 'Archive', fr: 'Archiver', sw: 'Weka kwenye kumbukumbu' },
8
+ 'Chat.delete': { de: 'Löschen', en: 'Delete', fr: 'Supprimer', sw: 'Futa' },
9
+ 'Chat.deleteConfirm': { de: 'Möchten Sie diesen Chat wirklich löschen?', en: 'Delete this chat?', fr: 'Supprimer cette discussion ?', sw: 'Futa mazungumzo haya?' },
10
+ 'Chat.cancel': { de: 'Abbrechen', en: 'Cancel', fr: 'Annuler', sw: 'Ghairi' },
11
+ 'Chat.save': { de: 'Speichern', en: 'Save', fr: 'Enregistrer', sw: 'Hifadhi' },
12
+ 'Chat.actions': { de: 'Chat-Aktionen', en: 'Chat actions', fr: 'Actions de discussion', sw: 'Vitendo vya mazungumzo' },
13
+ 'Chat.loadMore': { de: 'Mehr laden', en: 'Load more', fr: 'Charger plus', sw: 'Pakia zaidi' },
14
+ 'Chat.loadOlder': { de: 'Ältere laden', en: 'Load older', fr: 'Charger les plus anciens', sw: 'Pakia za zamani' },
15
+ 'Chat.send': { de: 'Senden', en: 'Send', fr: 'Envoyer', sw: 'Tuma' },
16
+ 'Chat.stop': { de: 'Stoppen', en: 'Stop', fr: 'Arrêter', sw: 'Simamisha' },
17
+ 'Chat.message': { de: 'Nachricht', en: 'Message', fr: 'Message', sw: 'Ujumbe' },
18
+ 'Chat.typing': { de: 'Antwort wird erstellt …', en: 'Thinking…', fr: 'Réflexion…', sw: 'Inafikiria…' },
19
+ 'Chat.retry': { de: 'Erneut versuchen', en: 'Retry', fr: 'Réessayer', sw: 'Jaribu tena' },
20
+ 'Chat.interrupted': { de: 'Unterbrochen', en: 'Interrupted', fr: 'Interrompue', sw: 'Imekatizwa' },
21
+ 'Chat.reconnecting': { de: 'Verbindung wird wiederhergestellt', en: 'Reconnecting', fr: 'Reconnexion', sw: 'Inaunganisha tena' },
22
+ 'Chat.payPerUse': { de: 'Nutzungsabhängig', en: 'pay per use', fr: 'à l’usage', sw: 'lipa kwa matumizi' },
23
+ 'Chat.chooseModel': { de: 'Modell wählen', en: 'Choose model', fr: 'Choisir un modèle', sw: 'Chagua modeli' },
24
+ 'Chat.effort.low': { de: 'Niedrig', en: 'Low', fr: 'Faible', sw: 'Chini' },
25
+ 'Chat.effort.medium': { de: 'Mittel', en: 'Medium', fr: 'Moyen', sw: 'Wastani' },
26
+ 'Chat.effort.high': { de: 'Hoch', en: 'High', fr: 'Élevé', sw: 'Juu' },
27
+ 'Chat.effort.max': { de: 'Maximum', en: 'Max', fr: 'Maximal', sw: 'Juu zaidi' },
28
+ 'Chat.driver.claude_cli': { de: 'Claude Code (Abo)', en: 'Claude Code subscription', fr: 'Claude Code (abonnement)', sw: 'Claude Code (usajili)' },
29
+ 'Chat.driver.anthropic_api': { de: 'Anthropic API', en: 'Anthropic API', fr: 'API Anthropic', sw: 'API ya Anthropic' },
30
+ 'Chat.driver.openai_api': { de: 'OpenAI API', en: 'OpenAI API', fr: 'API OpenAI', sw: 'API ya OpenAI' },
31
+ 'Chat.markdownTable': { de: 'Tabelleninhalt', en: 'Table content', fr: 'Contenu du tableau', sw: 'Yaliyomo kwenye jedwali' },
32
+ 'Chat.markdownCode': { de: 'Codeblock', en: 'Code block', fr: 'Bloc de code', sw: 'Kizuizi cha msimbo' },
33
+ 'Chat.changeModel': { de: 'Modell ändern', en: 'Change model', fr: 'Changer de modèle', sw: 'Badilisha modeli' },
34
+ 'Chat.usageLimit': { de: 'Nutzungslimit für {{model}} erreicht. Wählen Sie ein anderes Modell oder versuchen Sie es später erneut.', en: 'Usage limit reached for {{model}}. Choose another model or try again later.', fr: 'Limite d’utilisation atteinte pour {{model}}. Choisissez un autre modèle ou réessayez plus tard.', sw: 'Kikomo cha matumizi kimefikiwa kwa {{model}}. Chagua modeli nyingine au jaribu tena baadaye.' },
35
+ 'Chat.answerIncomplete': { de: 'Die Antwort konnte nicht abgeschlossen werden.', en: 'The answer could not be completed.', fr: 'La réponse n’a pas pu être terminée.', sw: 'Jibu halikuweza kukamilishwa.' },
36
+ 'Chat.unavailable': { de: 'Chat ist nicht verfügbar', en: 'Chat is not available', fr: 'Le chat n’est pas disponible', sw: 'Mazungumzo hayapatikani' },
37
+ 'Chat.noModel': { de: 'Auf diesem Server ist kein Sprachmodell konfiguriert.', en: 'No language model is configured on this server.', fr: 'Aucun modèle de langage n’est configuré sur ce serveur.', sw: 'Hakuna modeli ya lugha iliyosanidiwa kwenye seva hii.' },
38
+ 'Chat.modelChanged': { de: 'Modell gewechselt: {{model}}', en: 'Model changed: {{model}}', fr: 'Modèle changé : {{model}}', sw: 'Modeli imebadilishwa: {{model}}' },
39
+ 'Chat.connectionLost': { de: 'Verbindung verloren', en: 'Connection lost', fr: 'Connexion perdue', sw: 'Muunganisho umepotea' },
40
+ 'Chat.reconnect': { de: 'Erneut verbinden', en: 'Reconnect', fr: 'Se reconnecter', sw: 'Unganisha tena' },
41
+ 'Chat.sessionEnded': { de: 'Ihre Sitzung ist beendet. Bitte melden Sie sich erneut an.', en: 'Your session has ended. Please sign in again.', fr: 'Votre session a pris fin. Veuillez vous reconnecter.', sw: 'Kipindi chako kimeisha. Tafadhali ingia tena.' },
42
+ 'Chat.accessLost': { de: 'Sie haben keinen Zugriff mehr auf diesen Chat.', en: 'You no longer have access to this chat.', fr: 'Vous n’avez plus accès à cette discussion.', sw: 'Huna tena ufikiaji wa mazungumzo haya.' },
43
+ 'Chat.rateLimited': { de: 'Zu viele Anfragen. Bitte einen Moment warten.', en: 'Too many requests. Please wait a moment.', fr: 'Trop de requêtes. Veuillez patienter un instant.', sw: 'Maombi mengi mno. Tafadhali subiri kidogo.' },
44
+ 'Chat.untitled': { de: 'Neuer Chat', en: 'New chat', fr: 'Nouvelle discussion', sw: 'Mazungumzo mapya' },
45
+ 'Chat.emptyTitle': { de: 'Wie kann ich helfen?', en: 'How can I help?', fr: 'Comment puis-je aider ?', sw: 'Naweza kusaidiaje?' },
46
+ 'Chat.selectChat': { de: 'Wählen Sie einen Chat oder starten Sie einen neuen.', en: 'Select a chat or start a new one.', fr: 'Sélectionnez une discussion ou commencez-en une nouvelle.', sw: 'Chagua mazungumzo au anza mapya.' },
47
+ 'Chat.archivedReadOnly': { de: 'Archivierter Chat. Stellen Sie ihn wieder her, um weiterzuschreiben.', en: 'Archived chat. Restore it to continue.', fr: 'Discussion archivée. Restaurez-la pour continuer.', sw: 'Mazungumzo yaliyowekwa kwenye kumbukumbu. Yarejeshe ili kuendelea.' },
48
+ 'Chat.loadError': { de: 'Chats konnten nicht geladen werden.', en: 'Chats could not be loaded.', fr: 'Impossible de charger les discussions.', sw: 'Mazungumzo hayakuweza kupakiwa.' },
49
+ 'Chat.actionError': { de: 'Aktion fehlgeschlagen.', en: 'Action failed.', fr: 'Action impossible.', sw: 'Kitendo kimeshindwa.' },
50
+ 'Chat.sources': { de: 'Quellen', en: 'Sources', fr: 'Sources', sw: 'Vyanzo' },
51
+ 'Chat.model': { de: 'Modell', en: 'Model', fr: 'Modèle', sw: 'Modeli' },
52
+ 'Chat.effortLabel': { de: 'Aufwand', en: 'Effort', fr: 'Effort', sw: 'Juhudi' },
53
+ 'Chat.renameLabel': { de: 'Chat-Titel', en: 'Chat title', fr: 'Titre de la discussion', sw: 'Kichwa cha mazungumzo' },
54
+ 'Chat.thread': { de: 'Unterhaltung', en: 'Conversation', fr: 'Conversation', sw: 'Mazungumzo' },
55
+ };
@@ -11,12 +11,13 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { authTranslations } from './authTranslations';
13
13
  import { chartsTranslations } from './chartsTranslations';
14
+ import { chatTranslations } from './chatTranslations';
14
15
  import { messagingTranslations } from './messagingTranslations';
15
16
  import { notificationsTranslations } from './notificationsTranslations';
16
17
  import { onboardingTranslations } from './onboardingTranslations';
17
18
  import { sectionNavTranslations } from './sectionNavTranslations';
18
19
  import { userMenuTranslations } from './userMenuTranslations';
19
- export const uiCoreTranslations = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, authTranslations), chartsTranslations), messagingTranslations), notificationsTranslations), onboardingTranslations), sectionNavTranslations), userMenuTranslations);
20
+ export const uiCoreTranslations = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, authTranslations), chartsTranslations), chatTranslations), messagingTranslations), notificationsTranslations), onboardingTranslations), sectionNavTranslations), userMenuTranslations);
20
21
  export function createUiCoreTranslations({ germanVariant = 'formal' } = {}) {
21
22
  return Object.fromEntries(Object.entries(uiCoreTranslations).map(([key, value]) => {
22
23
  const { de_informal } = value, rest = __rest(value, ["de_informal"]);
package/dist/index.js CHANGED
@@ -78,6 +78,15 @@ export { AttachmentList } from './messaging/AttachmentList';
78
78
  export { ReactionBar } from './messaging/ReactionBar';
79
79
  export { PollCard } from './messaging/PollCard';
80
80
  export { MessagingScopeConfig } from './messaging/MessagingScopeConfig';
81
+ // --- Shared AI chat ---
82
+ export { ChatWorkspace } from './chat/ChatWorkspace';
83
+ export { ChatList } from './chat/ChatList';
84
+ export { ChatThread } from './chat/ChatThread';
85
+ export { ChatComposer } from './chat/ChatComposer';
86
+ export { ModelPicker } from './chat/ModelPicker';
87
+ export { default as MarkdownReply } from './chat/MarkdownReply';
88
+ export { useChatSocket } from './chat/useChatSocket';
89
+ export { createChatApi, defaultChatApi } from './chat/api';
81
90
  // --- 9. Onboarding ---
82
91
  export * from './onboarding/api';
83
92
  export { selectActiveSteps } from './onboarding/stepSelection';
@@ -92,4 +101,5 @@ export { notificationsTranslations } from './i18n/notificationsTranslations';
92
101
  export { onboardingTranslations } from './i18n/onboardingTranslations';
93
102
  export { chartsTranslations } from './i18n/chartsTranslations';
94
103
  export { messagingTranslations } from './i18n/messagingTranslations';
104
+ export { chatTranslations } from './i18n/chatTranslations';
95
105
  export { createUiCoreTranslations, uiCoreTranslations } from './i18n/uiCoreTranslations';
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useId, useState } from 'react';
3
3
  import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
4
4
  import { Box, ButtonBase, Drawer, List, ListItem, ListItemButton, ListItemText, Paper, Stack, Typography, } from '@mui/material';
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import CloseIcon from '@mui/icons-material/Close';
3
3
  import DownloadIcon from '@mui/icons-material/DownloadOutlined';
4
4
  import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
@@ -49,7 +49,7 @@ export function AttachmentList({ attachments = [] }) {
49
49
  setPreviews((current) => (Object.assign(Object.assign({}, current), { [attachment.id]: url })));
50
50
  }
51
51
  }
52
- catch ( /* The image remains available through the download action. */_a) { /* The image remains available through the download action. */ }
52
+ catch (_a) { /* The image remains available through the download action. */ }
53
53
  });
54
54
  return () => { live = false; urls.forEach((url) => URL.revokeObjectURL(url)); };
55
55
  }, [attachments, getAttachmentThumbnail]);
@@ -110,7 +110,8 @@ export function Composer({ conversationId, conversation, replyTarget = null, onR
110
110
  };
111
111
  const removeFile = (index) => setFiles((current) => current.filter((_, fileIndex) => fileIndex !== index));
112
112
  const insertEmoji = (emoji) => {
113
- var _a, _b, _c, _d;
113
+ var _a, _b;
114
+ var _c, _d;
114
115
  const input = ((_b = (_a = messageInputRef.current) === null || _a === void 0 ? void 0 : _a.querySelector) === null || _b === void 0 ? void 0 : _b.call(_a, 'textarea')) || messageInputRef.current;
115
116
  const start = (_c = input === null || input === void 0 ? void 0 : input.selectionStart) !== null && _c !== void 0 ? _c : body.length;
116
117
  const end = (_d = input === null || input === void 0 ? void 0 : input.selectionEnd) !== null && _d !== void 0 ? _d : start;
@@ -32,8 +32,5 @@ export function ConversationLaunchers({ groupLaunchers = [], broadcastLauncher,
32
32
  autoOpened.current = true;
33
33
  open('broadcast', (_a = broadcastLauncher.payload) !== null && _a !== void 0 ? _a : broadcastLauncher);
34
34
  }, [autoOpenBroadcast, broadcastLauncher]); // one intentional auto-open per mounted launcher
35
- return (_jsxs(Stack, { spacing: 1, children: [broadcastLauncher && !autoOpenBroadcast && (_jsx(Button, { variant: "outlined", onClick: () => { var _a; return open('broadcast', (_a = broadcastLauncher.payload) !== null && _a !== void 0 ? _a : broadcastLauncher); }, children: broadcastLauncher.label || t('MessagingLaunchers.OPEN_BROADCAST') })), groupLaunchers.map((launcher) => {
36
- var _a;
37
- return (_jsx(Button, { variant: "outlined", onClick: () => { var _a; return open('group', (_a = launcher.payload) !== null && _a !== void 0 ? _a : launcher); }, children: launcher.label || t('MessagingLaunchers.OPEN_GROUP') }, (_a = launcher.id) !== null && _a !== void 0 ? _a : launcher.label));
38
- }), error && _jsx(Alert, { severity: "error", children: error })] }));
35
+ return (_jsxs(Stack, { spacing: 1, children: [broadcastLauncher && !autoOpenBroadcast && (_jsx(Button, { variant: "outlined", onClick: () => { var _a; return open('broadcast', (_a = broadcastLauncher.payload) !== null && _a !== void 0 ? _a : broadcastLauncher); }, children: broadcastLauncher.label || t('MessagingLaunchers.OPEN_BROADCAST') })), groupLaunchers.map((launcher) => { var _a; return (_jsx(Button, { variant: "outlined", onClick: () => { var _a; return open('group', (_a = launcher.payload) !== null && _a !== void 0 ? _a : launcher); }, children: launcher.label || t('MessagingLaunchers.OPEN_GROUP') }, (_a = launcher.id) !== null && _a !== void 0 ? _a : launcher.label)); }), error && _jsx(Alert, { severity: "error", children: error })] }));
39
36
  }
@@ -64,7 +64,7 @@ export function ConversationList({ onOpen, groupLaunchers, broadcastLauncher, au
64
64
  const [loadingMore, setLoadingMore] = useState(false);
65
65
  const [error, setError] = useState(null);
66
66
  const conversations = useMemo(() => ordered(Object.values(cache.conversations).filter((item) => includeArchived || !item.archived_at && !item.archived)), [cache.conversations, includeArchived]);
67
- const unreadFor = (conversation) => { var _a, _b, _c; return (_c = (_a = conversation.unread_count) !== null && _a !== void 0 ? _a : (_b = cache.unread.by_conversation) === null || _b === void 0 ? void 0 : _b[conversation.id]) !== null && _c !== void 0 ? _c : 0; };
67
+ const unreadFor = (conversation) => { var _a; var _b, _c; return (_c = (_b = conversation.unread_count) !== null && _b !== void 0 ? _b : (_a = cache.unread.by_conversation) === null || _a === void 0 ? void 0 : _a[conversation.id]) !== null && _c !== void 0 ? _c : 0; };
68
68
  // Once opened, the broadcast conversation becomes a normal, recency-sorted
69
69
  // row with real unread/preview state — hide the static launcher so it isn't
70
70
  // shown twice (button + list row) with the button's stale, unread-less copy.
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useId, useMemo, useState } from 'react';
3
3
  import { Alert, Autocomplete, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Typography, useMediaQuery, } from '@mui/material';
4
4
  import { useTheme } from '@mui/material/styles';
Binary file
@@ -24,7 +24,8 @@ export const POLL_CARD_CHECKBOX_SX = { pt: 0.25 };
24
24
  * rejected vote.
25
25
  */
26
26
  export function PollCard({ message, canClose: canCloseProp }) {
27
- var _a, _b, _c, _d, _e, _f, _g, _h;
27
+ var _a, _b;
28
+ var _c, _d, _e, _f, _g, _h;
28
29
  const { t } = useTranslation();
29
30
  const { castPollVote, closeConversationPoll, currentUser } = useMessaging();
30
31
  const poll = message === null || message === void 0 ? void 0 : message.poll;
@@ -47,8 +48,8 @@ export function PollCard({ message, canClose: canCloseProp }) {
47
48
  if (!poll)
48
49
  return null;
49
50
  const closed = Boolean(poll.closed_at);
50
- const creatorId = (_c = (_a = poll.created_by_id) !== null && _a !== void 0 ? _a : (_b = poll.created_by) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : poll.created_by;
51
- const canClose = (_g = (_e = (_d = canCloseProp !== null && canCloseProp !== void 0 ? canCloseProp : poll.can_close) !== null && _d !== void 0 ? _d : message === null || message === void 0 ? void 0 : message.can_close) !== null && _e !== void 0 ? _e : (_f = message === null || message === void 0 ? void 0 : message.permissions) === null || _f === void 0 ? void 0 : _f.can_close) !== null && _g !== void 0 ? _g : (creatorId != null && creatorId === (currentUser === null || currentUser === void 0 ? void 0 : currentUser.id));
51
+ const creatorId = (_d = (_c = poll.created_by_id) !== null && _c !== void 0 ? _c : (_a = poll.created_by) === null || _a === void 0 ? void 0 : _a.id) !== null && _d !== void 0 ? _d : poll.created_by;
52
+ const canClose = (_g = (_f = (_e = canCloseProp !== null && canCloseProp !== void 0 ? canCloseProp : poll.can_close) !== null && _e !== void 0 ? _e : message === null || message === void 0 ? void 0 : message.can_close) !== null && _f !== void 0 ? _f : (_b = message === null || message === void 0 ? void 0 : message.permissions) === null || _b === void 0 ? void 0 : _b.can_close) !== null && _g !== void 0 ? _g : (creatorId != null && creatorId === (currentUser === null || currentUser === void 0 ? void 0 : currentUser.id));
52
53
  // Percentage denominator (MSG-6i): the number of distinct people who
53
54
  // answered this poll, not sum(vote_count) across options. For a
54
55
  // multi-select poll one person can vote for several options, so summing
@@ -109,11 +110,12 @@ export function PollCard({ message, canClose: canCloseProp }) {
109
110
  }
110
111
  };
111
112
  const optionLabel = (option) => {
112
- var _a, _b, _c;
113
+ var _a;
114
+ var _b, _c;
113
115
  // dcm's poll projection gives voter user ids only, never names — there
114
116
  // is no user-directory endpoint (same structural limit already
115
117
  // established for the DM candidate list).
116
- const voteCount = (_c = (_a = option.vote_count) !== null && _a !== void 0 ? _a : (_b = option.voters) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
118
+ const voteCount = (_c = (_b = option.vote_count) !== null && _b !== void 0 ? _b : (_a = option.voters) === null || _a === void 0 ? void 0 : _a.length) !== null && _c !== void 0 ? _c : 0;
117
119
  const percent = respondentCount > 0 ? Math.round((voteCount / respondentCount) * 100) : 0;
118
120
  const votedByMe = selected.includes(option.id);
119
121
  const resultText = t('MessagingPoll.OPTION_RESULT', { count: voteCount, percent });
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import DoneAllOutlinedIcon from '@mui/icons-material/DoneAllOutlined';
3
3
  import DoneOutlinedIcon from '@mui/icons-material/DoneOutlined';
4
4
  import { ButtonBase, List, ListItem, Popover, Tooltip, Typography } from '@mui/material';
@@ -41,7 +41,7 @@ export function ReadTicks({ messageId, conversation }) {
41
41
  }, [getMessageReadStatus, messageId]);
42
42
  if (!status)
43
43
  return null;
44
- const recipientDetail = ((_a = status.recipient_detail) === null || _a === void 0 ? void 0 : _a.map((recipient) => { var _a, _b; return (Object.assign(Object.assign({}, recipient), { last_read_at: (_b = (_a = cache.receipts[`${conversation === null || conversation === void 0 ? void 0 : conversation.id}:${recipient.user_id}`]) === null || _a === void 0 ? void 0 : _a.last_read_at) !== null && _b !== void 0 ? _b : recipient.last_read_at })); })) || status.recipient_detail;
44
+ const recipientDetail = ((_a = status.recipient_detail) === null || _a === void 0 ? void 0 : _a.map((recipient) => { var _a; var _b; return (Object.assign(Object.assign({}, recipient), { last_read_at: (_b = (_a = cache.receipts[`${conversation === null || conversation === void 0 ? void 0 : conversation.id}:${recipient.user_id}`]) === null || _a === void 0 ? void 0 : _a.last_read_at) !== null && _b !== void 0 ? _b : recipient.last_read_at })); })) || status.recipient_detail;
45
45
  const directReceipt = (conversation === null || conversation === void 0 ? void 0 : conversation.kind) === 'direct' && (conversation === null || conversation === void 0 ? void 0 : conversation.other_user_id) != null
46
46
  ? cache.receipts[`${conversation.id}:${conversation.other_user_id}`]
47
47
  : null;
@@ -15,7 +15,8 @@ function replyToId(message) { var _a; return (_a = message.reply_to_id) !== null
15
15
  // MessagingProvider.jsx's applyFrame merge-trap handling). A null receipt with
16
16
  // at least one reply counts as unread; no replies means no marker at all.
17
17
  function hasUnreadReplies(message, receipts, userId) {
18
- var _a, _b;
18
+ var _a;
19
+ var _b;
19
20
  if (!message.reply_count)
20
21
  return false;
21
22
  const lastReadAt = (_b = (_a = receipts[`thread:${message.id}:${userId}`]) === null || _a === void 0 ? void 0 : _a.last_read_at) !== null && _b !== void 0 ? _b : message.thread_last_read_at;
@@ -77,10 +78,11 @@ export function Thread({ conversationId, onReplyTargetChange, canModerateMessage
77
78
  if (conversationId == null)
78
79
  return undefined;
79
80
  return subscribe(MESSAGING_ENVELOPE, (frame) => {
80
- var _a, _b;
81
+ var _a;
82
+ var _b;
81
83
  if (frame.type !== 'message')
82
84
  return;
83
- const frameConversationId = (_a = frame.conversation_id) !== null && _a !== void 0 ? _a : (_b = frame.message) === null || _b === void 0 ? void 0 : _b.conversation_id;
85
+ const frameConversationId = (_b = frame.conversation_id) !== null && _b !== void 0 ? _b : (_a = frame.message) === null || _a === void 0 ? void 0 : _a.conversation_id;
84
86
  if (String(frameConversationId) !== String(conversationId))
85
87
  return;
86
88
  if (document.visibilityState === 'hidden')
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import NotificationsIcon from '@mui/icons-material/Notifications';
4
4
  import { Badge, Box, Button, IconButton, List, ListItemButton, ListItemText, Menu, Typography, } from '@mui/material';
@@ -20,10 +20,7 @@ export function NotificationBell({ resolveLink }) {
20
20
  if (resolveLink && ((_a = notification.content) === null || _a === void 0 ? void 0 : _a.link))
21
21
  resolveLink(notification.content.link);
22
22
  };
23
- return (_jsxs(_Fragment, { children: [_jsx(IconButton, { "aria-label": t('NotificationBell.TITLE'), color: "inherit", onClick: (event) => setAnchorEl(event.currentTarget), children: _jsx(Badge, { badgeContent: unreadCount, color: "error", max: 99, children: _jsx(NotificationsIcon, {}) }) }), _jsx(Menu, { anchorEl: anchorEl, anchorOrigin: { horizontal: 'right', vertical: 'bottom' }, onClose: () => setAnchorEl(null), open: open, transformOrigin: { horizontal: 'right', vertical: 'top' }, children: _jsxs(Box, { sx: { minWidth: 320 }, children: [_jsxs(Box, { sx: { alignItems: 'center', display: 'flex', justifyContent: 'space-between', px: 2, py: 1 }, children: [_jsx(Typography, { variant: "subtitle1", children: t('NotificationBell.TITLE') }), unreadIds.length > 0 && (_jsx(Button, { onClick: () => markSeen(unreadIds), size: "small", children: t('NotificationBell.MARK_ALL') }))] }), notifications.length === 0 ? (_jsx(Typography, { sx: { px: 2, py: 2 }, variant: "body2", children: t('NotificationBell.EMPTY') })) : (_jsx(List, { dense: true, disablePadding: true, children: notifications.map((notification) => {
24
- var _a, _b, _c, _d;
25
- return (_jsx(ListItemButton, { onClick: () => handleItemClick(notification), children: _jsx(ListItemText, { primary: t((_a = notification.content) === null || _a === void 0 ? void 0 : _a.title_key, ((_b = notification.content) === null || _b === void 0 ? void 0 : _b.params) || {}), secondary: ((_c = notification.content) === null || _c === void 0 ? void 0 : _c.body_key)
26
- ? t(notification.content.body_key, notification.content.params || {})
27
- : undefined, slotProps: { primary: { fontWeight: notification.seen || notification.dismissed ? 'regular' : 'bold' } } }) }, (_d = notification.id) !== null && _d !== void 0 ? _d : notification.notification_id));
28
- }) }))] }) })] }));
23
+ return (_jsxs(_Fragment, { children: [_jsx(IconButton, { "aria-label": t('NotificationBell.TITLE'), color: "inherit", onClick: (event) => setAnchorEl(event.currentTarget), children: _jsx(Badge, { badgeContent: unreadCount, color: "error", max: 99, children: _jsx(NotificationsIcon, {}) }) }), _jsx(Menu, { anchorEl: anchorEl, anchorOrigin: { horizontal: 'right', vertical: 'bottom' }, onClose: () => setAnchorEl(null), open: open, transformOrigin: { horizontal: 'right', vertical: 'top' }, children: _jsxs(Box, { sx: { minWidth: 320 }, children: [_jsxs(Box, { sx: { alignItems: 'center', display: 'flex', justifyContent: 'space-between', px: 2, py: 1 }, children: [_jsx(Typography, { variant: "subtitle1", children: t('NotificationBell.TITLE') }), unreadIds.length > 0 && (_jsx(Button, { onClick: () => markSeen(unreadIds), size: "small", children: t('NotificationBell.MARK_ALL') }))] }), notifications.length === 0 ? (_jsx(Typography, { sx: { px: 2, py: 2 }, variant: "body2", children: t('NotificationBell.EMPTY') })) : (_jsx(List, { dense: true, disablePadding: true, children: notifications.map((notification) => { var _a, _b, _c; var _d; return (_jsx(ListItemButton, { onClick: () => handleItemClick(notification), children: _jsx(ListItemText, { primary: t((_a = notification.content) === null || _a === void 0 ? void 0 : _a.title_key, ((_b = notification.content) === null || _b === void 0 ? void 0 : _b.params) || {}), secondary: ((_c = notification.content) === null || _c === void 0 ? void 0 : _c.body_key)
24
+ ? t(notification.content.body_key, notification.content.params || {})
25
+ : undefined, slotProps: { primary: { fontWeight: notification.seen || notification.dismissed ? 'regular' : 'bold' } } }) }, (_d = notification.id) !== null && _d !== void 0 ? _d : notification.notification_id)); }) }))] }) })] }));
29
26
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useEffect, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import Alert from '@mui/material/Alert';
@@ -57,12 +57,9 @@ export const UNIVERSAL_STEP_DESCRIPTORS = [
57
57
  // verified their PWA manifest/icons are actually installable must not
58
58
  // pass pwaInstallEnabled, or this would show an iOS "Add to Home Screen"
59
59
  // hint for an app that isn't really installable.
60
- condition: (ctx) => {
61
- var _a, _b, _c;
62
- return Boolean(ctx.pwaInstallEnabled
63
- && !((_a = ctx.pwaInstall) === null || _a === void 0 ? void 0 : _a.isStandalone)
64
- && (Boolean((_b = ctx.pwaInstall) === null || _b === void 0 ? void 0 : _b.deferredPrompt) || Boolean((_c = ctx.pwaInstall) === null || _c === void 0 ? void 0 : _c.isIos)));
65
- },
60
+ condition: (ctx) => { var _a, _b, _c; return Boolean(ctx.pwaInstallEnabled
61
+ && !((_a = ctx.pwaInstall) === null || _a === void 0 ? void 0 : _a.isStandalone)
62
+ && (Boolean((_b = ctx.pwaInstall) === null || _b === void 0 ? void 0 : _b.deferredPrompt) || Boolean((_c = ctx.pwaInstall) === null || _c === void 0 ? void 0 : _c.isIos))); },
66
63
  blocking: false,
67
64
  skipable: true,
68
65
  persistDismissed: true,
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useState, useContext, useEffect } from 'react';
3
3
  import { useNavigate, useLocation } from 'react-router-dom';
4
4
  import { Helmet } from 'react-helmet';
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/auth/pages/PasswordChangePage.jsx
3
3
  import React, { useState } from 'react';
4
4
  import { Helmet } from 'react-helmet';
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/auth/pages/PasswordInvitePage.jsx
3
3
  import React, { useState, useEffect } from 'react';
4
4
  import { useNavigate, useParams, useLocation } from 'react-router-dom';
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/auth/pages/PasswordResetRequestPage.jsx
3
3
  import React, { useState } from 'react';
4
4
  import { Helmet } from 'react-helmet';
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { useNavigate, useLocation } from 'react-router-dom';
4
4
  import { Alert, Box, Button, Stack, TextField, Typography, } from '@mui/material';
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/pages/SignupConfirmPage.jsx
3
3
  //
4
4
  // S13: Completes a pending registration. Reads the signed pending-token from
@@ -266,7 +266,8 @@ function createPaletteAwareComponents(palette, isDark) {
266
266
  * objects are created, so runtime app/site overrides cannot leave stale values.
267
267
  */
268
268
  export function createAppTheme(appConfig = {}) {
269
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
269
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
270
+ var _r, _s;
270
271
  if (!((_a = appConfig === null || appConfig === void 0 ? void 0 : appConfig.palette) === null || _a === void 0 ? void 0 : _a.primary)) {
271
272
  throw new TypeError('createAppTheme: appConfig.palette.primary is required.');
272
273
  }
@@ -327,16 +328,16 @@ export function createAppTheme(appConfig = {}) {
327
328
  ? finalPalette.dataSeries.categorical.map((colour) => (deriveLightenedPageColour(colour, [finalPalette.background.paper], 3)))
328
329
  : finalPalette.dataSeries.categorical;
329
330
  const computedPalette = Object.assign(Object.assign({}, derivedStatusEntries), { controlBorder: Object.assign(Object.assign(Object.assign({}, finalPalette.controlBorder), derivedControlBorderEntries), { focus: isDark
330
- ? ((_o = (_m = (_l = appConfig.palette) === null || _l === void 0 ? void 0 : _l.controlBorder) === null || _m === void 0 ? void 0 : _m.focus) !== null && _o !== void 0 ? _o : deriveLightenedColour(primaryMain, contrastSurfaces))
331
+ ? ((_r = (_m = (_l = appConfig.palette) === null || _l === void 0 ? void 0 : _l.controlBorder) === null || _m === void 0 ? void 0 : _m.focus) !== null && _r !== void 0 ? _r : deriveLightenedColour(primaryMain, contrastSurfaces))
331
332
  : deriveFocusColour(finalPalette.primary.main, contrastSurfaces) }), primary: Object.assign(Object.assign(Object.assign({}, finalPalette.primary), (isDark && primaryMain !== finalPalette.primary.main
332
333
  ? withMainShades(primaryMain, primaryContrastText)
333
334
  : { main: primaryMain })), { contrastText: primaryContrastText }), dataSeries: { categorical: derivedSeries } });
334
335
  const resolvedPalette = Object.assign(Object.assign({}, finalPalette), computedPalette);
335
- const fontFamily = ((_p = appConfig.typography) === null || _p === void 0 ? void 0 : _p.fontFamily) || BASELINE_STATIC.typography.fontFamily;
336
+ const fontFamily = ((_o = appConfig.typography) === null || _o === void 0 ? void 0 : _o.fontFamily) || BASELINE_STATIC.typography.fontFamily;
336
337
  // The h4 step below `sm` follows the app's own `sm` if it overrides the breakpoint,
337
338
  // so the step always ends where theme.breakpoints.down('sm') ends.
338
339
  const _t = BASELINE_STATIC.typography.h4, _u = breakpointDownQuery(BREAKPOINT_SM), h4Mobile = _t[_u], h4Base = __rest(_t, [typeof _u === "symbol" ? _u : _u + ""]);
339
- const smValue = (_s = (_r = (_q = appConfig.breakpoints) === null || _q === void 0 ? void 0 : _q.values) === null || _r === void 0 ? void 0 : _r.sm) !== null && _s !== void 0 ? _s : BREAKPOINT_SM;
340
+ const smValue = (_s = (_q = (_p = appConfig.breakpoints) === null || _p === void 0 ? void 0 : _p.values) === null || _q === void 0 ? void 0 : _q.sm) !== null && _s !== void 0 ? _s : BREAKPOINT_SM;
340
341
  return createTheme(Object.assign(Object.assign({}, BASELINE_STATIC), { typography: Object.assign(Object.assign({}, BASELINE_STATIC.typography), { fontFamily, h4: Object.assign(Object.assign({}, h4Base), { [breakpointDownQuery(smValue)]: h4Mobile }) }), fontLoading: Object.assign(Object.assign({}, BASELINE_STATIC.fontLoading), { weights: [...BASELINE_STATIC.fontLoading.weights] }), shadow: isDark ? Object.assign(Object.assign({}, BASELINE_STATIC.shadow), { overlay: DARK_OVERLAY_SHADOW }) : BASELINE_STATIC.shadow, palette: resolvedPalette, themeCompleteness: {
341
342
  baseline: true,
342
343
  exemptions: (isDark