@hanzo/build 0.2.10 → 0.2.12
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/README.md +98 -25
- package/lib/api/agents.d.ts +80 -0
- package/lib/api/agents.js +148 -0
- package/lib/api/billing.d.ts +205 -0
- package/lib/api/billing.js +300 -0
- package/lib/api/call.d.ts +6 -0
- package/lib/api/call.js +11 -0
- package/lib/api/capabilities.d.ts +25 -0
- package/lib/api/capabilities.js +40 -0
- package/lib/api/coding.d.ts +31 -1
- package/lib/api/coding.js +50 -1
- package/lib/api/connectors.d.ts +90 -0
- package/lib/api/connectors.js +109 -0
- package/lib/api/consent.d.ts +22 -0
- package/lib/api/consent.js +28 -0
- package/lib/api/github.d.ts +13 -0
- package/lib/api/github.js +9 -0
- package/lib/api/harness.d.ts +44 -0
- package/lib/api/harness.js +348 -0
- package/lib/api/keys.d.ts +41 -0
- package/lib/api/keys.js +55 -0
- package/lib/api/machines.d.ts +56 -0
- package/lib/api/machines.js +84 -0
- package/lib/api/members.d.ts +47 -0
- package/lib/api/members.js +88 -0
- package/lib/api/memory.d.ts +25 -0
- package/lib/api/memory.js +34 -0
- package/lib/api/places.d.ts +2 -2
- package/lib/api/places.js +3 -26
- package/lib/api/plugins.d.ts +55 -0
- package/lib/api/plugins.js +62 -0
- package/lib/api/pref.d.ts +49 -0
- package/lib/api/pref.js +68 -0
- package/lib/api/profile.d.ts +21 -0
- package/lib/api/profile.js +40 -0
- package/lib/api/projects.d.ts +19 -2
- package/lib/api/projects.js +29 -4
- package/lib/api/provider.d.ts +42 -0
- package/lib/api/provider.js +58 -0
- package/lib/api/sessions.d.ts +32 -4
- package/lib/api/sessions.js +42 -6
- package/lib/api/skills.d.ts +70 -0
- package/lib/api/skills.js +90 -0
- package/lib/api/tools.d.ts +34 -0
- package/lib/api/tools.js +37 -0
- package/lib/api/turn.d.ts +84 -16
- package/lib/api/turn.js +221 -4
- package/lib/api/webhooks.d.ts +60 -0
- package/lib/api/webhooks.js +81 -0
- package/lib/ask.d.ts +18 -0
- package/lib/ask.js +69 -0
- package/lib/builder.js +39 -19
- package/lib/customize/agents.d.ts +2 -0
- package/lib/customize/agents.js +125 -0
- package/lib/customize/connectors.d.ts +2 -0
- package/lib/customize/connectors.js +199 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +52 -0
- package/lib/customize/plugins.d.ts +2 -0
- package/lib/customize/plugins.js +93 -0
- package/lib/customize/skills.d.ts +2 -0
- package/lib/customize/skills.js +127 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +4 -1
- package/lib/desk.js +3 -16
- package/lib/find.d.ts +7 -0
- package/lib/find.js +77 -0
- package/lib/foot.d.ts +16 -0
- package/lib/foot.js +36 -0
- package/lib/forge.js +4 -4
- package/lib/git.d.ts +4 -0
- package/lib/git.js +6 -2
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +30 -14
- package/lib/markdown.d.ts +49 -0
- package/lib/markdown.js +123 -0
- package/lib/plans.d.ts +1 -0
- package/lib/plans.js +113 -0
- package/lib/prefs.d.ts +30 -0
- package/lib/prefs.js +83 -0
- package/lib/prose.d.ts +4 -0
- package/lib/prose.js +53 -0
- package/lib/route.d.ts +16 -1
- package/lib/route.js +32 -1
- package/lib/run.js +184 -32
- package/lib/section.js +2 -2
- package/lib/settings/account.d.ts +5 -0
- package/lib/settings/account.js +111 -0
- package/lib/settings/billing.d.ts +1 -0
- package/lib/settings/billing.js +104 -0
- package/lib/settings/capabilities.d.ts +1 -0
- package/lib/settings/capabilities.js +55 -0
- package/lib/settings/card.d.ts +31 -0
- package/lib/settings/card.js +105 -0
- package/lib/settings/code.d.ts +1 -0
- package/lib/settings/code.js +51 -0
- package/lib/settings/environments.d.ts +1 -0
- package/lib/settings/environments.js +53 -0
- package/lib/settings/general.d.ts +1 -0
- package/lib/settings/general.js +44 -0
- package/lib/settings/index.d.ts +4 -0
- package/lib/settings/index.js +22 -0
- package/lib/settings/integrations.d.ts +1 -0
- package/lib/settings/integrations.js +81 -0
- package/lib/settings/keys.d.ts +1 -0
- package/lib/settings/keys.js +102 -0
- package/lib/settings/machines.d.ts +1 -0
- package/lib/settings/machines.js +118 -0
- package/lib/settings/members.d.ts +1 -0
- package/lib/settings/members.js +67 -0
- package/lib/settings/memory.d.ts +1 -0
- package/lib/settings/memory.js +56 -0
- package/lib/settings/notifications.d.ts +1 -0
- package/lib/settings/notifications.js +97 -0
- package/lib/settings/privacy.d.ts +1 -0
- package/lib/settings/privacy.js +63 -0
- package/lib/settings/sections.d.ts +16 -0
- package/lib/settings/sections.js +32 -0
- package/lib/settings/ui.d.ts +49 -0
- package/lib/settings/ui.js +47 -0
- package/lib/settings/usage.d.ts +1 -0
- package/lib/settings/usage.js +114 -0
- package/lib/shelf.d.ts +6 -0
- package/lib/shelf.js +86 -16
- package/lib/switch.js +1 -2
- package/lib/transcript.d.ts +12 -0
- package/lib/transcript.js +56 -0
- package/lib/voice.d.ts +3 -1
- package/lib/voice.js +10 -4
- package/package.json +1 -1
- package/lib/account.d.ts +0 -14
- package/lib/account.js +0 -38
- package/lib/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Privacy: the two data-sharing answers IAM records for the person — whether
|
|
4
|
+
* Hanzo may train on their data, and whether anonymous usage is counted — and
|
|
5
|
+
* who can see the organization's projects: a public one is in the community
|
|
6
|
+
* catalogue with its source readable by anyone, until it is made private.
|
|
7
|
+
*/
|
|
8
|
+
import { YStack } from '@hanzo/gui';
|
|
9
|
+
import { Button, Switch } from '@hanzo/ui';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { consent, setConsent } from '../api/consent.js';
|
|
12
|
+
import { change, projects } from '../api/projects.js';
|
|
13
|
+
import { useRead } from '../data.js';
|
|
14
|
+
import { useHost, useTarget } from '../host.js';
|
|
15
|
+
import { Card, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
16
|
+
export function Privacy() {
|
|
17
|
+
const host = useHost();
|
|
18
|
+
const t = useTarget();
|
|
19
|
+
const signed = Boolean(host.person);
|
|
20
|
+
const answers = useRead(signed ? () => consent(t) : null, null, [t, signed]);
|
|
21
|
+
const published = useRead(signed ? () => projects(t) : null, [], [t, signed]);
|
|
22
|
+
const [shown, setShown] = useState(null);
|
|
23
|
+
const [busy, setBusy] = useState('');
|
|
24
|
+
const [note, setNote] = useState('');
|
|
25
|
+
if (!signed) {
|
|
26
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Privacy" }), _jsx(Soft, { children: "Sign in to see your privacy settings." })] }));
|
|
27
|
+
}
|
|
28
|
+
const now = shown ?? answers.value;
|
|
29
|
+
const answer = async (change) => {
|
|
30
|
+
setBusy('consent');
|
|
31
|
+
setNote('');
|
|
32
|
+
try {
|
|
33
|
+
setShown(await setConsent(t, change));
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
setNote(e instanceof Error ? e.message : 'That was not saved');
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
setBusy('');
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const flip = async (p) => {
|
|
43
|
+
setBusy(p.slug);
|
|
44
|
+
setNote('');
|
|
45
|
+
try {
|
|
46
|
+
const saved = await change(t, p.slug, { visibility: p.visibility === 'private' ? 'public' : 'private' });
|
|
47
|
+
published.reload();
|
|
48
|
+
setNote(`${saved.name} is ${saved.visibility || 'saved'}`);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
setNote(e instanceof Error ? e.message : 'That was not saved');
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
setBusy('');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const training = now?.training ?? '';
|
|
58
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Privacy", detail: "What Hanzo may do with your data, and what your organization shows the world." }), _jsx(Group, { title: "Your data", detail: "Recorded on your Hanzo account and honoured by every Hanzo surface.", children: answers.error && !now ? (_jsx(Soft, { children: answers.error.message })) : !now ? (_jsx(Soft, { children: "Reading your answers\u2026" })) : (_jsxs(Card, { children: [_jsx(Row, { first: true, title: "Help improve Hanzo models", detail: training === 'granted'
|
|
59
|
+
? 'Hanzo may train on your prompts and runs.'
|
|
60
|
+
: training === 'refused'
|
|
61
|
+
? 'Hanzo does not train on your data.'
|
|
62
|
+
: 'Not answered yet. Until you say yes, Hanzo does not train on your data.', trailing: _jsx(Switch, { "aria-label": "Help improve Hanzo models", checked: training === 'granted', disabled: busy === 'consent', onCheckedChange: (on) => void answer({ training: on ? 'granted' : 'refused' }) }) }), _jsx(Row, { title: "Usage insights", detail: "Anonymous counts of how the product is used. No prompts, code or answers.", trailing: _jsx(Switch, { "aria-label": "Usage insights", checked: now.insights, disabled: busy === 'consent', onCheckedChange: (on) => void answer({ insights: on }) }) })] })) }), _jsx(Group, { title: "Projects", detail: "A public project is listed in the community catalogue, and anyone can read its source. Keeping one private needs a paid plan.", children: published.error ? (_jsx(Soft, { children: published.error.message })) : published.loading && !published.value.length ? (_jsx(Soft, { children: "Reading projects\u2026" })) : !published.value.length ? (_jsx(Soft, { children: "Your organization has no projects yet." })) : (_jsx(Card, { children: published.value.map((p, i) => (_jsx(Row, { first: i === 0, title: p.name, detail: p.visibility === 'private' ? 'Private' : p.visibility === 'public' ? 'Public' : 'Visibility not reported', trailing: p.visibility ? (_jsx(Button, { size: "sm", variant: "outline", disabled: busy === p.slug, onPress: () => void flip(p), children: p.visibility === 'private' ? 'Make public' : 'Make private' })) : null }, p.slug))) })) }), _jsx(Note, { children: note })] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sections of Settings, in the order the page lists them. A section is one
|
|
3
|
+
* file under settings/ and one entry here; the route grammar names every
|
|
4
|
+
* section there can be (route.ts SECTIONS), and this lists the ones built.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
import type { Section } from '../route.ts';
|
|
8
|
+
export type Group = 'Settings' | 'Code' | 'Organization';
|
|
9
|
+
export declare const GROUPS: readonly Group[];
|
|
10
|
+
export interface Entry {
|
|
11
|
+
id: Section;
|
|
12
|
+
label: string;
|
|
13
|
+
group: Group;
|
|
14
|
+
body: () => ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export declare const ENTRIES: Entry[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Account } from './account.js';
|
|
3
|
+
import { Billing } from './billing.js';
|
|
4
|
+
import { Capabilities } from './capabilities.js';
|
|
5
|
+
import { Code } from './code.js';
|
|
6
|
+
import { Environments } from './environments.js';
|
|
7
|
+
import { General } from './general.js';
|
|
8
|
+
import { Integrations } from './integrations.js';
|
|
9
|
+
import { Keys } from './keys.js';
|
|
10
|
+
import { Machines } from './machines.js';
|
|
11
|
+
import { Members } from './members.js';
|
|
12
|
+
import { Memory } from './memory.js';
|
|
13
|
+
import { Notifications } from './notifications.js';
|
|
14
|
+
import { Privacy } from './privacy.js';
|
|
15
|
+
import { Usage } from './usage.js';
|
|
16
|
+
export const GROUPS = ['Settings', 'Code', 'Organization'];
|
|
17
|
+
export const ENTRIES = [
|
|
18
|
+
{ id: 'general', label: 'General', group: 'Settings', body: () => _jsx(General, {}) },
|
|
19
|
+
{ id: 'account', label: 'Account', group: 'Settings', body: () => _jsx(Account, {}) },
|
|
20
|
+
{ id: 'privacy', label: 'Privacy', group: 'Settings', body: () => _jsx(Privacy, {}) },
|
|
21
|
+
{ id: 'billing', label: 'Billing', group: 'Settings', body: () => _jsx(Billing, {}) },
|
|
22
|
+
{ id: 'usage', label: 'Usage', group: 'Settings', body: () => _jsx(Usage, {}) },
|
|
23
|
+
{ id: 'capabilities', label: 'Capabilities', group: 'Settings', body: () => _jsx(Capabilities, {}) },
|
|
24
|
+
{ id: 'memory', label: 'Memory', group: 'Settings', body: () => _jsx(Memory, {}) },
|
|
25
|
+
{ id: 'code', label: 'Code', group: 'Code', body: () => _jsx(Code, {}) },
|
|
26
|
+
{ id: 'environments', label: 'Environments', group: 'Code', body: () => _jsx(Environments, {}) },
|
|
27
|
+
{ id: 'machines', label: 'Machines', group: 'Code', body: () => _jsx(Machines, {}) },
|
|
28
|
+
{ id: 'keys', label: 'API keys', group: 'Code', body: () => _jsx(Keys, {}) },
|
|
29
|
+
{ id: 'members', label: 'Members', group: 'Organization', body: () => _jsx(Members, {}) },
|
|
30
|
+
{ id: 'integrations', label: 'Integrations', group: 'Organization', body: () => _jsx(Integrations, {}) },
|
|
31
|
+
{ id: 'notifications', label: 'Notifications', group: 'Organization', body: () => _jsx(Notifications, {}) },
|
|
32
|
+
];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** A section's title, what it is for, and its one action. */
|
|
3
|
+
export declare function Heading({ title, detail, action }: {
|
|
4
|
+
title: string;
|
|
5
|
+
detail?: string;
|
|
6
|
+
action?: ReactNode;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
/** A group inside a section: a small title over what it holds. */
|
|
9
|
+
export declare function Group({ title, detail, action, children }: {
|
|
10
|
+
title: string;
|
|
11
|
+
detail?: string;
|
|
12
|
+
action?: ReactNode;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
/** A bordered list. Its rows draw their own dividers. */
|
|
16
|
+
export declare function Card({ children }: {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
/** One row of a Card: what it is, a line about it, and what can be done to it. */
|
|
20
|
+
export declare function Row({ title, detail, leading, trailing, first, mono, }: {
|
|
21
|
+
title: string;
|
|
22
|
+
detail?: string;
|
|
23
|
+
leading?: ReactNode;
|
|
24
|
+
trailing?: ReactNode;
|
|
25
|
+
first?: boolean;
|
|
26
|
+
mono?: boolean;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
28
|
+
/** A labelled control, with a line under it when there is more to say. */
|
|
29
|
+
export declare function Field({ label, hint, children }: {
|
|
30
|
+
label: string;
|
|
31
|
+
hint?: string;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}): import("react").JSX.Element;
|
|
34
|
+
/** The quiet line for empty, loading, signed-out and refused. */
|
|
35
|
+
export declare function Soft({ children }: {
|
|
36
|
+
children: string;
|
|
37
|
+
}): import("react").JSX.Element;
|
|
38
|
+
/** `Sep 27, 2026` from an RFC 3339 instant, or '' when there is none. UTC, the calendar the platform bills and resets on. */
|
|
39
|
+
export declare function day(iso: string): string;
|
|
40
|
+
/** A credential the platform answers once: the value to copy, and what it is for. */
|
|
41
|
+
export declare function Once({ value, label, children }: {
|
|
42
|
+
value: string;
|
|
43
|
+
label: string;
|
|
44
|
+
children?: ReactNode;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
46
|
+
/** What the last action did, said once, where it was done. */
|
|
47
|
+
export declare function Note({ children }: {
|
|
48
|
+
children: string;
|
|
49
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* What every Settings section is drawn from, so they read as one page: a
|
|
4
|
+
* heading with its action, bordered lists of rows, labelled fields, and the
|
|
5
|
+
* quiet lines for empty, loading and refused.
|
|
6
|
+
*/
|
|
7
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
8
|
+
import { CopyButton } from '@hanzo/ui/product';
|
|
9
|
+
/** A section's title, what it is for, and its one action. */
|
|
10
|
+
export function Heading({ title, detail, action }) {
|
|
11
|
+
return (_jsxs(XStack, { items: "flex-start", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$6", color: "$ink", children: title }), detail ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail })) : null] }), action] }));
|
|
12
|
+
}
|
|
13
|
+
/** A group inside a section: a small title over what it holds. */
|
|
14
|
+
export function Group({ title, detail, action, children }) {
|
|
15
|
+
return (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: title }), detail ? (_jsx(SizableText, { size: "$1", color: "$soft", children: detail })) : null] }), action] }), children] }));
|
|
16
|
+
}
|
|
17
|
+
/** A bordered list. Its rows draw their own dividers. */
|
|
18
|
+
export function Card({ children }) {
|
|
19
|
+
return (_jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: children }));
|
|
20
|
+
}
|
|
21
|
+
/** One row of a Card: what it is, a line about it, and what can be done to it. */
|
|
22
|
+
export function Row({ title, detail, leading, trailing, first, mono, }) {
|
|
23
|
+
return (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", children: [leading, _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, style: mono ? { fontFamily: 'var(--f-mono, ui-monospace, monospace)' } : undefined, children: title }), detail ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: detail })) : null] }), trailing] }));
|
|
24
|
+
}
|
|
25
|
+
/** A labelled control, with a line under it when there is more to say. */
|
|
26
|
+
export function Field({ label, hint, children }) {
|
|
27
|
+
return (_jsxs(YStack, { gap: "$1.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: label }), children, hint ? (_jsx(SizableText, { size: "$1", color: "$soft", children: hint })) : null] }));
|
|
28
|
+
}
|
|
29
|
+
/** The quiet line for empty, loading, signed-out and refused. */
|
|
30
|
+
export function Soft({ children }) {
|
|
31
|
+
return (_jsx(YStack, { py: "$5", items: "center", children: _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: children }) }));
|
|
32
|
+
}
|
|
33
|
+
/** `Sep 27, 2026` from an RFC 3339 instant, or '' when there is none. UTC, the calendar the platform bills and resets on. */
|
|
34
|
+
export function day(iso) {
|
|
35
|
+
const d = iso ? new Date(iso) : null;
|
|
36
|
+
return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }) : '';
|
|
37
|
+
}
|
|
38
|
+
/** A credential the platform answers once: the value to copy, and what it is for. */
|
|
39
|
+
export function Once({ value, label, children }) {
|
|
40
|
+
return (_jsxs(YStack, { gap: "$2", px: "$3", py: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": label, style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)', wordBreak: 'break-all' }, children: value }), _jsx(CopyButton, { value: value, label: `Copy ${label.toLowerCase()}` })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Copy it now. It is not shown again." }), children] }));
|
|
41
|
+
}
|
|
42
|
+
/** What the last action did, said once, where it was done. */
|
|
43
|
+
export function Note({ children }) {
|
|
44
|
+
if (!children)
|
|
45
|
+
return null;
|
|
46
|
+
return (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: children }));
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Usage(): import("react").JSX.Element;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Usage: what the plan includes and how much of it is used, what the
|
|
4
|
+
* organization has left to spend and has spent this month, and the monthly
|
|
5
|
+
* limit that stops spend past it. Buying more charges the saved card into the
|
|
6
|
+
* balance; the limit is an org admin's to set.
|
|
7
|
+
*/
|
|
8
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
|
+
import { Button, Input } from '@hanzo/ui';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { balance, caps, cents, chosen, credit, label, lift, limit, methods, money, month, monthly, spend, topup, } from '../api/billing.js';
|
|
12
|
+
import { useRead } from '../data.js';
|
|
13
|
+
import { useHost, useTarget } from '../host.js';
|
|
14
|
+
import { path } from '../route.js';
|
|
15
|
+
import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
16
|
+
const SPAN = { hour: 'This hour', day: 'Today', week: 'This week', month: 'This month' };
|
|
17
|
+
/** A thin bar: how much of a whole is used. */
|
|
18
|
+
function Bar({ part, whole, limit }) {
|
|
19
|
+
const pct = whole > 0 ? Math.min(100, Math.max(0, (part / whole) * 100)) : 0;
|
|
20
|
+
return (_jsx(YStack, { height: 6, rounded: 999, bg: "$hover", overflow: "hidden", role: "progressbar", "aria-valuenow": Math.round(pct), "aria-valuemin": 0, "aria-valuemax": 100, children: _jsx(YStack, { height: 6, width: `${pct}%`, rounded: 999, bg: limit && pct >= 100 ? '$red10' : '$ink' }) }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One measured row: what, how much, when it starts over, and its bar. A limit's
|
|
24
|
+
* bar turns red when it is used up; a share of a total never does.
|
|
25
|
+
*/
|
|
26
|
+
function Meter({ title, used, of, says, first, limit = true }) {
|
|
27
|
+
return (_jsxs(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "baseline", justify: "space-between", flexWrap: "wrap", columnGap: "$3", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: title }), _jsx(SizableText, { size: "$1", color: "$soft", children: says })] }), _jsx(Bar, { part: used, whole: of, limit: limit })] }));
|
|
28
|
+
}
|
|
29
|
+
/** `in 3 hr 12 min` or `Oct 1, 2026`, for when a window starts over. */
|
|
30
|
+
function resets(iso) {
|
|
31
|
+
const at = iso ? new Date(iso).getTime() : NaN;
|
|
32
|
+
if (Number.isNaN(at))
|
|
33
|
+
return '';
|
|
34
|
+
const mins = Math.round((at - Date.now()) / 60000);
|
|
35
|
+
if (mins <= 0)
|
|
36
|
+
return 'Resets now';
|
|
37
|
+
if (mins < 60)
|
|
38
|
+
return `Resets in ${mins} min`;
|
|
39
|
+
if (mins < 24 * 60)
|
|
40
|
+
return `Resets in ${Math.floor(mins / 60)} hr ${mins % 60} min`;
|
|
41
|
+
return `Resets ${day(iso)}`;
|
|
42
|
+
}
|
|
43
|
+
export function Usage() {
|
|
44
|
+
const host = useHost();
|
|
45
|
+
const t = useTarget();
|
|
46
|
+
const signed = Boolean(host.person);
|
|
47
|
+
const plan = useRead(signed ? () => month(t) : null, null, [t, signed]);
|
|
48
|
+
const left = useRead(signed ? () => balance(t) : null, 0, [t, signed]);
|
|
49
|
+
const granted = useRead(signed ? () => credit(t) : null, 0, [t, signed]);
|
|
50
|
+
const spent = useRead(signed ? () => spend(t) : null, null, [t, signed]);
|
|
51
|
+
const ceilings = useRead(signed ? () => caps(t) : null, [], [t, signed]);
|
|
52
|
+
const cards = useRead(signed ? () => methods(t) : null, [], [t, signed]);
|
|
53
|
+
const [buying, setBuying] = useState(false);
|
|
54
|
+
const [amount, setAmount] = useState('25');
|
|
55
|
+
const [ceiling, setCeiling] = useState('');
|
|
56
|
+
const [working, setWorking] = useState(false);
|
|
57
|
+
const [note, setNote] = useState('');
|
|
58
|
+
if (!signed) {
|
|
59
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Usage" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s usage." })] }));
|
|
60
|
+
}
|
|
61
|
+
const act = async (what, done, after) => {
|
|
62
|
+
setWorking(true);
|
|
63
|
+
setNote('');
|
|
64
|
+
try {
|
|
65
|
+
await what();
|
|
66
|
+
after();
|
|
67
|
+
setNote(done);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
setWorking(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const m = plan.value;
|
|
77
|
+
const windows = (m?.windows ?? []).filter((w) => w.limit > 0);
|
|
78
|
+
const card = chosen(cards.value);
|
|
79
|
+
const cap = monthly(ceilings.value);
|
|
80
|
+
const others = ceilings.value.filter((c) => c !== cap);
|
|
81
|
+
const s = spent.value;
|
|
82
|
+
const top = Math.max(0, ...(s?.categories ?? []).map((c) => c.cents));
|
|
83
|
+
const buy = () => {
|
|
84
|
+
const c = cents(amount);
|
|
85
|
+
if (!c) {
|
|
86
|
+
setNote('Enter an amount in dollars');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!card)
|
|
90
|
+
return;
|
|
91
|
+
void act(() => topup(t, c, card.id), `${money(c)} is added to the balance`, () => {
|
|
92
|
+
setBuying(false);
|
|
93
|
+
left.reload();
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const setLimit = () => {
|
|
97
|
+
const c = cents(ceiling);
|
|
98
|
+
if (!c) {
|
|
99
|
+
setNote('Enter a monthly limit in dollars');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
void act(() => limit(t, ceilings.value, c), `Spend stops at ${money(c)} a month`, () => {
|
|
103
|
+
setCeiling('');
|
|
104
|
+
ceilings.reload();
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: cents(amount) ? `Add ${money(cents(amount) ?? 0)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label": "Monthly limit in dollars", placeholder: cap ? String(cap.threshold / 100) : '500', inputMode: "decimal" }) }), _jsxs(XStack, { flex: 1, gap: "$2", justify: "flex-end", children: [cap ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => void act(() => lift(t, cap.id), 'The monthly limit is lifted', ceilings.reload), children: "Remove" })) : null, _jsx(Button, { size: "sm", variant: "outline", disabled: working || !ceiling.trim(), onPress: setLimit, children: cap ? 'Change limit' : 'Set limit' })] })] })) : null] })), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin sets the monthly limit." })) : null] }), _jsx(Note, { children: note })] }));
|
|
110
|
+
}
|
|
111
|
+
/** A money figure as read: the amount, or why there is none. */
|
|
112
|
+
function Figure({ read }) {
|
|
113
|
+
return (_jsx(SizableText, { size: "$2", color: read.error ? '$soft' : '$ink', numberOfLines: 1, maxW: 200, children: read.error ? 'Unavailable' : read.loading ? '…' : money(read.value) }));
|
|
114
|
+
}
|
package/lib/shelf.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this org has built: a few starters to make something new from, then
|
|
3
|
+
* every project under the month it was last edited, whether it is public or
|
|
4
|
+
* private, and its rename, visibility and delete. A project belongs to the org
|
|
5
|
+
* and names no author, so the list does not split yours from a colleague's.
|
|
6
|
+
*/
|
|
1
7
|
export declare function Artifacts(): import("react").JSX.Element;
|
|
2
8
|
export declare function Templates(): import("react").JSX.Element;
|
package/lib/shelf.js
CHANGED
|
@@ -9,9 +9,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
* so what opens is that template, not a model's imitation of it.
|
|
10
10
|
*/
|
|
11
11
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
-
import { Boxes, ExternalLink, Globe } from '@hanzogui/lucide-icons-2';
|
|
12
|
+
import { Boxes, ExternalLink, Globe, Lock, MoreHorizontal } from '@hanzogui/lucide-icons-2';
|
|
13
|
+
import { DropdownMenu } from '@hanzo/ui';
|
|
13
14
|
import { useState } from 'react';
|
|
14
|
-
import { fork, templates } from './api/projects.js';
|
|
15
|
+
import { change, fork, remove, templates } from './api/projects.js';
|
|
16
|
+
import { Confirm, Rename } from './ask.js';
|
|
15
17
|
import { useProjects, useRead } from './data.js';
|
|
16
18
|
import { useHost, useTarget } from './host.js';
|
|
17
19
|
import { Out } from './out.js';
|
|
@@ -19,23 +21,13 @@ const SHOTS = 'https://hanzo.ai/templates';
|
|
|
19
21
|
function Head({ title, says }) {
|
|
20
22
|
return (_jsxs(YStack, { gap: "$1", pb: "$4", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: title }), _jsx(SizableText, { size: "$2", color: "$soft", children: says })] }));
|
|
21
23
|
}
|
|
22
|
-
function Card({ onPress, label, children }) {
|
|
23
|
-
return (_jsx(YStack, { render: "button", onPress: onPress, "aria-label": label, flexBasis: 260, grow: 1, maxW: 360, minW: 0, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", overflow: "hidden", cursor: "pointer", hoverStyle: { borderColor: '$edge', bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: children }));
|
|
24
|
+
function Card({ onPress, label, small, children }) {
|
|
25
|
+
return (_jsx(YStack, { render: "button", onPress: onPress, "aria-label": label, flexBasis: small ? 150 : 260, grow: 1, maxW: small ? 260 : 360, minW: 0, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", overflow: "hidden", cursor: "pointer", hoverStyle: { borderColor: '$edge', bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, style: { textAlign: 'left' }, children: children }));
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const host = useHost();
|
|
28
|
-
const list = useProjects(t, Boolean(host.person));
|
|
29
|
-
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Artifacts", says: "What this organization has built. Open one to keep building it." }), list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : !host.person ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to see what your organization has built." })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading your projects\u2026" })) : list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Nothing built yet. Describe something on the New page and it lands here." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$3", children: list.value.map((p) => (_jsx(Card, { label: `Open ${p.name}`, onPress: () => host.go(p.slug), children: _jsxs(YStack, { p: "$3", gap: "$1.5", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Globe, { size: 14, opacity: 0.7 }), _jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, flex: 1, children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.status || 'draft' })] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: p.live ? new URL(p.live).host : 'Not deployed yet' })] }) }, p.slug))) }))] }) }));
|
|
30
|
-
}
|
|
31
|
-
function Shot({ slug }) {
|
|
32
|
-
const [gone, setGone] = useState(false);
|
|
33
|
-
return (_jsxs(YStack, { height: 140, bg: "$hover", items: "center", justify: "center", position: "relative", overflow: "hidden", children: [_jsxs(YStack, { items: "center", gap: "$2", opacity: 0.6, children: [_jsx(Boxes, { size: 24 }), _jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: slug })] }), gone ? null : (_jsx("img", { src: `${SHOTS}/${encodeURIComponent(slug)}.webp`, alt: "", loading: "lazy", onError: () => setGone(true), style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' } }))] }));
|
|
34
|
-
}
|
|
35
|
-
export function Templates() {
|
|
27
|
+
/** Copying a starter into a project of this org, then opening it: one copy at a time. */
|
|
28
|
+
function useFork() {
|
|
36
29
|
const t = useTarget();
|
|
37
30
|
const host = useHost();
|
|
38
|
-
const list = useRead(() => templates(t), [], [t.api]);
|
|
39
31
|
const [busy, setBusy] = useState('');
|
|
40
32
|
const [failed, setFailed] = useState('');
|
|
41
33
|
const take = async (slug) => {
|
|
@@ -56,5 +48,83 @@ export function Templates() {
|
|
|
56
48
|
setBusy('');
|
|
57
49
|
}
|
|
58
50
|
};
|
|
51
|
+
return { busy, failed, take };
|
|
52
|
+
}
|
|
53
|
+
/** When a project was last edited, or made if it never was; Unix seconds. */
|
|
54
|
+
const when = (p) => p.updated || p.created;
|
|
55
|
+
/** The org's projects under the month each was last edited, newest first. */
|
|
56
|
+
function months(list) {
|
|
57
|
+
const out = new Map();
|
|
58
|
+
for (const p of [...list].sort((a, b) => when(b) - when(a))) {
|
|
59
|
+
const s = when(p);
|
|
60
|
+
const label = s ? new Date(s * 1000).toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) : 'Earlier';
|
|
61
|
+
out.set(label, [...(out.get(label) ?? []), p]);
|
|
62
|
+
}
|
|
63
|
+
return [...out].map(([label, rows]) => ({ label, rows }));
|
|
64
|
+
}
|
|
65
|
+
function edited(s) {
|
|
66
|
+
if (!s)
|
|
67
|
+
return '';
|
|
68
|
+
const d = new Date(s * 1000);
|
|
69
|
+
const year = d.getFullYear() === new Date().getFullYear() ? undefined : 'numeric';
|
|
70
|
+
return `Edited ${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year })}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What this org has built: a few starters to make something new from, then
|
|
74
|
+
* every project under the month it was last edited, whether it is public or
|
|
75
|
+
* private, and its rename, visibility and delete. A project belongs to the org
|
|
76
|
+
* and names no author, so the list does not split yours from a colleague's.
|
|
77
|
+
*/
|
|
78
|
+
export function Artifacts() {
|
|
79
|
+
const t = useTarget();
|
|
80
|
+
const host = useHost();
|
|
81
|
+
const list = useProjects(t, Boolean(host.person));
|
|
82
|
+
const starters = useRead(() => templates(t), [], [t.api]);
|
|
83
|
+
const { busy, failed, take } = useFork();
|
|
84
|
+
const [renaming, setRenaming] = useState(null);
|
|
85
|
+
const [deleting, setDeleting] = useState(null);
|
|
86
|
+
const [note, setNote] = useState('');
|
|
87
|
+
const flip = async (p) => {
|
|
88
|
+
const visibility = p.visibility === 'private' ? 'public' : 'private';
|
|
89
|
+
setNote('');
|
|
90
|
+
try {
|
|
91
|
+
await change(t, p.slug, { visibility });
|
|
92
|
+
setNote(`${p.name} is ${visibility} now`);
|
|
93
|
+
list.reload();
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const said = failed || note;
|
|
100
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", "$max-md": { px: '$4' }, children: [_jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Artifacts", says: "What this organization has built. Open one to keep building it." }), said ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: said })) : null, starters.value.length ? (_jsxs(YStack, { gap: "$3", pb: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Make something new" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: starters.value.slice(0, 4).map((x) => (_jsxs(Card, { small: true, label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug, height: 110 }), _jsx(YStack, { px: "$3", py: "$2.5", children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: busy === x.slug ? 'Copying…' : x.title }) })] }, x.slug))) })] })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : !host.person ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to see what your organization has built." })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading your projects\u2026" })) : list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Nothing built yet. Describe something on the New page and it lands here." })) : (_jsx(YStack, { gap: "$5", children: months(list.value).map((g) => (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: g.label }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: g.rows.map((p, i) => (_jsxs(XStack, { items: "center", pr: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { flex: 1, minW: 0, render: "button", "aria-label": `Open ${p.name}`, onPress: () => host.go(p.slug), items: "center", gap: "$3", px: "$3", py: "$2.5", cursor: "pointer", style: { textAlign: 'left' }, children: [p.visibility === 'private' ? _jsx(Lock, { size: 16, opacity: 0.7 }) : _jsx(Globe, { size: 16, opacity: 0.7 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [p.visibility === 'private' ? 'Private' : 'Public', edited(when(p)), p.live ? new URL(p.live).host : 'Not deployed yet']
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(' · ') })] })] }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${p.name}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$raised' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
103
|
+
{ key: 'rename', label: 'Rename', onSelect: () => setRenaming(p) },
|
|
104
|
+
{ key: 'visibility', label: p.visibility === 'private' ? 'Make public' : 'Make private', onSelect: () => void flip(p) },
|
|
105
|
+
{ type: 'separator' },
|
|
106
|
+
{ key: 'delete', label: 'Delete', destructive: true, onSelect: () => setDeleting(p) },
|
|
107
|
+
] })] }, p.slug))) })] }, g.label))) }))] }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename", name: renaming?.name ?? '', run: async (name) => {
|
|
108
|
+
if (!renaming)
|
|
109
|
+
return;
|
|
110
|
+
await change(t, renaming.slug, { name });
|
|
111
|
+
setNote(`Renamed to ${name}`);
|
|
112
|
+
list.reload();
|
|
113
|
+
} }), _jsx(Confirm, { open: deleting !== null, onOpenChange: (o) => !o && setDeleting(null), title: `Delete ${deleting?.name ?? 'this project'}?`, says: "Its site stops answering, its releases are dropped, and its address is released. This cannot be undone.", act: "Delete", run: async () => {
|
|
114
|
+
if (!deleting)
|
|
115
|
+
return;
|
|
116
|
+
await remove(t, deleting.slug);
|
|
117
|
+
setNote(`${deleting.name} is deleted`);
|
|
118
|
+
list.reload();
|
|
119
|
+
} })] }));
|
|
120
|
+
}
|
|
121
|
+
function Shot({ slug, height = 140 }) {
|
|
122
|
+
const [gone, setGone] = useState(false);
|
|
123
|
+
return (_jsxs(YStack, { height: height, bg: "$hover", items: "center", justify: "center", position: "relative", overflow: "hidden", children: [_jsxs(YStack, { items: "center", gap: "$2", opacity: 0.6, children: [_jsx(Boxes, { size: 24 }), _jsx(SizableText, { size: "$1", color: "$soft", textTransform: "uppercase", letterSpacing: 1, children: slug })] }), gone ? null : (_jsx("img", { src: `${SHOTS}/${encodeURIComponent(slug)}.webp`, alt: "", loading: "lazy", onError: () => setGone(true), style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' } }))] }));
|
|
124
|
+
}
|
|
125
|
+
export function Templates() {
|
|
126
|
+
const t = useTarget();
|
|
127
|
+
const list = useRead(() => templates(t), [], [t.api]);
|
|
128
|
+
const { busy, failed, take } = useFork();
|
|
59
129
|
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Templates", says: "Start from a working app. Picking one copies its repository into a project of yours." }), failed ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: failed })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$3", children: list.value.map((x) => (_jsxs(Card, { label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsxs(YStack, { p: "$3", gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, flex: 1, children: busy === x.slug ? 'Copying…' : x.title }), x.framework ? (_jsx(SizableText, { size: "$1", color: "$soft", children: x.framework })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: x.description })] })] }, x.slug))) })), _jsx(YStack, { pt: "$5", children: _jsxs(Out, { href: "https://gallery.hanzo.ai", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "The whole catalog" }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })] }) }));
|
|
60
130
|
}
|
package/lib/switch.js
CHANGED
|
@@ -8,7 +8,6 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
8
8
|
*/
|
|
9
9
|
import { YStack } from '@hanzo/gui';
|
|
10
10
|
import { MenuLabel, MenuRow, MenuRule, OrgSwitcher } from '@hanzo/ui/product';
|
|
11
|
-
import { revealAccount } from './account.js';
|
|
12
11
|
import { projects } from './api/projects.js';
|
|
13
12
|
import { useRead } from './data.js';
|
|
14
13
|
import { useHost, useTarget } from './host.js';
|
|
@@ -35,6 +34,6 @@ export function Where() {
|
|
|
35
34
|
host.go(p.slug);
|
|
36
35
|
} }, p.slug))) })] })) : null, _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", onPress: () => {
|
|
37
36
|
close();
|
|
38
|
-
|
|
37
|
+
host.go('-/settings');
|
|
39
38
|
} })] })) }));
|
|
40
39
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Card } from './api/turn.ts';
|
|
3
|
+
export declare function Transcript({ cards, live, empty, header, onApprove, approving, }: {
|
|
4
|
+
cards: Card[];
|
|
5
|
+
/** The run is still working: a card still running shows it; otherwise it was cut off. */
|
|
6
|
+
live: boolean;
|
|
7
|
+
empty: ReactNode;
|
|
8
|
+
header?: ReactNode;
|
|
9
|
+
/** Build what a plan says. Absent, a plan has no button. */
|
|
10
|
+
onApprove?: (plan: string) => void;
|
|
11
|
+
approving?: boolean;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A run's transcript, drawn by what each part is (turn.ts `cards`): what the
|
|
4
|
+
* agent said as markdown, each command it ran as a card that opens onto its
|
|
5
|
+
* output, the files it changed with their diff, the files it read as chips,
|
|
6
|
+
* the run's own steps, and a plan run's plan with the button that builds it.
|
|
7
|
+
*
|
|
8
|
+
* It follows the run to the bottom as it streams and stops when the reader
|
|
9
|
+
* scrolls up (Thread). Everything drawn is text.
|
|
10
|
+
*/
|
|
11
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
+
import { FileText } from '@hanzogui/lucide-icons-2';
|
|
13
|
+
import { Button } from '@hanzo/ui';
|
|
14
|
+
import { Code, Message, Step, Thread } from '@hanzo/ui/chat';
|
|
15
|
+
import { Patch } from './git.js';
|
|
16
|
+
import { Prose } from './prose.js';
|
|
17
|
+
/** A card's output, its tail: what a command said last is why it stopped. */
|
|
18
|
+
const TAIL = 6000;
|
|
19
|
+
const tail = (s) => (s.length > TAIL ? `…${s.slice(-TAIL)}` : s);
|
|
20
|
+
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
21
|
+
/** Consecutive reads draw as one row of chips. */
|
|
22
|
+
function rows(cards) {
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const c of cards) {
|
|
25
|
+
const last = out[out.length - 1];
|
|
26
|
+
if (c.kind === 'read' && Array.isArray(last))
|
|
27
|
+
last.push(c);
|
|
28
|
+
else
|
|
29
|
+
out.push(c.kind === 'read' ? [c] : c);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export function Transcript({ cards, live, empty, header, onApprove, approving = false, }) {
|
|
34
|
+
// A card the run never finished is cut off once the run has ended, not still going.
|
|
35
|
+
const state = (ran) => (ran === 'running' && !live ? 'cancelled' : ran);
|
|
36
|
+
return (_jsxs(Thread, { maxWidth: 0, gap: 14, column: { px: 0 }, "aria-label": "Transcript", children: [header, cards.length === 0 ? _jsx(YStack, { py: "$2", children: empty }) : null, rows(cards).map((row) => {
|
|
37
|
+
if (Array.isArray(row)) {
|
|
38
|
+
return (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", children: row.map((r) => (_jsxs(XStack, { "aria-label": `Read ${r.file}`, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", maxW: "100%", children: [_jsx(FileText, { size: 12, color: "$soft" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Read" }), _jsx(SizableText, { size: "$1", color: "$ink", numberOfLines: 1, style: mono, children: r.file })] }, r.key))) }, row[0].key));
|
|
39
|
+
}
|
|
40
|
+
const c = row;
|
|
41
|
+
switch (c.kind) {
|
|
42
|
+
case 'said':
|
|
43
|
+
return c.who === 'person' ? (_jsx(Message, { role: "user", children: _jsx(SizableText, { size: "$3", color: "$ink", style: { whiteSpace: 'pre-wrap' }, children: c.text }) }, c.key)) : (_jsx(Message, { role: "assistant", children: _jsx(Prose, { text: c.text }) }, c.key));
|
|
44
|
+
case 'shell':
|
|
45
|
+
return (_jsx(Step, { name: "Shell", detail: c.command, status: state(c.ran), "aria-label": `Command ${c.command}`, children: _jsx(Code, { language: "shell", value: c.output ? `$ ${c.command}\n${c.output}` : `$ ${c.command}`, children: c.output ? `$ ${c.command}\n${tail(c.output)}` : `$ ${c.command}` }) }, c.key));
|
|
46
|
+
case 'edit':
|
|
47
|
+
return (_jsx(Step, { name: c.files.length === 1 ? 'Edited' : c.files.length ? `Edited ${c.files.length} files` : 'Edited files', detail: c.files.join(', '), status: state(c.ran), "aria-label": `Edited ${c.files.join(', ') || 'files'}`, children: c.patch ? (_jsx(YStack, { overflow: "scroll", maxH: 480, children: _jsx(Patch, { text: c.patch }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: c.files.length ? c.files.join('\n') : 'The harness named no files.' })) }, c.key));
|
|
48
|
+
case 'step':
|
|
49
|
+
return (_jsx(Step, { name: c.name, detail: c.detail, status: state(c.ran), "aria-label": `${c.name} ${c.detail}`.trim(), children: c.output.trim() ? (_jsx(Code, { language: "output", value: c.output, children: tail(c.output.replace(/\n+$/, '')) })) : undefined }, c.key));
|
|
50
|
+
case 'note':
|
|
51
|
+
return (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: c.text }, c.key));
|
|
52
|
+
case 'plan':
|
|
53
|
+
return (_jsxs(YStack, { gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", "aria-label": "Plan", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Plan" }), _jsx(Prose, { text: c.text }), onApprove ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", disabled: approving, onPress: () => onApprove(c.text), children: approving ? 'Starting the build…' : 'Approve and build' }) })) : null] }, c.key));
|
|
54
|
+
}
|
|
55
|
+
})] }));
|
|
56
|
+
}
|
package/lib/voice.d.ts
CHANGED
|
@@ -3,13 +3,15 @@ export declare const LANGUAGES: {
|
|
|
3
3
|
id: string;
|
|
4
4
|
label: string;
|
|
5
5
|
}[];
|
|
6
|
+
/** The language this browser is set to, when it is one dictation offers; English otherwise. */
|
|
7
|
+
export declare function spoken(): string;
|
|
6
8
|
export declare function useDictation(t: Target, onText: (text: string) => void, onNote: (note: string) => void): {
|
|
7
9
|
on: boolean;
|
|
8
10
|
busy: boolean;
|
|
9
11
|
able: boolean;
|
|
10
12
|
toggle: () => Promise<void>;
|
|
11
13
|
language: string;
|
|
12
|
-
setLanguage:
|
|
14
|
+
setLanguage: (id: string) => Promise<void>;
|
|
13
15
|
languages: {
|
|
14
16
|
id: string;
|
|
15
17
|
label: string;
|