@micha.bigler/ui-core-micha 4.4.0 → 4.6.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 +27 -0
- package/dist/billing/SubscriptionSection.js +8 -2
- package/dist/billing/WithdrawalDialog.js +95 -0
- package/dist/billing/WithdrawalSection.js +59 -0
- package/dist/billing/api.js +3 -0
- package/dist/billing/format.js +26 -4
- package/dist/billing/wire.js +3 -0
- package/dist/chat/ChatComposer.js +4 -3
- package/dist/chat/ChatThread.js +21 -8
- package/dist/chat/ChatWorkspace.js +68 -4
- package/dist/chat/ModelPicker.js +73 -23
- package/dist/chat/api.js +1 -1
- package/dist/chat/kitSxRegistry.js +7 -0
- package/dist/i18n/billingTranslations.js +20 -0
- package/dist/i18n/chatTranslations.js +28 -8
- package/dist/index.js +2 -0
- package/dist/theme/kitSxRegistry.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.6.0 - UCM-CHAT-6
|
|
4
|
+
|
|
5
|
+
Plain model choice, summary and cost on the shared chat surface, against `django-core-micha` 3.5.0 (DCM-CHAT-13).
|
|
6
|
+
`ModelPicker` moves next to "Send" (new `ChatComposer` prop `actions`) and shows only the models the server offers,
|
|
7
|
+
grouped by quality level (`level`), with the known name, "by <provider>", an estimate "about x-y tokens per reply" (a
|
|
8
|
+
relative three-step scale when the server sends no tokens, "no tokens, Claude subscription" for subscription entries)
|
|
9
|
+
and the effort in words (Quick, Thorough, Very thorough, Maximum); the groups by driver, the "pay per use" chip and
|
|
10
|
+
the old effort labels are removed (keys `Chat.driver.*`, `Chat.payPerUse` gone). The catalogue is requested with the
|
|
11
|
+
chat (`getCatalogue({ chat })`, `?chat=<id>`) so the estimate follows the chat's length; a stored selection the
|
|
12
|
+
catalogue no longer offers is replaced once by the default. New optional `ChatWorkspace` props: `tokenBalance`
|
|
13
|
+
(footer "Balance"), `usageToTokens(usage)` (per-reply cost "<model> - <effort> - n tokens" under a reply) and
|
|
14
|
+
`longChatTokens` (default 40000, 0 turns it off; a notice above the composer and in the picker with "New
|
|
15
|
+
conversation"). `ChatThread` shows the server's summary collapsed as "Summary up to here" before the first newer turn
|
|
16
|
+
(plain text), from the turns page, the `history` frame and the `summarised` notice. Without the new props behaviour as
|
|
17
|
+
in 4.5.x apart from the picker's new look.
|
|
18
|
+
|
|
19
|
+
## 4.5.0 - UCM-BILL-3
|
|
20
|
+
|
|
21
|
+
"Withdraw contract here" for EU/EEA consumers, against the `django-core-micha` 3.5.0 withdrawal API
|
|
22
|
+
(`withdrawal/`, `withdrawal/<id>/preview/`, `POST withdrawal/<id>/`). `WithdrawalSection` lists only the purchases the
|
|
23
|
+
server calls eligible, with the 14-day deadline (purchase date plus 14 days) and a button; `WithdrawalDialog` shows the
|
|
24
|
+
server's refund amount and a calculation composed from the server's numbers (`describeCalculation`), confirms exactly
|
|
25
|
+
once and shows the result. The kit never decides eligibility or the amount itself; a 409 shows "not possible". The
|
|
26
|
+
section is part of `SubscriptionSection` (prop `withdrawal`, default `true`, `false` hides it). `createBillingApi`
|
|
27
|
+
gets `listWithdrawals`, `previewWithdrawal`, `confirmWithdrawal`; `formatPrice` takes an optional currency and
|
|
28
|
+
`formatDateTime` is new. New keys `Billing.WITHDRAWAL_*` (de formal, de_informal, en, fr, sw).
|
|
29
|
+
|
|
3
30
|
## 4.4.0 - UCM-BILL-1
|
|
4
31
|
|
|
5
32
|
Subscription and token surfaces in the main entry, against the `django-core-micha` billing API (DCM-BILL-1, `me/`,
|
|
@@ -9,15 +9,17 @@ import { formatDate, formatPrice, formatTokens } from './format';
|
|
|
9
9
|
import { PlanPicker } from './PlanPicker';
|
|
10
10
|
import { TokenBalanceBadge } from './TokenBalanceBadge';
|
|
11
11
|
import { AccessEndedBanner } from './AccessEndedBanner';
|
|
12
|
+
import { WithdrawalSection } from './WithdrawalSection';
|
|
12
13
|
export const SUBSCRIPTION_SECTION_ALERT_SX = { width: '100%' };
|
|
13
14
|
function isStatus(error, status) { var _a; return ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === status; }
|
|
14
15
|
function activeUntil(access, t, language) {
|
|
15
16
|
const date = formatDate(access === null || access === void 0 ? void 0 : access.ends_at, language);
|
|
16
17
|
return date ? t('Billing.ACTIVE_UNTIL', { date }) : null;
|
|
17
18
|
}
|
|
18
|
-
export function SubscriptionSection({ api = defaultBillingApi, redirect = (url) => window.location.assign(url), allowedHosts = DEFAULT_PAYMENT_HOSTS, onRenew, describePlan, sx }) {
|
|
19
|
+
export function SubscriptionSection({ api = defaultBillingApi, redirect = (url) => window.location.assign(url), allowedHosts = DEFAULT_PAYMENT_HOSTS, onRenew, describePlan, withdrawal = true, sx }) {
|
|
19
20
|
const { t, i18n } = useTranslation();
|
|
20
21
|
const { state, me, reload } = useBilling(api);
|
|
22
|
+
const [withdrawnPurchases, setWithdrawnPurchases] = useState({});
|
|
21
23
|
const [seats, setSeats] = useState(null);
|
|
22
24
|
const [seatError, setSeatError] = useState(null);
|
|
23
25
|
const [seatInput, setSeatInput] = useState('');
|
|
@@ -90,6 +92,10 @@ export function SubscriptionSection({ api = defaultBillingApi, redirect = (url)
|
|
|
90
92
|
var _a, _b, _c, _d;
|
|
91
93
|
(_b = (_a = renewalPickerRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, { behavior: 'smooth', block: 'start' });
|
|
92
94
|
(_d = (_c = renewalPickerRef.current) === null || _c === void 0 ? void 0 : _c.querySelector('input[type="radio"]')) === null || _d === void 0 ? void 0 : _d.focus();
|
|
93
|
-
}) }), _jsx("div", { ref: renewalPickerRef, children: _jsx(PlanPicker, { plans: me.plans || [], api: api, redirect: redirect, allowedHosts: allowedHosts, describePlan: describePlan }) })] }) : _jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h5", children: t('Billing.HEADING') }), _jsx(Typography, { children: sourceLine }), access.source !== 'free' && dateLine && _jsx(Typography, { children: dateLine }), access.source === 'subscription' && _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.openPortal(), 'Billing.ERROR_PORTAL'), children: t('Billing.MANAGE') }), _jsx(Typography, { children: t('Billing.MANAGE_DETAILS') })] }), access.source === 'team' && seatError && !seats && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t('Billing.ERROR_LOAD') }), access.source === 'team' && seats && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { children: t('Billing.TEAM_SEATS') }), seats.map((seat) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: String(seat.user_id) }), _jsx(Button, { disabled: busy, onClick: () => updateSeat(() => api.removeSeat({ userId: seat.user_id })), children: t('Billing.SEAT_REMOVE') })] }, seat.user_id)), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { size: "small", label: t('Billing.SEAT_USER'), value: seatInput, onChange: (event) => setSeatInput(event.target.value) }), _jsx(Button, { disabled: busy || !seatInput.trim(), onClick: () => updateSeat(() => seatInput.includes('@') ? api.addSeat({ email: seatInput.trim() }) : api.addSeat({ userId: seatInput.trim() })), children: t('Billing.SEATS_ADD') })] }), seatError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: typeof seatError === 'string' ? t(seatError) : t('Billing.ERROR_LOAD') })] }), actionError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t(actionError) })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: t('Billing.BALANCE_LABEL') }), _jsx(TokenBalanceBadge, { balance: tokens.balance })] }), _jsx(Typography, { children: t('Billing.BALANCE', { tokens: formatTokens(tokens.balance, i18n.language) }) }), packagePlans.length > 0 && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('Billing.BUY_PACKAGE') }), packagePlans.map((plan) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsxs(Typography, { children: [formatPrice(plan.amount_cents, i18n.language), " \u00B7 ", t('Billing.PACKAGE_TOKENS', { tokens: formatTokens(plan.tokens, i18n.language) })] }), _jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.startCheckout(plan.key), 'Billing.ERROR_CHECKOUT'), children: t('Billing.PACKAGE_BUY') })] }, plan.key))] })
|
|
95
|
+
}) }), _jsx("div", { ref: renewalPickerRef, children: _jsx(PlanPicker, { plans: me.plans || [], api: api, redirect: redirect, allowedHosts: allowedHosts, describePlan: describePlan }) })] }) : _jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h5", children: t('Billing.HEADING') }), _jsx(Typography, { children: sourceLine }), access.source !== 'free' && dateLine && _jsx(Typography, { children: dateLine }), access.source === 'subscription' && _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.openPortal(), 'Billing.ERROR_PORTAL'), children: t('Billing.MANAGE') }), _jsx(Typography, { children: t('Billing.MANAGE_DETAILS') })] }), access.source === 'team' && seatError && !seats && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t('Billing.ERROR_LOAD') }), access.source === 'team' && seats && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { children: t('Billing.TEAM_SEATS') }), seats.map((seat) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: String(seat.user_id) }), _jsx(Button, { disabled: busy, onClick: () => updateSeat(() => api.removeSeat({ userId: seat.user_id })), children: t('Billing.SEAT_REMOVE') })] }, seat.user_id)), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { size: "small", label: t('Billing.SEAT_USER'), value: seatInput, onChange: (event) => setSeatInput(event.target.value) }), _jsx(Button, { disabled: busy || !seatInput.trim(), onClick: () => updateSeat(() => seatInput.includes('@') ? api.addSeat({ email: seatInput.trim() }) : api.addSeat({ userId: seatInput.trim() })), children: t('Billing.SEATS_ADD') })] }), seatError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: typeof seatError === 'string' ? t(seatError) : t('Billing.ERROR_LOAD') })] }), actionError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t(actionError) })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: t('Billing.BALANCE_LABEL') }), _jsx(TokenBalanceBadge, { balance: tokens.balance })] }), _jsx(Typography, { children: t('Billing.BALANCE', { tokens: formatTokens(tokens.balance, i18n.language) }) }), packagePlans.length > 0 && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('Billing.BUY_PACKAGE') }), packagePlans.map((plan) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsxs(Typography, { children: [formatPrice(plan.amount_cents, i18n.language), " \u00B7 ", t('Billing.PACKAGE_TOKENS', { tokens: formatTokens(plan.tokens, i18n.language) })] }), _jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.startCheckout(plan.key), 'Billing.ERROR_CHECKOUT'), children: t('Billing.PACKAGE_BUY') })] }, plan.key))] }), withdrawal && _jsx(WithdrawalSection, { api: api, plans: me.plans, sessionWithdrawals: withdrawnPurchases, onWithdrawn: (result, purchase) => {
|
|
96
|
+
if ((purchase === null || purchase === void 0 ? void 0 : purchase.id) != null)
|
|
97
|
+
setWithdrawnPurchases((current) => (Object.assign(Object.assign({}, current), { [purchase.id]: { result, purchase } })));
|
|
98
|
+
reload();
|
|
99
|
+
} })] });
|
|
94
100
|
}
|
|
95
101
|
export default SubscriptionSection;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Alert, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { formatDateTime, formatPrice } from './format';
|
|
6
|
+
export const WITHDRAWAL_DIALOG_ALERT_SX = { width: '100%' };
|
|
7
|
+
export function describeCalculation(preview, t, language) {
|
|
8
|
+
const currency = (preview === null || preview === void 0 ? void 0 : preview.currency) || 'USD';
|
|
9
|
+
const price = (value) => formatPrice(value, language, currency);
|
|
10
|
+
const items = [t('Billing.WITHDRAWAL_PAID', { amount: price(preview === null || preview === void 0 ? void 0 : preview.amount_paid_cents) })];
|
|
11
|
+
const time = Number(preview === null || preview === void 0 ? void 0 : preview.time_share_cents) || 0;
|
|
12
|
+
const token = Number(preview === null || preview === void 0 ? void 0 : preview.token_share_cents) || 0;
|
|
13
|
+
if (time !== 0 || ((preview === null || preview === void 0 ? void 0 : preview.kind) !== 'token_package' && token === 0))
|
|
14
|
+
items.push(t('Billing.WITHDRAWAL_TIME_SHARE', { amount: price(time) }));
|
|
15
|
+
if (token !== 0 || (preview === null || preview === void 0 ? void 0 : preview.kind) === 'token_package')
|
|
16
|
+
items.push(t('Billing.WITHDRAWAL_TOKEN_SHARE', { amount: price(token) }));
|
|
17
|
+
const used = Number.isFinite(preview === null || preview === void 0 ? void 0 : preview.used_cents) ? preview.used_cents : Math.max(time, token);
|
|
18
|
+
items.push(t('Billing.WITHDRAWAL_USED', { amount: price(used) }));
|
|
19
|
+
items.push(t('Billing.WITHDRAWAL_REFUND', { amount: price(preview === null || preview === void 0 ? void 0 : preview.refund_cents) }));
|
|
20
|
+
return items.join(' · ');
|
|
21
|
+
}
|
|
22
|
+
export function WithdrawalDialog({ open, purchase, api, onClose, onWithdrawn }) {
|
|
23
|
+
const { t, i18n } = useTranslation();
|
|
24
|
+
const [state, setState] = useState('loading');
|
|
25
|
+
const [preview, setPreview] = useState(null);
|
|
26
|
+
const [result, setResult] = useState(null);
|
|
27
|
+
const [errorStatus, setErrorStatus] = useState(null);
|
|
28
|
+
const [attempt, setAttempt] = useState(0);
|
|
29
|
+
const confirmGuard = useRef(false);
|
|
30
|
+
const notified = useRef(false);
|
|
31
|
+
const previewRequest = useRef(null);
|
|
32
|
+
const titleId = `withdrawal-title-${purchase === null || purchase === void 0 ? void 0 : purchase.id}`;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
var _a;
|
|
35
|
+
if (!open || !purchase || typeof (api === null || api === void 0 ? void 0 : api.previewWithdrawal) !== 'function') {
|
|
36
|
+
if (!open)
|
|
37
|
+
previewRequest.current = null;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const requestKey = String(purchase.id) + ':' + attempt;
|
|
41
|
+
let live = true;
|
|
42
|
+
setState('loading');
|
|
43
|
+
setPreview(null);
|
|
44
|
+
setResult(null);
|
|
45
|
+
setErrorStatus(null);
|
|
46
|
+
confirmGuard.current = false;
|
|
47
|
+
notified.current = false;
|
|
48
|
+
let request = ((_a = previewRequest.current) === null || _a === void 0 ? void 0 : _a.key) === requestKey ? previewRequest.current.promise : null;
|
|
49
|
+
if (!request) {
|
|
50
|
+
request = Promise.resolve().then(() => api.previewWithdrawal(purchase.id));
|
|
51
|
+
previewRequest.current = { key: requestKey, promise: request };
|
|
52
|
+
}
|
|
53
|
+
request.then((data) => {
|
|
54
|
+
if (!live)
|
|
55
|
+
return;
|
|
56
|
+
setPreview(data);
|
|
57
|
+
setState((data === null || data === void 0 ? void 0 : data.eligible) === true ? 'ready' : 'not_possible');
|
|
58
|
+
}).catch((error) => {
|
|
59
|
+
var _a;
|
|
60
|
+
if (!live)
|
|
61
|
+
return;
|
|
62
|
+
setErrorStatus((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status);
|
|
63
|
+
setState('preview_error');
|
|
64
|
+
});
|
|
65
|
+
return () => { live = false; };
|
|
66
|
+
}, [open, purchase === null || purchase === void 0 ? void 0 : purchase.id, api, attempt]);
|
|
67
|
+
const confirm = async () => {
|
|
68
|
+
var _a, _b;
|
|
69
|
+
if (confirmGuard.current || !['ready', 'confirm_error'].includes(state) || typeof (api === null || api === void 0 ? void 0 : api.confirmWithdrawal) !== 'function')
|
|
70
|
+
return;
|
|
71
|
+
confirmGuard.current = true;
|
|
72
|
+
setState('submitting');
|
|
73
|
+
setErrorStatus(null);
|
|
74
|
+
try {
|
|
75
|
+
const value = await api.confirmWithdrawal(purchase.id);
|
|
76
|
+
setResult(value);
|
|
77
|
+
setState('success');
|
|
78
|
+
if (!notified.current) {
|
|
79
|
+
notified.current = true;
|
|
80
|
+
onWithdrawn === null || onWithdrawn === void 0 ? void 0 : onWithdrawn(value, purchase);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
setErrorStatus((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status);
|
|
85
|
+
setState(((_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status) === 409 ? 'not_possible' : 'confirm_error');
|
|
86
|
+
confirmGuard.current = false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const close = () => { if (state !== 'submitting')
|
|
90
|
+
onClose === null || onClose === void 0 ? void 0 : onClose(); };
|
|
91
|
+
const amount = formatPrice(preview === null || preview === void 0 ? void 0 : preview.refund_cents, i18n.language, preview === null || preview === void 0 ? void 0 : preview.currency);
|
|
92
|
+
const none = !(Number(preview === null || preview === void 0 ? void 0 : preview.refund_cents) > 0);
|
|
93
|
+
return _jsxs(Dialog, { open: open, onClose: close, disableEscapeKeyDown: state === 'submitting', fullWidth: true, maxWidth: "sm", "aria-labelledby": titleId, children: [_jsx(DialogTitle, { id: titleId, children: t(state === 'success' ? 'Billing.WITHDRAWAL_DONE' : 'Billing.WITHDRAWAL_TITLE') }), _jsxs(DialogContent, { children: [state === 'loading' && _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", role: "status", children: [_jsx(CircularProgress, { size: 20 }), _jsx(Typography, { children: t('Billing.LOADING') })] }), state === 'preview_error' && _jsx(Alert, { severity: "error", sx: WITHDRAWAL_DIALOG_ALERT_SX, children: t('Billing.WITHDRAWAL_PREVIEW_ERROR') }), (state === 'not_possible') && _jsx(Alert, { severity: "info", children: t('Billing.WITHDRAWAL_NOT_POSSIBLE') }), (state === 'ready' || state === 'submitting' || state === 'confirm_error') && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { children: t('Billing.WITHDRAWAL_EFFECT') }), _jsx(Typography, { children: none ? t('Billing.WITHDRAWAL_REFUND_NONE') : t('Billing.WITHDRAWAL_REFUND', { amount }) }), _jsx(Typography, { children: t('Billing.WITHDRAWAL_CALCULATION', { explanation: describeCalculation(Object.assign(Object.assign({}, preview), { kind: purchase === null || purchase === void 0 ? void 0 : purchase.kind }), t, i18n.language) }) }), state === 'confirm_error' && _jsx(Alert, { severity: "error", sx: WITHDRAWAL_DIALOG_ALERT_SX, children: t('Billing.WITHDRAWAL_ERROR') }), state === 'submitting' && _jsxs(Typography, { role: "status", children: [_jsx(CircularProgress, { size: 16 }), " ", t('Billing.LOADING')] })] }), state === 'success' && _jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE_DETAIL', { date: formatDateTime(result === null || result === void 0 ? void 0 : result.withdrawn_at, i18n.language) }) })] }), _jsx(DialogActions, { children: state === 'ready' || state === 'confirm_error' ? _jsxs(_Fragment, { children: [_jsx(Button, { onClick: close, disabled: state === 'submitting', children: t('Billing.WITHDRAWAL_CANCEL') }), _jsx(Button, { variant: "contained", onClick: confirm, disabled: state === 'submitting', children: t('Billing.WITHDRAWAL_CONFIRM') })] }) : state === 'preview_error' ? _jsxs(_Fragment, { children: [_jsx(Button, { onClick: close, children: t('Billing.WITHDRAWAL_CLOSE') }), _jsx(Button, { onClick: () => setAttempt((n) => n + 1), children: t('Billing.RETRY') })] }) : (state === 'success' || state === 'not_possible') && _jsx(Button, { onClick: close, children: t('Billing.WITHDRAWAL_CLOSE') }) })] });
|
|
94
|
+
}
|
|
95
|
+
export default WithdrawalDialog;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Alert, Button, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { formatDate, formatDateTime } from './format';
|
|
6
|
+
import { WithdrawalDialog } from './WithdrawalDialog';
|
|
7
|
+
export const WITHDRAWAL_SECTION_ALERT_SX = { width: '100%' };
|
|
8
|
+
const is404 = (error) => { var _a; return ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 404; };
|
|
9
|
+
const deadline = (purchase) => {
|
|
10
|
+
const date = new Date(purchase.purchased_at);
|
|
11
|
+
if (!Number.isFinite(date.getTime()))
|
|
12
|
+
return '';
|
|
13
|
+
date.setUTCDate(date.getUTCDate() + 14);
|
|
14
|
+
return date.toISOString();
|
|
15
|
+
};
|
|
16
|
+
export function WithdrawalSection({ api, plans = [], onWithdrawn, sessionWithdrawals = {}, sx }) {
|
|
17
|
+
const { t, i18n } = useTranslation();
|
|
18
|
+
const [purchases, setPurchases] = useState([]);
|
|
19
|
+
const [withdrawn, setWithdrawn] = useState({});
|
|
20
|
+
const [selected, setSelected] = useState(null);
|
|
21
|
+
const [loadError, setLoadError] = useState(false);
|
|
22
|
+
const [reload, setReload] = useState(0);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (typeof (api === null || api === void 0 ? void 0 : api.listWithdrawals) !== 'function')
|
|
25
|
+
return undefined;
|
|
26
|
+
let live = true;
|
|
27
|
+
Promise.resolve().then(() => api.listWithdrawals()).then((data) => {
|
|
28
|
+
if (live) {
|
|
29
|
+
setPurchases(Array.isArray(data === null || data === void 0 ? void 0 : data.purchases) ? data.purchases : []);
|
|
30
|
+
setLoadError(false);
|
|
31
|
+
}
|
|
32
|
+
}).catch((error) => {
|
|
33
|
+
if (!live)
|
|
34
|
+
return;
|
|
35
|
+
if (is404(error)) {
|
|
36
|
+
setPurchases([]);
|
|
37
|
+
setLoadError(false);
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
setLoadError(true);
|
|
41
|
+
});
|
|
42
|
+
return () => { live = false; };
|
|
43
|
+
}, [api, reload]);
|
|
44
|
+
const handleWithdrawn = (purchaseId, result) => {
|
|
45
|
+
setWithdrawn((current) => (Object.assign(Object.assign({}, current), { [purchaseId]: { purchase: purchases.find((item) => String(item.id) === String(purchaseId)), result } })));
|
|
46
|
+
setSelected(null);
|
|
47
|
+
onWithdrawn === null || onWithdrawn === void 0 ? void 0 : onWithdrawn(result, purchases.find((item) => String(item.id) === String(purchaseId)));
|
|
48
|
+
setReload((count) => count + 1);
|
|
49
|
+
};
|
|
50
|
+
const successful = Object.assign(Object.assign({}, sessionWithdrawals), withdrawn);
|
|
51
|
+
const eligible = purchases.filter((item) => (item === null || item === void 0 ? void 0 : item.eligible) === true && item.withdrawn_at == null && !successful[item.id]);
|
|
52
|
+
if (loadError)
|
|
53
|
+
return _jsxs(Stack, { sx: sx, spacing: 1, children: [_jsx(Alert, { severity: "error", sx: WITHDRAWAL_SECTION_ALERT_SX, children: t('Billing.WITHDRAWAL_LOAD_ERROR') }), _jsx(Button, { onClick: () => setReload((count) => count + 1), children: t('Billing.RETRY') }), Object.entries(successful).map(([id, entry]) => { var _a; return _jsxs(Stack, { spacing: 0.5, role: "status", children: [_jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE') }), _jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE_DETAIL', { date: formatDateTime((_a = ((entry === null || entry === void 0 ? void 0 : entry.result) || entry)) === null || _a === void 0 ? void 0 : _a.withdrawn_at, i18n.language) }) })] }, id); })] });
|
|
54
|
+
if (!eligible.length && !Object.keys(successful).length)
|
|
55
|
+
return null;
|
|
56
|
+
const findPlan = (key) => { var _a; return ((_a = plans.find((plan) => (plan === null || plan === void 0 ? void 0 : plan.key) === key)) === null || _a === void 0 ? void 0 : _a.name) || key; };
|
|
57
|
+
return _jsxs(Stack, { sx: sx, spacing: 1, children: [eligible.map((purchase) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [_jsx(Typography, { children: findPlan(purchase.plan_key) }), _jsx(Typography, { children: formatDate(purchase.purchased_at, i18n.language) }), _jsx(Typography, { children: t('Billing.WITHDRAWAL_UNTIL', { date: formatDate(deadline(purchase), i18n.language) }) }), _jsx(Button, { onClick: () => setSelected(purchase), children: t('Billing.WITHDRAWAL_BUTTON') })] }, purchase.id)), Object.entries(successful).map(([id, entry]) => { var _a; return _jsxs(Stack, { spacing: 0.5, role: "status", children: [_jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE') }), _jsx(Typography, { children: t('Billing.WITHDRAWAL_DONE_DETAIL', { date: formatDateTime((_a = ((entry === null || entry === void 0 ? void 0 : entry.result) || entry)) === null || _a === void 0 ? void 0 : _a.withdrawn_at, i18n.language) }) })] }, id); }), _jsx(WithdrawalDialog, { open: Boolean(selected), purchase: selected, api: api, onClose: () => setSelected(null), onWithdrawn: (result) => selected && handleWithdrawn(selected.id, result) })] });
|
|
58
|
+
}
|
|
59
|
+
export default WithdrawalSection;
|
package/dist/billing/api.js
CHANGED
|
@@ -10,6 +10,9 @@ export function createBillingApi(overrides = {}, { apiBase = '/api/billing/' } =
|
|
|
10
10
|
listSeats: () => data(apiClient.get(urls.seats)),
|
|
11
11
|
addSeat: ({ userId, email } = {}) => data(apiClient.post(urls.seats, userId !== undefined ? { user_id: userId } : { email })),
|
|
12
12
|
removeSeat: ({ userId, email } = {}) => data(apiClient.delete(urls.seats, { data: userId !== undefined ? { user_id: userId } : { email } })),
|
|
13
|
+
listWithdrawals: () => data(apiClient.get(urls.withdrawals)),
|
|
14
|
+
previewWithdrawal: (id) => data(apiClient.get(urls.withdrawalPreview(id))),
|
|
15
|
+
confirmWithdrawal: (id) => data(apiClient.post(urls.withdrawalConfirm(id), { confirm: true })),
|
|
13
16
|
};
|
|
14
17
|
return Object.assign(Object.assign({}, defaults), overrides);
|
|
15
18
|
}
|
package/dist/billing/format.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
// The kit's own language codes (for example de_informal) are not valid Intl locales; use the base language.
|
|
2
|
+
export function intlLocale(language) {
|
|
3
|
+
return typeof language === 'string' && language ? language.split('_')[0] : undefined;
|
|
4
|
+
}
|
|
5
|
+
export function formatPrice(amountCents, language, currency = 'USD') {
|
|
2
6
|
if (!Number.isFinite(amountCents))
|
|
3
7
|
return '';
|
|
4
|
-
|
|
8
|
+
const candidate = typeof currency === 'string' ? currency.toUpperCase() : '';
|
|
9
|
+
let validCurrency = 'USD';
|
|
10
|
+
try {
|
|
11
|
+
const supported = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('currency') : null;
|
|
12
|
+
if (/^[A-Z]{3}$/.test(candidate) && (!supported || supported.includes(candidate))) {
|
|
13
|
+
new Intl.NumberFormat(intlLocale(language), { style: 'currency', currency: candidate });
|
|
14
|
+
validCurrency = candidate;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (_a) { /* use USD for unknown currency codes */ }
|
|
18
|
+
return new Intl.NumberFormat(intlLocale(language), { style: 'currency', currency: validCurrency }).format(amountCents / 100);
|
|
5
19
|
}
|
|
6
20
|
export function formatTokens(count, language) {
|
|
7
21
|
if (!Number.isFinite(count))
|
|
8
22
|
return '';
|
|
9
|
-
return new Intl.NumberFormat(language, { maximumFractionDigits: 0 }).format(count);
|
|
23
|
+
return new Intl.NumberFormat(intlLocale(language), { maximumFractionDigits: 0 }).format(count);
|
|
10
24
|
}
|
|
11
25
|
export function formatDate(iso, language) {
|
|
12
26
|
if (typeof iso !== 'string' || !iso)
|
|
@@ -14,5 +28,13 @@ export function formatDate(iso, language) {
|
|
|
14
28
|
const date = new Date(iso);
|
|
15
29
|
if (!Number.isFinite(date.getTime()))
|
|
16
30
|
return '';
|
|
17
|
-
return new Intl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(date);
|
|
31
|
+
return new Intl.DateTimeFormat(intlLocale(language), { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(date);
|
|
32
|
+
}
|
|
33
|
+
export function formatDateTime(iso, language) {
|
|
34
|
+
if (typeof iso !== 'string' || !iso)
|
|
35
|
+
return '';
|
|
36
|
+
const date = new Date(iso);
|
|
37
|
+
if (!Number.isFinite(date.getTime()))
|
|
38
|
+
return '';
|
|
39
|
+
return new Intl.DateTimeFormat(intlLocale(language), { dateStyle: 'long', timeStyle: 'short' }).format(date);
|
|
18
40
|
}
|
package/dist/billing/wire.js
CHANGED
|
@@ -9,5 +9,8 @@ export const billingApiUrls = (base = '/api/billing/') => {
|
|
|
9
9
|
checkout: root + 'checkout/',
|
|
10
10
|
portal: root + 'portal/',
|
|
11
11
|
seats: root + 'team/seats/',
|
|
12
|
+
withdrawals: root + 'withdrawal/',
|
|
13
|
+
withdrawalPreview: (id) => root + `withdrawal/${encodeURIComponent(id)}/preview/`,
|
|
14
|
+
withdrawalConfirm: (id) => root + `withdrawal/${encodeURIComponent(id)}/`,
|
|
12
15
|
};
|
|
13
16
|
};
|
|
@@ -6,18 +6,19 @@ 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
|
|
9
|
+
export const CHAT_COMPOSER_INPUT_SX = { flex: '1 1 12rem', minWidth: 0 };
|
|
10
|
+
export function ChatComposer({ value = '', onChange, onSend, onStop, running = false, archived = false, connected = true, inputRef, sx, actions }) {
|
|
10
11
|
const { t } = useTranslation();
|
|
11
12
|
const blocked = archived || !connected;
|
|
12
13
|
const canSend = !running && !blocked && value.trim().length > 0;
|
|
13
14
|
const submit = () => { if (canSend)
|
|
14
15
|
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,
|
|
16
|
+
return (_jsxs(Stack, { direction: "row", alignItems: "flex-end", flexWrap: "wrap", gap: 1, sx: Object.assign({ p: { xs: 1, md: 2 }, borderTop: 1, borderColor: 'divider' }, sx), children: [_jsx(TextField, { multiline: true, maxRows: 6, sx: CHAT_COMPOSER_INPUT_SX, 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
17
|
var _a;
|
|
17
18
|
if (event.key === 'Enter' && !event.shiftKey && !((_a = event.nativeEvent) === null || _a === void 0 ? void 0 : _a.isComposing)) {
|
|
18
19
|
event.preventDefault();
|
|
19
20
|
submit();
|
|
20
21
|
}
|
|
21
|
-
} }), running ? _jsx(Button, { variant: "outlined", onClick: onStop, children: t('Chat.stop') }) : null, _jsx(Button, { variant: "contained", onClick: submit, disabled: !canSend, children: t('Chat.send') })] }));
|
|
22
|
+
} }), actions ? _jsx(Stack, { direction: "row", alignItems: "flex-end", children: actions }) : null, running ? _jsx(Button, { variant: "outlined", onClick: onStop, children: t('Chat.stop') }) : null, _jsx(Button, { variant: "contained", onClick: submit, disabled: !canSend, children: t('Chat.send') })] }));
|
|
22
23
|
}
|
|
23
24
|
export default ChatComposer;
|
package/dist/chat/ChatThread.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Alert, Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import MarkdownReply from './MarkdownReply';
|
|
@@ -14,19 +14,27 @@ function SourceChips({ sources }) {
|
|
|
14
14
|
return null;
|
|
15
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}`))) }));
|
|
16
16
|
}
|
|
17
|
-
function Answer({ turn, tool, onRetry, chat, renderAnswerExtra }) {
|
|
17
|
+
function Answer({ turn, tool, onRetry, chat, renderAnswerExtra, usageToTokens }) {
|
|
18
18
|
var _a;
|
|
19
|
-
const { t } = useTranslation();
|
|
19
|
+
const { t, i18n } = useTranslation();
|
|
20
20
|
const generating = turn.state === 'generating';
|
|
21
21
|
const failed = ['failed', 'cancelled'].includes(turn.state) && turn.error_code;
|
|
22
|
-
|
|
22
|
+
const replyCaption = (turn) => {
|
|
23
|
+
const tokens = turn.usage && (usageToTokens === null || usageToTokens === void 0 ? void 0 : usageToTokens(turn.usage));
|
|
24
|
+
if (Number.isInteger(tokens) && tokens >= 0) {
|
|
25
|
+
const count = new Intl.NumberFormat(typeof (i18n === null || i18n === void 0 ? void 0 : i18n.language) === 'string' ? i18n.language.split('_')[0] || undefined : undefined).format(tokens);
|
|
26
|
+
return [turn.model, turn.effort && t(`Chat.effort.${turn.effort}`), t('Chat.replyTokens', { n: count })].filter(Boolean).join(' · ');
|
|
27
|
+
}
|
|
28
|
+
return selectionLine(turn, t);
|
|
29
|
+
};
|
|
30
|
+
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 && replyCaption(turn) ? _jsx(Typography, { variant: "caption", color: "text.secondary", children: replyCaption(turn) }) : 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] }));
|
|
23
31
|
}
|
|
24
32
|
/**
|
|
25
33
|
* The turns of one chat. The streaming answer is simply the last turn in state `generating`; the caller keeps its text
|
|
26
34
|
* up to date. `notice` is a connection or limit message shown under the last turn. `readOnly` (an archived chat) hides Retry.
|
|
27
35
|
*/
|
|
28
|
-
export function ChatThread({ turns = [], tool = null, notice = null, hasOlder = false, olderBusy = false, onLoadOlder, onRetry, readOnly = false, sx, chat, renderAnswerExtra, trimmedCount }) {
|
|
29
|
-
var _a;
|
|
36
|
+
export function ChatThread({ turns = [], tool = null, notice = null, hasOlder = false, olderBusy = false, onLoadOlder, onRetry, readOnly = false, sx, chat, renderAnswerExtra, trimmedCount, summary = null, usageToTokens }) {
|
|
37
|
+
var _a, _b;
|
|
30
38
|
const { t } = useTranslation();
|
|
31
39
|
const end = useRef(null);
|
|
32
40
|
const scroller = useRef(null);
|
|
@@ -58,6 +66,11 @@ export function ChatThread({ turns = [], tool = null, notice = null, hasOlder =
|
|
|
58
66
|
loadingOlder.current = false;
|
|
59
67
|
}
|
|
60
68
|
};
|
|
69
|
+
const [summaryOpen, setSummaryOpen] = useState(false);
|
|
70
|
+
useEffect(() => { setSummaryOpen(false); }, [chat === null || chat === void 0 ? void 0 : chat.id, summary === null || summary === void 0 ? void 0 : summary.up_to_seq]);
|
|
71
|
+
const summaryVisible = Boolean((_b = summary === null || summary === void 0 ? void 0 : summary.text) === null || _b === void 0 ? void 0 : _b.trim());
|
|
72
|
+
const renderSummary = () => summaryVisible ? _jsxs(Box, { sx: { mb: 2 }, children: [_jsxs(Button, { size: "small", "aria-expanded": summaryOpen, onClick: () => setSummaryOpen((value) => !value), children: [t('Chat.summaryUntilHere'), " ", summaryOpen ? '▾' : '▸'] }), summaryOpen ? _jsx(Typography, { component: "div", variant: "body2", sx: { whiteSpace: 'pre-wrap' }, children: summary.text }) : null] }) : null;
|
|
73
|
+
let summaryRendered = false;
|
|
61
74
|
return (_jsxs(Box, { ref: scroller, onScroll: (event) => {
|
|
62
75
|
const el = event.currentTarget;
|
|
63
76
|
pinned.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
@@ -66,7 +79,7 @@ export function ChatThread({ turns = [], tool = null, notice = null, hasOlder =
|
|
|
66
79
|
}, 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) => {
|
|
67
80
|
var _a;
|
|
68
81
|
const previous = turns[index - 1];
|
|
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 })] }));
|
|
82
|
+
return (_jsxs(Box, { sx: { mb: 3 }, children: [!summaryRendered && summaryVisible && turn.seq > summary.up_to_seq ? (summaryRendered = true, renderSummary()) : null, 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, usageToTokens: usageToTokens })] }, (_a = turn.id) !== null && _a !== void 0 ? _a : index));
|
|
83
|
+
}), !summaryRendered ? renderSummary() : null, 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 })] }));
|
|
71
84
|
}
|
|
72
85
|
export default ChatThread;
|
|
@@ -62,7 +62,8 @@ const appendDelta = (turns, text) => {
|
|
|
62
62
|
* Props: `apiBase` and `wsUrl` (where the app mounted the chat routes), `api` (partial overrides of the REST
|
|
63
63
|
* calls; every function not given keeps its default), `scope` (passed when creating a chat), `sx`.
|
|
64
64
|
*/
|
|
65
|
-
export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, unavailableReasonText, sidePanel, sidePanelLabel, renderAnswerExtra, renderComposerExtra, onAppFrame, activeChatId, onActiveChatChange, composerPrefill, getSendContext, sendRequest, onSendRequestHandled, layout = 'viewport', compactBelow = 720 }) {
|
|
65
|
+
export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, unavailableReasonText, sidePanel, sidePanelLabel, renderAnswerExtra, renderComposerExtra, onAppFrame, activeChatId, onActiveChatChange, composerPrefill, getSendContext, sendRequest, onSendRequestHandled, layout = 'viewport', compactBelow = 720, tokenBalance = null, usageToTokens, longChatTokens = 40000 }) {
|
|
66
|
+
var _a;
|
|
66
67
|
const { t } = useTranslation();
|
|
67
68
|
const theme = useTheme();
|
|
68
69
|
const viewportNarrow = useMediaQuery(theme.breakpoints.down('md'));
|
|
@@ -86,6 +87,9 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
86
87
|
}, [layout, compactBelow, viewportNarrow, status]);
|
|
87
88
|
const api = useMemo(() => createChatApi(apiOverrides, { apiBase }), [apiOverrides, apiBase]);
|
|
88
89
|
const [catalogue, setCatalogue] = useState({ entries: [], default: null });
|
|
90
|
+
const [summary, setSummary] = useState(null);
|
|
91
|
+
// The chat the current catalogue was requested for: a general catalogue must never drive the stored selection of a chat.
|
|
92
|
+
const [catalogueChatId, setCatalogueChatId] = useState(null);
|
|
89
93
|
const [chats, setChats] = useState([]);
|
|
90
94
|
const [archived, setArchived] = useState([]);
|
|
91
95
|
const [nextChats, setNextChats] = useState(null);
|
|
@@ -127,6 +131,36 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
127
131
|
const isArchived = Boolean(chat === null || chat === void 0 ? void 0 : chat.archived_at);
|
|
128
132
|
const selection = (chat === null || chat === void 0 ? void 0 : chat.selection) || catalogue.default;
|
|
129
133
|
const activeEntry = catalogue.entries.find((entry) => entry.id === (selection === null || selection === void 0 ? void 0 : selection.entry));
|
|
134
|
+
const latestUsageTurn = [...turns].reverse().find((turn) => turn.usage);
|
|
135
|
+
const longChat = longChatTokens > 0 && Number.isFinite((_a = latestUsageTurn === null || latestUsageTurn === void 0 ? void 0 : latestUsageTurn.usage) === null || _a === void 0 ? void 0 : _a.context_tokens) && latestUsageTurn.usage.context_tokens >= longChatTokens;
|
|
136
|
+
// Only the newest catalogue request may update the state, so a slow answer for a chat that was left cannot overwrite the active one.
|
|
137
|
+
const catalogueRequest = useRef(0);
|
|
138
|
+
const catalogueFor = useRef(null);
|
|
139
|
+
const [catalogueLoading, setCatalogueLoading] = useState(false);
|
|
140
|
+
const refreshCatalogue = useCallback(async (chatId) => {
|
|
141
|
+
const request = ++catalogueRequest.current;
|
|
142
|
+
setCatalogueLoading(true);
|
|
143
|
+
try {
|
|
144
|
+
const next = await api.getCatalogue(chatId !== undefined ? { chat: chatId } : {});
|
|
145
|
+
if (request !== catalogueRequest.current)
|
|
146
|
+
return null;
|
|
147
|
+
catalogueFor.current = chatId !== null && chatId !== void 0 ? chatId : null;
|
|
148
|
+
setCatalogue(next);
|
|
149
|
+
setCatalogueChatId(chatId !== null && chatId !== void 0 ? chatId : null);
|
|
150
|
+
return next;
|
|
151
|
+
}
|
|
152
|
+
catch (_a) {
|
|
153
|
+
// A failed load for another chat must not leave the previous chat's models on offer.
|
|
154
|
+
if (request === catalogueRequest.current && chatId !== undefined && catalogueFor.current !== chatId)
|
|
155
|
+
setCatalogue({ entries: [], default: null });
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (request === catalogueRequest.current)
|
|
160
|
+
setCatalogueLoading(false);
|
|
161
|
+
}
|
|
162
|
+
}, [api]);
|
|
163
|
+
const shownChatId = useRef(null);
|
|
130
164
|
// Bumped on every scope change so that a list response of the previous scope is dropped.
|
|
131
165
|
const scopeVersion = useRef(0);
|
|
132
166
|
useEffect(() => { scopeVersion.current += 1; setChat(null); setListsLoaded(false); appliedId.current = undefined; unloadableIds.current.clear(); }, [scope]);
|
|
@@ -166,6 +200,10 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
166
200
|
setStatus('error'); });
|
|
167
201
|
return () => { live = false; };
|
|
168
202
|
}, [api, loadLists, scope]);
|
|
203
|
+
useEffect(() => { if ((chat === null || chat === void 0 ? void 0 : chat.id) !== undefined)
|
|
204
|
+
refreshCatalogue(chat.id); }, [chat === null || chat === void 0 ? void 0 : chat.id, refreshCatalogue]);
|
|
205
|
+
useEffect(() => { if (pickerOpen)
|
|
206
|
+
refreshCatalogue(chat === null || chat === void 0 ? void 0 : chat.id); }, [pickerOpen]);
|
|
169
207
|
useEffect(() => {
|
|
170
208
|
var _a;
|
|
171
209
|
var _b;
|
|
@@ -213,7 +251,9 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
213
251
|
}, [activeChatId, status, listsLoaded]);
|
|
214
252
|
// Clear the previous chat's content before the browser paints, so a host-driven switch never flashes it under the new title.
|
|
215
253
|
useLayoutEffect(() => {
|
|
254
|
+
var _a;
|
|
216
255
|
connectedChatId.current = null; // the socket of the new chat has not opened yet
|
|
256
|
+
shownChatId.current = (_a = chat === null || chat === void 0 ? void 0 : chat.id) !== null && _a !== void 0 ? _a : null;
|
|
217
257
|
setTurns([]);
|
|
218
258
|
setOlderCursor(null);
|
|
219
259
|
setTool(null);
|
|
@@ -222,6 +262,7 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
222
262
|
setSidePanelOpen(false);
|
|
223
263
|
setDraft('');
|
|
224
264
|
setOpenedOnce(false);
|
|
265
|
+
setSummary(null);
|
|
225
266
|
}, [chat === null || chat === void 0 ? void 0 : chat.id]);
|
|
226
267
|
useEffect(() => {
|
|
227
268
|
if (!(chat === null || chat === void 0 ? void 0 : chat.id))
|
|
@@ -231,6 +272,7 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
231
272
|
.then((page) => { if (live) {
|
|
232
273
|
setTurns(chronological((page.results || []).map((turn) => (Object.assign(Object.assign({}, turn), { appFrames: [] })))));
|
|
233
274
|
setOlderCursor(page.next || null);
|
|
275
|
+
setSummary(page.summary || null);
|
|
234
276
|
} })
|
|
235
277
|
.catch(() => { if (live)
|
|
236
278
|
setProblem('load_error'); });
|
|
@@ -240,8 +282,10 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
240
282
|
var _a;
|
|
241
283
|
if (chatId !== (chat === null || chat === void 0 ? void 0 : chat.id))
|
|
242
284
|
return;
|
|
243
|
-
if (frame.type === 'history')
|
|
285
|
+
if (frame.type === 'history') {
|
|
244
286
|
setTurns(chronological((frame.turns || []).map((turn) => (Object.assign(Object.assign({}, turn), { appFrames: [] })))));
|
|
287
|
+
setSummary(frame.summary || null);
|
|
288
|
+
}
|
|
245
289
|
else if (frame.type === 'delta')
|
|
246
290
|
setTurns((current) => appendDelta(current, frame.text || ''));
|
|
247
291
|
else if (frame.type === 'app') {
|
|
@@ -251,11 +295,16 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
251
295
|
}
|
|
252
296
|
else if (frame.type === 'notice' && frame.code === 'history_trimmed')
|
|
253
297
|
setTrimmedCount(frame.left_out || 0);
|
|
298
|
+
else if (frame.type === 'notice' && frame.code === 'summarised') {
|
|
299
|
+
api.listTurns(chatId).then((page) => { if (shownChatId.current === chatId)
|
|
300
|
+
setSummary(page.summary || null); }).catch(() => { });
|
|
301
|
+
}
|
|
254
302
|
else if (frame.type === 'tool')
|
|
255
303
|
setTool(frame.phase === 'start' ? { label: frame.label, name: frame.name } : null);
|
|
256
304
|
else if (frame.type === 'turn' && frame.turn) {
|
|
257
305
|
setTool(null);
|
|
258
306
|
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) || [] })); });
|
|
307
|
+
refreshCatalogue(chatId);
|
|
259
308
|
if (frame.turn.error_code === 'quota_exceeded')
|
|
260
309
|
setProblem('quota_exceeded');
|
|
261
310
|
}
|
|
@@ -266,7 +315,7 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
266
315
|
if (frame.code === 'unavailable')
|
|
267
316
|
setUnavailableReason(frame.reason || unavailableReason);
|
|
268
317
|
}
|
|
269
|
-
}, [chat === null || chat === void 0 ? void 0 : chat.id]);
|
|
318
|
+
}, [chat === null || chat === void 0 ? void 0 : chat.id, api, refreshCatalogue]);
|
|
270
319
|
const onOpen = useCallback((chatId) => { connectedChatId.current = String(chatId); setOpenedOnce(true); setProblem((current) => (current === 'connection_lost' ? null : current)); }, []);
|
|
271
320
|
const onClose = useCallback(({ reason, retrying }) => { connectedChatId.current = null; if (!retrying)
|
|
272
321
|
setProblem(reason); }, []);
|
|
@@ -370,6 +419,21 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
370
419
|
setChat(updated);
|
|
371
420
|
await loadLists();
|
|
372
421
|
});
|
|
422
|
+
const fallbackApplied = useRef(new Set());
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
var _a;
|
|
425
|
+
if (!chat || catalogueChatId !== chat.id || !catalogue.entries.length || fallbackApplied.current.has(chat.id))
|
|
426
|
+
return;
|
|
427
|
+
const valid = catalogue.entries.some((entry) => { var _a, _b, _c; return entry.id === ((_a = chat.selection) === null || _a === void 0 ? void 0 : _a.entry) && ((_b = entry.efforts) === null || _b === void 0 ? void 0 : _b.includes((_c = chat.selection) === null || _c === void 0 ? void 0 : _c.effort)); });
|
|
428
|
+
if (valid)
|
|
429
|
+
return;
|
|
430
|
+
const served = catalogue.entries.find((item) => { var _a, _b, _c; return item.id === ((_a = catalogue.default) === null || _a === void 0 ? void 0 : _a.entry) && ((_b = item.efforts) === null || _b === void 0 ? void 0 : _b.includes((_c = catalogue.default) === null || _c === void 0 ? void 0 : _c.effort)); });
|
|
431
|
+
const entry = served || catalogue.entries.find((item) => item.default) || catalogue.entries[0];
|
|
432
|
+
if (!entry)
|
|
433
|
+
return;
|
|
434
|
+
fallbackApplied.current.add(chat.id);
|
|
435
|
+
updateChat(chat, { selection: { entry: entry.id, effort: served ? catalogue.default.effort : (_a = entry.efforts) === null || _a === void 0 ? void 0 : _a[0] } });
|
|
436
|
+
}, [chat, catalogue, catalogueChatId, updateChat]);
|
|
373
437
|
const deleteChat = guarded(async (target) => {
|
|
374
438
|
await api.deleteChat(target.id);
|
|
375
439
|
setChats((current) => current.filter((item) => item.id !== target.id));
|
|
@@ -462,6 +526,6 @@ export function ChatWorkspace({ apiBase, wsUrl, api: apiOverrides, scope, sx, un
|
|
|
462
526
|
if (unavailable) {
|
|
463
527
|
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') }) }));
|
|
464
528
|
}
|
|
465
|
-
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
|
|
529
|
+
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 && 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, summary: summary, usageToTokens: usageToTokens }), 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, longChat ? _jsx(Alert, { severity: "info", action: _jsx(Button, { color: "inherit", size: "small", onClick: createChat, children: t('Chat.newConversation') }), children: t('Chat.longChat') }) : null, _jsx(ChatComposer, { value: draft, onChange: setDraft, onSend: send, onStop: () => socket.send({ type: 'stop' }), running: running, archived: isArchived, connected: socket.connected, inputRef: composerInputRef, actions: _jsx(ModelPicker, { entries: catalogue.entries, selection: selection, disabled: isArchived || running || (catalogueLoading && catalogueChatId !== chat.id), open: pickerOpen, onOpenChange: setPickerOpen, onChange: (next) => updateChat(chat, { selection: next }), sheet: narrow, balance: tokenBalance, longChat: longChat, onNewChat: createChat }) })] })) : (_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] }));
|
|
466
530
|
}
|
|
467
531
|
export default ChatWorkspace;
|
package/dist/chat/ModelPicker.js
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Box, Button,
|
|
3
|
+
import { Alert, Box, Button, Drawer, IconButton, List, ListItemButton, ListItemText, Popover, Stack, ToggleButton, ToggleButtonGroup, Typography, useMediaQuery, useTheme } from '@mui/material';
|
|
4
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
4
5
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
const LEVELS = ['top', 'best', 'balanced', 'fast'];
|
|
7
|
+
const EFFORTS = ['low', 'medium', 'high', 'max'];
|
|
8
|
+
export const CHAT_MODEL_TRIGGER_SX = { minWidth: 0, px: 1.25, color: 'text.secondary', whiteSpace: 'nowrap', flexShrink: 0, maxWidth: { xs: 140, sm: 'none' } };
|
|
9
|
+
export const CHAT_MODEL_ENTRY_SX = { borderRadius: 1, alignItems: 'flex-start' };
|
|
10
|
+
export const CHAT_MODEL_EFFORT_SX = { flex: 1, minWidth: 0 };
|
|
11
|
+
export const CHAT_MODEL_CLOSE_SX = { ml: 'auto' };
|
|
12
|
+
const supported = (entry, effort) => { var _a; return (_a = entry === null || entry === void 0 ? void 0 : entry.efforts) === null || _a === void 0 ? void 0 : _a.includes(effort); };
|
|
13
|
+
const validSelection = (entries, selection) => {
|
|
14
|
+
const entry = entries.find((item) => item.id === (selection === null || selection === void 0 ? void 0 : selection.entry));
|
|
15
|
+
return entry && supported(entry, selection === null || selection === void 0 ? void 0 : selection.effort) ? selection : null;
|
|
16
|
+
};
|
|
17
|
+
const fallback = (entries) => {
|
|
18
|
+
var _a;
|
|
19
|
+
const entry = entries.find((item) => item.default) || entries[0];
|
|
20
|
+
return entry ? { entry: entry.id, effort: (_a = entry.efforts) === null || _a === void 0 ? void 0 : _a[0] } : null;
|
|
21
|
+
};
|
|
22
|
+
const cleanLabel = (entry) => ((entry === null || entry === void 0 ? void 0 : entry.label) || (entry === null || entry === void 0 ? void 0 : entry.model) || '').replace(/\s+\d+(?:\.\d+)*$/, '');
|
|
23
|
+
function estimateText(entry, effort, t, language) {
|
|
24
|
+
var _a;
|
|
25
|
+
if (entry.billing === 'subscription')
|
|
26
|
+
return t('Chat.noTokenSubscription');
|
|
27
|
+
const estimate = (_a = entry.estimate) === null || _a === void 0 ? void 0 : _a[effort];
|
|
28
|
+
if (!estimate)
|
|
29
|
+
return null;
|
|
30
|
+
if (estimate.unit === 'tokens') {
|
|
31
|
+
const format = new Intl.NumberFormat(language);
|
|
32
|
+
return t('Chat.estimateTokens', { lo: format.format(estimate.min), hi: format.format(estimate.max) });
|
|
33
|
+
}
|
|
34
|
+
if (estimate.unit === 'level') {
|
|
35
|
+
const level = Math.min(3, Math.max(1, Math.round(estimate.min)));
|
|
36
|
+
return _jsxs(Box, { component: "span", "aria-label": t(`Chat.costScale.${level}`), children: ['●'.repeat(level), '○'.repeat(3 - level)] });
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function ModelPicker({ entries = [], selection, onChange, disabled = false, open, onOpenChange, sheet, balance = null, longChat = false, onNewChat }) {
|
|
41
|
+
const { t, i18n } = useTranslation();
|
|
42
|
+
const language = typeof (i18n === null || i18n === void 0 ? void 0 : i18n.language) === 'string' ? i18n.language.split('_')[0] || undefined : undefined;
|
|
17
43
|
const theme = useTheme();
|
|
18
44
|
const viewportNarrow = useMediaQuery(theme.breakpoints.down('md'));
|
|
19
45
|
const narrow = sheet !== null && sheet !== void 0 ? sheet : viewportNarrow;
|
|
@@ -21,16 +47,40 @@ export function ModelPicker({ entries = [], selection, onChange, disabled = fals
|
|
|
21
47
|
const [innerOpen, setInnerOpen] = useState(false);
|
|
22
48
|
const isOpen = open !== null && open !== void 0 ? open : innerOpen;
|
|
23
49
|
const setOpen = (value) => { setInnerOpen(value); onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(value); };
|
|
24
|
-
const
|
|
25
|
-
const active = entries.find((entry) => entry.id === (
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
const activeSelection = useMemo(() => validSelection(entries, selection) || fallback(entries), [entries, selection]);
|
|
51
|
+
const active = entries.find((entry) => entry.id === (activeSelection === null || activeSelection === void 0 ? void 0 : activeSelection.entry));
|
|
52
|
+
const effort = activeSelection === null || activeSelection === void 0 ? void 0 : activeSelection.effort;
|
|
53
|
+
const hasTokenEstimates = entries.some((entry) => Object.values(entry.estimate || {}).some((item) => item.unit === 'tokens'));
|
|
54
|
+
const grouped = LEVELS.map((level) => [level, entries.filter((entry) => entry.level === level)]).filter(([, items]) => items.length);
|
|
55
|
+
const unknown = entries.filter((entry) => !LEVELS.includes(entry.level));
|
|
56
|
+
const selectEntry = (entry) => { var _a; return onChange === null || onChange === void 0 ? void 0 : onChange({ entry: entry.id, effort: supported(entry, effort) ? effort : (_a = entry.efforts) === null || _a === void 0 ? void 0 : _a[0] }); };
|
|
57
|
+
const providerName = (entry) => ({ anthropic: 'Anthropic', claude_code: 'Anthropic', openai: 'OpenAI' })[entry.provider];
|
|
58
|
+
const entryRow = (entry) => {
|
|
59
|
+
var _a;
|
|
60
|
+
const shown = supported(entry, effort) ? effort : (_a = entry.efforts) === null || _a === void 0 ? void 0 : _a[0];
|
|
61
|
+
const estimate = estimateText(entry, shown, t, language);
|
|
62
|
+
const provider = providerName(entry);
|
|
63
|
+
const checked = entry.id === (active === null || active === void 0 ? void 0 : active.id);
|
|
64
|
+
return _jsx(ListItemButton, { role: "radio", "aria-checked": checked, tabIndex: checked ? 0 : -1, "data-model-entry": entry.id, selected: checked, sx: CHAT_MODEL_ENTRY_SX, onClick: () => selectEntry(entry), children: _jsx(ListItemText, { primary: _jsx(Typography, { variant: "body2", sx: { fontWeight: 600 }, children: entry.label || entry.model }), secondary: _jsxs(_Fragment, { children: [provider ? t('Chat.by', { provider }) : null, estimate ? _jsx(Box, { component: "span", sx: { display: 'block' }, children: estimate }) : null] }) }) }, entry.id);
|
|
65
|
+
};
|
|
66
|
+
// Radio-group keyboard behaviour: arrows move the focus and the choice, only the checked row is a tab stop.
|
|
67
|
+
const moveSelection = (event) => {
|
|
68
|
+
var _a;
|
|
69
|
+
const keys = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 };
|
|
70
|
+
if (!(event.key in keys))
|
|
71
|
+
return;
|
|
72
|
+
const order = [...grouped.flatMap(([, items]) => items), ...unknown];
|
|
73
|
+
const at = order.findIndex((entry) => entry.id === (active === null || active === void 0 ? void 0 : active.id));
|
|
74
|
+
const next = order[(at + keys[event.key] + order.length) % order.length];
|
|
75
|
+
if (!next)
|
|
76
|
+
return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
selectEntry(next);
|
|
79
|
+
(_a = [...event.currentTarget.querySelectorAll('[data-model-entry]')].find((node) => node.dataset.modelEntry === next.id)) === null || _a === void 0 ? void 0 : _a.focus();
|
|
30
80
|
};
|
|
31
|
-
const content = (_jsx(Box, { sx: { p: 2,
|
|
32
|
-
onChange === null || onChange === void 0 ? void 0 : onChange({ entry: active.id, effort }); }, children:
|
|
33
|
-
const
|
|
34
|
-
return
|
|
81
|
+
const content = (_jsx(Box, { role: "dialog", "aria-labelledby": "chat-model-picker-title", sx: { p: 2, width: narrow ? 'auto' : 360, maxWidth: '100%', maxHeight: narrow ? '82vh' : 'min(640px, 80vh)', overflowY: 'auto' }, children: _jsxs(Stack, { spacing: 1.25, children: [_jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: [_jsx(Typography, { id: "chat-model-picker-title", variant: "subtitle1", sx: { fontWeight: 600 }, children: t('Chat.pickerTitle') }), _jsx(IconButton, { "aria-label": t('Chat.close'), size: "small", sx: CHAT_MODEL_CLOSE_SX, onClick: () => setOpen(false), children: _jsx(CloseIcon, { fontSize: "small" }) })] }), _jsxs(Box, { role: "radiogroup", "aria-labelledby": "chat-model-picker-title", onKeyDown: moveSelection, children: [grouped.map(([level, items]) => _jsxs(Box, { children: [_jsx(Typography, { variant: "subtitle2", children: t(`Chat.level.${level}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`Chat.levelHint.${level}`) }), _jsx(List, { dense: true, disablePadding: true, children: items.map(entryRow) })] }, level)), unknown.length ? _jsx(List, { dense: true, disablePadding: true, children: unknown.map(entryRow) }) : null] }), active ? _jsxs(Box, { children: [_jsx(Typography, { variant: "subtitle2", sx: { mb: 0.75 }, children: t('Chat.effortQuestion') }), _jsx(ToggleButtonGroup, { exclusive: true, fullWidth: true, size: "small", value: effort || null, "aria-label": t('Chat.effortQuestion'), onChange: (_event, next) => { if (next)
|
|
82
|
+
onChange === null || onChange === void 0 ? void 0 : onChange({ entry: active.id, effort: next }); }, children: EFFORTS.filter((item) => { var _a; return (_a = active.efforts) === null || _a === void 0 ? void 0 : _a.includes(item); }).map((item) => _jsx(ToggleButton, { value: item, sx: CHAT_MODEL_EFFORT_SX, children: t(`Chat.effort.${item}`) }, item)) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(hasTokenEstimates ? 'Chat.effortHintTokens' : 'Chat.effortHint') })] }) : null, longChat ? _jsx(Alert, { severity: "warning", action: onNewChat ? _jsx(Button, { color: "inherit", size: "small", onClick: onNewChat, children: t('Chat.newConversation') }) : null, children: t('Chat.longChat') }) : null, Number.isInteger(balance) && balance >= 0 ? _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('Chat.balanceLabel', { n: new Intl.NumberFormat(language).format(balance) }) }) : null] }) }));
|
|
83
|
+
const trigger = _jsx(Button, { ref: anchor, size: "small", color: "inherit", sx: CHAT_MODEL_TRIGGER_SX, disabled: disabled || !entries.length, onClick: () => setOpen(true), "aria-haspopup": "dialog", children: _jsx(Box, { component: "span", sx: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: active ? `${cleanLabel(active)} · ${t(`Chat.effort.${effort}`)} ▾` : t('Chat.chooseModel') }) });
|
|
84
|
+
return _jsxs(_Fragment, { children: [trigger, narrow ? _jsx(Drawer, { anchor: "bottom", open: isOpen, onClose: () => setOpen(false), children: content }) : _jsx(Popover, { open: isOpen, anchorEl: anchor.current, onClose: () => setOpen(false), anchorOrigin: { vertical: 'top', horizontal: 'left' }, transformOrigin: { vertical: 'bottom', horizontal: 'left' }, children: content })] });
|
|
35
85
|
}
|
|
36
86
|
export default ModelPicker;
|
package/dist/chat/api.js
CHANGED
|
@@ -4,7 +4,7 @@ export function createChatApi(overrides = {}, { apiBase = '/api/assistant/' } =
|
|
|
4
4
|
const urls = chatApiUrls(apiBase);
|
|
5
5
|
const data = (request) => request.then((response) => response.data);
|
|
6
6
|
const defaults = {
|
|
7
|
-
getCatalogue: () => data(apiClient.get(urls.catalogue)),
|
|
7
|
+
getCatalogue: ({ chat } = {}) => data(chat !== undefined ? apiClient.get(urls.catalogue, { params: { chat } }) : apiClient.get(urls.catalogue)),
|
|
8
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)),
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { createKitSxEntry } from '../theme/kitSxEntry';
|
|
2
|
+
import { CHAT_COMPOSER_INPUT_SX } from './ChatComposer';
|
|
2
3
|
import { CHAT_LIST_ARCHIVED_ACTION_SX, CHAT_LIST_ROW_ACTION_SX } from './ChatList';
|
|
3
4
|
import { CHAT_THREAD_NOTICE_SX, CHAT_THREAD_TOOL_CHIP_SX, CHAT_THREAD_TRIMMED_SX } from './ChatThread';
|
|
5
|
+
import { CHAT_MODEL_TRIGGER_SX, CHAT_MODEL_ENTRY_SX, CHAT_MODEL_EFFORT_SX, CHAT_MODEL_CLOSE_SX } from './ModelPicker';
|
|
4
6
|
export const CHAT_KIT_SX_REGISTRY = [
|
|
7
|
+
createKitSxEntry('ChatComposer.input', 'MuiTextField', CHAT_COMPOSER_INPUT_SX, 'CHAT_COMPOSER_INPUT_SX'),
|
|
8
|
+
createKitSxEntry('ModelPicker.trigger', 'MuiButton', CHAT_MODEL_TRIGGER_SX, 'CHAT_MODEL_TRIGGER_SX'),
|
|
9
|
+
createKitSxEntry('ModelPicker.entry', 'MuiListItemButton', CHAT_MODEL_ENTRY_SX, 'CHAT_MODEL_ENTRY_SX'),
|
|
10
|
+
createKitSxEntry('ModelPicker.effort', 'MuiToggleButton', CHAT_MODEL_EFFORT_SX, 'CHAT_MODEL_EFFORT_SX'),
|
|
11
|
+
createKitSxEntry('ModelPicker.close', 'MuiIconButton', CHAT_MODEL_CLOSE_SX, 'CHAT_MODEL_CLOSE_SX'),
|
|
5
12
|
createKitSxEntry('ChatList.rowAction', 'MuiIconButton', CHAT_LIST_ROW_ACTION_SX, 'CHAT_LIST_ROW_ACTION_SX'),
|
|
6
13
|
createKitSxEntry('ChatList.archivedAction', 'MuiButton', CHAT_LIST_ARCHIVED_ACTION_SX, 'CHAT_LIST_ARCHIVED_ACTION_SX'),
|
|
7
14
|
createKitSxEntry('ChatThread.toolChip', 'MuiChip', CHAT_THREAD_TOOL_CHIP_SX, 'CHAT_THREAD_TOOL_CHIP_SX'),
|
|
@@ -40,4 +40,24 @@ export const billingTranslations = {
|
|
|
40
40
|
'Billing.TEAM_SEATS': { de: 'Sitzplätze im Team-Paket', en: 'Seats in the team package', fr: 'Places dans le forfait d’équipe', sw: 'Nafasi katika kifurushi cha timu' },
|
|
41
41
|
'Billing.PACKAGE_TOKENS': { de: '{{tokens}} Token', en: '{{tokens}} tokens', fr: '{{tokens}} jetons', sw: 'Tokeni {{tokens}}' },
|
|
42
42
|
'Billing.PLANS': { de: 'Pläne', en: 'Plans', fr: 'Offres', sw: 'Mipango' },
|
|
43
|
+
'Billing.WITHDRAWAL_BUTTON': { de: 'Vertrag hier widerrufen', de_informal: 'Vertrag hier widerrufen', en: 'Withdraw this contract', fr: 'Révoquer ce contrat ici', sw: 'Ondoa mkataba huu hapa' },
|
|
44
|
+
'Billing.WITHDRAWAL_UNTIL': { de: 'bis {{date}}', de_informal: 'bis {{date}}', en: 'until {{date}}', fr: 'jusqu’au {{date}}', sw: 'hadi {{date}}' },
|
|
45
|
+
'Billing.WITHDRAWAL_TITLE': { de: 'Vertrag widerrufen?', de_informal: 'Vertrag widerrufen?', en: 'Withdraw this contract?', fr: 'Révoquer ce contrat ?', sw: 'Uondoe mkataba huu?' },
|
|
46
|
+
'Billing.WITHDRAWAL_EFFECT': { de: 'Mit dem Widerruf endet der Zugang aus diesem Kauf sofort, und das Abo verlängert sich nicht.', de_informal: 'Mit dem Widerruf endet der Zugang aus diesem Kauf sofort, und das Abo verlängert sich nicht.', en: 'With the withdrawal, access from this purchase ends immediately and the subscription will not renew.', fr: 'La révocation met immédiatement fin à l’accès lié à cet achat et l’abonnement ne sera pas renouvelé.', sw: 'Ukiondoa, ufikiaji wa ununuzi huu utaisha mara moja na usajili hautasasishwa.' },
|
|
47
|
+
'Billing.WITHDRAWAL_REFUND': { de: 'Sie erhalten {{amount}} zurück.', de_informal: 'Du erhältst {{amount}} zurück.', en: 'You will receive {{amount}} back.', fr: 'Vous recevrez {{amount}} en remboursement.', sw: 'Utarejeshewa {{amount}}.' },
|
|
48
|
+
'Billing.WITHDRAWAL_REFUND_NONE': { de: 'Sie erhalten keine Rückzahlung.', de_informal: 'Du erhältst keine Rückzahlung.', en: 'You will receive no refund.', fr: 'Vous ne recevrez aucun remboursement.', sw: 'Hutapokea marejesho ya pesa.' },
|
|
49
|
+
'Billing.WITHDRAWAL_CALCULATION': { de: 'Berechnung: {{explanation}}', de_informal: 'Berechnung: {{explanation}}', en: 'Calculation: {{explanation}}', fr: 'Calcul : {{explanation}}', sw: 'Hesabu: {{explanation}}' },
|
|
50
|
+
'Billing.WITHDRAWAL_CONFIRM': { de: 'Widerruf bestätigen', de_informal: 'Widerruf bestätigen', en: 'Confirm withdrawal', fr: 'Confirmer la révocation', sw: 'Thibitisha uondoaji' },
|
|
51
|
+
'Billing.WITHDRAWAL_CANCEL': { de: 'Abbrechen', de_informal: 'Abbrechen', en: 'Cancel', fr: 'Annuler', sw: 'Ghairi' },
|
|
52
|
+
'Billing.WITHDRAWAL_DONE': { de: 'Widerruf eingegangen', de_informal: 'Widerruf eingegangen', en: 'Withdrawal received', fr: 'Révocation reçue', sw: 'Uondoaji umepokelewa' },
|
|
53
|
+
'Billing.WITHDRAWAL_DONE_DETAIL': { de: 'Eingegangen am {{date}}. Eine Bestätigung ist per E-Mail unterwegs; die Rückzahlung folgt innerhalb von 14 Tagen.', de_informal: 'Eingegangen am {{date}}. Eine Bestätigung ist per E-Mail unterwegs; die Rückzahlung folgt innert 14 Tagen.', en: 'Received on {{date}}. A confirmation email is on its way; the refund will follow within 14 days.', fr: 'Reçu le {{date}}. Un e-mail de confirmation est en route ; le remboursement suivra sous 14 jours.', sw: 'Imepokelewa {{date}}. Barua pepe ya uthibitisho inatumwa; marejesho yatafuata ndani ya siku 14.' },
|
|
54
|
+
'Billing.WITHDRAWAL_PREVIEW_ERROR': { de: 'Die Vorschau konnte nicht geladen werden.', de_informal: 'Die Vorschau konnte nicht geladen werden.', en: 'The preview could not be loaded.', fr: 'Impossible de charger l’aperçu.', sw: 'Muhtasari haukuweza kupakiwa.' },
|
|
55
|
+
'Billing.WITHDRAWAL_NOT_POSSIBLE': { de: 'Dieser Vertrag kann derzeit nicht widerrufen werden.', de_informal: 'Dieser Vertrag kann derzeit nicht widerrufen werden.', en: 'This contract cannot be withdrawn right now.', fr: 'Ce contrat ne peut pas être révoqué pour le moment.', sw: 'Mkataba huu hauwezi kuondolewa kwa sasa.' },
|
|
56
|
+
'Billing.WITHDRAWAL_ERROR': { de: 'Der Widerruf konnte nicht übermittelt werden. Bitte versuchen Sie es erneut.', de_informal: 'Der Widerruf konnte nicht übermittelt werden. Bitte versuche es erneut.', en: 'The withdrawal could not be submitted. Please try again.', fr: 'La révocation n’a pas pu être envoyée. Veuillez réessayer.', sw: 'Uondoaji haukuweza kutumwa. Tafadhali jaribu tena.' },
|
|
57
|
+
'Billing.WITHDRAWAL_LOAD_ERROR': { de: 'Widerrufsmöglichkeiten konnten nicht geladen werden.', de_informal: 'Widerrufsmöglichkeiten konnten nicht geladen werden.', en: 'Withdrawal options could not be loaded.', fr: 'Impossible de charger les possibilités de révocation.', sw: 'Chaguo za uondoaji hazikuweza kupakiwa.' },
|
|
58
|
+
'Billing.WITHDRAWAL_PAID': { de: 'Gezahlt: {{amount}}', de_informal: 'Gezahlt: {{amount}}', en: 'Paid: {{amount}}', fr: 'Payé : {{amount}}', sw: 'Imelipwa: {{amount}}' },
|
|
59
|
+
'Billing.WITHDRAWAL_TIME_SHARE': { de: 'Zeitanteil: {{amount}}', de_informal: 'Zeitanteil: {{amount}}', en: 'Time share: {{amount}}', fr: 'Part temporelle : {{amount}}', sw: 'Sehemu ya muda: {{amount}}' },
|
|
60
|
+
'Billing.WITHDRAWAL_TOKEN_SHARE': { de: 'Tokenanteil: {{amount}}', de_informal: 'Tokenanteil: {{amount}}', en: 'Token share: {{amount}}', fr: 'Part des jetons : {{amount}}', sw: 'Sehemu ya tokeni: {{amount}}' },
|
|
61
|
+
'Billing.WITHDRAWAL_USED': { de: 'Genutzt: {{amount}}', de_informal: 'Genutzt: {{amount}}', en: 'Used: {{amount}}', fr: 'Utilisé : {{amount}}', sw: 'Imetumika: {{amount}}' },
|
|
62
|
+
'Billing.WITHDRAWAL_CLOSE': { de: 'Schließen', de_informal: 'Schließen', en: 'Close', fr: 'Fermer', sw: 'Funga' },
|
|
43
63
|
};
|
|
@@ -19,15 +19,35 @@ export const chatTranslations = {
|
|
|
19
19
|
'Chat.retry': { de: 'Erneut versuchen', en: 'Retry', fr: 'Réessayer', sw: 'Jaribu tena' },
|
|
20
20
|
'Chat.interrupted': { de: 'Unterbrochen', en: 'Interrupted', fr: 'Interrompue', sw: 'Imekatizwa' },
|
|
21
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
22
|
'Chat.chooseModel': { de: 'Modell wählen', en: 'Choose model', fr: 'Choisir un modèle', sw: 'Chagua modeli' },
|
|
24
|
-
'Chat.effort.low': { de: '
|
|
25
|
-
'Chat.effort.medium': { de: '
|
|
26
|
-
'Chat.effort.high': { de: '
|
|
27
|
-
'Chat.effort.max': { de: '
|
|
28
|
-
'Chat.
|
|
29
|
-
'Chat.
|
|
30
|
-
'Chat.
|
|
23
|
+
'Chat.effort.low': { de: 'Schnell', en: 'Quick', fr: 'Rapide', sw: 'Haraka' },
|
|
24
|
+
'Chat.effort.medium': { de: 'Gründlich', en: 'Thorough', fr: 'Approfondi', sw: 'Kwa kina' },
|
|
25
|
+
'Chat.effort.high': { de: 'Sehr gründlich', en: 'Very thorough', fr: 'Très approfondi', sw: 'Kwa kina zaidi' },
|
|
26
|
+
'Chat.effort.max': { de: 'Maximal', en: 'Maximum', fr: 'Maximum', sw: 'Kiwango cha juu' },
|
|
27
|
+
'Chat.pickerTitle': { de: 'Modell für diese Unterhaltung', en: 'Model for this conversation', fr: 'Modèle pour cette conversation', sw: 'Modeli ya mazungumzo haya' },
|
|
28
|
+
'Chat.close': { de: 'Schliessen', en: 'Close', fr: 'Fermer', sw: 'Funga' },
|
|
29
|
+
'Chat.level.top': { de: 'Höchste Qualität', en: 'Highest quality', fr: 'Qualité maximale', sw: 'Ubora wa juu zaidi' },
|
|
30
|
+
'Chat.levelHint.top': { de: 'für besonders anspruchsvolle Auswertungen', en: 'for especially demanding analysis', fr: 'pour les analyses particulièrement exigeantes', sw: 'kwa uchanganuzi wenye mahitaji makubwa' },
|
|
31
|
+
'Chat.level.best': { de: 'Hohe Qualität', en: 'High quality', fr: 'Haute qualité', sw: 'Ubora wa juu' },
|
|
32
|
+
'Chat.levelHint.best': { de: 'für schwierige Abgrenzungs- und Codierfragen', en: 'for difficult distinction and coding questions', fr: 'pour les questions difficiles de distinction et de codage', sw: 'kwa maswali magumu ya kutofautisha na kuweka misimbo' },
|
|
33
|
+
'Chat.level.balanced': { de: 'Ausgewogen', en: 'Balanced', fr: 'Équilibré', sw: 'Wastani' },
|
|
34
|
+
'Chat.levelHint.balanced': { de: 'für die meisten Fragen', en: 'for most questions', fr: 'pour la plupart des questions', sw: 'kwa maswali mengi' },
|
|
35
|
+
'Chat.level.fast': { de: 'Schnell und günstig', en: 'Fast and cheap', fr: 'Rapide et économique', sw: 'Haraka na nafuu' },
|
|
36
|
+
'Chat.levelHint.fast': { de: 'für kurze Nachfragen', en: 'for short follow-up questions', fr: 'pour les courtes questions de suivi', sw: 'kwa maswali mafupi ya ufuatiliaji' },
|
|
37
|
+
'Chat.by': { de: 'von {{provider}}', en: 'by {{provider}}', fr: 'par {{provider}}', sw: 'na {{provider}}' },
|
|
38
|
+
'Chat.estimateTokens': { de: '≈ {{lo}}–{{hi}} Token pro Antwort', en: '≈ {{lo}}–{{hi}} tokens per reply', fr: '≈ {{lo}}–{{hi}} jetons par réponse', sw: '≈ {{lo}}–{{hi}} tokeni kwa jibu' },
|
|
39
|
+
'Chat.noTokenSubscription': { de: 'ohne Token · Claude-Abo', en: 'no tokens · Claude subscription', fr: 'sans jetons · abonnement Claude', sw: 'bila tokeni · usajili wa Claude' },
|
|
40
|
+
'Chat.costScale.1': { de: 'Kosten niedrig', en: 'Low cost', fr: 'Coût faible', sw: 'Gharama ndogo' },
|
|
41
|
+
'Chat.costScale.2': { de: 'Kosten mittel', en: 'Medium cost', fr: 'Coût moyen', sw: 'Gharama ya wastani' },
|
|
42
|
+
'Chat.costScale.3': { de: 'Kosten hoch', en: 'High cost', fr: 'Coût élevé', sw: 'Gharama kubwa' },
|
|
43
|
+
'Chat.effortQuestion': { de: 'Wie gründlich soll die Antwort sein?', en: 'How thorough should the answer be?', fr: 'Quel niveau d’approfondissement souhaitez-vous ?', sw: 'Jibu liwe na kina kiasi gani?' },
|
|
44
|
+
'Chat.effortHintTokens': { de: 'Gründlicher heisst bessere Antworten, aber längere Wartezeit und mehr Token.', en: 'More thorough means better answers, but longer waits and more tokens.', fr: 'Une réponse plus approfondie est meilleure, mais prend plus de temps et consomme plus de jetons.', sw: 'Jibu lenye kina zaidi huwa bora, lakini husubiriwa muda mrefu na hutumia tokeni zaidi.' },
|
|
45
|
+
'Chat.effortHint': { de: 'Gründlicher heisst bessere Antworten, aber längere Wartezeit.', en: 'More thorough means better answers, but longer waits.', fr: 'Une réponse plus approfondie est meilleure, mais prend plus de temps.', sw: 'Jibu lenye kina zaidi huwa bora, lakini husubiriwa muda mrefu.' },
|
|
46
|
+
'Chat.longChat': { de: 'Diese Unterhaltung ist lang. Ältere Teile werden zusammengefasst; eine neue Unterhaltung ist günstiger.', en: 'This conversation is long. Older parts will be summarized; a new conversation costs less.', fr: 'Cette conversation est longue. Les anciens passages seront résumés ; une nouvelle conversation coûte moins cher.', sw: 'Mazungumzo haya ni marefu. Sehemu za awali zitafupishwa; mazungumzo mapya yatagharimu kidogo.' },
|
|
47
|
+
'Chat.newConversation': { de: 'Neue Unterhaltung', en: 'New conversation', fr: 'Nouvelle conversation', sw: 'Mazungumzo mapya' },
|
|
48
|
+
'Chat.balanceLabel': { de: 'Guthaben {{n}} Token', en: 'Balance: {{n}} tokens', fr: 'Solde : {{n}} jetons', sw: 'Salio: tokeni {{n}}' },
|
|
49
|
+
'Chat.summaryUntilHere': { de: 'Zusammenfassung bis hier', en: 'Summary so far', fr: 'Résumé jusqu’ici', sw: 'Muhtasari hadi hapa' },
|
|
50
|
+
'Chat.replyTokens': { de: '{{n}} Token', en: '{{n}} tokens', fr: '{{n}} jetons', sw: 'tokeni {{n}}' },
|
|
31
51
|
'Chat.markdownTable': { de: 'Tabelleninhalt', en: 'Table content', fr: 'Contenu du tableau', sw: 'Yaliyomo kwenye jedwali' },
|
|
32
52
|
'Chat.markdownCode': { de: 'Codeblock', en: 'Code block', fr: 'Bloc de code', sw: 'Kizuizi cha msimbo' },
|
|
33
53
|
'Chat.changeModel': { de: 'Modell ändern', en: 'Change model', fr: 'Changer de modèle', sw: 'Badilisha modeli' },
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,8 @@ export { TokenCostPreview } from './billing/TokenCostPreview';
|
|
|
48
48
|
export { AccessEndedBanner } from './billing/AccessEndedBanner';
|
|
49
49
|
export { PlanPicker } from './billing/PlanPicker';
|
|
50
50
|
export { SubscriptionSection } from './billing/SubscriptionSection';
|
|
51
|
+
export { WithdrawalSection } from './billing/WithdrawalSection';
|
|
52
|
+
export { WithdrawalDialog } from './billing/WithdrawalDialog';
|
|
51
53
|
export { useBilling } from './billing/useBilling';
|
|
52
54
|
export { createBillingApi, defaultBillingApi } from './billing/api';
|
|
53
55
|
// --- 6. Charts ---
|
|
@@ -37,6 +37,8 @@ import { ACCESS_ENDED_BANNER_ALERT_SX } from '../billing/AccessEndedBanner';
|
|
|
37
37
|
import { TOKEN_BALANCE_BADGE_SX } from '../billing/TokenBalanceBadge';
|
|
38
38
|
import { PLAN_PICKER_ALERT_SX, PLAN_PICKER_CARD_SX, PLAN_PICKER_LABEL_SX, PLAN_PICKER_SELECTED_CARD_SX } from '../billing/PlanPicker';
|
|
39
39
|
import { SUBSCRIPTION_SECTION_ALERT_SX } from '../billing/SubscriptionSection';
|
|
40
|
+
import { WITHDRAWAL_DIALOG_ALERT_SX } from '../billing/WithdrawalDialog';
|
|
41
|
+
import { WITHDRAWAL_SECTION_ALERT_SX } from '../billing/WithdrawalSection';
|
|
40
42
|
const SX_EXPORTS = {
|
|
41
43
|
ACCESS_CODE_ACTION_SX,
|
|
42
44
|
ACCESS_ENDED_BANNER_ALERT_SX,
|
|
@@ -112,6 +114,8 @@ const SX_EXPORTS = {
|
|
|
112
114
|
PLAN_PICKER_LABEL_SX,
|
|
113
115
|
PLAN_PICKER_SELECTED_CARD_SX,
|
|
114
116
|
SUBSCRIPTION_SECTION_ALERT_SX,
|
|
117
|
+
WITHDRAWAL_DIALOG_ALERT_SX,
|
|
118
|
+
WITHDRAWAL_SECTION_ALERT_SX,
|
|
115
119
|
USER_INVITE_ACTION_SX,
|
|
116
120
|
USER_INVITE_ALERT_SX,
|
|
117
121
|
USER_LIST_ACTION_SX,
|
|
@@ -217,4 +221,6 @@ export const KIT_COMPONENT_SX_REGISTRY = [
|
|
|
217
221
|
entry('PlanPicker.label', 'MuiFormControlLabel', PLAN_PICKER_LABEL_SX),
|
|
218
222
|
entry('PlanPicker.alert', 'MuiAlert', PLAN_PICKER_ALERT_SX),
|
|
219
223
|
entry('SubscriptionSection.alert', 'MuiAlert', SUBSCRIPTION_SECTION_ALERT_SX),
|
|
224
|
+
entry('WithdrawalDialog.alert', 'MuiAlert', WITHDRAWAL_DIALOG_ALERT_SX),
|
|
225
|
+
entry('WithdrawalSection.alert', 'MuiAlert', WITHDRAWAL_SECTION_ALERT_SX),
|
|
220
226
|
];
|