@columenlabs/lumen-ui 0.1.82 → 0.1.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistant/client.d.ts +7 -1
- package/dist/assistant/client.js +6 -0
- package/dist/assistant/lumen-assistant.d.ts +3 -1
- package/dist/assistant/lumen-assistant.js +24 -1
- package/dist/assistant/settings.d.ts +6 -2
- package/dist/assistant/settings.js +120 -48
- package/dist/chatgpt-signin.d.ts +18 -0
- package/dist/chatgpt-signin.js +64 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +4 -0
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantChat, AssistantChatRow, AssistantEvent, AssistantMessage, AssistantProvider, AssistantSettingsPatch, AssistantStatus, DraftItem, KeyState, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AllowanceRequest, AssistantChat, AssistantChatRow, AssistantEvent, AssistantMessage, AssistantProvider, AssistantSettingsPatch, AssistantStatus, DraftItem, KeyState, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
type Test = NonNullable<KeyState['test']>;
|
|
3
3
|
export interface AssistantAdapter {
|
|
4
4
|
/** Names the cache its reads keep (two assistants on one page need two keys). */
|
|
@@ -36,6 +36,12 @@ export interface AssistantAdapter {
|
|
|
36
36
|
settleDraft(chatId: string, draftId: string, action: 'save' | 'dismiss'): Promise<DraftItem>;
|
|
37
37
|
/** Takes back a change the assistant made at once. */
|
|
38
38
|
undoWrite(chatId: string, stepId: string): Promise<StepItem>;
|
|
39
|
+
/** Asks for more allowance on the shared keys (an app with `allowance`). */
|
|
40
|
+
requestMore?(amount: number, why: string): Promise<AssistantStatus>;
|
|
41
|
+
/** Approvers: every request, newest first. */
|
|
42
|
+
allowanceRequests?(): Promise<AllowanceRequest[]>;
|
|
43
|
+
/** Approvers: grants (the asked amount unless `amount` says) or declines a request. */
|
|
44
|
+
decideRequest?(id: string, action: 'grant' | 'decline', amount?: number): Promise<AllowanceRequest>;
|
|
39
45
|
}
|
|
40
46
|
/** The adapter over `assistantRoutes` mounted at `base` (`/api/assistant`). */
|
|
41
47
|
export declare function assistantClient(base: string): AssistantAdapter;
|
package/dist/assistant/client.js
CHANGED
|
@@ -63,6 +63,12 @@ export function assistantClient(base) {
|
|
|
63
63
|
action,
|
|
64
64
|
}),
|
|
65
65
|
undoWrite: (chatId, stepId) => send('POST', `/chats/${encodeURIComponent(chatId)}/writes/${encodeURIComponent(stepId)}/undo`),
|
|
66
|
+
requestMore: (amount, why) => send('POST', '/allowance/request', { amount, why }),
|
|
67
|
+
allowanceRequests: () => get('/allowance/requests'),
|
|
68
|
+
decideRequest: (id, action, amount) => send('POST', `/allowance/requests/${encodeURIComponent(id)}`, {
|
|
69
|
+
action,
|
|
70
|
+
...(amount !== undefined ? { amount } : {}),
|
|
71
|
+
}),
|
|
66
72
|
async send(msg, onEvent, signal) {
|
|
67
73
|
const url = `${root}/chat`;
|
|
68
74
|
const res = await fetch(url, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantMessage, AssistantRefNote, AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AllowanceUnit, AssistantMessage, AssistantRefNote, AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type LumenOption } from '../lumen-selector.js';
|
|
3
3
|
import type { AssistantAdapter } from './client.js';
|
|
4
4
|
import { type AssistantSource } from './sources.js';
|
|
@@ -42,6 +42,8 @@ export interface AssistantCommand {
|
|
|
42
42
|
}
|
|
43
43
|
/** The assistant's chat: header, messages and composer. */
|
|
44
44
|
export declare function LumenAssistant({ client, context, onSettings, keepDays, onClose, chat: outside, commands, sources, }: LumenAssistantProps): import("react").JSX.Element;
|
|
45
|
+
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
46
|
+
export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
45
47
|
/** What the composer sends with the text: the tier picked with `@` and the things pointed at with `#`. */
|
|
46
48
|
export interface ComposerExtra {
|
|
47
49
|
tier?: string;
|
|
@@ -9,6 +9,7 @@ import { errorText } from '../integrations.js';
|
|
|
9
9
|
import { cn } from '../lib/utils.js';
|
|
10
10
|
import { LumenSelector, LumenSelectorList, } from '../lumen-selector.js';
|
|
11
11
|
import { lumenToast } from '../lumen-toast.js';
|
|
12
|
+
import { useOpenSettings } from '../shell.js';
|
|
12
13
|
import { cardsOnScreen, kindName, pointAt, sectionsOnScreen, targetEl, textOf, } from '../spotlight.js';
|
|
13
14
|
import { Alert, AlertDescription } from '../ui/alert.js';
|
|
14
15
|
import { Button } from '../ui/button.js';
|
|
@@ -148,7 +149,18 @@ function Messages({ chat, status, onSettings, }) {
|
|
|
148
149
|
useEffect(() => {
|
|
149
150
|
end.current?.scrollIntoView({ block: 'end' });
|
|
150
151
|
}, [count]);
|
|
151
|
-
|
|
152
|
+
// The app's own link to Settings, else the shell's, at the section that answers the wait.
|
|
153
|
+
const shellSettings = useOpenSettings();
|
|
154
|
+
const settings = onSettings ? (_section) => onSettings() : shellSettings;
|
|
155
|
+
return (_jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 py-3", children: [status?.wait && (_jsxs(Alert, { className: "mb-3", children: [_jsx(CircleAlertIcon, {}), _jsxs(AlertDescription, { className: "flex flex-col items-start gap-2", children: [status.wait === 'app_cap' && status.allowance
|
|
156
|
+
? allowanceWait(status.allowance)
|
|
157
|
+
: WAIT[status.wait](status.admin, status.providers.find((p) => p.id === status.wait_provider)?.label ?? 'API'), settings && (_jsx(Button, { size: "sm", variant: "outline", onClick: () => settings(status.wait === 'app_cap' && status.allowance
|
|
158
|
+
? 'assistant-allowance'
|
|
159
|
+
: 'assistant-providers'), children: status.wait === 'app_cap' &&
|
|
160
|
+
status.allowance &&
|
|
161
|
+
status.allowance.request?.status !== 'open'
|
|
162
|
+
? 'Request more'
|
|
163
|
+
: 'Open Settings' }))] })] })), chat.loading ? (_jsxs("p", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Loading the chat\u2026"] })) : chat.items.length === 0 && !status?.wait ? (_jsxs("p", { className: "pt-8 text-center text-muted-foreground text-sm", children: ["Ask ", status?.name ?? 'the assistant', " anything."] })) : (_jsx(TierNames.Provider, { value: (id) => status?.tiers.find((t) => t.id === id)?.label ?? id, children: _jsx(UndoWrite.Provider, { value: chat.undo, children: _jsx(Items, { items: chat.items, onSettle: chat.settle, busy: chat.busy, turns: true }) }) })), chat.busy && !chat.items.some(isLive) && (_jsxs("p", { className: "flex items-center gap-2 py-1 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Thinking\u2026"] })), _jsx("div", { ref: end })] }));
|
|
152
164
|
}
|
|
153
165
|
const isLive = (i) => !!i.live || (i.kind === 'handoff' && i.items.some(isLive));
|
|
154
166
|
const WAIT = {
|
|
@@ -158,6 +170,17 @@ const WAIT = {
|
|
|
158
170
|
app_cap: () => "You reached this month's cap on the shared keys. Add your own key, or wait for next month.",
|
|
159
171
|
own_cap: () => 'Your own keys reached their monthly cap. Raise it in Settings ▸ Model providers.',
|
|
160
172
|
};
|
|
173
|
+
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
174
|
+
export const allowanceAmount = (u, n) => u.usd === 1 && u.many === 'dollars'
|
|
175
|
+
? usd(n)
|
|
176
|
+
: `${n.toLocaleString()} ${n === 1 ? u.one : u.many}`;
|
|
177
|
+
function allowanceWait(a) {
|
|
178
|
+
if (a.request?.status === 'open')
|
|
179
|
+
return `You used your allowance on the shared keys. Your request for ${allowanceAmount(a.unit, a.request.amount)} is waiting, or add your own key.`;
|
|
180
|
+
return a.period === 'once'
|
|
181
|
+
? 'You used your allowance on the shared keys. Request more, or add your own key.'
|
|
182
|
+
: "You used this month's allowance on the shared keys. Request more, or add your own key.";
|
|
183
|
+
}
|
|
161
184
|
/** The assistant's name, for a copied turn's headings. */
|
|
162
185
|
const AssistantName = createContext('Assistant');
|
|
163
186
|
/** A tier's name for a message's `@` chip. */
|
|
@@ -4,17 +4,21 @@ import type { AssistantAdapter } from './client.js';
|
|
|
4
4
|
* The admin's section: the app's shared keys and each person's monthly allowance on them. Put it
|
|
5
5
|
* where the app keeps its admin settings, shown only to admins.
|
|
6
6
|
*/
|
|
7
|
-
export declare function assistantAdminSections({ client, }: {
|
|
7
|
+
export declare function assistantAdminSections({ client, allowance, }: {
|
|
8
8
|
client: AssistantAdapter;
|
|
9
|
+
/** Add Allowance requests (the app set `allowance` on the server). */
|
|
10
|
+
allowance?: boolean;
|
|
9
11
|
}): SettingsSectionDef[];
|
|
10
12
|
/**
|
|
11
13
|
* The assistant's Settings sections: the assistant, then Model providers, Models, Tools, Web and
|
|
12
14
|
* Spend under it. Pass them to LumenShell's `shared` (or an app's own `settings`).
|
|
13
15
|
*/
|
|
14
|
-
export declare function assistantSettingsSections({ client, name, web, }: {
|
|
16
|
+
export declare function assistantSettingsSections({ client, name, web, allowance, }: {
|
|
15
17
|
client: AssistantAdapter;
|
|
16
18
|
/** The assistant's name, the parent section's title. */
|
|
17
19
|
name: string;
|
|
18
20
|
/** Show the Web section (the app set up `web` on the server). */
|
|
19
21
|
web?: boolean;
|
|
22
|
+
/** Show Allowance: what is left on the shared keys, and Request more (`allowance` on the server). */
|
|
23
|
+
allowance?: boolean;
|
|
20
24
|
}): SettingsSectionDef[];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
3
|
-
import { CheckIcon, ExternalLinkIcon, GlobeIcon, KeyRoundIcon, LayersIcon, ReceiptIcon, SparklesIcon, Trash2Icon, WrenchIcon, } from 'lucide-react';
|
|
2
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { CheckIcon, CoinsIcon, ExternalLinkIcon, GlobeIcon, InboxIcon, KeyRoundIcon, LayersIcon, ReceiptIcon, SparklesIcon, Trash2Icon, WrenchIcon, } from 'lucide-react';
|
|
4
4
|
import { useId, useState } from 'react';
|
|
5
|
+
import { LumenChatgptSignin } from '../chatgpt-signin.js';
|
|
5
6
|
import { LumenExpandList, LumenExpandRow } from '../expand-list.js';
|
|
6
7
|
import { SaveButton } from '../form-issues.js';
|
|
7
8
|
import { IconButton } from '../icon-button.js';
|
|
@@ -9,12 +10,14 @@ import { InfoLabel } from '../info-tip.js';
|
|
|
9
10
|
import { KeyTestLine, TestBadge, errorText } from '../integrations.js';
|
|
10
11
|
import { LumenSelector } from '../lumen-selector.js';
|
|
11
12
|
import { lumenToast } from '../lumen-toast.js';
|
|
13
|
+
import { ShareMeter } from '../share-meter.js';
|
|
12
14
|
import { Badge } from '../ui/badge.js';
|
|
15
|
+
import { Button } from '../ui/button.js';
|
|
13
16
|
import { Input } from '../ui/input.js';
|
|
14
17
|
import { Label } from '../ui/label.js';
|
|
15
18
|
import { Switch } from '../ui/switch.js';
|
|
16
19
|
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group.js';
|
|
17
|
-
import { assistantStatusKey, usd, useAssistantStatus } from './lumen-assistant.js';
|
|
20
|
+
import { allowanceAmount, assistantStatusKey, usd, useAssistantStatus } from './lumen-assistant.js';
|
|
18
21
|
const EFFORTS = [
|
|
19
22
|
{ value: 'default', label: 'Model default' },
|
|
20
23
|
{ value: 'low', label: 'Low' },
|
|
@@ -128,50 +131,10 @@ function CapInput({ label, value, onSave, }) {
|
|
|
128
131
|
};
|
|
129
132
|
return (_jsxs("div", { className: "flex items-center gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "$" }), _jsx(Input, { "aria-label": label, inputMode: "decimal", className: "h-8 w-20 text-right tabular-nums", value: text, onChange: (e) => setText(e.target.value), onBlur: commit, onKeyDown: (e) => e.key === 'Enter' && commit() }), _jsx("span", { className: "text-muted-foreground", children: "/ month" })] }));
|
|
130
133
|
}
|
|
131
|
-
/** A person's ChatGPT plan sign-in
|
|
134
|
+
/** A person's ChatGPT plan sign-in, on the assistant's own routes. */
|
|
132
135
|
function ChatgptSignin({ client, p }) {
|
|
133
136
|
const { apply, refresh } = useApply(client);
|
|
134
|
-
|
|
135
|
-
const [started, setStarted] = useState(null);
|
|
136
|
-
const [pasted, setPasted] = useState('');
|
|
137
|
-
const [busy, setBusy] = useState(false);
|
|
138
|
-
const signedIn = p.own.via === 'chatgpt';
|
|
139
|
-
const start = async () => {
|
|
140
|
-
// Open the tab inside the click, or the browser blocks it; point it once the address is known.
|
|
141
|
-
const tab = window.open('about:blank', '_blank');
|
|
142
|
-
if (tab)
|
|
143
|
-
tab.opener = null;
|
|
144
|
-
try {
|
|
145
|
-
const { url, redirect } = await client.startSignin(p.id);
|
|
146
|
-
if (tab)
|
|
147
|
-
tab.location.href = url;
|
|
148
|
-
else
|
|
149
|
-
window.open(url, '_blank', 'noopener');
|
|
150
|
-
setStarted(redirect);
|
|
151
|
-
}
|
|
152
|
-
catch (e) {
|
|
153
|
-
tab?.close();
|
|
154
|
-
lumenToast.error(errorText(e));
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
const finish = async () => {
|
|
158
|
-
setBusy(true);
|
|
159
|
-
if (await apply(`signin:${p.id}`, () => client.finishSignin(p.id, pasted.trim()), 'Signed in')) {
|
|
160
|
-
setPasted('');
|
|
161
|
-
setStarted(null);
|
|
162
|
-
}
|
|
163
|
-
setBusy(false);
|
|
164
|
-
};
|
|
165
|
-
const issues = [];
|
|
166
|
-
if (pasted.trim() && started && !pasted.trim().startsWith(started.split('/auth')[0] ?? ''))
|
|
167
|
-
issues.push({ where: 'Address', message: `It starts with ${started}`, target: id });
|
|
168
|
-
if (signedIn)
|
|
169
|
-
return (_jsxs("div", { className: "flex flex-col gap-3 border-b py-3 last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex-1 font-medium text-sm", children: "Your ChatGPT plan" }), _jsx("span", { className: "text-muted-foreground text-xs", children: p.own.hint }), p.own.test && _jsx(TestBadge, { test: p.own.test })] }), _jsx(KeyTestLine, { test: testOf(p.own), run: () => client.testOwnKey(p.id).finally(refresh) }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(IconButton, { icon: Trash2Icon, label: "Sign out of ChatGPT", variant: "outline", onClick: () => void apply(`own:${p.id}`, () => client.ownKey(p.id, null), 'Signed out') }), _jsx(SaveButton, { issues: [], icon: KeyRoundIcon, label: "Sign in again", onSave: start })] }), started && (_jsx(SigninPaste, { id: id, redirect: started, pasted: pasted, setPasted: setPasted, issues: issues, busy: busy, finish: finish }))] }));
|
|
170
|
-
return (_jsxs("div", { className: "flex flex-col gap-3 border-b py-3 last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(InfoLabel, { className: "flex-1 font-medium text-sm", info: "Run OpenAI models on your own ChatGPT plan instead of an API key.", children: "Your ChatGPT plan" }), _jsx(Badge, { variant: "outline", children: "Not signed in" })] }), _jsx("div", { className: "flex justify-end", children: _jsx(SaveButton, { issues: [], icon: ExternalLinkIcon, label: started ? 'Open the sign-in again' : 'Sign in with ChatGPT', onSave: start }) }), started && (_jsx(SigninPaste, { id: id, redirect: started, pasted: pasted, setPasted: setPasted, issues: issues, busy: busy, finish: finish }))] }));
|
|
171
|
-
}
|
|
172
|
-
/** Where the sign-in tab ends, and the box its address is pasted into. */
|
|
173
|
-
function SigninPaste({ id, redirect, pasted, setPasted, issues, busy, finish, }) {
|
|
174
|
-
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: id, children: _jsx(InfoLabel, { info: `The sign-in ends on ${redirect}, a page that does not load. That is expected: copy the whole address from that tab's address bar.`, children: "Paste the address the sign-in ended on" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { id: id, autoComplete: "off", placeholder: `${redirect}?code=…`, className: "font-code text-xs", value: pasted, onChange: (e) => setPasted(e.target.value) }), _jsx(SaveButton, { issues: issues, busy: busy, disabled: !pasted.trim(), label: "Finish", onSave: finish })] })] }));
|
|
137
|
+
return (_jsx(LumenChatgptSignin, { state: p.own, start: () => client.startSignin(p.id), finish: (url) => apply(`signin:${p.id}`, () => client.finishSignin(p.id, url), 'Signed in'), signOut: () => apply(`own:${p.id}`, () => client.ownKey(p.id, null), 'Signed out'), test: () => client.testOwnKey(p.id).finally(refresh) }));
|
|
175
138
|
}
|
|
176
139
|
const ready = (_jsxs(Badge, { variant: "outline", className: "text-gain", children: [_jsx(CheckIcon, {}), "Ready"] }));
|
|
177
140
|
const notSet = _jsx(Badge, { variant: "outline", children: "Not set" });
|
|
@@ -197,7 +160,7 @@ function SharedProviderKey({ client, p }) {
|
|
|
197
160
|
/** Admins only: the app's shared keys and what each person may spend on them. */
|
|
198
161
|
function SharedKeys({ client }) {
|
|
199
162
|
const { apply } = useApply(client);
|
|
200
|
-
return (_jsx(WithStatus, { client: client, children: (s) => s.admin ? (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: s.providers.map((p) => (_jsx(SharedProviderKey, { client: client, p: p }, p.id))) }), _jsx(Row, { label: "Each person may spend", info: "A month on the shared keys, every provider together. Past it, a person's own key pays.", children: _jsx(CapInput, { label: "Allowance per person", value: s.caps.app, onSave: (n) => void apply('cap', () => client.appCap(n)) }) })] })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: "Only an admin can change these." })) }));
|
|
163
|
+
return (_jsx(WithStatus, { client: client, children: (s) => s.admin ? (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: s.providers.map((p) => (_jsx(SharedProviderKey, { client: client, p: p }, p.id))) }), s.allowance?.period !== 'once' && (_jsx(Row, { label: "Each person may spend", info: "A month on the shared keys, every provider together. Past it, a person's own key pays.", children: _jsx(CapInput, { label: "Allowance per person", value: s.caps.app, onSave: (n) => void apply('cap', () => client.appCap(n)) }) }))] })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: "Only an admin can change these." })) }));
|
|
201
164
|
}
|
|
202
165
|
// ---------------------------------------------------------------- models
|
|
203
166
|
function Models({ client }) {
|
|
@@ -264,11 +227,97 @@ function Spend({ client }) {
|
|
|
264
227
|
` · ${usd(sp.cap)} cap on ${sp.payer === 'app' ? 'the app key' : 'your key'}`] })] }), sp.rows.length ? (_jsx("div", { className: "flex flex-col text-sm", children: sp.rows.map((r) => (_jsxs("div", { className: "flex items-center gap-2 border-b py-2 last:border-b-0", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: r.label }), _jsx("span", { className: "text-muted-foreground text-xs", children: r.payer === 'app' ? 'Shared key' : 'Your key' }), _jsxs("span", { className: "w-16 text-right text-muted-foreground text-xs tabular-nums", children: [r.calls, " calls"] }), _jsx("span", { className: "w-16 text-right tabular-nums", children: usd(r.usd) })] }, `${r.label}:${r.payer}`))) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: "Nothing spent this month." }))] }));
|
|
265
228
|
} }));
|
|
266
229
|
}
|
|
230
|
+
// ---------------------------------------------------------------- allowance
|
|
231
|
+
const day = (at) => new Date(at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
232
|
+
function RequestBadge({ r }) {
|
|
233
|
+
if (r.status === 'open')
|
|
234
|
+
return _jsx(Badge, { variant: "outline", children: "Waiting" });
|
|
235
|
+
if (r.status === 'granted')
|
|
236
|
+
return _jsx(Badge, { variant: "secondary", children: "Granted" });
|
|
237
|
+
return _jsx(Badge, { variant: "outline", children: "Declined" });
|
|
238
|
+
}
|
|
239
|
+
/** A person's allowance on the shared keys, their newest request and Request more. */
|
|
240
|
+
function Allowance({ client }) {
|
|
241
|
+
const { apply, busy } = useApply(client);
|
|
242
|
+
const [amount, setAmount] = useState('');
|
|
243
|
+
const [why, setWhy] = useState('');
|
|
244
|
+
const whyId = useId();
|
|
245
|
+
return (_jsx(WithStatus, { client: client, children: (s) => {
|
|
246
|
+
const a = s.allowance;
|
|
247
|
+
const requestMore = client.requestMore;
|
|
248
|
+
if (!a || !requestMore)
|
|
249
|
+
return _jsx("p", { className: "text-muted-foreground text-sm", children: "This app sets no allowance." });
|
|
250
|
+
const n = (x) => allowanceAmount(a.unit, x);
|
|
251
|
+
const r = a.request;
|
|
252
|
+
const pick = amount || String(a.amounts[0] ?? '');
|
|
253
|
+
const issues = [
|
|
254
|
+
...(pick ? [] : [{ where: 'Amount', message: 'Pick how much to ask for' }]),
|
|
255
|
+
...(why.trim()
|
|
256
|
+
? []
|
|
257
|
+
: [{ where: 'What for', message: 'Say what it is for', target: whyId }]),
|
|
258
|
+
];
|
|
259
|
+
const ask = () => void apply('request', () => requestMore(Number(pick), why.trim()), 'Request sent').then((ok) => ok && setWhy(''));
|
|
260
|
+
return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex flex-col gap-2 border-b pb-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "font-semibold text-2xl tabular-nums", children: n(a.left) }), _jsxs("span", { className: "text-muted-foreground text-sm", children: ["left of ", n(a.total), a.period === 'month' ? ' this month' : ''] })] }), _jsx(ShareMeter, { value: a.total > 0 ? a.used / a.total : 1, wide: true })] }), r && (_jsx(Row, { label: `Asked for ${n(r.amount)} · ${day(r.at)}`, info: r.why || undefined, children: _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [r.status === 'granted' && r.granted !== undefined && (_jsxs("span", { className: "tabular-nums", children: ["+", n(r.granted)] })), _jsx(RequestBadge, { r: r })] }) })), r?.status === 'open' ? (_jsx("p", { className: "pt-3 text-muted-foreground text-sm", children: "You can ask again once this one is decided." })) : (_jsxs("div", { className: "flex flex-col gap-2 pt-3", children: [_jsx(Input, { id: whyId, "aria-label": "What for", placeholder: "What it is for", className: "h-8 w-full", maxLength: 500, value: why, onChange: (e) => setWhy(e.target.value) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(LumenSelector, { label: "Amount", size: "sm", className: "w-40", value: pick, options: a.amounts.map((x) => ({ value: String(x), label: n(x) })), onChange: (v) => setAmount(v) }), _jsx(SaveButton, { issues: issues, label: "Request more", icon: null, busy: busy === 'request', onSave: ask })] })] }))] }));
|
|
261
|
+
} }));
|
|
262
|
+
}
|
|
263
|
+
/** One request, opened in place to grant (any amount) or decline. */
|
|
264
|
+
function RequestRow({ client, r, n, onDone, }) {
|
|
265
|
+
const [text, setText] = useState(String(r.amount));
|
|
266
|
+
const [busy, setBusy] = useState(null);
|
|
267
|
+
const amountId = useId();
|
|
268
|
+
const value = Number(text);
|
|
269
|
+
const decide = async (action) => {
|
|
270
|
+
if (!client.decideRequest)
|
|
271
|
+
return;
|
|
272
|
+
setBusy(action);
|
|
273
|
+
try {
|
|
274
|
+
await client.decideRequest(r.id, action, action === 'grant' ? value : undefined);
|
|
275
|
+
lumenToast.success(action === 'grant' ? `Granted ${n(value)}` : 'Declined');
|
|
276
|
+
onDone();
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
lumenToast.error(errorText(e));
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
setBusy(null);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const issues = Number.isFinite(value) && value > 0
|
|
286
|
+
? []
|
|
287
|
+
: [{ where: 'Grant', message: 'Grant more than 0', target: amountId }];
|
|
288
|
+
return (_jsx(LumenExpandRow, { id: r.id, name: r.who, meta: `${n(r.amount)} · ${day(r.at)}`, badge: _jsx(RequestBadge, { r: r }), children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsx("p", { className: "whitespace-pre-wrap", children: r.why || 'No reason given.' }), r.status === 'open' ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Input, { id: amountId, "aria-label": "Grant", inputMode: "decimal", className: "h-8 w-24 text-right tabular-nums", value: text, onChange: (e) => setText(e.target.value) }), _jsx(SaveButton, { issues: issues, label: "Grant", busy: busy === 'grant', onSave: () => void decide('grant') }), _jsx(Button, { size: "sm", variant: "outline", disabled: !!busy, onClick: () => void decide('decline'), children: "Decline" })] })) : (_jsxs("p", { className: "text-muted-foreground", children: [r.status === 'granted' ? `Granted ${n(r.granted ?? r.amount)}` : 'Declined', r.decided_by ? ` by ${r.decided_by}` : '', r.decided_at ? ` · ${day(r.decided_at)}` : ''] }))] }) }));
|
|
289
|
+
}
|
|
290
|
+
/** Approvers: every request for more allowance, open ones first. */
|
|
291
|
+
function AllowanceRequests({ client }) {
|
|
292
|
+
const qc = useQueryClient();
|
|
293
|
+
const key = [...assistantStatusKey(client), 'allowance-requests'];
|
|
294
|
+
const q = useQuery({
|
|
295
|
+
queryKey: key,
|
|
296
|
+
queryFn: () => client.allowanceRequests?.() ?? Promise.resolve([]),
|
|
297
|
+
enabled: !!client.allowanceRequests,
|
|
298
|
+
});
|
|
299
|
+
return (_jsx(WithStatus, { client: client, children: (s) => {
|
|
300
|
+
const a = s.allowance;
|
|
301
|
+
if (!a || !client.allowanceRequests)
|
|
302
|
+
return _jsx("p", { className: "text-muted-foreground text-sm", children: "This app sets no allowance." });
|
|
303
|
+
if (!a.approver)
|
|
304
|
+
return _jsx("p", { className: "text-muted-foreground text-sm", children: "Only an approver can see these." });
|
|
305
|
+
if (!q.data)
|
|
306
|
+
return (_jsx("p", { className: "text-muted-foreground text-sm", children: q.isError ? errorText(q.error) : 'Loading…' }));
|
|
307
|
+
if (!q.data.length)
|
|
308
|
+
return _jsx("p", { className: "text-muted-foreground text-sm", children: "No requests yet." });
|
|
309
|
+
const rows = [...q.data].sort((x, y) => Number(y.status === 'open') - Number(x.status === 'open') || y.at - x.at);
|
|
310
|
+
const done = () => {
|
|
311
|
+
void qc.invalidateQueries({ queryKey: assistantStatusKey(client) });
|
|
312
|
+
};
|
|
313
|
+
return (_jsx(LumenExpandList, { children: rows.map((r) => (_jsx(RequestRow, { client: client, r: r, n: (x) => allowanceAmount(a.unit, x), onDone: done }, r.id))) }));
|
|
314
|
+
} }));
|
|
315
|
+
}
|
|
267
316
|
/**
|
|
268
317
|
* The admin's section: the app's shared keys and each person's monthly allowance on them. Put it
|
|
269
318
|
* where the app keeps its admin settings, shown only to admins.
|
|
270
319
|
*/
|
|
271
|
-
export function assistantAdminSections({ client, }) {
|
|
320
|
+
export function assistantAdminSections({ client, allowance = false, }) {
|
|
272
321
|
return [
|
|
273
322
|
{
|
|
274
323
|
id: 'assistant-shared-keys',
|
|
@@ -277,13 +326,24 @@ export function assistantAdminSections({ client, }) {
|
|
|
277
326
|
description: "Keys everyone's answers run on, and the allowance",
|
|
278
327
|
content: _jsx(SharedKeys, { client: client }),
|
|
279
328
|
},
|
|
329
|
+
...(allowance
|
|
330
|
+
? [
|
|
331
|
+
{
|
|
332
|
+
id: 'assistant-allowance-requests',
|
|
333
|
+
title: 'Allowance requests',
|
|
334
|
+
icon: InboxIcon,
|
|
335
|
+
description: 'Grant or decline asks for more allowance',
|
|
336
|
+
content: _jsx(AllowanceRequests, { client: client }),
|
|
337
|
+
},
|
|
338
|
+
]
|
|
339
|
+
: []),
|
|
280
340
|
];
|
|
281
341
|
}
|
|
282
342
|
/**
|
|
283
343
|
* The assistant's Settings sections: the assistant, then Model providers, Models, Tools, Web and
|
|
284
344
|
* Spend under it. Pass them to LumenShell's `shared` (or an app's own `settings`).
|
|
285
345
|
*/
|
|
286
|
-
export function assistantSettingsSections({ client, name, web = true, }) {
|
|
346
|
+
export function assistantSettingsSections({ client, name, web = true, allowance = false, }) {
|
|
287
347
|
return [
|
|
288
348
|
{
|
|
289
349
|
id: 'assistant',
|
|
@@ -336,5 +396,17 @@ export function assistantSettingsSections({ client, name, web = true, }) {
|
|
|
336
396
|
description: 'What answers cost this month',
|
|
337
397
|
content: _jsx(Spend, { client: client }),
|
|
338
398
|
},
|
|
399
|
+
...(allowance
|
|
400
|
+
? [
|
|
401
|
+
{
|
|
402
|
+
id: 'assistant-allowance',
|
|
403
|
+
title: 'Allowance',
|
|
404
|
+
icon: CoinsIcon,
|
|
405
|
+
sub: true,
|
|
406
|
+
description: 'What is left on the shared keys, and Request more',
|
|
407
|
+
content: _jsx(Allowance, { client: client }),
|
|
408
|
+
},
|
|
409
|
+
]
|
|
410
|
+
: []),
|
|
339
411
|
];
|
|
340
412
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { KeyState } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
|
+
export interface LumenChatgptSigninProps {
|
|
3
|
+
/** The person's key state: signed in when `via` is `chatgpt`, `hint` the account's email. */
|
|
4
|
+
state: KeyState;
|
|
5
|
+
/** Starts a sign-in on the server: the address to open and the one it ends on. */
|
|
6
|
+
start: () => Promise<{
|
|
7
|
+
url: string;
|
|
8
|
+
redirect: string;
|
|
9
|
+
}>;
|
|
10
|
+
/** Finishes it from the pasted address; true once kept (the app says why when it is not). */
|
|
11
|
+
finish: (url: string) => Promise<boolean>;
|
|
12
|
+
/** Signs out (drops the sealed tokens). */
|
|
13
|
+
signOut: () => Promise<unknown>;
|
|
14
|
+
/** One free call that says whether the sign-in still works; the app refreshes its data after. */
|
|
15
|
+
test: () => Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
/** Sign in with ChatGPT: opens in a new tab, finished by pasting back where it ended. */
|
|
18
|
+
export declare function LumenChatgptSignin({ state, start, finish, signOut, test, }: LumenChatgptSigninProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExternalLinkIcon, KeyRoundIcon, Trash2Icon } from 'lucide-react';
|
|
3
|
+
import { useId, useState } from 'react';
|
|
4
|
+
import { SaveButton } from './form-issues.js';
|
|
5
|
+
import { IconButton } from './icon-button.js';
|
|
6
|
+
import { InfoLabel } from './info-tip.js';
|
|
7
|
+
import { KeyTestLine, TestBadge, errorText } from './integrations.js';
|
|
8
|
+
import { lumenToast } from './lumen-toast.js';
|
|
9
|
+
import { Badge } from './ui/badge.js';
|
|
10
|
+
import { Input } from './ui/input.js';
|
|
11
|
+
import { Label } from './ui/label.js';
|
|
12
|
+
const testOf = (k) => k.test
|
|
13
|
+
? {
|
|
14
|
+
at: new Date(k.test.at).toISOString(),
|
|
15
|
+
ok: k.test.ok,
|
|
16
|
+
detail: k.test.detail,
|
|
17
|
+
error: k.test.ok ? null : k.test.detail,
|
|
18
|
+
}
|
|
19
|
+
: null;
|
|
20
|
+
/** Sign in with ChatGPT: opens in a new tab, finished by pasting back where it ended. */
|
|
21
|
+
export function LumenChatgptSignin({ state, start, finish, signOut, test, }) {
|
|
22
|
+
const id = useId();
|
|
23
|
+
const [started, setStarted] = useState(null);
|
|
24
|
+
const [pasted, setPasted] = useState('');
|
|
25
|
+
const [busy, setBusy] = useState(false);
|
|
26
|
+
const signedIn = state.via === 'chatgpt';
|
|
27
|
+
const open = async () => {
|
|
28
|
+
// Open the tab inside the click, or the browser blocks it; point it once the address is known.
|
|
29
|
+
const tab = window.open('about:blank', '_blank');
|
|
30
|
+
if (tab)
|
|
31
|
+
tab.opener = null;
|
|
32
|
+
try {
|
|
33
|
+
const { url, redirect } = await start();
|
|
34
|
+
if (tab)
|
|
35
|
+
tab.location.href = url;
|
|
36
|
+
else
|
|
37
|
+
window.open(url, '_blank', 'noopener');
|
|
38
|
+
setStarted(redirect);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
tab?.close();
|
|
42
|
+
lumenToast.error(errorText(e));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const done = async () => {
|
|
46
|
+
setBusy(true);
|
|
47
|
+
if (await finish(pasted.trim())) {
|
|
48
|
+
setPasted('');
|
|
49
|
+
setStarted(null);
|
|
50
|
+
}
|
|
51
|
+
setBusy(false);
|
|
52
|
+
};
|
|
53
|
+
const issues = [];
|
|
54
|
+
if (pasted.trim() && started && !pasted.trim().startsWith(started.split('/auth')[0] ?? ''))
|
|
55
|
+
issues.push({ where: 'Address', message: `It starts with ${started}`, target: id });
|
|
56
|
+
const paste = started && (_jsx(SigninPaste, { id: id, redirect: started, pasted: pasted, setPasted: setPasted, issues: issues, busy: busy, finish: done }));
|
|
57
|
+
if (signedIn)
|
|
58
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 border-b py-3 last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex-1 font-medium text-sm", children: "Your ChatGPT plan" }), _jsx("span", { className: "text-muted-foreground text-xs", children: state.hint }), state.test && _jsx(TestBadge, { test: state.test })] }), _jsx(KeyTestLine, { test: testOf(state), run: test }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(IconButton, { icon: Trash2Icon, label: "Sign out of ChatGPT", variant: "outline", onClick: () => void signOut() }), _jsx(SaveButton, { issues: [], icon: KeyRoundIcon, label: "Sign in again", onSave: open })] }), paste] }));
|
|
59
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 border-b py-3 last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(InfoLabel, { className: "flex-1 font-medium text-sm", info: "Run OpenAI models on your own ChatGPT plan instead of an API key.", children: "Your ChatGPT plan" }), _jsx(Badge, { variant: "outline", children: "Not signed in" })] }), _jsx("div", { className: "flex justify-end", children: _jsx(SaveButton, { issues: [], icon: ExternalLinkIcon, label: started ? 'Open the sign-in again' : 'Sign in with ChatGPT', onSave: open }) }), paste] }));
|
|
60
|
+
}
|
|
61
|
+
/** Where the sign-in tab ends, and the box its address is pasted into. */
|
|
62
|
+
function SigninPaste({ id, redirect, pasted, setPasted, issues, busy, finish, }) {
|
|
63
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: id, children: _jsx(InfoLabel, { info: `The sign-in ends on ${redirect}, a page that does not load. That is expected: copy the whole address from that tab's address bar.`, children: "Paste the address the sign-in ended on" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { id: id, autoComplete: "off", placeholder: `${redirect}?code=…`, className: "font-code text-xs", value: pasted, onChange: (e) => setPasted(e.target.value) }), _jsx(SaveButton, { issues: issues, busy: busy, disabled: !pasted.trim(), label: "Finish", onSave: finish })] })] }));
|
|
64
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { type Crumb, IconButton, type IconButtonProps, Page, PageBody, PageHeade
|
|
|
49
49
|
export { DEFAULT_PREFS, deviceTimeZone, type Prefs, TIME_ZONES, type Theme, type TimeZoneId, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, ASSISTANT_WIDTH, } from './prefs.js';
|
|
50
50
|
export { KIT, type KitConfig, configureLumen } from './kit-config.js';
|
|
51
51
|
export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, type KeyTestResult, TestBadge, errorText, useClock, } from './integrations.js';
|
|
52
|
+
export { LumenChatgptSignin, type LumenChatgptSigninProps } from './chatgpt-signin.js';
|
|
52
53
|
export { LumenExpandList, LumenExpandRow } from './expand-list.js';
|
|
53
54
|
export { LumenPager } from './pager.js';
|
|
54
55
|
export { createDeskQueryClient } from './query-client.js';
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ export { IconButton, Page, PageBody, PageHeader, } from './page.js';
|
|
|
56
56
|
export { DEFAULT_PREFS, deviceTimeZone, TIME_ZONES, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, ASSISTANT_WIDTH, } from './prefs.js';
|
|
57
57
|
export { KIT, configureLumen } from './kit-config.js';
|
|
58
58
|
export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, TestBadge, errorText, useClock, } from './integrations.js';
|
|
59
|
+
export { LumenChatgptSignin } from './chatgpt-signin.js';
|
|
59
60
|
export { LumenExpandList, LumenExpandRow } from './expand-list.js';
|
|
60
61
|
export { LumenPager } from './pager.js';
|
|
61
62
|
export { createDeskQueryClient } from './query-client.js';
|
package/dist/shell.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ interface ShellContext {
|
|
|
21
21
|
headerActions: ReactNode;
|
|
22
22
|
}
|
|
23
23
|
export declare function useShell(): ShellContext;
|
|
24
|
+
/** Opens Settings at a section, or null outside a shell (a preview). */
|
|
25
|
+
export declare function useOpenSettings(): ShellContext['openSettings'] | null;
|
|
24
26
|
/** The app this page renders in, or null outside a shell (a preview). */
|
|
25
27
|
export declare function useApp(): {
|
|
26
28
|
id: string;
|
package/dist/shell.js
CHANGED
|
@@ -27,6 +27,10 @@ export function useShell() {
|
|
|
27
27
|
throw new Error('useShell must be used inside <LumenShell>.');
|
|
28
28
|
return c;
|
|
29
29
|
}
|
|
30
|
+
/** Opens Settings at a section, or null outside a shell (a preview). */
|
|
31
|
+
export function useOpenSettings() {
|
|
32
|
+
return useContext(Ctx)?.openSettings ?? null;
|
|
33
|
+
}
|
|
30
34
|
/** The app this page renders in, or null outside a shell (a preview). */
|
|
31
35
|
export function useApp() {
|
|
32
36
|
const c = useContext(Ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.84",
|
|
4
4
|
"description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@codemirror/lang-javascript": "6.2.5",
|
|
50
50
|
"@codemirror/lang-json": "6.0.2",
|
|
51
51
|
"@codemirror/lang-sql": "6.10.0",
|
|
52
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.84",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|