@micha.bigler/ui-core-micha 4.0.1 → 4.1.0
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/CHANGELOG.md +13 -0
- package/dist/chat/ChatThread.js +7 -5
- package/dist/chat/ChatWorkspace.js +78 -18
- package/dist/chat/ModelPicker.js +3 -2
- package/dist/chat/api.js +2 -2
- package/dist/chat/kitSxRegistry.js +2 -1
- package/dist/chat/wire.js +3 -1
- package/dist/i18n/chatTranslations.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.1.0 - UCM-CHAT-3
|
|
4
|
+
|
|
5
|
+
Extension points on the `/chat` surface; with none of the new props `ChatWorkspace` renders and behaves as in 4.0.x.
|
|
6
|
+
New `ChatWorkspace` props: `scope` (now also sent on the chat lists and the status request), `unavailableReasonText`
|
|
7
|
+
(object or function mapping the server's `unavailable` reason to the app's text; generic fallback otherwise),
|
|
8
|
+
`sidePanel(chat)` with `sidePanelLabel` (a column next to the thread, a sheet in the narrow and compact layouts),
|
|
9
|
+
`renderAnswerExtra({ turn, chat, appFrames })` (under each answer, with the `app` frames that arrived during that turn),
|
|
10
|
+
`renderComposerExtra({ chat })`, `onAppFrame({ name, payload, chatId, turnId })`, and `layout` (`'viewport'` default,
|
|
11
|
+
`'container'` follows the container width below `compactBelow` px, `'compact'` always narrow). `ModelPicker` takes an
|
|
12
|
+
optional `sheet` boolean. A `history_trimmed` notice shows a quiet chip with the number of left-out messages. New texts
|
|
13
|
+
`Chat.notAvailableHere`, `Chat.historyTrimmed_one/_other` and the fallback `Chat.sidePanel` (de/en/fr/sw). Built
|
|
14
|
+
against the `django-core-micha` DCM-CHAT-10 contract.
|
|
15
|
+
|
|
3
16
|
## 4.0.1 - UCM-DEP-3
|
|
4
17
|
|
|
5
18
|
The `exports` map no longer lists `./package.json`; the public entries are exactly `.`, `./charts`, `./chat` and `./markdown`.
|
package/dist/chat/ChatThread.js
CHANGED
|
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
5
5
|
import MarkdownReply from './MarkdownReply';
|
|
6
6
|
export const CHAT_THREAD_TOOL_CHIP_SX = { alignSelf: 'flex-start' };
|
|
7
7
|
export const CHAT_THREAD_NOTICE_SX = { mb: 2 };
|
|
8
|
+
export const CHAT_THREAD_TRIMMED_SX = { mb: 1.5 };
|
|
8
9
|
const selectionLine = (turn, t) => [turn.model, turn.effort && t(`Chat.effort.${turn.effort}`)].filter(Boolean).join(' · ');
|
|
9
10
|
const sameSelection = (a, b) => a.driver === b.driver && a.model === b.model && a.effort === b.effort;
|
|
10
11
|
function SourceChips({ sources }) {
|
|
@@ -13,17 +14,18 @@ function SourceChips({ sources }) {
|
|
|
13
14
|
return null;
|
|
14
15
|
return (_jsx(Stack, { direction: "row", flexWrap: "wrap", gap: 0.75, role: "list", "aria-label": t('Chat.sources'), children: sources.map((source, index) => (_jsx(Tooltip, { title: source.reference || '', describeChild: true, children: _jsx(Chip, { role: "listitem", size: "small", variant: "outlined", label: source.label }) }, `${source.label}-${index}`))) }));
|
|
15
16
|
}
|
|
16
|
-
function Answer({ turn, tool, onRetry }) {
|
|
17
|
+
function Answer({ turn, tool, onRetry, chat, renderAnswerExtra }) {
|
|
18
|
+
var _a;
|
|
17
19
|
const { t } = useTranslation();
|
|
18
20
|
const generating = turn.state === 'generating';
|
|
19
21
|
const failed = ['failed', 'cancelled'].includes(turn.state) && turn.error_code;
|
|
20
|
-
return (_jsxs(Stack, { spacing: 1, "aria-busy": generating, children: [turn.assistant_text ? _jsx(MarkdownReply, { text: turn.assistant_text }) : null, generating && !turn.assistant_text && !tool ? _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Chat.typing') }) : null, generating && tool ? _jsx(Chip, { size: "small", color: "primary", variant: "outlined", label: tool.label, sx: CHAT_THREAD_TOOL_CHIP_SX }) : null, _jsx(SourceChips, { sources: turn.sources }), !generating && selectionLine(turn, t) ? _jsx(Typography, { variant: "caption", color: "text.secondary", children: selectionLine(turn, t) }) : null, turn.state === 'interrupted' ? (_jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: [_jsx(Chip, { size: "small", color: "warning", label: t('Chat.interrupted') }), onRetry ? _jsx(Button, { size: "small", onClick: () => onRetry(turn), children: t('Chat.retry') }) : null] })) : null, failed ? (_jsx(Alert, { severity: "error", action: onRetry ? _jsx(Button, { color: "inherit", size: "small", onClick: () => onRetry(turn), children: t('Chat.retry') }) : null, children: t('Chat.answerIncomplete') })) : null] }));
|
|
22
|
+
return (_jsxs(Stack, { spacing: 1, "aria-busy": generating, children: [turn.assistant_text ? _jsx(MarkdownReply, { text: turn.assistant_text }) : null, generating && !turn.assistant_text && !tool ? _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Chat.typing') }) : null, generating && tool ? _jsx(Chip, { size: "small", color: "primary", variant: "outlined", label: tool.label, sx: CHAT_THREAD_TOOL_CHIP_SX }) : null, _jsx(SourceChips, { sources: turn.sources }), !generating && selectionLine(turn, t) ? _jsx(Typography, { variant: "caption", color: "text.secondary", children: selectionLine(turn, t) }) : null, renderAnswerExtra ? _jsx(React.Fragment, { children: renderAnswerExtra({ turn, chat, appFrames: (_a = turn.appFrames) !== null && _a !== void 0 ? _a : [] }) }, chat === null || chat === void 0 ? void 0 : chat.id) : null, turn.state === 'interrupted' ? (_jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: [_jsx(Chip, { size: "small", color: "warning", label: t('Chat.interrupted') }), onRetry ? _jsx(Button, { size: "small", onClick: () => onRetry(turn), children: t('Chat.retry') }) : null] })) : null, failed ? (_jsx(Alert, { severity: "error", action: onRetry ? _jsx(Button, { color: "inherit", size: "small", onClick: () => onRetry(turn), children: t('Chat.retry') }) : null, children: t('Chat.answerIncomplete') })) : null] }));
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* The turns of one chat. The streaming answer is simply the last turn in state `generating`; the caller keeps its text
|
|
24
26
|
* up to date. `notice` is a connection or limit message shown under the last turn. `readOnly` (an archived chat) hides Retry.
|
|
25
27
|
*/
|
|
26
|
-
export function ChatThread({ turns = [], tool = null, notice = null, hasOlder = false, olderBusy = false, onLoadOlder, onRetry, readOnly = false, sx }) {
|
|
28
|
+
export function ChatThread({ turns = [], tool = null, notice = null, hasOlder = false, olderBusy = false, onLoadOlder, onRetry, readOnly = false, sx, chat, renderAnswerExtra, trimmedCount }) {
|
|
27
29
|
var _a;
|
|
28
30
|
const { t } = useTranslation();
|
|
29
31
|
const end = useRef(null);
|
|
@@ -64,7 +66,7 @@ export function ChatThread({ turns = [], tool = null, notice = null, hasOlder =
|
|
|
64
66
|
}, role: "log", "aria-live": "polite", "aria-label": t('Chat.thread'), sx: Object.assign({ minHeight: 0, flex: 1, overflowY: 'auto', p: { xs: 1.5, md: 2.5 } }, sx), children: [hasOlder ? (_jsx(Box, { sx: { textAlign: 'center', mb: 2 }, children: _jsx(Button, { size: "small", onClick: loadOlder, disabled: olderBusy, children: t('Chat.loadOlder') }) })) : null, turns.length === 0 ? (_jsx(Typography, { variant: "h6", color: "text.secondary", align: "center", sx: { mt: 6 }, children: t('Chat.emptyTitle') })) : null, turns.map((turn, index) => {
|
|
65
67
|
var _a;
|
|
66
68
|
const previous = turns[index - 1];
|
|
67
|
-
return (_jsxs(Box, { sx: { mb: 3 }, children: [previous && !sameSelection(previous, turn) ? (_jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, sx: { mb: 2, color: 'text.secondary' }, role: "separator", children: [_jsx(Box, { sx: { flex: 1, borderTop: 1, borderColor: 'divider' } }), _jsx(Typography, { variant: "caption", children: t('Chat.modelChanged', { model: selectionLine(turn, t) }) }), _jsx(Box, { sx: { flex: 1, borderTop: 1, borderColor: 'divider' } })] })) : null, _jsx(Box, { sx: { display: 'flex', justifyContent: 'flex-end', mb: 1.5 }, children: _jsx(Typography, { sx: { maxWidth: '85%', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', bgcolor: 'action.hover', borderRadius: 2, px: 1.5, py: 1 }, children: turn.user_text }) }), _jsx(Answer, { turn: turn, tool: index === turns.length - 1 ? tool : null, onRetry: readOnly ? undefined : onRetry })] }, (_a = turn.id) !== null && _a !== void 0 ? _a : index));
|
|
68
|
-
}), notice ? (_jsx(Alert, { severity: notice.severity || 'warning', sx: CHAT_THREAD_NOTICE_SX, action: notice.action ? _jsx(Button, { color: "inherit", size: "small", onClick: notice.action.onClick, children: notice.action.label }) : null, children: notice.text })) : null, _jsx("div", { ref: end })] }));
|
|
69
|
+
return (_jsxs(Box, { sx: { mb: 3 }, children: [previous && !sameSelection(previous, turn) ? (_jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, sx: { mb: 2, color: 'text.secondary' }, role: "separator", children: [_jsx(Box, { sx: { flex: 1, borderTop: 1, borderColor: 'divider' } }), _jsx(Typography, { variant: "caption", children: t('Chat.modelChanged', { model: selectionLine(turn, t) }) }), _jsx(Box, { sx: { flex: 1, borderTop: 1, borderColor: 'divider' } })] })) : null, _jsx(Box, { sx: { display: 'flex', justifyContent: 'flex-end', mb: 1.5 }, children: _jsx(Typography, { sx: { maxWidth: '85%', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', bgcolor: 'action.hover', borderRadius: 2, px: 1.5, py: 1 }, children: turn.user_text }) }), _jsx(Answer, { turn: turn, tool: index === turns.length - 1 ? tool : null, onRetry: readOnly ? undefined : onRetry, chat: chat, renderAnswerExtra: renderAnswerExtra })] }, (_a = turn.id) !== null && _a !== void 0 ? _a : index));
|
|
70
|
+
}), trimmedCount ? _jsx(Chip, { size: "small", variant: "outlined", label: t(trimmedCount === 1 ? 'Chat.historyTrimmed_one' : 'Chat.historyTrimmed_other', { count: trimmedCount }), sx: CHAT_THREAD_TRIMMED_SX }) : null, notice ? (_jsx(Alert, { severity: notice.severity || 'warning', sx: CHAT_THREAD_NOTICE_SX, action: notice.action ? _jsx(Button, { color: "inherit", size: "small", onClick: notice.action.onClick, children: notice.action.label }) : null, children: notice.text })) : null, _jsx("div", { ref: end })] }));
|
|
69
71
|
}
|
|
70
72
|
export default ChatThread;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Alert, Box, Button, Chip, Drawer, Stack, Typography, useMediaQuery, useTheme } from '@mui/material';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { createChatApi } from './api';
|
|
@@ -21,6 +21,15 @@ const settleTurn = (turns, final) => {
|
|
|
21
21
|
};
|
|
22
22
|
const failGenerating = (turns, code) => turns.map((turn) => (isGenerating(turn)
|
|
23
23
|
? Object.assign(Object.assign({}, turn), { state: 'failed', error_code: code === 'quota_exceeded' ? null : code }) : turn));
|
|
24
|
+
/** Stores an app frame on the turn that is generating; frames outside a running turn are dropped. */
|
|
25
|
+
const attachAppFrame = (turns, frame) => {
|
|
26
|
+
const index = turns.findLastIndex(isGenerating);
|
|
27
|
+
if (index < 0)
|
|
28
|
+
return turns;
|
|
29
|
+
const turn = turns[index];
|
|
30
|
+
const appFrames = [...(turn.appFrames || []), { name: frame.name, payload: frame.payload }];
|
|
31
|
+
return turns.map((item, i) => (i === index ? Object.assign(Object.assign({}, turn), { appFrames }) : item));
|
|
32
|
+
};
|
|
24
33
|
const appendDelta = (turns, text) => {
|
|
25
34
|
const index = turns.findLastIndex(isGenerating);
|
|
26
35
|
if (index < 0)
|
|
@@ -33,12 +42,29 @@ const appendDelta = (turns, text) => {
|
|
|
33
42
|
* Props: `apiBase` and `wsUrl` (where the app mounted the chat routes), `api` (partial overrides of the REST
|
|
34
43
|
* calls; every function not given keeps its default), `scope` (passed when creating a chat), `sx`.
|
|
35
44
|
*/
|
|
36
|
-
export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx }) {
|
|
45
|
+
export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, unavailableReasonText, sidePanel, sidePanelLabel, renderAnswerExtra, renderComposerExtra, onAppFrame, layout = 'viewport', compactBelow = 720 }) {
|
|
37
46
|
const { t } = useTranslation();
|
|
38
47
|
const theme = useTheme();
|
|
39
|
-
const
|
|
40
|
-
const
|
|
48
|
+
const viewportNarrow = useMediaQuery(theme.breakpoints.down('md'));
|
|
49
|
+
const rootRef = useRef(null);
|
|
50
|
+
const [containerNarrow, setContainerNarrow] = useState(false);
|
|
41
51
|
const [status, setStatus] = useState('loading'); // loading | available | not_configured | error
|
|
52
|
+
const narrow = layout === 'compact' || (layout === 'container' ? containerNarrow : viewportNarrow);
|
|
53
|
+
const appFrameCallback = useRef(onAppFrame);
|
|
54
|
+
useEffect(() => { appFrameCallback.current = onAppFrame; }, [onAppFrame]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (layout !== 'container')
|
|
57
|
+
return undefined;
|
|
58
|
+
if (typeof ResizeObserver === 'undefined' || !rootRef.current) {
|
|
59
|
+
setContainerNarrow(viewportNarrow);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const observer = new ResizeObserver((entries) => { var _a, _b; const width = (_b = (_a = entries[0]) === null || _a === void 0 ? void 0 : _a.contentRect) === null || _b === void 0 ? void 0 : _b.width; if (width !== undefined)
|
|
63
|
+
setContainerNarrow(width < compactBelow); });
|
|
64
|
+
observer.observe(rootRef.current);
|
|
65
|
+
return () => observer.disconnect();
|
|
66
|
+
}, [layout, compactBelow, viewportNarrow, status]);
|
|
67
|
+
const api = useMemo(() => createChatApi(apiOverrides, { apiBase }), [apiOverrides, apiBase]);
|
|
42
68
|
const [catalogue, setCatalogue] = useState({ entries: [], default: null });
|
|
43
69
|
const [chats, setChats] = useState([]);
|
|
44
70
|
const [archived, setArchived] = useState([]);
|
|
@@ -49,7 +75,12 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
49
75
|
const [olderBusy, setOlderBusy] = useState(false);
|
|
50
76
|
const [tool, setTool] = useState(null);
|
|
51
77
|
const [draft, setDraft] = useState('');
|
|
52
|
-
const [problem, setProblem] = useState(null);
|
|
78
|
+
const [problem, setProblem] = useState(null);
|
|
79
|
+
const [unavailableReason, setUnavailableReason] = useState(null);
|
|
80
|
+
const [trimmedCount, setTrimmedCount] = useState(null);
|
|
81
|
+
const [sidePanelOpen, setSidePanelOpen] = useState(false);
|
|
82
|
+
const turnsRef = useRef(turns);
|
|
83
|
+
useEffect(() => { turnsRef.current = turns; }, [turns]);
|
|
53
84
|
const [listOpen, setListOpen] = useState(false);
|
|
54
85
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
55
86
|
const [openedOnce, setOpenedOnce] = useState(false); // distinguishes a reconnect from the first connect
|
|
@@ -57,19 +88,31 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
57
88
|
const isArchived = Boolean(chat === null || chat === void 0 ? void 0 : chat.archived_at);
|
|
58
89
|
const selection = (chat === null || chat === void 0 ? void 0 : chat.selection) || catalogue.default;
|
|
59
90
|
const activeEntry = catalogue.entries.find((entry) => entry.id === (selection === null || selection === void 0 ? void 0 : selection.entry));
|
|
91
|
+
// Bumped on every scope change so that a list response of the previous scope is dropped.
|
|
92
|
+
const scopeVersion = useRef(0);
|
|
93
|
+
useEffect(() => { scopeVersion.current += 1; setChat(null); }, [scope]);
|
|
60
94
|
const loadLists = useCallback(async () => {
|
|
61
|
-
const
|
|
95
|
+
const version = scopeVersion.current;
|
|
96
|
+
// Without a scope the api is called exactly as before the scope existed.
|
|
97
|
+
const [active, old] = scope === undefined
|
|
98
|
+
? await Promise.all([api.listChats(), api.listChats({ archived: true })])
|
|
99
|
+
: await Promise.all([api.listChats({ scope }), api.listChats({ scope, archived: true })]);
|
|
100
|
+
if (version !== scopeVersion.current)
|
|
101
|
+
return;
|
|
62
102
|
setChats(active.results || []);
|
|
63
103
|
setNextChats(active.next || null);
|
|
64
104
|
setArchived(old.results || []);
|
|
65
|
-
}, [api]);
|
|
105
|
+
}, [api, scope]);
|
|
66
106
|
useEffect(() => {
|
|
67
107
|
let live = true;
|
|
68
|
-
api.getStatus()
|
|
108
|
+
const request = scope === undefined ? api.getStatus() : api.getStatus({ scope });
|
|
109
|
+
request
|
|
69
110
|
.then(async (state) => {
|
|
70
111
|
if (state.status !== 'available') {
|
|
71
|
-
if (live)
|
|
112
|
+
if (live) {
|
|
72
113
|
setStatus(state.status);
|
|
114
|
+
setUnavailableReason(state.reason || null);
|
|
115
|
+
}
|
|
73
116
|
return;
|
|
74
117
|
}
|
|
75
118
|
const [entries] = await Promise.all([api.getCatalogue(), loadLists()]);
|
|
@@ -81,12 +124,14 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
81
124
|
.catch(() => { if (live)
|
|
82
125
|
setStatus('error'); });
|
|
83
126
|
return () => { live = false; };
|
|
84
|
-
}, [api, loadLists]);
|
|
127
|
+
}, [api, loadLists, scope]);
|
|
85
128
|
useEffect(() => {
|
|
86
129
|
setTurns([]);
|
|
87
130
|
setOlderCursor(null);
|
|
88
131
|
setTool(null);
|
|
89
132
|
setProblem(null);
|
|
133
|
+
setTrimmedCount(null);
|
|
134
|
+
setSidePanelOpen(false);
|
|
90
135
|
setDraft('');
|
|
91
136
|
setOpenedOnce(false);
|
|
92
137
|
if (!(chat === null || chat === void 0 ? void 0 : chat.id))
|
|
@@ -94,7 +139,7 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
94
139
|
let live = true;
|
|
95
140
|
api.listTurns(chat.id)
|
|
96
141
|
.then((page) => { if (live) {
|
|
97
|
-
setTurns(chronological(page.results));
|
|
142
|
+
setTurns(chronological((page.results || []).map((turn) => (Object.assign(Object.assign({}, turn), { appFrames: [] })))));
|
|
98
143
|
setOlderCursor(page.next || null);
|
|
99
144
|
} })
|
|
100
145
|
.catch(() => { if (live)
|
|
@@ -102,17 +147,25 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
102
147
|
return () => { live = false; };
|
|
103
148
|
}, [api, chat === null || chat === void 0 ? void 0 : chat.id]);
|
|
104
149
|
const onFrame = useCallback((frame, chatId) => {
|
|
150
|
+
var _a;
|
|
105
151
|
if (chatId !== (chat === null || chat === void 0 ? void 0 : chat.id))
|
|
106
152
|
return;
|
|
107
153
|
if (frame.type === 'history')
|
|
108
|
-
setTurns(chronological(frame.turns));
|
|
154
|
+
setTurns(chronological((frame.turns || []).map((turn) => (Object.assign(Object.assign({}, turn), { appFrames: [] })))));
|
|
109
155
|
else if (frame.type === 'delta')
|
|
110
156
|
setTurns((current) => appendDelta(current, frame.text || ''));
|
|
157
|
+
else if (frame.type === 'app') {
|
|
158
|
+
setTurns((current) => attachAppFrame(current, frame));
|
|
159
|
+
const running = turnsRef.current.findLast(isGenerating);
|
|
160
|
+
(_a = appFrameCallback.current) === null || _a === void 0 ? void 0 : _a.call(appFrameCallback, { name: frame.name, payload: frame.payload, chatId, turnId: running === null || running === void 0 ? void 0 : running.id });
|
|
161
|
+
}
|
|
162
|
+
else if (frame.type === 'notice' && frame.code === 'history_trimmed')
|
|
163
|
+
setTrimmedCount(frame.left_out || 0);
|
|
111
164
|
else if (frame.type === 'tool')
|
|
112
165
|
setTool(frame.phase === 'start' ? { label: frame.label, name: frame.name } : null);
|
|
113
166
|
else if (frame.type === 'turn' && frame.turn) {
|
|
114
167
|
setTool(null);
|
|
115
|
-
setTurns((current) => settleTurn(current, frame.turn));
|
|
168
|
+
setTurns((current) => { const pending = current.findLast(isGenerating); return settleTurn(current, Object.assign(Object.assign({}, frame.turn), { appFrames: (pending === null || pending === void 0 ? void 0 : pending.appFrames) || [] })); });
|
|
116
169
|
if (frame.turn.error_code === 'quota_exceeded')
|
|
117
170
|
setProblem('quota_exceeded');
|
|
118
171
|
}
|
|
@@ -120,6 +173,8 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
120
173
|
setTool(null);
|
|
121
174
|
setTurns((current) => failGenerating(current, frame.code));
|
|
122
175
|
setProblem(frame.code);
|
|
176
|
+
if (frame.code === 'unavailable')
|
|
177
|
+
setUnavailableReason(frame.reason || unavailableReason);
|
|
123
178
|
}
|
|
124
179
|
}, [chat === null || chat === void 0 ? void 0 : chat.id]);
|
|
125
180
|
const onOpen = useCallback(() => { setOpenedOnce(true); setProblem((current) => (current === 'connection_lost' ? null : current)); }, []);
|
|
@@ -137,7 +192,7 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
137
192
|
};
|
|
138
193
|
const selectChat = (next) => { setChat(next); setListOpen(false); };
|
|
139
194
|
const createChat = guarded(async () => {
|
|
140
|
-
const created = await api.createChat(scope ? { scope } : {});
|
|
195
|
+
const created = await api.createChat(scope !== undefined ? { scope } : {});
|
|
141
196
|
setChats((current) => [created, ...current]);
|
|
142
197
|
selectChat(created);
|
|
143
198
|
});
|
|
@@ -155,7 +210,7 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
155
210
|
setChat(null);
|
|
156
211
|
});
|
|
157
212
|
const loadMoreChats = guarded(async () => {
|
|
158
|
-
const page = await api.listChats({ cursor: nextChats });
|
|
213
|
+
const page = await api.listChats(Object.assign(Object.assign({}, (scope !== undefined ? { scope } : {})), { cursor: nextChats }));
|
|
159
214
|
setChats((current) => [...current, ...(page.results || [])]);
|
|
160
215
|
setNextChats(page.next || null);
|
|
161
216
|
});
|
|
@@ -205,14 +260,19 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
205
260
|
return null;
|
|
206
261
|
};
|
|
207
262
|
const list = (_jsx(ChatList, { chats: chats, archivedChats: archived, activeId: chat === null || chat === void 0 ? void 0 : chat.id, hasMore: Boolean(nextChats), onSelect: selectChat, onNew: createChat, onRename: (target, title) => updateChat(target, { title }), onArchive: (target) => updateChat(target, { archived: true }), onRestore: (target) => updateChat(target, { archived: false }), onDelete: deleteChat, onLoadMore: loadMoreChats }));
|
|
208
|
-
const
|
|
263
|
+
const notConfigured = status === 'not_configured' || problem === 'not_configured';
|
|
264
|
+
const unavailable = status === 'unavailable' || problem === 'unavailable';
|
|
265
|
+
const unavailableText = typeof unavailableReasonText === 'function' ? unavailableReasonText(unavailableReason) : unavailableReasonText === null || unavailableReasonText === void 0 ? void 0 : unavailableReasonText[unavailableReason];
|
|
209
266
|
if (status === 'loading')
|
|
210
267
|
return _jsx(Box, { sx: sx, "aria-busy": "true" });
|
|
211
268
|
if (status === 'error')
|
|
212
269
|
return _jsx(Box, { sx: sx, children: _jsx(Alert, { severity: "error", children: t('Chat.loadError') }) });
|
|
213
|
-
if (
|
|
270
|
+
if (notConfigured) {
|
|
214
271
|
return (_jsxs(Stack, { alignItems: "center", justifyContent: "center", spacing: 1, sx: Object.assign({ minHeight: 320, p: 3, textAlign: 'center' }, sx), children: [_jsx(Typography, { variant: "h6", children: t('Chat.unavailable') }), _jsx(Typography, { color: "text.secondary", children: t('Chat.noModel') })] }));
|
|
215
272
|
}
|
|
216
|
-
|
|
273
|
+
if (unavailable) {
|
|
274
|
+
return (_jsx(Stack, { alignItems: "center", justifyContent: "center", spacing: 1, sx: Object.assign({ minHeight: 320, p: 3, textAlign: 'center' }, sx), children: _jsx(Typography, { color: "text.secondary", children: unavailableText || t('Chat.notAvailableHere') }) }));
|
|
275
|
+
}
|
|
276
|
+
return (_jsxs(Box, { ref: rootRef, sx: Object.assign({ display: 'grid', gridTemplateColumns: narrow ? 'minmax(0, 1fr)' : `minmax(240px, 320px) minmax(0, 1fr)${chat && sidePanel ? ' minmax(240px, 320px)' : ''}`, height: '100%', minHeight: 480, border: 1, borderColor: 'divider', borderRadius: 1, overflow: 'hidden', bgcolor: 'background.paper' }, sx), children: [narrow ? null : _jsx(Box, { sx: { borderRight: 1, borderColor: 'divider', minHeight: 0 }, children: list }), _jsxs(Stack, { sx: { minWidth: 0, minHeight: 0 }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, sx: { px: 1.5, py: 0.5, borderBottom: 1, borderColor: 'divider', minHeight: 56 }, children: [narrow ? _jsx(Button, { color: "inherit", onClick: () => setListOpen(true), children: t('Chat.chats') }) : null, _jsx(Typography, { noWrap: true, sx: { flex: 1, fontWeight: 600 }, children: chat ? (chat.title || t('Chat.untitled')) : '' }), socket.status === 'connecting' && openedOnce ? _jsx(Chip, { size: "small", color: "warning", variant: "outlined", label: t('Chat.reconnecting') }) : null, isArchived ? _jsx(Chip, { size: "small", label: t('Chat.archived') }) : null, isArchived ? _jsx(Button, { size: "small", onClick: () => updateChat(chat, { archived: false }), children: t('Chat.restore') }) : null, chat ? (_jsx(ModelPicker, { entries: catalogue.entries, selection: selection, disabled: isArchived || running, open: pickerOpen, onOpenChange: setPickerOpen, onChange: (next) => updateChat(chat, { selection: next }), sheet: narrow })) : null, chat && sidePanel && narrow ? _jsx(Button, { color: "inherit", onClick: () => setSidePanelOpen(true), children: sidePanelLabel || t('Chat.sidePanel') }) : null] }), chat ? (_jsxs(_Fragment, { children: [_jsx(ChatThread, { turns: turns, chat: chat, renderAnswerExtra: renderAnswerExtra, trimmedCount: trimmedCount, tool: tool, notice: noticeFor(), hasOlder: Boolean(olderCursor), olderBusy: olderBusy, onLoadOlder: loadOlder, onRetry: retry, readOnly: isArchived }), isArchived ? _jsx(Typography, { variant: "caption", color: "text.secondary", sx: { px: 2, pb: 1 }, children: t('Chat.archivedReadOnly') }) : null, renderComposerExtra ? _jsx(Box, { children: renderComposerExtra({ chat }) }, chat.id) : null, _jsx(ChatComposer, { value: draft, onChange: setDraft, onSend: send, onStop: () => socket.send({ type: 'stop' }), running: running, archived: isArchived, connected: socket.connected })] })) : (_jsxs(Stack, { alignItems: "center", justifyContent: "center", spacing: 2, sx: { flex: 1, p: 3 }, children: [_jsx(Typography, { color: "text.secondary", align: "center", children: t('Chat.selectChat') }), _jsx(Button, { variant: "contained", onClick: createChat, children: t('Chat.newChat') })] }))] }), chat && sidePanel && !narrow ? _jsx(Box, { sx: { borderLeft: 1, borderColor: 'divider', minHeight: 0, overflow: 'auto' }, children: sidePanel(chat) }, chat.id) : null, narrow && chat && sidePanel ? _jsx(Drawer, { anchor: "right", open: sidePanelOpen, onClose: () => setSidePanelOpen(false), slotProps: { paper: { 'aria-label': sidePanelLabel || t('Chat.sidePanel') } }, children: _jsx(Box, { children: sidePanel(chat) }, chat.id) }) : null, narrow ? _jsx(Drawer, { open: listOpen, onClose: () => setListOpen(false), slotProps: { paper: { sx: { width: 'min(320px, 86vw)' } } }, children: list }) : null] }));
|
|
217
277
|
}
|
|
218
278
|
export default ChatWorkspace;
|
package/dist/chat/ModelPicker.js
CHANGED
|
@@ -12,10 +12,11 @@ const groupByDriver = (entries) => entries.reduce((groups, entry) => {
|
|
|
12
12
|
* Model and effort of one chat. A popover at wide widths, a bottom sheet at narrow widths. `onChange` receives
|
|
13
13
|
* `{ entry, effort }`; the caller persists it (the change applies from the next turn). Can be controlled with `open`.
|
|
14
14
|
*/
|
|
15
|
-
export function ModelPicker({ entries = [], selection, onChange, disabled = false, open, onOpenChange }) {
|
|
15
|
+
export function ModelPicker({ entries = [], selection, onChange, disabled = false, open, onOpenChange, sheet }) {
|
|
16
16
|
const { t } = useTranslation();
|
|
17
17
|
const theme = useTheme();
|
|
18
|
-
const
|
|
18
|
+
const viewportNarrow = useMediaQuery(theme.breakpoints.down('md'));
|
|
19
|
+
const narrow = sheet !== null && sheet !== void 0 ? sheet : viewportNarrow;
|
|
19
20
|
const anchor = useRef(null);
|
|
20
21
|
const [innerOpen, setInnerOpen] = useState(false);
|
|
21
22
|
const isOpen = open !== null && open !== void 0 ? open : innerOpen;
|
package/dist/chat/api.js
CHANGED
|
@@ -5,12 +5,12 @@ export function createChatApi(overrides = {}, { apiBase = '/api/assistant/' } =
|
|
|
5
5
|
const data = (request) => request.then((response) => response.data);
|
|
6
6
|
const defaults = {
|
|
7
7
|
getCatalogue: () => data(apiClient.get(urls.catalogue)),
|
|
8
|
-
listChats: ({ archived = false, cursor } = {}) => data(apiClient.get(urls.chats, { params: Object.assign({ archived }, (cursor ? { cursor } : {})) })),
|
|
8
|
+
listChats: ({ archived = false, cursor, scope } = {}) => data(apiClient.get(urls.chats, { params: Object.assign(Object.assign({ archived }, (cursor ? { cursor } : {})), (scope !== undefined ? { scope } : {})) })),
|
|
9
9
|
createChat: (payload = {}) => data(apiClient.post(urls.chats, payload)),
|
|
10
10
|
updateChat: (id, patch) => data(apiClient.patch(urls.chat(id), patch)),
|
|
11
11
|
deleteChat: (id) => data(apiClient.delete(urls.chat(id))),
|
|
12
12
|
listTurns: (id, { cursor } = {}) => data(apiClient.get(urls.turns(id), { params: cursor ? { cursor } : {} })),
|
|
13
|
-
getStatus: () => data(apiClient.get(urls.status)),
|
|
13
|
+
getStatus: ({ scope } = {}) => data(scope !== undefined ? apiClient.get(urls.status, { params: { scope } }) : apiClient.get(urls.status)),
|
|
14
14
|
};
|
|
15
15
|
return Object.assign(Object.assign({}, defaults), overrides);
|
|
16
16
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createKitSxEntry } from '../theme/kitSxEntry';
|
|
2
2
|
import { CHAT_LIST_ARCHIVED_ACTION_SX, CHAT_LIST_ROW_ACTION_SX } from './ChatList';
|
|
3
|
-
import { CHAT_THREAD_NOTICE_SX, CHAT_THREAD_TOOL_CHIP_SX } from './ChatThread';
|
|
3
|
+
import { CHAT_THREAD_NOTICE_SX, CHAT_THREAD_TOOL_CHIP_SX, CHAT_THREAD_TRIMMED_SX } from './ChatThread';
|
|
4
4
|
export const CHAT_KIT_SX_REGISTRY = [
|
|
5
5
|
createKitSxEntry('ChatList.rowAction', 'MuiIconButton', CHAT_LIST_ROW_ACTION_SX, 'CHAT_LIST_ROW_ACTION_SX'),
|
|
6
6
|
createKitSxEntry('ChatList.archivedAction', 'MuiButton', CHAT_LIST_ARCHIVED_ACTION_SX, 'CHAT_LIST_ARCHIVED_ACTION_SX'),
|
|
7
7
|
createKitSxEntry('ChatThread.toolChip', 'MuiChip', CHAT_THREAD_TOOL_CHIP_SX, 'CHAT_THREAD_TOOL_CHIP_SX'),
|
|
8
|
+
createKitSxEntry('ChatThread.trimmed', 'MuiChip', CHAT_THREAD_TRIMMED_SX, 'CHAT_THREAD_TRIMMED_SX'),
|
|
8
9
|
createKitSxEntry('ChatThread.notice', 'MuiAlert', CHAT_THREAD_NOTICE_SX, 'CHAT_THREAD_NOTICE_SX'),
|
|
9
10
|
];
|
package/dist/chat/wire.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
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'];
|
|
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', 'unavailable'];
|
|
3
3
|
export const chatApiUrls = (base = '/api/assistant/') => {
|
|
4
4
|
const root = base.replace(/\/$/, '') + '/';
|
|
5
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
6
|
};
|
|
7
7
|
export const defaultChatWsUrl = (chatId) => (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + '/ws/assistant/chats/' + encodeURIComponent(chatId) + '/';
|
|
8
|
+
export const CHAT_FRAME_TYPES = { APP: 'app', NOTICE: 'notice' };
|
|
9
|
+
export const CHAT_NOTICE_CODES = { HISTORY_TRIMMED: 'history_trimmed' };
|
|
@@ -33,6 +33,10 @@ export const chatTranslations = {
|
|
|
33
33
|
'Chat.changeModel': { de: 'Modell ändern', en: 'Change model', fr: 'Changer de modèle', sw: 'Badilisha modeli' },
|
|
34
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
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.notAvailableHere': { de: 'Der Assistent ist hier nicht verfügbar.', en: 'The assistant is not available here.', fr: 'L’assistant n’est pas disponible ici.', sw: 'Msaidizi hapatikani hapa.' },
|
|
37
|
+
'Chat.historyTrimmed_one': { de: '{{count}} frühere Nachricht wurde ausgelassen', en: '{{count}} earlier message left out', fr: '{{count}} message précédent omis', sw: 'Ujumbe {{count}} wa awali umeachwa' },
|
|
38
|
+
'Chat.historyTrimmed_other': { de: '{{count}} frühere Nachrichten wurden ausgelassen', en: '{{count}} earlier messages left out', fr: '{{count}} messages précédents omis', sw: 'Ujumbe {{count}} wa awali umeachwa' },
|
|
39
|
+
'Chat.sidePanel': { de: 'Details', en: 'Details', fr: 'Détails', sw: 'Maelezo' },
|
|
36
40
|
'Chat.unavailable': { de: 'Chat ist nicht verfügbar', en: 'Chat is not available', fr: 'Le chat n’est pas disponible', sw: 'Mazungumzo hayapatikani' },
|
|
37
41
|
'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
42
|
'Chat.modelChanged': { de: 'Modell gewechselt: {{model}}', en: 'Model changed: {{model}}', fr: 'Modèle changé : {{model}}', sw: 'Modeli imebadilishwa: {{model}}' },
|