@micha.bigler/ui-core-micha 4.0.1 → 4.2.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 +30 -0
- package/dist/chat/ChatComposer.js +2 -2
- package/dist/chat/ChatThread.js +7 -5
- package/dist/chat/ChatWorkspace.js +187 -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,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.2.0 - UCM-CHAT-4
|
|
4
|
+
|
|
5
|
+
`ChatWorkspace` takes two optional props so a host can control which chat is shown. `activeChatId` shows that chat
|
|
6
|
+
(`null` shows none, `undefined`, the default, keeps the selection internal); an id that is not in the loaded lists
|
|
7
|
+
triggers one reload of the lists, and an id that still cannot be found leaves the normal no-chat state. It is applied
|
|
8
|
+
only when the prop value changes, so a host that echoes `onActiveChatChange` back causes no extra requests.
|
|
9
|
+
`onActiveChatChange(chatId)` is called when the id of the shown chat changes (selection, creation, deletion as `null`,
|
|
10
|
+
a change driven by `activeChatId`), not for the initial state with no chat and not when the shown chat is only
|
|
11
|
+
archived, renamed or updated.
|
|
12
|
+
`composerPrefill` (`{ id, text }`) puts the text into the composer of the shown chat and focuses it; each new `id` is
|
|
13
|
+
applied (the same text twice, if asked twice), and a request with no chat shown, for an archived chat, or while
|
|
14
|
+
`activeChatId` is `null` is ignored. A prefill sent together with an `activeChatId` switch waits for the requested chat.
|
|
15
|
+
`renderAnswerExtra` additionally receives `send(text)`, which sends the text as the next message exactly like a typed
|
|
16
|
+
send, and `canSend` (false while a turn runs, for an archived chat and while disconnected). `ChatComposer` takes an
|
|
17
|
+
optional `inputRef`. Switching chats now clears the previous chat's content before paint.
|
|
18
|
+
Without the new props nothing changes.
|
|
19
|
+
|
|
20
|
+
## 4.1.0 - UCM-CHAT-3
|
|
21
|
+
|
|
22
|
+
Extension points on the `/chat` surface; with none of the new props `ChatWorkspace` renders and behaves as in 4.0.x.
|
|
23
|
+
New `ChatWorkspace` props: `scope` (now also sent on the chat lists and the status request), `unavailableReasonText`
|
|
24
|
+
(object or function mapping the server's `unavailable` reason to the app's text; generic fallback otherwise),
|
|
25
|
+
`sidePanel(chat)` with `sidePanelLabel` (a column next to the thread, a sheet in the narrow and compact layouts),
|
|
26
|
+
`renderAnswerExtra({ turn, chat, appFrames })` (under each answer, with the `app` frames that arrived during that turn),
|
|
27
|
+
`renderComposerExtra({ chat })`, `onAppFrame({ name, payload, chatId, turnId })`, and `layout` (`'viewport'` default,
|
|
28
|
+
`'container'` follows the container width below `compactBelow` px, `'compact'` always narrow). `ModelPicker` takes an
|
|
29
|
+
optional `sheet` boolean. A `history_trimmed` notice shows a quiet chip with the number of left-out messages. New texts
|
|
30
|
+
`Chat.notAvailableHere`, `Chat.historyTrimmed_one/_other` and the fallback `Chat.sidePanel` (de/en/fr/sw). Built
|
|
31
|
+
against the `django-core-micha` DCM-CHAT-10 contract.
|
|
32
|
+
|
|
3
33
|
## 4.0.1 - UCM-DEP-3
|
|
4
34
|
|
|
5
35
|
The `exports` map no longer lists `./package.json`; the public entries are exactly `.`, `./charts`, `./chat` and `./markdown`.
|
|
@@ -6,13 +6,13 @@ import { useTranslation } from 'react-i18next';
|
|
|
6
6
|
* Message input. Send is disabled while a turn runs, for an archived chat and while the connection is down;
|
|
7
7
|
* Stop replaces it during a turn. Enter sends, Shift+Enter inserts a line break.
|
|
8
8
|
*/
|
|
9
|
-
export function ChatComposer({ value = '', onChange, onSend, onStop, running = false, archived = false, connected = true, sx }) {
|
|
9
|
+
export function ChatComposer({ value = '', onChange, onSend, onStop, running = false, archived = false, connected = true, inputRef, sx }) {
|
|
10
10
|
const { t } = useTranslation();
|
|
11
11
|
const blocked = archived || !connected;
|
|
12
12
|
const canSend = !running && !blocked && value.trim().length > 0;
|
|
13
13
|
const submit = () => { if (canSend)
|
|
14
14
|
onSend === null || onSend === void 0 ? void 0 : onSend(); };
|
|
15
|
-
return (_jsxs(Stack, { direction: "row", alignItems: "flex-end", gap: 1, sx: Object.assign({ p: { xs: 1, md: 2 }, borderTop: 1, borderColor: 'divider' }, sx), children: [_jsx(TextField, { multiline: true, maxRows: 6, fullWidth: true, size: "small", value: value, disabled: blocked, placeholder: t('Chat.message'), slotProps: { htmlInput: { 'aria-label': t('Chat.message') } }, onChange: (event) => onChange === null || onChange === void 0 ? void 0 : onChange(event.target.value), onKeyDown: (event) => {
|
|
15
|
+
return (_jsxs(Stack, { direction: "row", alignItems: "flex-end", gap: 1, sx: Object.assign({ p: { xs: 1, md: 2 }, borderTop: 1, borderColor: 'divider' }, sx), children: [_jsx(TextField, { multiline: true, maxRows: 6, fullWidth: true, size: "small", value: value, inputRef: inputRef, disabled: blocked, placeholder: t('Chat.message'), slotProps: { htmlInput: { 'aria-label': t('Chat.message') } }, onChange: (event) => onChange === null || onChange === void 0 ? void 0 : onChange(event.target.value), onKeyDown: (event) => {
|
|
16
16
|
var _a;
|
|
17
17
|
if (event.key === 'Enter' && !event.shiftKey && !((_a = event.nativeEvent) === null || _a === void 0 ? void 0 : _a.isComposing)) {
|
|
18
18
|
event.preventDefault();
|
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, useLayoutEffect, 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,23 +42,57 @@ 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, activeChatId, onActiveChatChange, composerPrefill, 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([]);
|
|
45
71
|
const [nextChats, setNextChats] = useState(null);
|
|
46
72
|
const [chat, setChat] = useState(null);
|
|
73
|
+
const [listsLoaded, setListsLoaded] = useState(false);
|
|
47
74
|
const [turns, setTurns] = useState([]);
|
|
48
75
|
const [olderCursor, setOlderCursor] = useState(null);
|
|
49
76
|
const [olderBusy, setOlderBusy] = useState(false);
|
|
50
77
|
const [tool, setTool] = useState(null);
|
|
51
78
|
const [draft, setDraft] = useState('');
|
|
52
|
-
const
|
|
79
|
+
const composerInputRef = useRef(null);
|
|
80
|
+
const lastPrefillId = useRef(undefined);
|
|
81
|
+
const [focusRequest, setFocusRequest] = useState(0);
|
|
82
|
+
const focusPending = useRef(false);
|
|
83
|
+
const [problem, setProblem] = useState(null);
|
|
84
|
+
const [unavailableReason, setUnavailableReason] = useState(null);
|
|
85
|
+
const [trimmedCount, setTrimmedCount] = useState(null);
|
|
86
|
+
const [sidePanelOpen, setSidePanelOpen] = useState(false);
|
|
87
|
+
const turnsRef = useRef(turns);
|
|
88
|
+
useEffect(() => { turnsRef.current = turns; }, [turns]);
|
|
89
|
+
const activeChangeCallback = useRef(onActiveChatChange);
|
|
90
|
+
activeChangeCallback.current = onActiveChatChange;
|
|
91
|
+
const lastReportedId = useRef(null);
|
|
92
|
+
const latestWantedId = useRef(activeChatId);
|
|
93
|
+
latestWantedId.current = activeChatId;
|
|
94
|
+
const appliedId = useRef(undefined);
|
|
95
|
+
const unloadableIds = useRef(new Set()); // ids the host asked for that no list contains
|
|
53
96
|
const [listOpen, setListOpen] = useState(false);
|
|
54
97
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
55
98
|
const [openedOnce, setOpenedOnce] = useState(false); // distinguishes a reconnect from the first connect
|
|
@@ -57,44 +100,108 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
57
100
|
const isArchived = Boolean(chat === null || chat === void 0 ? void 0 : chat.archived_at);
|
|
58
101
|
const selection = (chat === null || chat === void 0 ? void 0 : chat.selection) || catalogue.default;
|
|
59
102
|
const activeEntry = catalogue.entries.find((entry) => entry.id === (selection === null || selection === void 0 ? void 0 : selection.entry));
|
|
103
|
+
// Bumped on every scope change so that a list response of the previous scope is dropped.
|
|
104
|
+
const scopeVersion = useRef(0);
|
|
105
|
+
useEffect(() => { scopeVersion.current += 1; setChat(null); setListsLoaded(false); appliedId.current = undefined; unloadableIds.current.clear(); }, [scope]);
|
|
60
106
|
const loadLists = useCallback(async () => {
|
|
61
|
-
const
|
|
107
|
+
const version = scopeVersion.current;
|
|
108
|
+
// Without a scope the api is called exactly as before the scope existed.
|
|
109
|
+
const [active, old] = scope === undefined
|
|
110
|
+
? await Promise.all([api.listChats(), api.listChats({ archived: true })])
|
|
111
|
+
: await Promise.all([api.listChats({ scope }), api.listChats({ scope, archived: true })]);
|
|
112
|
+
if (version !== scopeVersion.current)
|
|
113
|
+
return undefined;
|
|
62
114
|
setChats(active.results || []);
|
|
63
115
|
setNextChats(active.next || null);
|
|
64
116
|
setArchived(old.results || []);
|
|
65
|
-
|
|
117
|
+
return [...(active.results || []), ...(old.results || [])];
|
|
118
|
+
}, [api, scope]);
|
|
66
119
|
useEffect(() => {
|
|
67
120
|
let live = true;
|
|
68
|
-
api.getStatus()
|
|
121
|
+
const request = scope === undefined ? api.getStatus() : api.getStatus({ scope });
|
|
122
|
+
request
|
|
69
123
|
.then(async (state) => {
|
|
70
124
|
if (state.status !== 'available') {
|
|
71
|
-
if (live)
|
|
125
|
+
if (live) {
|
|
72
126
|
setStatus(state.status);
|
|
127
|
+
setUnavailableReason(state.reason || null);
|
|
128
|
+
}
|
|
73
129
|
return;
|
|
74
130
|
}
|
|
75
131
|
const [entries] = await Promise.all([api.getCatalogue(), loadLists()]);
|
|
76
132
|
if (live) {
|
|
77
133
|
setStatus(state.status);
|
|
78
134
|
setCatalogue(entries);
|
|
135
|
+
setListsLoaded(true);
|
|
79
136
|
}
|
|
80
137
|
})
|
|
81
138
|
.catch(() => { if (live)
|
|
82
139
|
setStatus('error'); });
|
|
83
140
|
return () => { live = false; };
|
|
84
|
-
}, [api, loadLists]);
|
|
141
|
+
}, [api, loadLists, scope]);
|
|
85
142
|
useEffect(() => {
|
|
143
|
+
var _a;
|
|
144
|
+
var _b;
|
|
145
|
+
const id = (_b = chat === null || chat === void 0 ? void 0 : chat.id) !== null && _b !== void 0 ? _b : null;
|
|
146
|
+
if (id === lastReportedId.current)
|
|
147
|
+
return;
|
|
148
|
+
lastReportedId.current = id;
|
|
149
|
+
(_a = activeChangeCallback.current) === null || _a === void 0 ? void 0 : _a.call(activeChangeCallback, id);
|
|
150
|
+
}, [chat === null || chat === void 0 ? void 0 : chat.id]);
|
|
151
|
+
// A layout effect: a host-driven switch to a known chat is applied before the browser paints.
|
|
152
|
+
useLayoutEffect(() => {
|
|
153
|
+
if (activeChatId === undefined || status !== 'available' || !listsLoaded)
|
|
154
|
+
return undefined;
|
|
155
|
+
const wantedKey = activeChatId === null ? null : String(activeChatId);
|
|
156
|
+
if (wantedKey === null) {
|
|
157
|
+
appliedId.current = null;
|
|
158
|
+
setChat(null);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (String(chat === null || chat === void 0 ? void 0 : chat.id) === wantedKey)
|
|
162
|
+
return;
|
|
163
|
+
const existing = [...chats, ...archived].find((item) => String(item.id) === wantedKey);
|
|
164
|
+
if (existing) {
|
|
165
|
+
setChat(existing);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (appliedId.current === wantedKey)
|
|
169
|
+
return;
|
|
170
|
+
appliedId.current = wantedKey;
|
|
171
|
+
let live = true;
|
|
172
|
+
loadLists().then((loaded) => {
|
|
173
|
+
if (!live || loaded === undefined || latestWantedId.current === undefined || String(latestWantedId.current) !== wantedKey)
|
|
174
|
+
return;
|
|
175
|
+
const found = loaded.find((item) => String(item.id) === wantedKey);
|
|
176
|
+
if (!found)
|
|
177
|
+
unloadableIds.current.add(wantedKey);
|
|
178
|
+
setChat(found || null);
|
|
179
|
+
}).catch(() => {
|
|
180
|
+
if (live && latestWantedId.current !== undefined && String(latestWantedId.current) === wantedKey) {
|
|
181
|
+
unloadableIds.current.add(wantedKey);
|
|
182
|
+
setChat(null);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return () => { live = false; };
|
|
186
|
+
}, [activeChatId, status, listsLoaded]);
|
|
187
|
+
// Clear the previous chat's content before the browser paints, so a host-driven switch never flashes it under the new title.
|
|
188
|
+
useLayoutEffect(() => {
|
|
86
189
|
setTurns([]);
|
|
87
190
|
setOlderCursor(null);
|
|
88
191
|
setTool(null);
|
|
89
192
|
setProblem(null);
|
|
193
|
+
setTrimmedCount(null);
|
|
194
|
+
setSidePanelOpen(false);
|
|
90
195
|
setDraft('');
|
|
91
196
|
setOpenedOnce(false);
|
|
197
|
+
}, [chat === null || chat === void 0 ? void 0 : chat.id]);
|
|
198
|
+
useEffect(() => {
|
|
92
199
|
if (!(chat === null || chat === void 0 ? void 0 : chat.id))
|
|
93
200
|
return undefined;
|
|
94
201
|
let live = true;
|
|
95
202
|
api.listTurns(chat.id)
|
|
96
203
|
.then((page) => { if (live) {
|
|
97
|
-
setTurns(chronological(page.results));
|
|
204
|
+
setTurns(chronological((page.results || []).map((turn) => (Object.assign(Object.assign({}, turn), { appFrames: [] })))));
|
|
98
205
|
setOlderCursor(page.next || null);
|
|
99
206
|
} })
|
|
100
207
|
.catch(() => { if (live)
|
|
@@ -102,17 +209,25 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
102
209
|
return () => { live = false; };
|
|
103
210
|
}, [api, chat === null || chat === void 0 ? void 0 : chat.id]);
|
|
104
211
|
const onFrame = useCallback((frame, chatId) => {
|
|
212
|
+
var _a;
|
|
105
213
|
if (chatId !== (chat === null || chat === void 0 ? void 0 : chat.id))
|
|
106
214
|
return;
|
|
107
215
|
if (frame.type === 'history')
|
|
108
|
-
setTurns(chronological(frame.turns));
|
|
216
|
+
setTurns(chronological((frame.turns || []).map((turn) => (Object.assign(Object.assign({}, turn), { appFrames: [] })))));
|
|
109
217
|
else if (frame.type === 'delta')
|
|
110
218
|
setTurns((current) => appendDelta(current, frame.text || ''));
|
|
219
|
+
else if (frame.type === 'app') {
|
|
220
|
+
setTurns((current) => attachAppFrame(current, frame));
|
|
221
|
+
const running = turnsRef.current.findLast(isGenerating);
|
|
222
|
+
(_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 });
|
|
223
|
+
}
|
|
224
|
+
else if (frame.type === 'notice' && frame.code === 'history_trimmed')
|
|
225
|
+
setTrimmedCount(frame.left_out || 0);
|
|
111
226
|
else if (frame.type === 'tool')
|
|
112
227
|
setTool(frame.phase === 'start' ? { label: frame.label, name: frame.name } : null);
|
|
113
228
|
else if (frame.type === 'turn' && frame.turn) {
|
|
114
229
|
setTool(null);
|
|
115
|
-
setTurns((current) => settleTurn(current, frame.turn));
|
|
230
|
+
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
231
|
if (frame.turn.error_code === 'quota_exceeded')
|
|
117
232
|
setProblem('quota_exceeded');
|
|
118
233
|
}
|
|
@@ -120,12 +235,45 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
120
235
|
setTool(null);
|
|
121
236
|
setTurns((current) => failGenerating(current, frame.code));
|
|
122
237
|
setProblem(frame.code);
|
|
238
|
+
if (frame.code === 'unavailable')
|
|
239
|
+
setUnavailableReason(frame.reason || unavailableReason);
|
|
123
240
|
}
|
|
124
241
|
}, [chat === null || chat === void 0 ? void 0 : chat.id]);
|
|
125
242
|
const onOpen = useCallback(() => { setOpenedOnce(true); setProblem((current) => (current === 'connection_lost' ? null : current)); }, []);
|
|
126
243
|
const onClose = useCallback(({ reason, retrying }) => { if (!retrying)
|
|
127
244
|
setProblem(reason); }, []);
|
|
128
245
|
const socket = useChatSocket(chat === null || chat === void 0 ? void 0 : chat.id, { wsUrl, onFrame, onOpen, onClose });
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (composerPrefill == null || composerPrefill.id === lastPrefillId.current)
|
|
248
|
+
return;
|
|
249
|
+
// `null` means the host wants no chat shown: nothing to fill, and the chat being left must not receive the text.
|
|
250
|
+
if (activeChatId === null) {
|
|
251
|
+
lastPrefillId.current = composerPrefill.id;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// The host switches chats and prefills in one go: wait for the requested chat instead of filling the one being left.
|
|
255
|
+
const wanted = activeChatId == null ? null : String(activeChatId);
|
|
256
|
+
if (wanted !== null && String(chat === null || chat === void 0 ? void 0 : chat.id) !== wanted && !unloadableIds.current.has(wanted))
|
|
257
|
+
return;
|
|
258
|
+
lastPrefillId.current = composerPrefill.id;
|
|
259
|
+
if (!chat || isArchived)
|
|
260
|
+
return;
|
|
261
|
+
setDraft(composerPrefill.text);
|
|
262
|
+
focusPending.current = true;
|
|
263
|
+
setFocusRequest((count) => count + 1);
|
|
264
|
+
}, [composerPrefill, chat, isArchived, activeChatId]);
|
|
265
|
+
// Runs after the render that carries the prefilled draft; a counter, so the same text requested twice focuses twice.
|
|
266
|
+
// A disabled input (connection down) cannot take focus, so the request stays pending until it is enabled again.
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
var _a;
|
|
269
|
+
const input = composerInputRef.current;
|
|
270
|
+
if (!focusPending.current || !input || input.disabled)
|
|
271
|
+
return;
|
|
272
|
+
focusPending.current = false;
|
|
273
|
+
input.focus();
|
|
274
|
+
const end = input.value.length;
|
|
275
|
+
(_a = input.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(input, end, end);
|
|
276
|
+
}, [focusRequest, socket.connected]);
|
|
129
277
|
const guarded = (action) => async (...args) => {
|
|
130
278
|
try {
|
|
131
279
|
return await action(...args);
|
|
@@ -137,7 +285,7 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
137
285
|
};
|
|
138
286
|
const selectChat = (next) => { setChat(next); setListOpen(false); };
|
|
139
287
|
const createChat = guarded(async () => {
|
|
140
|
-
const created = await api.createChat(scope ? { scope } : {});
|
|
288
|
+
const created = await api.createChat(scope !== undefined ? { scope } : {});
|
|
141
289
|
setChats((current) => [created, ...current]);
|
|
142
290
|
selectChat(created);
|
|
143
291
|
});
|
|
@@ -155,7 +303,7 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
155
303
|
setChat(null);
|
|
156
304
|
});
|
|
157
305
|
const loadMoreChats = guarded(async () => {
|
|
158
|
-
const page = await api.listChats({ cursor: nextChats });
|
|
306
|
+
const page = await api.listChats(Object.assign(Object.assign({}, (scope !== undefined ? { scope } : {})), { cursor: nextChats }));
|
|
159
307
|
setChats((current) => [...current, ...(page.results || [])]);
|
|
160
308
|
setNextChats(page.next || null);
|
|
161
309
|
});
|
|
@@ -180,6 +328,22 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
180
328
|
}]);
|
|
181
329
|
return true;
|
|
182
330
|
};
|
|
331
|
+
const canSendFromSlot = Boolean(chat && !isArchived && socket.connected && !running);
|
|
332
|
+
// A slot may keep an old `send` around: read the latest availability, and close it right away after a send.
|
|
333
|
+
const canSendRef = useRef(false);
|
|
334
|
+
canSendRef.current = canSendFromSlot;
|
|
335
|
+
const sendFromSlot = (text) => {
|
|
336
|
+
const message = typeof text === 'string' ? text : '';
|
|
337
|
+
if (!canSendRef.current || !message.trim())
|
|
338
|
+
return false;
|
|
339
|
+
const started = startTurn(message);
|
|
340
|
+
if (started)
|
|
341
|
+
canSendRef.current = false;
|
|
342
|
+
return started;
|
|
343
|
+
};
|
|
344
|
+
const answerExtra = renderAnswerExtra
|
|
345
|
+
? (args) => renderAnswerExtra(Object.assign(Object.assign({}, args), { send: sendFromSlot, canSend: canSendFromSlot }))
|
|
346
|
+
: undefined;
|
|
183
347
|
const send = () => { if (startTurn(draft))
|
|
184
348
|
setDraft(''); };
|
|
185
349
|
const retry = (turn) => {
|
|
@@ -205,14 +369,19 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx })
|
|
|
205
369
|
return null;
|
|
206
370
|
};
|
|
207
371
|
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
|
|
372
|
+
const notConfigured = status === 'not_configured' || problem === 'not_configured';
|
|
373
|
+
const unavailable = status === 'unavailable' || problem === 'unavailable';
|
|
374
|
+
const unavailableText = typeof unavailableReasonText === 'function' ? unavailableReasonText(unavailableReason) : unavailableReasonText === null || unavailableReasonText === void 0 ? void 0 : unavailableReasonText[unavailableReason];
|
|
209
375
|
if (status === 'loading')
|
|
210
376
|
return _jsx(Box, { sx: sx, "aria-busy": "true" });
|
|
211
377
|
if (status === 'error')
|
|
212
378
|
return _jsx(Box, { sx: sx, children: _jsx(Alert, { severity: "error", children: t('Chat.loadError') }) });
|
|
213
|
-
if (
|
|
379
|
+
if (notConfigured) {
|
|
214
380
|
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
381
|
}
|
|
216
|
-
|
|
382
|
+
if (unavailable) {
|
|
383
|
+
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') }) }));
|
|
384
|
+
}
|
|
385
|
+
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: answerExtra, 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, inputRef: composerInputRef })] })) : (_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
386
|
}
|
|
218
387
|
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}}' },
|