@columenlabs/lumen-ui 0.1.188 → 0.1.190
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/connection.d.ts +9 -3
- package/dist/connection.js +82 -69
- package/dist/frame-script.d.ts +4 -1
- package/dist/frame-script.js +11 -2
- package/dist/index.d.ts +1 -1
- package/dist/mcp-settings.js +3 -6
- package/dist/schedules.js +8 -6
- package/dist/setting-parts.d.ts +6 -2
- package/dist/setting-parts.js +3 -3
- package/dist/workspaces.d.ts +15 -0
- package/dist/workspaces.js +47 -5
- package/package.json +2 -2
package/dist/connection.d.ts
CHANGED
|
@@ -101,10 +101,11 @@ export type ConnectionStatus = 'connected' | 'untested' | 'expired' | 'failed' |
|
|
|
101
101
|
export declare function connectionStatus(way: ConnectionWay | undefined): ConnectionStatus;
|
|
102
102
|
/** The way in use: the picked one when it is set up, else the first set up. */
|
|
103
103
|
export declare function activeWay(ways: ConnectionWay[], use?: string): ConnectionWay | undefined;
|
|
104
|
-
/** The row's pill for a status. */
|
|
105
|
-
export declare function ConnectionBadge({ status, signin, }: {
|
|
104
|
+
/** The row's pill for a status: Works, Failed, Not tested or Off; hover says to sign in again. */
|
|
105
|
+
export declare function ConnectionBadge({ status, signin, title, }: {
|
|
106
106
|
status: ConnectionStatus;
|
|
107
107
|
signin?: boolean;
|
|
108
|
+
title?: string;
|
|
108
109
|
}): import("react").JSX.Element;
|
|
109
110
|
/** A connection's mark: an icon or a letter on a tile. */
|
|
110
111
|
export declare function ConnectionLogo({ icon: Icon, text, }: {
|
|
@@ -113,5 +114,10 @@ export declare function ConnectionLogo({ icon: Icon, text, }: {
|
|
|
113
114
|
}>;
|
|
114
115
|
text?: string;
|
|
115
116
|
}): import("react").JSX.Element;
|
|
116
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* One connection on the settings slots: the header carries its name, the way in use and one
|
|
119
|
+
* status pill; each way is a row (its label beside its value, Replace or Sign in again inside);
|
|
120
|
+
* with one way the action bar holds its last test, Test and Remove, with several each row ends in
|
|
121
|
+
* its own. `lines` are rows of their own; `children` the app's settings below them.
|
|
122
|
+
*/
|
|
117
123
|
export declare function LumenConnection({ id, name, meta, logo, ways, use, onUse, lines, defaultOpen, children, }: LumenConnectionProps): import("react").JSX.Element;
|
package/dist/connection.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// One connection, one look on every app: an AI provider, an email relay, a Google account, a
|
|
3
|
-
// connector.
|
|
4
|
-
// connect (a pasted key or token, a sign-in that ends on
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// `children`
|
|
9
|
-
import {
|
|
3
|
+
// connector. Built on the settings slots (docs/parts/settings.md): a LumenExpandRow whose header has
|
|
4
|
+
// one status pill; inside, each way to connect is a row (a pasted key or token, a sign-in that ends on
|
|
5
|
+
// an address the person pastes back, or the app's own form) with Replace or Sign in again inside its
|
|
6
|
+
// value. Several ways add a Use row, so a person who has both an API key and a plan sign-in picks which
|
|
7
|
+
// one runs, and each way's row ends in its own status, Test and Remove; one way puts its last test,
|
|
8
|
+
// Test and Remove in the action bar. `lines` and `children` are the app's own rows below the ways.
|
|
9
|
+
import { ExternalLinkIcon, LogInIcon, PlusIcon, RefreshCwIcon, Trash2Icon } from 'lucide-react';
|
|
10
10
|
import { useId, useState } from 'react';
|
|
11
11
|
import { LumenConfirm } from './confirm.js';
|
|
12
12
|
import { LumenExpandRow } from './expand-list.js';
|
|
13
13
|
import { SaveButton } from './form-issues.js';
|
|
14
14
|
import { IconButton } from './icon-button.js';
|
|
15
|
-
import {
|
|
16
|
-
import { ErrorLine, errorText, useClock } from './integrations.js';
|
|
15
|
+
import { ErrorLine, errorText } from './integrations.js';
|
|
17
16
|
import { cn } from './lib/utils.js';
|
|
18
17
|
import { lumenToast } from './lumen-toast.js';
|
|
19
|
-
import {
|
|
18
|
+
import { SettingActions, SettingField, SettingStatus, SettingValue, } from './setting-parts.js';
|
|
20
19
|
import { Button } from './ui/button.js';
|
|
21
20
|
import { Input } from './ui/input.js';
|
|
22
21
|
import { Label } from './ui/label.js';
|
|
@@ -35,44 +34,54 @@ export function connectionStatus(way) {
|
|
|
35
34
|
export function activeWay(ways, use) {
|
|
36
35
|
return ways.find((w) => w.id === use && w.set) ?? ways.find((w) => w.set);
|
|
37
36
|
}
|
|
38
|
-
/** The row's pill for a status. */
|
|
39
|
-
export function ConnectionBadge({ status, signin, }) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return (_jsxs(Badge, { variant: "outline", className: "text-loss", children: [_jsx(AlertTriangleIcon, {}), "Failed"] }));
|
|
49
|
-
default:
|
|
50
|
-
return (_jsx(Badge, { variant: "outline", className: "border-dashed text-muted-foreground", children: "Not connected" }));
|
|
51
|
-
}
|
|
37
|
+
/** The row's pill for a status: Works, Failed, Not tested or Off; hover says to sign in again. */
|
|
38
|
+
export function ConnectionBadge({ status, signin, title, }) {
|
|
39
|
+
const health = status === 'connected'
|
|
40
|
+
? 'works'
|
|
41
|
+
: status === 'untested'
|
|
42
|
+
? 'untested'
|
|
43
|
+
: status === 'off'
|
|
44
|
+
? 'off'
|
|
45
|
+
: 'failed';
|
|
46
|
+
return (_jsx(SettingStatus, { health: health, title: title ?? (status === 'expired' ? (signin ? 'Sign in again' : 'Key expired') : undefined) }));
|
|
52
47
|
}
|
|
53
48
|
/** A connection's mark: an icon or a letter on a tile. */
|
|
54
49
|
export function ConnectionLogo({ icon: Icon, text, }) {
|
|
55
50
|
return (_jsx("span", { "aria-hidden": true, className: "flex size-8 shrink-0 items-center justify-center rounded-md border bg-muted font-semibold text-muted-foreground text-sm", children: Icon ? _jsx(Icon, { className: "size-4" }) : text }));
|
|
56
51
|
}
|
|
57
|
-
/** `just now`, `5 min ago`, `3 h ago`, `2 d ago`. */
|
|
58
|
-
function ago(ms) {
|
|
59
|
-
const s = Math.max(0, (Date.now() - ms) / 1000);
|
|
60
|
-
if (s < 60)
|
|
61
|
-
return 'just now';
|
|
62
|
-
if (s < 3600)
|
|
63
|
-
return `${Math.floor(s / 60)} min ago`;
|
|
64
|
-
if (s < 86400)
|
|
65
|
-
return `${Math.floor(s / 3600)} h ago`;
|
|
66
|
-
return `${Math.floor(s / 86400)} d ago`;
|
|
67
|
-
}
|
|
68
52
|
/** `Service token` → `service token`; `API key` stays. */
|
|
69
53
|
const lower = (label) => /^[A-Z][a-z]/.test(label) ? label.charAt(0).toLowerCase() + label.slice(1) : label;
|
|
70
|
-
const
|
|
71
|
-
/**
|
|
54
|
+
const toIso = (at) => new Date(typeof at === 'number' ? at : Date.parse(at)).toISOString();
|
|
55
|
+
/** A way's last test as the action bar reads it. */
|
|
56
|
+
const lastTest = (t) => t
|
|
57
|
+
? {
|
|
58
|
+
at: toIso(t.at),
|
|
59
|
+
ok: t.ok,
|
|
60
|
+
detail: t.detail,
|
|
61
|
+
error: t.ok ? t.error : (t.error ?? (t.expired ? 'Expired' : null)),
|
|
62
|
+
}
|
|
63
|
+
: null;
|
|
64
|
+
/** The confirm that removes a way. */
|
|
65
|
+
const removeOf = (way) => !way.readOnly && !way.note && way.onRemove
|
|
66
|
+
? {
|
|
67
|
+
label: way.removeLabel ?? `Remove ${way.label}`,
|
|
68
|
+
title: way.removeTitle ?? `Remove ${lower(way.label)}?`,
|
|
69
|
+
description: way.removeText ?? 'You can add it again any time.',
|
|
70
|
+
action: way.removeLabel ?? 'Remove',
|
|
71
|
+
run: () => way.onRemove?.(),
|
|
72
|
+
}
|
|
73
|
+
: undefined;
|
|
74
|
+
/**
|
|
75
|
+
* One connection on the settings slots: the header carries its name, the way in use and one
|
|
76
|
+
* status pill; each way is a row (its label beside its value, Replace or Sign in again inside);
|
|
77
|
+
* with one way the action bar holds its last test, Test and Remove, with several each row ends in
|
|
78
|
+
* its own. `lines` are rows of their own; `children` the app's settings below them.
|
|
79
|
+
*/
|
|
72
80
|
export function LumenConnection({ id, name, meta, logo, ways, use, onUse, lines, defaultOpen, children, }) {
|
|
73
81
|
const active = activeWay(ways, use);
|
|
74
82
|
const status = connectionStatus(active);
|
|
75
83
|
const pick = ways.length > 1 && !!onUse;
|
|
84
|
+
const solo = ways.length === 1 ? ways[0] : null;
|
|
76
85
|
const [picking, setPicking] = useState(false);
|
|
77
86
|
const choose = async (way) => {
|
|
78
87
|
if (!onUse || way === active?.id)
|
|
@@ -90,49 +99,52 @@ export function LumenConnection({ id, name, meta, logo, ways, use, onUse, lines,
|
|
|
90
99
|
};
|
|
91
100
|
const line = meta ??
|
|
92
101
|
(active ? (_jsxs(_Fragment, { children: [ways.length > 1 && active.label, ways.length > 1 && active.ident ? ' · ' : '', active.ident] })) : ('Not set up'));
|
|
93
|
-
const
|
|
94
|
-
return (_jsxs(LumenExpandRow, { id: id, name: name, meta: line, icon: logo, defaultOpen: defaultOpen,
|
|
102
|
+
const t = active?.test;
|
|
103
|
+
return (_jsxs(LumenExpandRow, { id: id, name: name, meta: line, icon: logo, defaultOpen: defaultOpen, status: _jsx(ConnectionBadge, { status: status, signin: !!active?.signin, title: t ? (t.ok ? (t.detail ?? undefined) : (t.error ?? undefined)) : undefined }), actions: solo?.set && !solo.note && (solo.onTest || removeOf(solo)) ? (_jsx(SettingActions, { test: lastTest(solo.test), onTest: solo.onTest, remove: removeOf(solo) })) : undefined, children: [pick && (_jsx(SettingField, { label: "Use", children: _jsx(RadioGroup, { value: active?.id ?? '', onValueChange: (v) => void choose(v), disabled: picking, "aria-label": `Which way ${typeof name === 'string' ? name : ''} connects`, className: "flex flex-wrap gap-x-4 gap-y-1", children: ways.map((w) => (_jsx(WayChoice, { way: w }, w.id))) }) })), ways.map((w) => (_jsx(WayField, { way: w, solo: !!solo }, w.id))), lines?.map((l) => (_jsx(SettingField, { label: l.label, info: l.info, children: _jsx("span", { className: cn('min-w-0 truncate text-sm', l.mono && 'font-code text-xs'), title: typeof l.value === 'string' ? l.value : undefined, children: l.value }) }, l.label))), children] }));
|
|
95
104
|
}
|
|
96
|
-
/** One way
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
105
|
+
/** One way in the Use picker. */
|
|
106
|
+
function WayChoice({ way }) {
|
|
107
|
+
const rid = useId();
|
|
108
|
+
return (_jsxs("span", { className: "flex min-h-8 items-center gap-2", children: [_jsx(RadioGroupItem, { id: rid, value: way.id, disabled: !way.set, "aria-label": `Use ${way.label}` }), _jsx(Label, { htmlFor: rid, className: cn('font-normal', !way.set && 'text-muted-foreground'), children: way.label })] }));
|
|
109
|
+
}
|
|
110
|
+
/** One way as a row: its label, then its value with Replace inside, or Add; its form when open. */
|
|
111
|
+
function WayField({ way, solo }) {
|
|
100
112
|
const [editing, setEditing] = useState(false);
|
|
101
|
-
const
|
|
102
|
-
const [error, setError] = useState(null);
|
|
103
|
-
const Icon = way.icon;
|
|
104
|
-
const status = connectionStatus(way);
|
|
105
|
-
const failing = status === 'failed' || status === 'expired';
|
|
113
|
+
const close = () => setEditing(false);
|
|
106
114
|
const editable = !way.readOnly && !way.note;
|
|
107
115
|
const hasForm = !!(way.form || way.fields?.length || way.signin);
|
|
116
|
+
if (editing)
|
|
117
|
+
return way.form ? (_jsx("div", { className: "py-2", children: way.form(close) })) : way.signin ? (_jsx(SigninForm, { way: way, close: close })) : (_jsx(FieldsForm, { way: way, close: close }));
|
|
118
|
+
const Icon = way.signin ? LogInIcon : PlusIcon;
|
|
119
|
+
const addLabel = way.addLabel ??
|
|
120
|
+
(way.signin
|
|
121
|
+
? way.signin.provider
|
|
122
|
+
? `Sign in with ${way.signin.provider}`
|
|
123
|
+
: 'Sign in'
|
|
124
|
+
: `Add ${lower(way.label)}`);
|
|
125
|
+
const control = way.note ? (_jsx("span", { className: "min-w-0 truncate text-muted-foreground text-sm", children: way.note })) : way.set ? ((typeof way.ident === 'string' || way.ident == null) && editable && hasForm ? (_jsx(SettingValue, { value: way.ident ?? 'Set', label: way.label, mono: true, onReplace: () => setEditing(true), replaceLabel: way.signin ? 'Sign in again' : 'Replace', replaceIcon: way.signin ? LogInIcon : undefined })) : (_jsx("span", { className: cn('min-w-0 truncate text-sm', typeof way.ident === 'string' && 'font-code text-xs'), title: typeof way.ident === 'string' ? way.ident : undefined, children: way.ident ?? 'Set' }))) : editable && hasForm ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setEditing(true), children: [_jsx(Icon, {}), addLabel] })) : (_jsx("span", { className: "text-muted-foreground text-sm", children: "Not set up" }));
|
|
126
|
+
return (_jsx(SettingField, { label: way.label, info: way.info, stack: !solo, end: !solo && way.set && !way.note ? _jsx(WayEnd, { way: way }) : undefined, children: control }));
|
|
127
|
+
}
|
|
128
|
+
/** With several ways, each set-up way's row ends in its status, Test and Remove. */
|
|
129
|
+
function WayEnd({ way }) {
|
|
130
|
+
const [testing, setTesting] = useState(false);
|
|
131
|
+
const remove = removeOf(way);
|
|
132
|
+
const t = way.test;
|
|
108
133
|
const test = async () => {
|
|
109
134
|
if (!way.onTest)
|
|
110
135
|
return;
|
|
111
136
|
setTesting(true);
|
|
112
|
-
setError(null);
|
|
113
137
|
try {
|
|
114
138
|
await way.onTest();
|
|
115
139
|
}
|
|
116
140
|
catch (e) {
|
|
117
|
-
|
|
141
|
+
lumenToast.error(errorText(e));
|
|
118
142
|
}
|
|
119
143
|
finally {
|
|
120
144
|
setTesting(false);
|
|
121
145
|
}
|
|
122
146
|
};
|
|
123
|
-
|
|
124
|
-
const fixLabel = way.signin ? 'Sign in again' : 'Replace';
|
|
125
|
-
const addLabel = way.addLabel ??
|
|
126
|
-
(way.signin
|
|
127
|
-
? way.signin.provider
|
|
128
|
-
? `Sign in with ${way.signin.provider}`
|
|
129
|
-
: 'Sign in'
|
|
130
|
-
: `Add ${lower(way.label)}`);
|
|
131
|
-
const t = way.test;
|
|
132
|
-
return (_jsxs("div", { className: cn('grid grid-cols-1 gap-2 rounded-md border px-3 py-2.5', inUse && 'border-primary/60 bg-primary/5'), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [pick && (_jsx(RadioGroupItem, { id: radioId, value: way.id, disabled: !way.set, "aria-label": `Use ${way.label}` })), Icon && _jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "min-w-0 flex-1 basis-40", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [pick ? (_jsx(Label, { htmlFor: radioId, className: "whitespace-nowrap font-medium", children: _jsx(InfoLabel, { info: way.info, children: way.label }) })) : (_jsx(InfoLabel, { className: "whitespace-nowrap font-medium text-sm", info: way.info, children: way.label })), inUse && (_jsx(Badge, { variant: "outline", className: "h-5 shrink-0 border-primary/40 text-primary", children: "In use" }))] }), _jsx("span", { className: "flex min-w-0 items-center gap-1 text-muted-foreground text-xs", children: way.set ? (_jsxs(_Fragment, { children: [way.ident && (_jsx("span", { className: "truncate", title: typeof way.ident === 'string' ? way.ident : undefined, children: way.ident })), way.ident && _jsx("span", { "aria-hidden": true, children: "\u00B7" }), t ? (_jsxs("span", { className: cn('flex shrink-0 items-center gap-1', t.ok ? 'text-gain' : 'text-loss'), title: `Tested ${clock(new Date(toMs(t.at)).toISOString())}${t.ok && t.detail ? ` · ${t.detail}` : ''}`, children: [t.ok ? (_jsx(CheckIcon, { className: "size-3" })) : (_jsx(AlertTriangleIcon, { className: "size-3" })), t.ok ? 'Works' : t.expired ? 'Expired' : 'Failed', " ", ago(toMs(t.at))] })) : (_jsx("span", { className: "shrink-0", children: "Not tested" }))] })) : (_jsx("span", { children: "Not set up" })) })] }), way.note ? (_jsx("span", { className: "ml-auto shrink-0 text-muted-foreground text-xs", children: way.note })) : way.set ? (_jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: [way.onTest && (_jsx(IconButton, { icon: RefreshCwIcon, label: testing ? 'Testing…' : `Test ${way.label}`, busy: testing, disabled: testing, onClick: () => void test() })), editable && way.onRemove && (_jsx(LumenConfirm, { title: way.removeTitle ?? `Remove ${lower(way.label)}?`, description: way.removeText ?? 'You can add it again any time.', action: way.removeLabel ?? 'Remove', onConfirm: () => way.onRemove?.(), problem: errorText, children: _jsx(IconButton, { icon: Trash2Icon, label: way.removeLabel ?? `Remove ${way.label}` }) })), editable && hasForm && !editing && (_jsxs(Button, { size: "sm", variant: failing ? 'default' : 'outline', onClick: () => setEditing(true), children: [way.signin ? _jsx(LogInIcon, {}) : _jsx(KeyRoundIcon, {}), fixLabel] }))] })) : (editable &&
|
|
133
|
-
hasForm &&
|
|
134
|
-
!editing && (_jsxs(Button, { size: "sm", className: "ml-auto shrink-0", onClick: () => setEditing(true), children: [way.signin ? _jsx(LogInIcon, {}) : _jsx(PlusIcon, {}), addLabel] })))] }), t && !t.ok && t.error && (_jsx("p", { role: "alert", className: "text-xs text-loss", children: t.error })), _jsx(ErrorLine, { error: error }), editing &&
|
|
135
|
-
(way.form ? (way.form(close)) : way.signin ? (_jsx(SigninForm, { way: way, close: close })) : (_jsx(FieldsForm, { way: way, close: close })))] }));
|
|
147
|
+
return (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(ConnectionBadge, { status: connectionStatus(way), signin: !!way.signin, title: t ? (t.ok ? (t.detail ?? undefined) : (t.error ?? undefined)) : undefined }), way.onTest && (_jsx(IconButton, { icon: RefreshCwIcon, label: testing ? 'Testing…' : `Test ${way.label}`, busy: testing, disabled: testing, onClick: () => void test() })), remove && (_jsx(LumenConfirm, { title: remove.title, description: remove.description, action: remove.action, onConfirm: remove.run, problem: errorText, children: _jsx(IconButton, { icon: Trash2Icon, label: remove.label, className: "text-loss" }) }))] }));
|
|
136
148
|
}
|
|
137
149
|
/** Runs a save; false or a throw keeps the form open (a throw says why). */
|
|
138
150
|
async function keep(run, setError) {
|
|
@@ -145,7 +157,11 @@ async function keep(run, setError) {
|
|
|
145
157
|
return false;
|
|
146
158
|
}
|
|
147
159
|
}
|
|
148
|
-
/**
|
|
160
|
+
/** A form's footer: a link on the left, then Cancel and its Save. */
|
|
161
|
+
function FormFooter({ left, children }) {
|
|
162
|
+
return (_jsxs("div", { className: "mt-1 flex min-h-10 min-w-0 items-center gap-2 border-t pt-2 text-sm", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: left }), children] }));
|
|
163
|
+
}
|
|
164
|
+
/** The way's fields, a row each, checked before Test and save. */
|
|
149
165
|
function FieldsForm({ way, close }) {
|
|
150
166
|
const ids = useId();
|
|
151
167
|
const fields = way.fields ?? [];
|
|
@@ -173,7 +189,7 @@ function FieldsForm({ way, close }) {
|
|
|
173
189
|
close();
|
|
174
190
|
setBusy(false);
|
|
175
191
|
};
|
|
176
|
-
return (_jsxs("div", { className: "
|
|
192
|
+
return (_jsxs("div", { className: "flex flex-col", children: [fields.map((f) => (_jsx(SettingField, { label: fields.length > 1 ? f.label : way.label, info: fields.length > 1 ? f.info : (f.info ?? way.info), htmlFor: `${ids}-${f.id}`, children: _jsx(Input, { id: `${ids}-${f.id}`, type: f.secret ? 'password' : 'text', autoComplete: "off", placeholder: f.placeholder ?? (f.prefix ? `${f.prefix}…` : f.label), className: "h-8 font-code text-xs", value: values[f.id] ?? '', onChange: (e) => setValues((s) => ({ ...s, [f.id]: e.target.value })), onKeyDown: (e) => e.key === 'Enter' && !empty && !issues.length && void save() }) }, f.id))), _jsx(ErrorLine, { error: error }), _jsxs(FormFooter, { left: way.getUrl && (_jsxs("a", { className: "underline", href: way.getUrl, target: "_blank", rel: "noreferrer", children: ["Get ", /^[AEIOU]/i.test(way.label) ? 'an' : 'a', " ", lower(way.label), " \u2197"] })), children: [_jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: close, children: "Cancel" }), _jsx(SaveButton, { issues: issues, busy: busy, disabled: empty, label: "Test and save", onSave: () => void save() })] })] }));
|
|
177
193
|
}
|
|
178
194
|
/** Open the sign-in in a new tab, then paste back the address it ended on. */
|
|
179
195
|
function SigninForm({ way, close }) {
|
|
@@ -222,8 +238,5 @@ function SigninForm({ way, close }) {
|
|
|
222
238
|
close();
|
|
223
239
|
setBusy(false);
|
|
224
240
|
};
|
|
225
|
-
return (_jsxs("
|
|
226
|
-
}
|
|
227
|
-
function Step({ n }) {
|
|
228
|
-
return (_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center rounded-full bg-muted font-medium text-xs tabular-nums", children: n }));
|
|
241
|
+
return (_jsxs("div", { className: "flex flex-col", children: [_jsx(SettingField, { label: way.label, info: way.info, children: _jsxs(Button, { size: "sm", variant: redirect === null ? 'default' : 'outline', onClick: () => void open(), children: [_jsx(ExternalLinkIcon, {}), redirect === null ? 'Open sign-in' : 'Open again'] }) }), _jsx(SettingField, { label: "Address", htmlFor: id, stack: true, info: `The sign-in ends on ${redirect || signin.redirect || 'an address'} that does not load. That is expected: copy the whole address from that tab's address bar and paste it here.`, children: _jsx(Input, { id: id, autoComplete: "off", disabled: redirect === null, placeholder: `${redirect || signin.redirect || 'http://localhost'}…`, className: "h-8 font-code text-xs", value: pasted, onChange: (e) => setPasted(e.target.value) }) }), _jsx(ErrorLine, { error: error }), _jsxs(FormFooter, { children: [_jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: close, children: "Cancel" }), _jsx(SaveButton, { issues: issues, busy: busy, disabled: !p, label: "Finish", onSave: () => void finish() })] })] }));
|
|
229
242
|
}
|
package/dist/frame-script.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import type { FrameRun } from '@columenlabs/lumen-ui-types/frames';
|
|
2
|
+
export declare function FrameScriptDialog({ api, frame, name, runs, open, onOpenChange, }: {
|
|
2
3
|
api: string;
|
|
3
4
|
frame: string;
|
|
4
5
|
name: string;
|
|
6
|
+
/** The last runs, newest first: when, how many rows, how long, who, and why one failed. */
|
|
7
|
+
runs?: FrameRun[];
|
|
5
8
|
open: boolean;
|
|
6
9
|
onOpenChange: (open: boolean) => void;
|
|
7
10
|
}): import("react").JSX.Element;
|
package/dist/frame-script.js
CHANGED
|
@@ -47,7 +47,7 @@ function parseScript(was, text) {
|
|
|
47
47
|
args: (o.args ?? {}),
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
export function FrameScriptDialog({ api, frame, name, open, onOpenChange, }) {
|
|
50
|
+
export function FrameScriptDialog({ api, frame, name, runs, open, onOpenChange, }) {
|
|
51
51
|
const qc = useQueryClient();
|
|
52
52
|
const assistant = useAssistantName();
|
|
53
53
|
const read = useQuery({
|
|
@@ -96,8 +96,17 @@ export function FrameScriptDialog({ api, frame, name, open, onOpenChange, }) {
|
|
|
96
96
|
setError(e.message);
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[min(44rem,calc(100dvh-2rem))] flex-col sm:max-w-4xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Script \u00B7 ", name] }), _jsxs(DialogDescription, { children: ["What ", assistant, " runs to make these rows. Saved only if it runs."] })] }), read.isPending ? (_jsx(Skeleton, { className: "min-h-0 flex-1" })) : read.error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTitle, { children: "Couldn't read the script" }), _jsx(AlertDescription, { children: read.error.message })] })) : !script ? (_jsxs(Alert, { children: [_jsx(AlertTitle, { children: "No script" }), _jsxs(AlertDescription, { children: [assistant, " wrote these rows once: nothing runs to make them."] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-end gap-1", children: [lang && (_jsx(Badge, { variant: "secondary", className: "font-mono", children: lang })), _jsx(IconButton, { icon: WrapTextIcon, label: wrap ? 'Stop wrapping lines' : 'Wrap long lines', pressed: wrap, onClick: () => setWrap(!wrap) })] }), _jsx(LumenCodeEditor, { className: "flex-1", value: text, lang: lang, wrap: wrap, onChange: setText }), error && (_jsxs(Alert, { variant: "destructive", className: "max-h-32 overflow-y-auto", children: [_jsx(AlertTitle, { children: "Didn't save" }), _jsx(AlertDescription, { className: "break-words font-mono text-xs", children: error })] }))] })), _jsxs(DialogFooter, { children: [_jsx(IconButton, { icon: RotateCcwIcon, label: "Undo my changes", disabled: !changed || save.isPending, onClick: () => {
|
|
99
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[min(44rem,calc(100dvh-2rem))] flex-col sm:max-w-4xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Script \u00B7 ", name] }), _jsxs(DialogDescription, { children: ["What ", assistant, " runs to make these rows. Saved only if it runs."] })] }), read.isPending ? (_jsx(Skeleton, { className: "min-h-0 flex-1" })) : read.error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTitle, { children: "Couldn't read the script" }), _jsx(AlertDescription, { children: read.error.message })] })) : !script ? (_jsxs(Alert, { children: [_jsx(AlertTitle, { children: "No script" }), _jsxs(AlertDescription, { children: [assistant, " wrote these rows once: nothing runs to make them."] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-end gap-1", children: [lang && (_jsx(Badge, { variant: "secondary", className: "font-mono", children: lang })), _jsx(IconButton, { icon: WrapTextIcon, label: wrap ? 'Stop wrapping lines' : 'Wrap long lines', pressed: wrap, onClick: () => setWrap(!wrap) })] }), _jsx(LumenCodeEditor, { className: "flex-1", value: text, lang: lang, wrap: wrap, onChange: setText }), runs && runs.length > 0 && _jsx(RunList, { runs: runs, assistant: assistant }), error && (_jsxs(Alert, { variant: "destructive", className: "max-h-32 overflow-y-auto", children: [_jsx(AlertTitle, { children: "Didn't save" }), _jsx(AlertDescription, { className: "break-words font-mono text-xs", children: error })] }))] })), _jsxs(DialogFooter, { children: [_jsx(IconButton, { icon: RotateCcwIcon, label: "Undo my changes", disabled: !changed || save.isPending, onClick: () => {
|
|
100
100
|
setText(original);
|
|
101
101
|
setError(null);
|
|
102
102
|
} }), _jsxs(Button, { disabled: !script || save.isPending, onClick: run, children: [save.isPending ? _jsx(Spinner, {}) : _jsx(PlayIcon, {}), " Run and save"] })] })] }) }));
|
|
103
103
|
}
|
|
104
|
+
/** The frame's last runs, one line each; a failure's reason in its hover. */
|
|
105
|
+
function RunList({ runs, assistant }) {
|
|
106
|
+
return (_jsx("ul", { className: "flex max-h-28 flex-col gap-0.5 overflow-y-auto text-xs text-muted-foreground", children: runs.map((r) => (_jsxs("li", { className: "flex min-w-0 items-center gap-2", title: r.error, children: [_jsx("span", { className: `size-1.5 shrink-0 rounded-full ${r.ok ? 'bg-[var(--meter-ok)]' : 'bg-destructive'}` }), _jsx("time", { className: "shrink-0", dateTime: r.at, children: new Date(r.at).toLocaleString('en-US', {
|
|
107
|
+
month: 'short',
|
|
108
|
+
day: 'numeric',
|
|
109
|
+
hour: 'numeric',
|
|
110
|
+
minute: '2-digit',
|
|
111
|
+
}) }), _jsx("span", { className: "num shrink-0", children: r.ok ? `${r.rows.toLocaleString('en-US')} rows` : 'failed' }), _jsxs("span", { className: "num shrink-0", children: [(r.ms / 1000).toFixed(1), " s"] }), _jsx("span", { className: "min-w-0 truncate", children: r.ok ? `by ${r.by === 'you' ? 'you' : assistant}` : r.error })] }, r.at))) }));
|
|
112
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ export { type AppViewState, useAppView } from './app-views.js';
|
|
|
105
105
|
export { type ScreenMap, type ScreenOutcome, type ScreenStatus, type WorkspaceOnScreen, type AssistantMove, type AssistantMoveHandlers, type AssistantPage, type AssistantPageType, AssistantScreenProvider, useAssistantMoves, useAssistantPages, useAssistantScreen, useAssistantWorkspace, } from './assistant-screen.js';
|
|
106
106
|
export { type AssistantActions, AssistantContext, type AssistantNote, type AssistantRef, type AssistantSlot, useAssistant, AssistantPendingContext, useAssistantName, useAssistantNote, useAssistantPending, } from './assistant-context.js';
|
|
107
107
|
export { WORKSPACES_KEY, frameKey, workspacesListQuery } from './workspace-keys.js';
|
|
108
|
-
export { type WorkspaceApp, type WorkspaceDraftProps, WorkspacePage, type WorkspacesConfig, WorkspacesProvider, WorkspacesSection, } from './workspaces.js';
|
|
108
|
+
export { type WorkspaceApp, type WorkspaceDraftProps, WorkspacePage, type WorkspaceRecipe, type WorkspacesConfig, WorkspacesProvider, WorkspacesSection, } from './workspaces.js';
|
|
109
109
|
export { CHART_INDICATORS, type ChartIndicator, type IndicatorValues, LINE_COLOR, computeIndicators, formulaIndicator, lineColor, resolveIndicator, runFormula, } from './market/indicators.js';
|
|
110
110
|
export { IndicatorPicker } from './market/indicator-picker.js';
|
|
111
111
|
export { type OptionChainField, type OptionChainRow, OptionChainTable, } from './market/option-chain.js';
|
package/dist/mcp-settings.js
CHANGED
|
@@ -7,7 +7,8 @@ import { useState } from 'react';
|
|
|
7
7
|
// common `mcp-remote` bridge on the person's own computer, whose sign-in returns to localhost.
|
|
8
8
|
import { LumenSelector } from './lumen-selector.js';
|
|
9
9
|
import { IconButton } from './page.js';
|
|
10
|
-
import {
|
|
10
|
+
import { SettingField, SettingValue } from './setting-parts.js';
|
|
11
|
+
import { SettingsFooter } from './settings.js';
|
|
11
12
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './ui/input-group.js';
|
|
12
13
|
import { Tabs, TabsList, TabsTrigger } from './ui/tabs.js';
|
|
13
14
|
const CLIENTS = {
|
|
@@ -52,14 +53,10 @@ function ClientSteps({ client, server }) {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
export function McpSettings({ servers, current }) {
|
|
55
|
-
const { copied, copy } = useCopy();
|
|
56
56
|
const [serverId, setServerId] = useState(current ?? servers[0]?.id);
|
|
57
57
|
const [client, setClient] = useState('claude');
|
|
58
58
|
const server = servers.find((d) => d.id === serverId) ?? servers[0];
|
|
59
59
|
if (!server)
|
|
60
60
|
return null;
|
|
61
|
-
return (_jsxs("div", { className: "flex flex-col
|
|
62
|
-
const url = d.url;
|
|
63
|
-
return (_jsx(SettingRow, { label: d.label, description: "MCP address", children: _jsxs(InputGroup, { className: "w-56 sm:w-80", children: [_jsx(InputGroupInput, { readOnly: true, value: url, "aria-label": `${d.label} MCP address`, className: "font-mono text-xs", onFocus: (e) => e.currentTarget.select() }), _jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupButton, { size: "icon-xs", "aria-label": `Copy the ${d.label} address`, onClick: () => copy(url), children: copied === url ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) }) })] }) }, d.id));
|
|
64
|
-
})] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h4", { className: "text-xs font-medium text-muted-foreground", children: "Connect" }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [servers.length > 1 && (_jsx(LumenSelector, { label: "Server", size: "sm", className: "w-40", options: servers.map((d) => ({ value: d.id, label: d.label })), value: server.id, onChange: setServerId })), _jsx(LumenSelector, { label: "App", size: "sm", className: "w-36 md:hidden", options: Object.entries(CLIENTS).map(([k, label]) => ({ value: k, label })), value: client, onChange: (v) => setClient(v) })] }), _jsx(Tabs, { value: client, onValueChange: (v) => setClient(v), className: "max-md:hidden", children: _jsx(TabsList, { children: Object.entries(CLIENTS).map(([k, label]) => (_jsx(TabsTrigger, { value: k, children: label }, k))) }) }), _jsx(ClientSteps, { client: client, server: server })] }), _jsxs("p", { className: "flex items-start gap-1.5 text-xs text-muted-foreground", children: [_jsx(ShieldIcon, { className: "mt-0.5 size-3.5 shrink-0" }), "Every client signs in as you through your browser; there are no API keys. Sign-ins return only to known apps and to http://localhost or http://127.0.0.1 on your own computer."] })] }));
|
|
61
|
+
return (_jsxs("div", { className: "flex flex-col", children: [servers.map((d) => (_jsx(SettingField, { label: d.label, stack: true, children: _jsx(SettingValue, { value: d.url, label: `${d.label} address`, mono: true, copy: true }) }, d.id))), servers.length > 1 && (_jsx(SettingField, { label: "Connect", children: _jsx(LumenSelector, { label: "Server", size: "sm", className: "w-40", options: servers.map((d) => ({ value: d.id, label: d.label })), value: server.id, onChange: setServerId }) })), _jsxs(SettingField, { label: "From", children: [_jsx(LumenSelector, { label: "App", size: "sm", className: "w-36 md:hidden", options: Object.entries(CLIENTS).map(([k, label]) => ({ value: k, label })), value: client, onChange: (v) => setClient(v) }), _jsx(Tabs, { value: client, onValueChange: (v) => setClient(v), className: "max-md:hidden", children: _jsx(TabsList, { children: Object.entries(CLIENTS).map(([k, label]) => (_jsx(TabsTrigger, { value: k, children: label }, k))) }) })] }), _jsx("div", { className: "pt-3", children: _jsx(ClientSteps, { client: client, server: server }) }), _jsx(SettingsFooter, { children: _jsxs("p", { className: "flex items-start gap-1.5 text-xs text-muted-foreground", children: [_jsx(ShieldIcon, { className: "mt-0.5 size-3.5 shrink-0" }), "Every client signs in as you through your browser; there are no API keys. Sign-ins return only to known apps and to http://localhost or http://127.0.0.1 on your own computer."] }) })] }));
|
|
65
62
|
}
|
package/dist/schedules.js
CHANGED
|
@@ -12,6 +12,7 @@ import { useIsMobile } from './hooks/use-mobile.js';
|
|
|
12
12
|
import { LumenSelector } from './lumen-selector.js';
|
|
13
13
|
import { IconButton } from './page.js';
|
|
14
14
|
import { usePrefs, zoneAbbr } from './prefs.js';
|
|
15
|
+
import { SettingField } from './setting-parts.js';
|
|
15
16
|
import { useShell } from './shell.js';
|
|
16
17
|
import { Badge } from './ui/badge.js';
|
|
17
18
|
import { Button } from './ui/button.js';
|
|
@@ -118,13 +119,14 @@ function OnSwitch({ s, onSave }) {
|
|
|
118
119
|
void onSave(s.id, { on }).finally(() => setPending(false));
|
|
119
120
|
} }));
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
/** One setting in the editor: its label beside its control (`stack` puts it above on a phone). */
|
|
123
|
+
function Row({ label, stack, end, children, }) {
|
|
124
|
+
return (_jsx(SettingField, { label: label, stack: stack, end: end, children: children }));
|
|
123
125
|
}
|
|
124
126
|
/** At (a daily schedule), Days and Time zone: the schedule editor's and New brief's. */
|
|
125
127
|
function TimeFields({ kind, draft, set, }) {
|
|
126
128
|
const zones = !draft.tz || ZONES.includes(draft.tz) ? ZONES : [draft.tz, ...ZONES];
|
|
127
|
-
return (_jsxs(_Fragment, { children: [kind === 'daily' ? (_jsx(Row, { label: "At", children: _jsx(Input, { type: "time", "aria-label": "At", className: "num w-32", value: draft.from, onChange: (e) => set({ from: e.target.value }) }) })) : null, _jsx(Row, { label: "Days", children: _jsx(ToggleGroup, { type: "multiple", variant: "outline", size: "sm", spacing: 1, className: "w-full", value: (draft.days ?? []).map(String), onValueChange: (v) => v.length && set({ days: v.map(Number) }), children: [1, 2, 3, 4, 5, 6, 0].map((d) => (_jsx(ToggleGroupItem, { value: String(d), className: "flex-1 px-0", children: DAYS[d] }, d))) }) }), _jsx(Row, { label: "Time zone", children: _jsx(LumenSelector, { label: "Time zone", className: "w-full", options: zones.map((z) => ({ value: z, label: z.replace('_', ' ') })), value: draft.tz, onChange: (tz) => set({ tz }) }) })] }));
|
|
129
|
+
return (_jsxs(_Fragment, { children: [kind === 'daily' ? (_jsx(Row, { label: "At", children: _jsx(Input, { type: "time", "aria-label": "At", className: "num w-32", value: draft.from, onChange: (e) => set({ from: e.target.value }) }) })) : null, _jsx(Row, { label: "Days", stack: true, children: _jsx(ToggleGroup, { type: "multiple", variant: "outline", size: "sm", spacing: 1, className: "w-full", value: (draft.days ?? []).map(String), onValueChange: (v) => v.length && set({ days: v.map(Number) }), children: [1, 2, 3, 4, 5, 6, 0].map((d) => (_jsx(ToggleGroupItem, { value: String(d), className: "flex-1 px-0", children: DAYS[d] }, d))) }) }), _jsx(Row, { label: "Time zone", children: _jsx(LumenSelector, { label: "Time zone", className: "w-full", options: zones.map((z) => ({ value: z, label: z.replace('_', ' ') })), value: draft.tz, onChange: (tz) => set({ tz }) }) })] }));
|
|
128
130
|
}
|
|
129
131
|
/** One schedule's settings, with Run now. */
|
|
130
132
|
function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
|
|
@@ -182,7 +184,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
|
|
|
182
184
|
setBusy(null);
|
|
183
185
|
}
|
|
184
186
|
};
|
|
185
|
-
return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: s.name }), _jsx(SheetDescription, { children: when(s, timeZone) })] }), _jsxs("div", { className: "
|
|
187
|
+
return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: s.name }), _jsx(SheetDescription, { children: when(s, timeZone) })] }), _jsxs("div", { className: "flex flex-col overflow-y-auto px-4", children: [_jsx("dl", { className: "mb-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-xs", children: [
|
|
186
188
|
['Inspects', s.about.inspects],
|
|
187
189
|
['Processes', s.about.processes],
|
|
188
190
|
['Enriches', s.about.enriches],
|
|
@@ -190,7 +192,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
|
|
|
190
192
|
['Cost', s.about.cost],
|
|
191
193
|
]
|
|
192
194
|
.filter(([, v]) => v)
|
|
193
|
-
.map(([k, v]) => (_jsxs(Fragment, { children: [_jsx("dt", { className: "whitespace-nowrap text-muted-foreground", children: k }), _jsx("dd", { children: v })] }, k))) }), s.brief ? (_jsx(Row, { label: "Instructions", children: own ? (_jsx(Textarea, { "aria-label": "Instructions", rows: 5, maxLength: 4000, value: brief.instructions ?? '', onChange: (e) => setBrief((b) => ({ ...b, instructions: e.target.value })) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: s.brief.instructions })) })) : null,
|
|
195
|
+
.map(([k, v]) => (_jsxs(Fragment, { children: [_jsx("dt", { className: "whitespace-nowrap text-muted-foreground", children: k }), _jsx("dd", { children: v })] }, k))) }), s.brief ? (_jsx(Row, { label: "Instructions", stack: true, children: own ? (_jsx(Textarea, { "aria-label": "Instructions", rows: 5, maxLength: 4000, value: brief.instructions ?? '', onChange: (e) => setBrief((b) => ({ ...b, instructions: e.target.value })) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: s.brief.instructions })) })) : null, _jsx(Row, { label: "On", end: _jsx(Switch, { "aria-label": `${s.name} on`, checked: draft.on, onCheckedChange: (on) => set({ on }) }) }), s.choices.length ? (_jsx(Row, { label: "Every", stack: true, children: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", value: String(draft.every), onValueChange: (v) => v && set({ every: Number(v) }), children: s.choices.map((m) => (_jsx(ToggleGroupItem, { value: String(m), className: "num", children: everyText(m) }, m))) }) })) : null, s.kind === 'window' ? (_jsx(_Fragment, { children: _jsx(Row, { label: "Window", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "time", "aria-label": "From", className: "num w-32", value: draft.from, onChange: (e) => set({ from: e.target.value }) }), _jsx("span", { className: "text-muted-foreground", children: "to" }), _jsx(Input, { type: "time", "aria-label": "To", className: "num w-32", value: draft.to, onChange: (e) => set({ to: e.target.value }) })] }) }) })) : null, s.kind === 'window' || s.kind === 'daily' ? (_jsx(TimeFields, { kind: s.kind, draft: draft, set: set })) : null, s.brief ? (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Cap a run", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "$" }), _jsx(Input, { type: "number", "aria-label": "Spend cap a run, USD", className: "num w-24", min: 0.05, max: 5, step: 0.05, value: brief.cap_usd ?? '', onChange: (e) => setBrief((b) => ({ ...b, cap_usd: Number(e.target.value) })) })] }) }), _jsx(Row, { label: "Notify", end: _jsx(Switch, { "aria-label": "Notify when ready", checked: brief.notify ?? false, onCheckedChange: (notify) => setBrief((b) => ({ ...b, notify })) }) })] })) : null, s.waiting ? (_jsx("p", { className: "py-2 text-sm text-warning", children: s.waiting })) : s.next ? (_jsx(Row, { label: "Next run", children: _jsx("span", { className: "truncate text-sm", children: time(s.next) }) })) : null, recent && !ran ? (_jsx(Row, { label: "Last runs", stack: true, children: recent(s.id) })) : (_jsx(Row, { label: "Last run", children: _jsx("span", { className: "text-sm", children: _jsx(Last, { s: ran ? { ...s, last: ran } : s }) }) })), last && assistant ? (_jsxs(Button, { variant: "outline", size: "sm", className: "mt-2 self-start", onClick: () => {
|
|
194
196
|
assistant.loadChat(last.chat);
|
|
195
197
|
assistant.setOpen(true);
|
|
196
198
|
onClose();
|
|
@@ -258,5 +260,5 @@ function NewBrief({ create, onDone, }) {
|
|
|
258
260
|
setBusy(false);
|
|
259
261
|
}
|
|
260
262
|
};
|
|
261
|
-
return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "New brief" }), _jsxs(SheetDescription, { children: [assistantName, " works it out on a schedule, reads only, and sends it when it is ready."] })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx(Row, { label: "Name", children: _jsx(Input, { "aria-label": "Name", maxLength: 60, placeholder: "Options brief", value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(Row, { label: "Instructions", children: _jsx(Textarea, { "aria-label": "Instructions", rows: 6, maxLength: 4000, placeholder: `What ${assistantName} should look at and report, e.g. my short options expiring this week with their delta and P&L.`, value: instructions, onChange: (e) => setInstructions(e.target.value) }) }), _jsx(TimeFields, { kind: "daily", draft: time, set: (c) => setTime((t) => ({ ...t, ...c })) }), error ? (_jsx("p", { role: "alert", className: "text-xs text-loss", children: error })) : null] }), _jsx(SheetFooter, { className: "flex-row justify-end", children: _jsxs(Button, { size: "sm", disabled: busy || !name.trim() || !instructions.trim(), onClick: () => void add(), children: [_jsx(PlusIcon, {}), "Add brief"] }) })] }));
|
|
263
|
+
return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "New brief" }), _jsxs(SheetDescription, { children: [assistantName, " works it out on a schedule, reads only, and sends it when it is ready."] })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx(Row, { label: "Name", children: _jsx(Input, { "aria-label": "Name", maxLength: 60, placeholder: "Options brief", value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(Row, { label: "Instructions", stack: true, children: _jsx(Textarea, { "aria-label": "Instructions", rows: 6, maxLength: 4000, placeholder: `What ${assistantName} should look at and report, e.g. my short options expiring this week with their delta and P&L.`, value: instructions, onChange: (e) => setInstructions(e.target.value) }) }), _jsx(TimeFields, { kind: "daily", draft: time, set: (c) => setTime((t) => ({ ...t, ...c })) }), error ? (_jsx("p", { role: "alert", className: "text-xs text-loss", children: error })) : null] }), _jsx(SheetFooter, { className: "flex-row justify-end", children: _jsxs(Button, { size: "sm", disabled: busy || !name.trim() || !instructions.trim(), onClick: () => void add(), children: [_jsx(PlusIcon, {}), "Add brief"] }) })] }));
|
|
262
264
|
}
|
package/dist/setting-parts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
1
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
2
2
|
import { type KeyTestResult } from './integrations.js';
|
|
3
3
|
/** One setting inside an item: its label, then its control group, then an optional switch at the end. */
|
|
4
4
|
export declare function SettingField({ label, info, htmlFor, end, stack, className, children, }: {
|
|
@@ -16,7 +16,7 @@ export declare function SettingField({ label, info, htmlFor, end, stack, classNa
|
|
|
16
16
|
children?: ReactNode;
|
|
17
17
|
}): import("react").JSX.Element;
|
|
18
18
|
/** A value read in place: cut to one line with the full text on hover, with Copy and Replace inside. */
|
|
19
|
-
export declare function SettingValue({ value, label, mono, copy, onReplace, replaceLabel, }: {
|
|
19
|
+
export declare function SettingValue({ value, label, mono, copy, onReplace, replaceLabel, replaceIcon: ReplaceIcon, }: {
|
|
20
20
|
value: string;
|
|
21
21
|
/** The value's name for Copy's and Replace's accessible names. */
|
|
22
22
|
label: string;
|
|
@@ -24,6 +24,10 @@ export declare function SettingValue({ value, label, mono, copy, onReplace, repl
|
|
|
24
24
|
copy?: boolean;
|
|
25
25
|
onReplace?: () => void;
|
|
26
26
|
replaceLabel?: string;
|
|
27
|
+
/** Replace's icon: a pencil, or a sign-in for `Sign in again`. */
|
|
28
|
+
replaceIcon?: ComponentType<{
|
|
29
|
+
className?: string;
|
|
30
|
+
}>;
|
|
27
31
|
}): import("react").JSX.Element;
|
|
28
32
|
/** The five words a thing's status takes: Works, Failed, Not tested, Off, or a count. */
|
|
29
33
|
export type SettingHealth = 'works' | 'failed' | 'untested' | 'off';
|
package/dist/setting-parts.js
CHANGED
|
@@ -18,10 +18,10 @@ import { InputGroup, InputGroupAddon, InputGroupButton } from './ui/input-group.
|
|
|
18
18
|
import { Label } from './ui/label.js';
|
|
19
19
|
/** One setting inside an item: its label, then its control group, then an optional switch at the end. */
|
|
20
20
|
export function SettingField({ label, info, htmlFor, end, stack = false, className, children, }) {
|
|
21
|
-
return (_jsxs("div", { "data-slot": "setting-field", "data-stack": stack || undefined, className: cn('flex min-h-10 min-w-0 items-center gap-3 border-
|
|
21
|
+
return (_jsxs("div", { "data-slot": "setting-field", "data-stack": stack || undefined, className: cn('flex min-h-10 min-w-0 items-center gap-3 border-t border-border/60 py-1.5 first:border-t-0', stack && 'max-sm:flex-wrap max-sm:gap-y-1.5', className), children: [_jsxs("div", { className: cn('flex w-32 shrink-0 items-center gap-1.5 max-sm:w-24', stack && 'max-sm:w-auto max-sm:flex-1'), children: [_jsx(Label, { htmlFor: htmlFor, className: "truncate font-normal text-muted-foreground", title: label, children: label }), info && _jsx(InfoTip, { label: `About ${label}`, children: info })] }), children !== undefined && (_jsx("div", { className: cn('flex min-w-0 max-w-md flex-1 items-center gap-2', stack && 'max-sm:order-2 max-sm:max-w-none max-sm:basis-full'), children: children })), end && _jsx("div", { className: cn('ml-auto shrink-0', stack && 'max-sm:order-1'), children: end })] }));
|
|
22
22
|
}
|
|
23
23
|
/** A value read in place: cut to one line with the full text on hover, with Copy and Replace inside. */
|
|
24
|
-
export function SettingValue({ value, label, mono = false, copy = false, onReplace, replaceLabel = 'Replace', }) {
|
|
24
|
+
export function SettingValue({ value, label, mono = false, copy = false, onReplace, replaceLabel = 'Replace', replaceIcon: ReplaceIcon = PencilIcon, }) {
|
|
25
25
|
const [copied, setCopied] = useState(false);
|
|
26
26
|
const doCopy = async () => {
|
|
27
27
|
try {
|
|
@@ -33,7 +33,7 @@ export function SettingValue({ value, label, mono = false, copy = false, onRepla
|
|
|
33
33
|
setCopied(false);
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
|
-
return (_jsxs(InputGroup, { className: "h-8 min-w-0", children: [_jsx("span", { className: cn('min-w-0 flex-1 truncate px-2.5 text-sm', mono && 'font-code text-xs'), title: value, children: value }), (copy || onReplace) && (_jsxs(InputGroupAddon, { align: "inline-end", children: [copy && (_jsx(InputGroupButton, { size: "icon-xs", "aria-label": copied ? 'Copied' : `Copy ${label.toLowerCase()}`, title: copied ? 'Copied' : `Copy ${label.toLowerCase()}`, onClick: () => void doCopy(), children: copied ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) })), onReplace && (_jsx(InputGroupButton, { size: "icon-xs", "aria-label": `${replaceLabel} ${label.toLowerCase()}`, title: `${replaceLabel} ${label.toLowerCase()}`, onClick: onReplace, children: _jsx(
|
|
36
|
+
return (_jsxs(InputGroup, { className: "h-8 min-w-0", children: [_jsx("span", { className: cn('min-w-0 flex-1 truncate px-2.5 text-sm', mono && 'font-code text-xs'), title: value, children: value }), (copy || onReplace) && (_jsxs(InputGroupAddon, { align: "inline-end", children: [copy && (_jsx(InputGroupButton, { size: "icon-xs", "aria-label": copied ? 'Copied' : `Copy ${label.toLowerCase()}`, title: copied ? 'Copied' : `Copy ${label.toLowerCase()}`, onClick: () => void doCopy(), children: copied ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) })), onReplace && (_jsx(InputGroupButton, { size: "icon-xs", "aria-label": `${replaceLabel} ${label.toLowerCase()}`, title: `${replaceLabel} ${label.toLowerCase()}`, onClick: onReplace, children: _jsx(ReplaceIcon, {}) }))] }))] }));
|
|
37
37
|
}
|
|
38
38
|
/** Where a thing stands from its switch and last test. */
|
|
39
39
|
export function settingHealth(on, test) {
|
package/dist/workspaces.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FrameScript } from '@columenlabs/lumen-ui-types/frames';
|
|
1
2
|
import { type CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
|
|
2
3
|
import { type ComponentType, type ReactNode } from 'react';
|
|
3
4
|
import { type Crumb } from './page.js';
|
|
@@ -24,6 +25,20 @@ export interface WorkspacesConfig {
|
|
|
24
25
|
defaultConfig?: (domain: string) => CustomViewConfig | null;
|
|
25
26
|
/** The empty workspace's example ask, on an app: `put my holdings with their earnings dates in`. */
|
|
26
27
|
example?: (app: string) => string;
|
|
28
|
+
/** Recipes a person picks to start a tab by hand (+ in the tab bar); none hides the +. */
|
|
29
|
+
recipes?: WorkspaceRecipe[];
|
|
30
|
+
}
|
|
31
|
+
/** A saved data pull a person can start a tab from: one call to one of the product's read tools. */
|
|
32
|
+
export interface WorkspaceRecipe {
|
|
33
|
+
id: string;
|
|
34
|
+
/** The tab's name and the picker's row: `Jobs scoring 70+`. */
|
|
35
|
+
label: string;
|
|
36
|
+
/** When to pick it, under the label. */
|
|
37
|
+
description?: string;
|
|
38
|
+
/** The picker's section: the app the rows come from. */
|
|
39
|
+
group?: string;
|
|
40
|
+
icon?: Icon;
|
|
41
|
+
recipe: FrameScript;
|
|
27
42
|
}
|
|
28
43
|
/** The product's apps and domains for its workspaces. */
|
|
29
44
|
export declare const WorkspacesProvider: import("react").Provider<WorkspacesConfig>;
|
package/dist/workspaces.js
CHANGED
|
@@ -4,7 +4,7 @@ import { customViewConfigSchema, frameSpace, } from '@columenlabs/lumen-ui-types
|
|
|
4
4
|
import { workspacePath, } from '@columenlabs/lumen-ui-types/workspaces';
|
|
5
5
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
6
6
|
import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
|
|
7
|
-
import { ArrowRightLeftIcon, CalendarIcon, ExternalLinkIcon, HashIcon, LayersIcon, PencilIcon, SparklesIcon, TableIcon, TextCursorInputIcon, Trash2Icon, TypeIcon, } from 'lucide-react';
|
|
7
|
+
import { ArrowRightLeftIcon, CalendarIcon, ExternalLinkIcon, HashIcon, LayersIcon, PencilIcon, PlusIcon, RefreshCwIcon, SparklesIcon, TableIcon, TextCursorInputIcon, Trash2Icon, TypeIcon, } from 'lucide-react';
|
|
8
8
|
import { createContext, useContext, useEffect, useMemo, useState, } from 'react';
|
|
9
9
|
import { apiJson } from './api-fetch.js';
|
|
10
10
|
import { useAssistant, useAssistantName, useAssistantPending } from './assistant-context.js';
|
|
@@ -206,9 +206,48 @@ function WorkspaceBody({ api, w, tab, draft, }) {
|
|
|
206
206
|
}).then(() => qc.invalidateQueries({ queryKey: WORKSPACES_KEY }));
|
|
207
207
|
}, { id: `ws-save:${tab.id}`, duration: 4000 });
|
|
208
208
|
};
|
|
209
|
-
|
|
209
|
+
// + in the tab bar: a tab from a recipe, run once now; ↻ in the header runs every tab again.
|
|
210
|
+
const addTab = useMutation({
|
|
211
|
+
mutationFn: (r) => json(`${api}/workspaces/${w.id}/tabs`, {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
body: JSON.stringify({ name: r.label, recipe: r.recipe }),
|
|
214
|
+
}),
|
|
215
|
+
onSuccess: ({ workspace, tab: t }) => {
|
|
216
|
+
put(workspace);
|
|
217
|
+
void qc.invalidateQueries({ queryKey: [...WORKSPACES_KEY, api, 'list'] });
|
|
218
|
+
void navigate({ href: workspacePath(w.id, t), replace: true });
|
|
219
|
+
},
|
|
220
|
+
onError: (e) => lumenToast.error(`Couldn’t add the tab: ${e.message}`),
|
|
221
|
+
});
|
|
222
|
+
const runAll = useMutation({
|
|
223
|
+
mutationFn: () => json(`${api}/workspaces/${w.id}/refresh`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
}),
|
|
226
|
+
onSuccess: (r) => {
|
|
227
|
+
void qc.invalidateQueries({ queryKey: WORKSPACES_KEY });
|
|
228
|
+
const ran = `Ran ${r.ran} ${r.ran === 1 ? 'tab' : 'tabs'}: ${r.rows.toLocaleString('en-US')} rows`;
|
|
229
|
+
if (r.failed.length)
|
|
230
|
+
lumenToast.warning(`${ran}. Failed: ${r.failed.join(', ')}`);
|
|
231
|
+
else
|
|
232
|
+
lumenToast.success(ran);
|
|
233
|
+
},
|
|
234
|
+
onError: (e) => lumenToast.error(`Couldn’t run the tabs: ${e.message}`),
|
|
235
|
+
});
|
|
236
|
+
const recipes = config.recipes ?? [];
|
|
237
|
+
const runnable = w.tabs.some((t) => 'frame' in t.source);
|
|
238
|
+
const intro = (_jsx(LumenTabs, { tabs: w.tabs.map((t) => ({ value: t.id, label: t.name, icon: tabIcon(t) })), value: tab?.id, onChange: (t) => void navigate({ href: workspacePath(w.id, t), replace: true }), actions: _jsxs(_Fragment, { children: [recipes.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: PlusIcon, label: addTab.isPending ? 'Adding a tab…' : 'New tab from a recipe', align: "end", disabled: addTab.isPending, options: recipes.map((r) => ({
|
|
239
|
+
value: r.id,
|
|
240
|
+
label: r.label,
|
|
241
|
+
description: r.description,
|
|
242
|
+
group: r.group,
|
|
243
|
+
icon: r.icon,
|
|
244
|
+
})), value: null, onChange: (id) => {
|
|
245
|
+
const r = recipes.find((x) => x.id === id);
|
|
246
|
+
if (r)
|
|
247
|
+
addTab.mutate(r);
|
|
248
|
+
} })), tab && (_jsx(IconButton, { icon: PencilIcon, label: "Edit tab", onClick: () => setEditingTab(true) }))] }) }));
|
|
210
249
|
const crumbs = [{ label: 'Workspaces' }, { label: w.name }];
|
|
211
|
-
const actions = (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: TextCursorInputIcon, label: "Rename workspace", onClick: () => setNaming('workspace') }), moveTo.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: ArrowRightLeftIcon, label: `Move to ${noun}`, align: "end", search: false, options: moveTo.map((a) => ({ value: a.id, label: a.label, icon: a.icon })), value: null, onChange: (a) => move.mutate(a) })), _jsx(IconButton, { icon: Trash2Icon, label: "Delete workspace", className: "text-muted-foreground hover:text-destructive", onClick: () => setRemoving('workspace') })] }));
|
|
250
|
+
const actions = (_jsxs(_Fragment, { children: [runnable && (_jsx(IconButton, { icon: RefreshCwIcon, label: runAll.isPending ? 'Running every tab…' : 'Run every tab again', disabled: runAll.isPending, className: runAll.isPending ? '[&_svg]:animate-spin' : undefined, onClick: () => runAll.mutate() })), _jsx(IconButton, { icon: TextCursorInputIcon, label: "Rename workspace", onClick: () => setNaming('workspace') }), moveTo.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: ArrowRightLeftIcon, label: `Move to ${noun}`, align: "end", search: false, options: moveTo.map((a) => ({ value: a.id, label: a.label, icon: a.icon })), value: null, onChange: (a) => move.mutate(a) })), _jsx(IconButton, { icon: Trash2Icon, label: "Delete workspace", className: "text-muted-foreground hover:text-destructive", onClick: () => setRemoving('workspace') })] }));
|
|
212
251
|
const dialogs = (_jsxs(_Fragment, { children: [_jsx(NameDialog, { open: naming !== null, title: "Rename workspace", value: w.name, onClose: () => setNaming(null), onSave: (name) => rename.mutate(name) }), tab && (_jsx(LumenTabDrawer, { open: editingTab, onClose: () => setEditingTab(false), name: tab.name, icon: tab.config?.icon, fallback: tabIcon({ ...tab, config: null }), description: "Its filters and columns save as you change them.", maxName: 60, busy: editTab.isPending, error: editTab.error?.message, onSave: ({ name, icon }) => editTab.mutate(icon === (tab.config?.icon ?? null)
|
|
213
252
|
? { name }
|
|
214
253
|
: { name, config: { ...tabConfig(tab, w, config.defaultConfig), name, icon } }), remove: {
|
|
@@ -226,7 +265,9 @@ function WorkspaceBody({ api, w, tab, draft, }) {
|
|
|
226
265
|
title: 'No tabs yet',
|
|
227
266
|
icon: LayersIcon,
|
|
228
267
|
kind: 'widget',
|
|
229
|
-
children: (
|
|
268
|
+
children: (_jsx("p", { className: "text-sm text-muted-foreground", children: recipes.length > 0
|
|
269
|
+
? 'Pick a recipe from + above: its rows open here as a tab, and run again whenever you tap ↻.'
|
|
270
|
+
: `Ask ${assistant} for data and to open it here: “${config.example?.(here) ?? 'put this month’s numbers in'} ${w.name}”.` })),
|
|
230
271
|
},
|
|
231
272
|
] }));
|
|
232
273
|
}
|
|
@@ -290,6 +331,7 @@ assistant = 'Assistant') {
|
|
|
290
331
|
value: (r) => r.v[i] ?? null,
|
|
291
332
|
format: c.format,
|
|
292
333
|
amount: c.amount,
|
|
334
|
+
...(c.tags ? { tags: true } : {}),
|
|
293
335
|
filterable: true,
|
|
294
336
|
groupable: c.type !== 'number',
|
|
295
337
|
...(c.type === 'number' ? { aggregate: c.format === 'percent' ? 'mean' : 'sum' } : {}),
|
|
@@ -438,7 +480,7 @@ function FrameTab({ api, tab, meta, crumbs, actions, intro, onChange, }) {
|
|
|
438
480
|
day: 'numeric',
|
|
439
481
|
hour: 'numeric',
|
|
440
482
|
minute: '2-digit',
|
|
441
|
-
}) }), failed ? (_jsx(StatusItem, { label: "Refresh failed", className: "min-w-0", value: _jsx("span", { className: "inline-block max-w-72 truncate align-bottom", title: failed, children: failed }), ok: false })) : null] }) }, `frame:${tab.id}`), assistantMade && (_jsx(FrameScriptDialog, { api: api, frame: current.id, name: current.name, open: scriptOpen, onOpenChange: setScriptOpen }))] }));
|
|
483
|
+
}) }), failed ? (_jsx(StatusItem, { label: "Refresh failed", className: "min-w-0", value: _jsx("span", { className: "inline-block max-w-72 truncate align-bottom", title: failed, children: failed }), ok: false })) : null] }) }, `frame:${tab.id}`), assistantMade && (_jsx(FrameScriptDialog, { api: api, frame: current.id, name: current.name, runs: current.runs, open: scriptOpen, onOpenChange: setScriptOpen }))] }));
|
|
442
484
|
}
|
|
443
485
|
/** A tab's icon: the person's pick, else the assistant's mark on a frame, else a table. */
|
|
444
486
|
function tabIcon(t) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.190",
|
|
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.190",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|