@hanzo/rooms 0.1.25 → 0.1.27
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/Chat.js +34 -3
- package/dist/Settings.js +4 -4
- package/dist/Shell.js +4 -4
- package/dist/lib/limits.d.ts +9 -1
- package/dist/lib/limits.js +21 -2
- package/package.json +1 -1
package/dist/Chat.js
CHANGED
|
@@ -44,7 +44,7 @@ import { Anchor, Box, Button, Text, Tooltip, TooltipContent, TooltipTrigger, Vie
|
|
|
44
44
|
import { Control } from "@hanzo/composer";
|
|
45
45
|
import { useIam, useIamToken } from "@hanzo/iam/react";
|
|
46
46
|
import { hasSession, org } from "./lib/session.js";
|
|
47
|
-
import { chats, house, useLimits } from "./lib/limits.js";
|
|
47
|
+
import { chats, house, paidPlan, setCreditsAfterAllowance, useLimits } from "./lib/limits.js";
|
|
48
48
|
import { parseModels } from "@hanzo/ui/models/catalog";
|
|
49
49
|
import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
|
|
50
50
|
import { WEB } from "./lib/web.js";
|
|
@@ -540,7 +540,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
540
540
|
// only once the reader is turned away: a reply answered from a free model, or
|
|
541
541
|
// a refusal.
|
|
542
542
|
const { limits, notice, reload: reread } = useLimits(!anonymous, org(), (id) => nameOf(catalog, id));
|
|
543
|
-
const held =
|
|
543
|
+
const held = paidPlan(limits);
|
|
544
544
|
const pause = notice && (notice.fallback || notice.refused) ? notice : null;
|
|
545
545
|
// EVERY MODEL IS CHOSEN, never asked for: one the plan does not cover still
|
|
546
546
|
// picks, and the gateway answers it with the policy's refusal or from Enso,
|
|
@@ -829,6 +829,37 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
829
829
|
}
|
|
830
830
|
void latest.current(text);
|
|
831
831
|
}, []);
|
|
832
|
+
// THE WAYS PAST A PAUSE, as the server sends them. A page opens; Continue
|
|
833
|
+
// with credits turns the org's choice on and asks the last question again on
|
|
834
|
+
// the model the reader picked; a switch moves the picker. A refusal of the
|
|
835
|
+
// write (only an org admin may make it) is said under the banner.
|
|
836
|
+
const [actWrong, setActWrong] = useState("");
|
|
837
|
+
const act = useCallback((a) => {
|
|
838
|
+
setActWrong("");
|
|
839
|
+
if (a.kind === "switch") {
|
|
840
|
+
if (a.model)
|
|
841
|
+
choose(a.model);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (a.kind !== "credits") {
|
|
845
|
+
if (a.url)
|
|
846
|
+
window.location.assign(a.url);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
setCreditsAfterAllowance(true)
|
|
850
|
+
.then(() => {
|
|
851
|
+
reread();
|
|
852
|
+
// A refused question is asked again in its place; one a free model
|
|
853
|
+
// answered is asked once more after that answer.
|
|
854
|
+
const q = [...turns.current].reverse().find((m) => m.role === "user");
|
|
855
|
+
if (q)
|
|
856
|
+
speak(words(q.content), failures[q.id] ? q.id : undefined);
|
|
857
|
+
})
|
|
858
|
+
.catch((e) => setActWrong(e instanceof Error ? e.message : "Credits could not be turned on."));
|
|
859
|
+
},
|
|
860
|
+
// `choose` is rebuilt each render; the handler follows it.
|
|
861
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
862
|
+
[reread, speak, failures, picked, preferred, listed]);
|
|
832
863
|
// A TURN THAT NEVER SPEAKS IS STOPPED. A stream that stays open with nothing
|
|
833
864
|
// in it would leave a Stop button and no answer for as long as the upstream
|
|
834
865
|
// holds the connection; after `STALL` it is ended and its question is said to
|
|
@@ -1208,7 +1239,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1208
1239
|
},
|
|
1209
1240
|
// WHAT GOES WITH THE MESSAGE, above the frame: one chip a file, each
|
|
1210
1241
|
// with its own remove, and the reason any file was refused.
|
|
1211
|
-
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 3rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.actions, onUsage: () => showSettings("usage") })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
|
|
1242
|
+
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 3rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.actions, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
|
|
1212
1243
|
? worded(talk.refusal)
|
|
1213
1244
|
: talk.reply
|
|
1214
1245
|
? `Hanzo: ${talk.reply}`
|
package/dist/Settings.js
CHANGED
|
@@ -35,11 +35,11 @@ import { parseModels } from '@hanzo/ui/models/catalog';
|
|
|
35
35
|
import { UsagePanel } from '@hanzo/usage/panel';
|
|
36
36
|
import { Catalog } from './Directory.js';
|
|
37
37
|
import { useModel } from './model.js';
|
|
38
|
-
import { payPage } from './lib/pay.js';
|
|
38
|
+
import { checkoutUrl, payPage } from './lib/pay.js';
|
|
39
39
|
import { planName } from './lib/plans.js';
|
|
40
40
|
import { api } from './lib/api.js';
|
|
41
41
|
import { renewal, useSubscription, useTier } from './lib/tier.js';
|
|
42
|
-
import { spendShown, useLimits } from './lib/limits.js';
|
|
42
|
+
import { paidPlan, setCreditsAfterAllowance, spendShown, useLimits } from './lib/limits.js';
|
|
43
43
|
import { PlanUsage } from '@hanzo/ui/product/PlanUsage';
|
|
44
44
|
import { Look } from './look.js';
|
|
45
45
|
import { ProviderMark } from './ProviderMark.js';
|
|
@@ -238,8 +238,8 @@ function Usage({ sections, title, meters = false }) {
|
|
|
238
238
|
if (!isAuthenticated || !accessToken) {
|
|
239
239
|
return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your usage." });
|
|
240
240
|
}
|
|
241
|
-
const plan = meters && read.limits ? _jsx(PlanUsage, { limits: read.limits, plan: planName(read.limits.plan) || undefined, notice: read.notice }) : null;
|
|
242
|
-
if (read.limits
|
|
241
|
+
const plan = meters && read.limits ? (_jsx(PlanUsage, { limits: read.limits, plan: planName(read.limits.plan) || undefined, notice: read.notice, onCredits: (on) => setCreditsAfterAllowance(on).then(read.reload), creditsHref: payPage(), addCreditsHref: checkoutUrl() })) : null;
|
|
242
|
+
if (paidPlan(read.limits) || !spendShown(read))
|
|
243
243
|
return plan;
|
|
244
244
|
// The address is read HERE rather than at import: the panel sends the bearer
|
|
245
245
|
// with every read, and a credentialed read to an absolute address from a page
|
package/dist/Shell.js
CHANGED
|
@@ -761,7 +761,7 @@ function Bots({ show, owner, filter = "", onPick, navigate, }) {
|
|
|
761
761
|
return (_jsxs(YStack, { children: [_jsxs(XStack, { items: "center", gap: "$1.5", px: "$3", py: "$1", rounded: "$2", hoverStyle: { bg: "$hover" }, children: [_jsx(Box, { render: "button", onClick: () => navigate?.(show === "agents" ? "/bots" : "/contacts"), flex: 1, minW: 0, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Text, { fontSize: "$2", color: "$quiet", numberOfLines: 1, children: show === "agents" ? "Agents" : "People" }), folk.length + shown.length > 0 ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: folk.length + shown.length })) : null] }) }), _jsx(Box, { render: "button", onClick: (e) => {
|
|
762
762
|
e.stopPropagation();
|
|
763
763
|
setSortMode((m) => (m === "alpha" ? "recent" : "alpha"));
|
|
764
|
-
}, title: sortMode === "alpha" ? "Sorting A-Z (Click for Recent)" : "Sorting Recent (Click for A-Z)", px: "$1.5", py: "$0.5", rounded: "$1", bg: "$raised", hoverStyle: { opacity: 0.8 }, children: _jsx(Text, { fontSize:
|
|
764
|
+
}, title: sortMode === "alpha" ? "Sorting A-Z (Click for Recent)" : "Sorting Recent (Click for A-Z)", px: "$1.5", py: "$0.5", rounded: "$1", bg: "$raised", hoverStyle: { opacity: 0.8 }, display: "flex", items: "center", justify: "center", $touchable: { minH: 40, minW: 40 }, children: _jsx(Text, { fontSize: 12, fontWeight: "600", color: "$soft", children: sortMode === "alpha" ? "A-Z" : "Recent" }) }), show === "agents" ? (_jsx(Box, { render: "button", onClick: () => {
|
|
765
765
|
if (busy)
|
|
766
766
|
return;
|
|
767
767
|
setOpen(true);
|
|
@@ -940,8 +940,8 @@ function Rail({ mode, places, team, open, navigate, onSettings, onMore, }) {
|
|
|
940
940
|
overflow: "hidden",
|
|
941
941
|
}, children: [places.map((place, i) => {
|
|
942
942
|
const here = place.id === mode;
|
|
943
|
-
return (_jsxs(Box, { render: "button", onClick: () => navigate?.(place.route), "aria-label": place.label, "aria-current": here ? "page" : undefined, title: place.title, display: "flex", "$max-md": { display: i < 4 ? "flex" : "none", width: "auto", minW: 0 }, flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, bg: here ? "$edge" : undefined, hoverStyle: { bg: here ? "$edge" : "$hover" }, children: [_jsx(place.icon, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize:
|
|
944
|
-
}), team ? (_jsx(XStack, { "$max-md": { display: places.length < 4 ? "flex" : "none", width: "auto", minW: 0 }, children: _jsx(Text, { render: _jsx("a", { href: team, "aria-label": "Workspace", title: "Hanzo Team" }), display: "flex", width: 56, color: "inherit", children: _jsxs(Box, { display: "flex", flex: 1, flexDirection: "column", minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(LayoutGrid, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize:
|
|
943
|
+
return (_jsxs(Box, { render: "button", onClick: () => navigate?.(place.route), "aria-label": place.label, "aria-current": here ? "page" : undefined, title: place.title, display: "flex", "$max-md": { display: i < 4 ? "flex" : "none", width: "auto", minW: 0 }, flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, bg: here ? "$edge" : undefined, hoverStyle: { bg: here ? "$edge" : "$hover" }, children: [_jsx(place.icon, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 12, lineHeight: 14, color: here ? "$ink" : "$quiet", numberOfLines: 1, children: place.label })] }, place.id));
|
|
944
|
+
}), team ? (_jsx(XStack, { "$max-md": { display: places.length < 4 ? "flex" : "none", width: "auto", minW: 0 }, children: _jsx(Text, { render: _jsx("a", { href: team, "aria-label": "Workspace", title: "Hanzo Team" }), display: "flex", width: 56, color: "inherit", children: _jsxs(Box, { display: "flex", flex: 1, flexDirection: "column", minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(LayoutGrid, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 12, lineHeight: 14, color: "$quiet", numberOfLines: 1, children: "Workspace" })] }) }) })) : null, _jsxs(Box, { "data-slot": "more", render: "button", onClick: onMore, "aria-label": "More places", title: "More", display: "none", "$max-md": { display: "flex", width: "auto", minW: 0 }, flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(Search, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 12, lineHeight: 14, color: "$quiet", children: "More" })] })] }), _jsx(YStack, { items: "center", pb: "$2", children: _jsxs(Box, { render: "button", onClick: onSettings, "aria-label": "Settings", title: "Settings", display: "flex", flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(Settings2, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 12, lineHeight: 14, color: "$quiet", children: "Settings" })] }) })] }));
|
|
945
945
|
}
|
|
946
946
|
/**
|
|
947
947
|
* Chat's column: start a chat, the projects, then every conversation. The app
|
|
@@ -1210,7 +1210,7 @@ function Frame({ mode, children, navigate, back, forward, }) {
|
|
|
1210
1210
|
return;
|
|
1211
1211
|
e.preventDefault();
|
|
1212
1212
|
navigate("/");
|
|
1213
|
-
}, "aria-label": `${brand()} home`, title: "Home" }), "data-slot": "home", display: "flex", items: "center", justify: "center", width: 28, height: 28, color: "inherit", children: _jsx(HanzoMark, { size: 18 }) }), settled && isAuthenticated ? (_jsx(XStack, { shrink: 1, minW: 0, children: _jsx(OrgProjectSwitcher, { ...orgState, alwaysShow: true }) })) : null] }), _jsxs(YStack, { "data-slot": "places-list", px: "$2", pb: "$2", "$max-md": { display: "none" }, children: [places.filter((place) => matches(place.label, filter)).map((place) => (_jsx(SidebarItem, { icon: _jsx(place.icon, { size: 16, "aria-hidden": true }), active: place.id === mode, onPress: () => {
|
|
1213
|
+
}, "aria-label": `${brand()} home`, title: "Home" }), "data-slot": "home", display: "flex", items: "center", justify: "center", width: 28, height: 28, $touchable: { width: 40, height: 40 }, color: "inherit", children: _jsx(HanzoMark, { size: 18 }) }), settled && isAuthenticated ? (_jsx(XStack, { shrink: 1, minW: 0, children: _jsx(OrgProjectSwitcher, { ...orgState, alwaysShow: true }) })) : null] }), _jsxs(YStack, { "data-slot": "places-list", px: "$2", pb: "$2", "$max-md": { display: "none" }, children: [places.filter((place) => matches(place.label, filter)).map((place) => (_jsx(SidebarItem, { icon: _jsx(place.icon, { size: 16, "aria-hidden": true }), active: place.id === mode, onPress: () => {
|
|
1214
1214
|
navigate?.(place.route);
|
|
1215
1215
|
onPick?.();
|
|
1216
1216
|
}, children: place.label }, place.id))), matches("Settings", filter) ? (_jsx(SidebarItem, { icon: _jsx(Settings2, { size: 16, "aria-hidden": true }), onPress: () => showSettings(FIRST), children: "Settings" })) : null] }), _jsx(XStack, { px: "$3", pb: "$2", children: _jsxs(XStack, { flex: 1, height: 30, items: "center", gap: "$2", px: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Search, { size: 13, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: filter, onChange: (e) => setFilter(e.target.value), onKeyDown: (e) => {
|
package/dist/lib/limits.d.ts
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
* bearer is the subject and `X-Org-Id` the organization this browser works in.
|
|
8
8
|
*/
|
|
9
9
|
import { type UseLimits } from '@hanzo/ui/product/useLimits';
|
|
10
|
+
import { paidPlan } from '@hanzo/ui/product/limits';
|
|
10
11
|
/** Reads the limits as whoever holds the bearer, in this browser's organization. */
|
|
11
12
|
export declare function readLimits(signal: AbortSignal): Promise<unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* The org's choice to keep paying from credits once included usage runs out —
|
|
15
|
+
* `PUT /v1/ai/limits`, org admins only. A refusal throws with the server's own
|
|
16
|
+
* words, so the reader is told why.
|
|
17
|
+
*/
|
|
18
|
+
export declare function setCreditsAfterAllowance(on: boolean): Promise<void>;
|
|
12
19
|
/**
|
|
13
20
|
* The limits for a signed-in reader, in organization `org`; `name` turns a
|
|
14
21
|
* fallback model id into the name the reader knows. Signed out reads nothing,
|
|
@@ -17,9 +24,10 @@ export declare function readLimits(signal: AbortSignal): Promise<unknown>;
|
|
|
17
24
|
export declare const useLimits: (enabled?: boolean, org?: string | null, name?: (id: string) => string) => UseLimits;
|
|
18
25
|
/**
|
|
19
26
|
* Whether a reader's dollar spend may be shown: their limits came back and name no
|
|
20
|
-
* plan, or could not be read. A plan holder sees usage as shares, never money.
|
|
27
|
+
* paid plan, or could not be read. A plan holder sees usage as shares, never money.
|
|
21
28
|
*/
|
|
22
29
|
export declare const spendShown: ({ limits, answered }: Pick<UseLimits, 'limits' | 'answered'>) => boolean;
|
|
30
|
+
export { paidPlan };
|
|
23
31
|
/** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
|
|
24
32
|
export declare const house: (family: string | undefined) => boolean;
|
|
25
33
|
/**
|
package/dist/lib/limits.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* bearer is the subject and `X-Org-Id` the organization this browser works in.
|
|
9
9
|
*/
|
|
10
10
|
import { useLimits as useHeld } from '@hanzo/ui/product/useLimits';
|
|
11
|
+
import { paidPlan } from '@hanzo/ui/product/limits';
|
|
11
12
|
import { api } from './api.js';
|
|
12
13
|
import { scope } from './session.js';
|
|
13
14
|
/** Reads the limits as whoever holds the bearer, in this browser's organization. */
|
|
@@ -17,6 +18,23 @@ export async function readLimits(signal) {
|
|
|
17
18
|
throw Object.assign(new Error(`Limits answered ${res.status}`), { status: res.status });
|
|
18
19
|
return res.json();
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The org's choice to keep paying from credits once included usage runs out —
|
|
23
|
+
* `PUT /v1/ai/limits`, org admins only. A refusal throws with the server's own
|
|
24
|
+
* words, so the reader is told why.
|
|
25
|
+
*/
|
|
26
|
+
export async function setCreditsAfterAllowance(on) {
|
|
27
|
+
const res = await fetch(`${api()}/v1/ai/limits`, {
|
|
28
|
+
method: 'PUT',
|
|
29
|
+
headers: { ...scope(), 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({ creditsAfterAllowance: on }),
|
|
31
|
+
});
|
|
32
|
+
if (res.ok)
|
|
33
|
+
return;
|
|
34
|
+
const body = (await res.json().catch(() => null));
|
|
35
|
+
const said = typeof body?.error === 'string' ? body.error : typeof body?.error?.message === 'string' ? body.error.message : typeof body?.message === 'string' ? body.message : '';
|
|
36
|
+
throw Object.assign(new Error(said || (res.status === 403 ? 'Only an organization admin can turn on credits.' : `Credits answered ${res.status}`)), { status: res.status });
|
|
37
|
+
}
|
|
20
38
|
/**
|
|
21
39
|
* The limits for a signed-in reader, in organization `org`; `name` turns a
|
|
22
40
|
* fallback model id into the name the reader knows. Signed out reads nothing,
|
|
@@ -25,9 +43,10 @@ export async function readLimits(signal) {
|
|
|
25
43
|
export const useLimits = (enabled = true, org = null, name) => useHeld(enabled ? readLimits : null, org, name);
|
|
26
44
|
/**
|
|
27
45
|
* Whether a reader's dollar spend may be shown: their limits came back and name no
|
|
28
|
-
* plan, or could not be read. A plan holder sees usage as shares, never money.
|
|
46
|
+
* paid plan, or could not be read. A plan holder sees usage as shares, never money.
|
|
29
47
|
*/
|
|
30
|
-
export const spendShown = ({ limits, answered }) => answered && !limits
|
|
48
|
+
export const spendShown = ({ limits, answered }) => answered && !paidPlan(limits);
|
|
49
|
+
export { paidPlan };
|
|
31
50
|
/** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
|
|
32
51
|
export const house = (family) => family === 'enso' || family === 'zen';
|
|
33
52
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|