@hanzo/rooms 0.1.24 → 0.1.26
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 +64 -22
- package/dist/Settings.js +13 -16
- package/dist/Shell.js +4 -2
- package/dist/Vibe.js +8 -7
- package/dist/enso.d.ts +7 -5
- package/dist/enso.js +3 -3
- package/dist/lib/limits.d.ts +11 -3
- package/dist/lib/limits.js +23 -4
- package/dist/lib/plans.d.ts +0 -2
- package/dist/lib/plans.js +0 -11
- package/package.json +5 -5
package/dist/Chat.js
CHANGED
|
@@ -44,7 +44,8 @@ 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,
|
|
47
|
+
import { chats, house, paidPlan, setCreditsAfterAllowance, useLimits } from "./lib/limits.js";
|
|
48
|
+
import { parseModels } from "@hanzo/ui/models/catalog";
|
|
48
49
|
import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
|
|
49
50
|
import { WEB } from "./lib/web.js";
|
|
50
51
|
import { useHydrated } from "./lib/hydrated.js";
|
|
@@ -63,7 +64,6 @@ import { useStarred } from "./stars.js";
|
|
|
63
64
|
import { useModel } from "./model.js";
|
|
64
65
|
import { Enso, nameOf, useEffort } from "./enso.js";
|
|
65
66
|
import { checkoutUrl } from './lib/pay.js';
|
|
66
|
-
import { onFree } from './lib/plans.js';
|
|
67
67
|
import { Upgrade, useFree } from './Upgrade.js';
|
|
68
68
|
import { enter, LOGIN } from './lib/destination.js';
|
|
69
69
|
import { first } from './lib/first.js';
|
|
@@ -518,32 +518,43 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
518
518
|
// outputs and names no text (`chats`) is neither offered nor fallen back to.
|
|
519
519
|
const { models: catalog } = useModels();
|
|
520
520
|
const models = useMemo(() => catalog.filter(chats), [catalog]);
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
521
|
+
// EVERY MODEL THE GATEWAY LISTS is offered here, through the one picker, and
|
|
522
|
+
// read from the catalog's own fields (`parseModels`): its class, its family.
|
|
523
|
+
// A premium model is picked for THIS conversation and never remembered as
|
|
524
|
+
// the default, so the next chat starts on Enso again; any other pick is the
|
|
525
|
+
// reader's default. A preference kept from before for a premium model is
|
|
526
|
+
// not a default either, so the house router is asked for instead. An agent's
|
|
527
|
+
// own model is sent as it is named; the gateway says which model answered
|
|
528
|
+
// (`servedBy`).
|
|
529
|
+
const listed = useMemo(() => parseModels(catalog), [catalog]);
|
|
530
|
+
const rowOf = (id) => listed.find((m) => m.id === id);
|
|
531
|
+
const [picked, setPicked] = useState(null);
|
|
532
|
+
const wanted = picked ?? (preferred && rowOf(preferred)?.class !== "premium" ? preferred : ENSO);
|
|
533
|
+
// A model the catalog lists is sent as picked, whatever it answers with; only
|
|
534
|
+
// a name the catalog does not list falls to one that converses (`served`).
|
|
535
|
+
const sent = anonymous ? FREE : wanted;
|
|
536
|
+
const model = persona?.model || (rowOf(sent) ? sent : served(models, sent));
|
|
530
537
|
// THE PLAN'S USAGE (lib/limits.ts), kept current by every chat answer's
|
|
531
538
|
// headers and read again whenever a send settles, below. The room draws no
|
|
532
539
|
// bars and no near note — those live on Settings → Usage. It says something
|
|
533
540
|
// only once the reader is turned away: a reply answered from a free model, or
|
|
534
541
|
// a refusal.
|
|
535
542
|
const { limits, notice, reload: reread } = useLimits(!anonymous, org(), (id) => nameOf(catalog, id));
|
|
536
|
-
const held =
|
|
543
|
+
const held = paidPlan(limits);
|
|
537
544
|
const pause = notice && (notice.fallback || notice.refused) ? notice : null;
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
// the
|
|
542
|
-
//
|
|
543
|
-
// `isFree` still drives the free-lane notice.
|
|
545
|
+
// EVERY MODEL IS CHOSEN, never asked for: one the plan does not cover still
|
|
546
|
+
// picks, and the gateway answers it with the policy's refusal or from Enso,
|
|
547
|
+
// which the room then shows. A turn the gateway refuses `plan_required`
|
|
548
|
+
// opens the upgrade, titled with the model. `isFree` still drives the
|
|
549
|
+
// free-lane notice.
|
|
544
550
|
const free = useFree(!anonymous) && !held;
|
|
545
551
|
const [ask, setAsk] = useState(null);
|
|
546
|
-
const choose = (id) =>
|
|
552
|
+
const choose = (id) => {
|
|
553
|
+
if (rowOf(id)?.class === "premium")
|
|
554
|
+
return setPicked(id);
|
|
555
|
+
setPicked(null);
|
|
556
|
+
setPreferred(id);
|
|
557
|
+
};
|
|
547
558
|
// KEEPING THE CONVERSATION is the reader's account, not this pane's choice:
|
|
548
559
|
// the store is per-org, so a signed-in reader has somewhere for it to go and
|
|
549
560
|
// an anonymous one does not. `thread` names the conversation being continued
|
|
@@ -560,7 +571,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
560
571
|
// lookups the platform's assistant uses, run inside the completion by the
|
|
561
572
|
// client's fetch (lib/web.ts), so a question about the weather or the news
|
|
562
573
|
// is answered from what the web says now.
|
|
563
|
-
params: { reasoning_effort: effort, ...(!anonymous &&
|
|
574
|
+
params: { reasoning_effort: effort, ...(!anonymous && house(rowOf(model)?.family) ? { tools: WEB } : {}) },
|
|
564
575
|
...(open ? { thread: open } : {}),
|
|
565
576
|
onError: (e) => {
|
|
566
577
|
const needs = planRequired(e);
|
|
@@ -818,6 +829,37 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
818
829
|
}
|
|
819
830
|
void latest.current(text);
|
|
820
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]);
|
|
821
863
|
// A TURN THAT NEVER SPEAKS IS STOPPED. A stream that stays open with nothing
|
|
822
864
|
// in it would leave a Stop button and no answer for as long as the upstream
|
|
823
865
|
// holds the connection; after `STALL` it is ended and its question is said to
|
|
@@ -1197,14 +1239,14 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1197
1239
|
},
|
|
1198
1240
|
// WHAT GOES WITH THE MESSAGE, above the frame: one chip a file, each
|
|
1199
1241
|
// with its own remove, and the reason any file was refused.
|
|
1200
|
-
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
|
|
1201
1243
|
? worded(talk.refusal)
|
|
1202
1244
|
: talk.reply
|
|
1203
1245
|
? `Hanzo: ${talk.reply}`
|
|
1204
1246
|
: talk.state === "listening"
|
|
1205
1247
|
? "Listening — speak, and Hanzo answers aloud."
|
|
1206
1248
|
: "Connecting…" })) : null] })) : undefined,
|
|
1207
|
-
send: (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Enso, { effort: effort, onEffort: setEffort, model: wanted, onModel: choose, models:
|
|
1249
|
+
send: (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Enso, { effort: effort, onEffort: setEffort, model: wanted, onModel: choose, models: listed, limits: limits }), _jsx(Control, { asChild: true, size: ROUND, children: _jsx(Voice, { voice: voice, says: { idle: "Dictate", listening: "Dictating — click to stop" }, children: (state) => (_jsx(Mic, { size: 15, fill: state === "idle" ? "none" : "currentColor" })) }) }), _jsx(Control, { asChild: true, size: ROUND, children: _jsx(Voice, { voice: talk, "data-slot": "composer-talk", says: {
|
|
1208
1250
|
idle: "Talk with Hanzo",
|
|
1209
1251
|
listening: "In conversation — click to hang up",
|
|
1210
1252
|
speaking: "Hanzo is speaking — talk to interrupt",
|
package/dist/Settings.js
CHANGED
|
@@ -30,15 +30,16 @@ import { tap } from './lib/tap.js';
|
|
|
30
30
|
import { useIam, useIamIdentity, useOrganizations } from '@hanzo/iam/react';
|
|
31
31
|
import { useSignOut } from './lib/signout.js';
|
|
32
32
|
import { useModels } from '@hanzo/ai/react';
|
|
33
|
-
import {
|
|
33
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
34
|
+
import { parseModels } from '@hanzo/ui/models/catalog';
|
|
34
35
|
import { UsagePanel } from '@hanzo/usage/panel';
|
|
35
36
|
import { Catalog } from './Directory.js';
|
|
36
37
|
import { useModel } from './model.js';
|
|
37
|
-
import { payPage } from './lib/pay.js';
|
|
38
|
+
import { checkoutUrl, payPage } from './lib/pay.js';
|
|
38
39
|
import { planName } from './lib/plans.js';
|
|
39
40
|
import { api } from './lib/api.js';
|
|
40
41
|
import { renewal, useSubscription, useTier } from './lib/tier.js';
|
|
41
|
-
import {
|
|
42
|
+
import { paidPlan, setCreditsAfterAllowance, spendShown, useLimits } from './lib/limits.js';
|
|
42
43
|
import { PlanUsage } from '@hanzo/ui/product/PlanUsage';
|
|
43
44
|
import { Look } from './look.js';
|
|
44
45
|
import { ProviderMark } from './ProviderMark.js';
|
|
@@ -169,20 +170,16 @@ function Pane({ id, note }) {
|
|
|
169
170
|
return _jsx(MemorySettings, {});
|
|
170
171
|
return (_jsx(Text, { fontSize: "$3", color: "$soft", children: note ?? 'Nothing to configure here yet.' }));
|
|
171
172
|
}
|
|
172
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* One default in one place; agents may still bring their own model. A premium
|
|
175
|
+
* model is never a default: it is picked per conversation, in the composer, so
|
|
176
|
+
* this picker offers the rest of the catalog.
|
|
177
|
+
*/
|
|
173
178
|
function Model() {
|
|
174
179
|
const { models: served } = useModels();
|
|
175
|
-
|
|
176
|
-
const models = useMemo(() => served.filter((m) => sku(m.id)), [served]);
|
|
180
|
+
const models = useMemo(() => parseModels(served).filter((m) => m.class !== 'premium'), [served]);
|
|
177
181
|
const [model, choose] = useModel();
|
|
178
|
-
return (_jsxs(YStack, { gap: "$3", maxW: 420, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Used for new chats. An agent with its own model keeps that choice." }), _jsx(XStack, { self: "flex-start", children: _jsx(
|
|
179
|
-
.filter((m) => !m.id.includes('openrouter'))
|
|
180
|
-
.slice(0, 8)
|
|
181
|
-
.map((m) => {
|
|
182
|
-
const isSel = model === m.id;
|
|
183
|
-
const p = m.id.startsWith('zen') ? 'zen' : 'enso';
|
|
184
|
-
return (_jsx(Box, { render: "button", onClick: () => choose(m.id), px: "$3", py: "$2", rounded: "$2", borderWidth: 1, borderColor: isSel ? '$ink' : '$borderColor', bg: isSel ? '$raised' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(ProviderMark, { provider: p, size: 16 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: m.id }), isSel ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: "Active" })) : null] }) }, m.id));
|
|
185
|
-
}) })] }));
|
|
182
|
+
return (_jsxs(YStack, { gap: "$3", maxW: 420, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Used for new chats. An agent with its own model keeps that choice. Premium models are picked per conversation in the composer." }), _jsx(XStack, { self: "flex-start", children: _jsx(ModelPicker, { models: models, scope: "chat", value: model, onChange: choose, size: "sm" }) })] }));
|
|
186
183
|
}
|
|
187
184
|
/**
|
|
188
185
|
* WHAT THE GATEWAY SERVES, read from the same `/v1/models` the rooms read.
|
|
@@ -241,8 +238,8 @@ function Usage({ sections, title, meters = false }) {
|
|
|
241
238
|
if (!isAuthenticated || !accessToken) {
|
|
242
239
|
return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your usage." });
|
|
243
240
|
}
|
|
244
|
-
const plan = meters && read.limits ? _jsx(PlanUsage, { limits: read.limits, plan: planName(read.limits.plan) || undefined, notice: read.notice }) : null;
|
|
245
|
-
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))
|
|
246
243
|
return plan;
|
|
247
244
|
// The address is read HERE rather than at import: the panel sends the bearer
|
|
248
245
|
// with every read, and a credentialed read to an absolute address from a page
|
package/dist/Shell.js
CHANGED
|
@@ -36,7 +36,8 @@ import { hasSession, superAdmin } from "./lib/session.js";
|
|
|
36
36
|
import { enter, LOGIN } from "./lib/destination.js";
|
|
37
37
|
import { address, sharedWithMe } from "./lib/share.js";
|
|
38
38
|
import { list } from "./lib/list.js";
|
|
39
|
-
import {
|
|
39
|
+
import { ModelPicker } from "@hanzo/ui/models";
|
|
40
|
+
import { parseModels } from "@hanzo/ui/models/catalog";
|
|
40
41
|
import { mark } from "./lib/mark.js";
|
|
41
42
|
import { useOpen } from "./open.js";
|
|
42
43
|
import { useSpan } from "./span.js";
|
|
@@ -667,6 +668,7 @@ function Bots({ show, owner, filter = "", onPick, navigate, }) {
|
|
|
667
668
|
const [wrong, setWrong] = useState(null);
|
|
668
669
|
const picking = useRef(null);
|
|
669
670
|
const { models } = useModels();
|
|
671
|
+
const listed = useMemo(() => parseModels(models), [models]);
|
|
670
672
|
const allPeopleMap = new Map();
|
|
671
673
|
for (const p of (Array.isArray(storedPeople) ? storedPeople : []))
|
|
672
674
|
if (p?.id)
|
|
@@ -776,7 +778,7 @@ function Bots({ show, owner, filter = "", onPick, navigate, }) {
|
|
|
776
778
|
setEmoji(e.target.value);
|
|
777
779
|
if (e.target.value)
|
|
778
780
|
setAvatar("");
|
|
779
|
-
}, placeholder: "\uD83D\uDE42", "aria-label": "Emoji" }), ...FIELD, width: 44, text: "center", px: 4 }),
|
|
781
|
+
}, placeholder: "\uD83D\uDE42", "aria-label": "Emoji" }), ...FIELD, width: 44, text: "center", px: 4 }), _jsx(YStack, { flex: 1, minW: 0, children: _jsx(ModelPicker, { models: listed, scope: "chat", value: model, onChange: setModel, size: "sm" }) }), _jsx(Box, { render: "button", onClick: () => {
|
|
780
782
|
if (!busy)
|
|
781
783
|
void add();
|
|
782
784
|
}, "aria-disabled": busy || !name.trim(), opacity: busy || !name.trim() ? 0.5 : 1, px: "$2.5", py: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: "$raised" }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: busy ? "Adding" : "Add" }) })] }), wrong ? (_jsx(Text, { fontSize: "$1", color: BAD, numberOfLines: 2, children: wrong })) : null] })) : null, open ? (_jsxs(YStack, { pl: "$4", gap: "$0.5", children: [broke ? (_jsx(Note, { children: say(broke, "this org's roster") })) : waiting && empty ? (_jsx(Note, { children: "Reading the roster." })) : empty ? (_jsx(Note, { children: show === "agents" ? "No agents yet. Add one with +." : "No one here yet. Invite a teammate." })) : null, folk.map((person) => {
|
package/dist/Vibe.js
CHANGED
|
@@ -26,9 +26,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
26
26
|
import { useRooms } from './host.js';
|
|
27
27
|
import { ArrowUp, Globe, Monitor, RefreshCw, Smartphone, Sparkles, Tablet, UserPlus, Users, } from 'lucide-react';
|
|
28
28
|
import { View, XStack, YStack, Text } from '@hanzo/gui';
|
|
29
|
-
import { Button
|
|
29
|
+
import { Button } from '@hanzo/ui';
|
|
30
30
|
import { AiProvider, useAgents, useModels, usePeople } from '@hanzo/ai/react';
|
|
31
|
-
import {
|
|
31
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
32
|
+
import { parseModels } from '@hanzo/ui/models/catalog';
|
|
32
33
|
import { useIam, useOrganizations } from '@hanzo/iam/react';
|
|
33
34
|
import { hasSession } from './lib/session.js';
|
|
34
35
|
import { useHydrated } from './lib/hydrated.js';
|
|
@@ -122,9 +123,9 @@ function VibeWorkspaceInner() {
|
|
|
122
123
|
function Vibe() {
|
|
123
124
|
const { Link } = useRooms();
|
|
124
125
|
const { user } = useIam();
|
|
125
|
-
//
|
|
126
|
-
const { models:
|
|
127
|
-
const models = useMemo(() =>
|
|
126
|
+
// Every model the gateway lists, through the one picker.
|
|
127
|
+
const { models: catalog } = useModels();
|
|
128
|
+
const models = useMemo(() => parseModels(catalog), [catalog]);
|
|
128
129
|
const [activeTabMobile, setActiveTabMobile] = useState('stream');
|
|
129
130
|
// The frame shows what the bar says. It starts empty because a run publishes
|
|
130
131
|
// no URL — the 202 names a session, not an address — so this room has nothing
|
|
@@ -136,7 +137,7 @@ function Vibe() {
|
|
|
136
137
|
// The house name for the lane, resolved against what the gateway serves —
|
|
137
138
|
// `served` falls to a model that answers rather than sending one nobody has.
|
|
138
139
|
const [wanted, setWanted] = useState(user ? ENSO : FREE);
|
|
139
|
-
const model = served(models, wanted);
|
|
140
|
+
const model = models.some((m) => m.id === wanted) ? wanted : served(models, wanted);
|
|
140
141
|
const [busy, setBusy] = useState(false);
|
|
141
142
|
const [turns, setTurns] = useState([]);
|
|
142
143
|
const bottom = useRef(null);
|
|
@@ -168,7 +169,7 @@ function Vibe() {
|
|
|
168
169
|
};
|
|
169
170
|
return (_jsxs(YStack, { "data-vibe": true, flex: 1, minH: 0, height: "100vh", width: "100%", maxW: "100%", overflowX: "auto", overflowY: "hidden", bg: "$background", pb: 56, children: [_jsxs(XStack, { height: 52, items: "center", justify: "space-between", px: "$3.5", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", z: 30, shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(TopOrgSelector, {}), _jsxs(XStack, { display: "none", $md: { display: 'flex' }, items: "center", gap: "$2", py: 4, px: 10, rounded: 20, bg: "$panel", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(View, { width: 7, height: 7, rounded: 4, bg: busy ? 'var(--state-online)' : '$faint' }), _jsx(Text, { fontSize: 11, color: "$quiet", fontWeight: "500", children: busy ? 'Starting a run' : 'Idle' })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { $md: { display: 'none' }, bg: "$panel", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", children: ['stream', 'preview', 'team'].map((tab) => (_jsx(Button, { size: "sm", px: "$2", variant: activeTabMobile === tab ? 'secondary' : 'ghost', color: activeTabMobile === tab ? '$ink' : '$soft', "aria-pressed": activeTabMobile === tab, onClick: () => setActiveTabMobile(tab), children: tab === 'stream' ? 'Stream' : tab === 'preview' ? 'Preview' : 'Team' }, tab))) }), _jsx(Button, { asChild: true, variant: "primary", size: "sm", children: _jsxs(Link, { href: invite(), children: [_jsx(UserPlus, { size: 13 }), "Invite"] }) })] })] }), _jsxs(View, { flex: 1, minH: 0, width: "100%", height: "100%", overflow: "hidden", display: "grid", gridTemplateRows: "100%", gridTemplateColumns: "100%", $md: { gridTemplateColumns: 'minmax(320px, 380px) minmax(0, 1fr)' }, $lg: { gridTemplateColumns: 'minmax(340px, 400px) minmax(360px, 1fr) minmax(300px, 340px)' }, $xl: { gridTemplateColumns: 'minmax(380px, 440px) minmax(480px, 1fr) minmax(340px, 380px)' }, children: [_jsxs(YStack, { height: "100%", borderRightWidth: 1, borderColor: "$borderColor", bg: "$background", minW: 0, display: activeTabMobile === 'stream' ? 'flex' : 'none', $md: { display: 'flex' }, children: [_jsxs(XStack, { height: 42, items: "center", gap: "$2", px: "$3.5", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Sparkles, { size: 14, color: "var(--foreground)" }), _jsx(Text, { fontSize: 12, fontWeight: "600", color: "$ink", children: "Runs" }), turns.length > 0 && (_jsx(View, { px: 6, py: 2, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", children: turns.length }) }))] }), _jsxs(YStack, { flex: 1, overflowY: "auto", overflowX: "hidden", p: "$3.5", gap: "$4", children: [turns.length === 0 ? (_jsx(Quiet, { children: "Nothing has been sent from this room yet. Describe a task below and the run it opens appears here." })) : null, turns.map((turn) => (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(XStack, { gap: "$2.5", children: [_jsx(View, { width: 24, height: 24, rounded: 12, bg: "$raised", items: "center", justify: "center", shrink: 0, children: _jsx(Text, { fontSize: 10, fontWeight: "700", color: "$ink", children: turn.who.charAt(0).toUpperCase() }) }), _jsxs(YStack, { flex: 1, gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { fontSize: 12, fontWeight: "700", color: "$ink", children: turn.who }), _jsx(Text, { fontSize: 11, color: "$soft", children: turn.time }), _jsx(View, { px: 5, py: 1, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", fontFamily: "$mono", children: turn.model }) })] }), _jsx(Text, { fontSize: 12, color: "$quiet", lineHeight: 18, children: turn.prompt })] })] }), _jsx(YStack, { ml: "$4", gap: "$1.5", children: turn.refused ? (_jsx(Text, { fontSize: 11, color: "$ink", lineHeight: 16, children: turn.refused })) : !turn.run ? (_jsx(Quiet, { children: "Starting." })) : (_jsxs(YStack, { p: "$2", rounded: 8, bg: "$panel", borderWidth: 1, borderColor: "$borderColor", gap: "$1", children: [_jsx(Text, { fontSize: 11, color: "$quiet", fontFamily: "$mono", children: turn.run.sessionId }), _jsxs(Text, { fontSize: 11, color: "$soft", children: [turn.run.routed
|
|
170
171
|
? `Running on ${turn.run.targetId}`
|
|
171
|
-
: 'Running in a sandbox', turn.run.branch ? ` · ${turn.run.branch}` : ''] }), _jsx(Text, { render: _jsx(Link, { href: dev(turn.run.sessionId) }), color: "$ink", fontSize: "$1", children: "Watch it work" })] })) })] }, turn.id))), _jsx("div", { ref: bottom })] }), _jsxs(YStack, { p: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", gap: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [
|
|
172
|
+
: 'Running in a sandbox', turn.run.branch ? ` · ${turn.run.branch}` : ''] }), _jsx(Text, { render: _jsx(Link, { href: dev(turn.run.sessionId) }), color: "$ink", fontSize: "$1", children: "Watch it work" })] })) })] }, turn.id))), _jsx("div", { ref: bottom })] }), _jsxs(YStack, { p: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", gap: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(ModelPicker, { models: models, scope: "chat", value: model, onChange: setWanted, size: "sm", quiet: true }), _jsx(Text, { fontSize: 10, color: "$faint", children: "Press Enter to send" })] }), _jsxs(YStack, { bg: "$panel", borderWidth: 1, borderColor: "var(--border-control)", rounded: 12, overflow: "hidden", children: [_jsx(Text, { render: _jsx("textarea", { value: agentInput, onChange: (e) => setAgentInput(e.target.value), onKeyDown: (e) => {
|
|
172
173
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
173
174
|
e.preventDefault();
|
|
174
175
|
send();
|
package/dist/enso.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ModelCatalogEntry, type PauseSource } from '@hanzo/ui/models';
|
|
1
2
|
/**
|
|
2
3
|
* How hard to think — and, one press inside, what thinks.
|
|
3
4
|
*
|
|
@@ -45,7 +46,7 @@ export type EffortId = (typeof EFFORTS)[number]['id'];
|
|
|
45
46
|
*/
|
|
46
47
|
export declare function useEffort(): readonly [EffortId, (next: EffortId) => void];
|
|
47
48
|
/** The name a served model goes by: its row's label, its name, or its id made readable. */
|
|
48
|
-
export declare function nameOf(models: {
|
|
49
|
+
export declare function nameOf(models: readonly {
|
|
49
50
|
id: string;
|
|
50
51
|
}[], id: string): string;
|
|
51
52
|
export interface EnsoProps {
|
|
@@ -55,9 +56,10 @@ export interface EnsoProps {
|
|
|
55
56
|
/** The override. `models` empty hides it rather than offering an empty menu. */
|
|
56
57
|
model?: string;
|
|
57
58
|
onModel?: (id: string) => void;
|
|
58
|
-
models
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
/** The catalog, as `parseModels` reads `GET /v1/models`: every model is offered. */
|
|
60
|
+
models?: readonly ModelCatalogEntry[];
|
|
61
|
+
/** The payer's plan usage: a paused model says so and still picks. */
|
|
62
|
+
limits?: PauseSource | null;
|
|
61
63
|
disabled?: boolean;
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
@@ -67,4 +69,4 @@ export interface EnsoProps {
|
|
|
67
69
|
* (`ENSO`, or `FREE` on the free route), and the model itself once one is
|
|
68
70
|
* picked. Beside the name it states the effort where the surface sends one.
|
|
69
71
|
*/
|
|
70
|
-
export declare function Enso({ effort, onEffort, model, onModel, models, disabled }: EnsoProps): import("react").JSX.Element;
|
|
72
|
+
export declare function Enso({ effort, onEffort, model, onModel, models, limits, disabled }: EnsoProps): import("react").JSX.Element;
|
package/dist/enso.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { Check, ChevronDown } from 'lucide-react';
|
|
5
5
|
import { Box, Popover, PopoverContent, PopoverTrigger, Separator, Text, XStack, YStack, } from '@hanzo/ui';
|
|
6
|
-
import {
|
|
6
|
+
import { ModelPicker } from '@hanzo/ui/models';
|
|
7
7
|
import { ProviderMark } from './ProviderMark.js';
|
|
8
8
|
import { ENSO, FREE } from './lib/ai.js';
|
|
9
9
|
import { modelName } from './lib/models.js';
|
|
@@ -90,7 +90,7 @@ export function nameOf(models, id) {
|
|
|
90
90
|
* (`ENSO`, or `FREE` on the free route), and the model itself once one is
|
|
91
91
|
* picked. Beside the name it states the effort where the surface sends one.
|
|
92
92
|
*/
|
|
93
|
-
export function Enso({ effort, onEffort, model, onModel, models = [], disabled }) {
|
|
93
|
+
export function Enso({ effort, onEffort, model, onModel, models = [], limits, disabled }) {
|
|
94
94
|
const [open, setOpen] = useState(false);
|
|
95
95
|
const chosen = EFFORTS.find((e) => e.id === effort);
|
|
96
96
|
const pace = onEffort && chosen ? chosen.label : null;
|
|
@@ -105,7 +105,7 @@ export function Enso({ effort, onEffort, model, onModel, models = [], disabled }
|
|
|
105
105
|
onEffort(e.id);
|
|
106
106
|
setOpen(false);
|
|
107
107
|
}, px: "$2", py: "$2", rounded: "$3", width: "100%", hoverStyle: { bg: '$hover' }, "aria-pressed": e.id === effort, children: _jsxs(XStack, { items: "center", gap: "$2", width: "100%", children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", color: "$ink", text: "left", children: e.label }), _jsx(Text, { fontSize: "$1", color: "$soft", text: "left", children: e.note })] }), e.id === effort ? _jsx(Check, { size: 14, "aria-hidden": true }) : null] }) }, e.id)))
|
|
108
|
-
: null, onModel && models.length ? (_jsxs(_Fragment, { children: [onEffort ? _jsx(Separator, { my: "$1" }) : null, _jsxs(YStack, { px: "$2", pt: "$1", pb: "$2", gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Enso picks the model. Override it for this account:" }), _jsx(
|
|
108
|
+
: null, onModel && models.length ? (_jsxs(_Fragment, { children: [onEffort ? _jsx(Separator, { my: "$1" }) : null, _jsxs(YStack, { px: "$2", pt: "$1", pb: "$2", gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Enso picks the model. Override it for this account:" }), _jsx(ModelPicker, { models: models, limits: limits, value: model, onChange: (id) => {
|
|
109
109
|
onModel(id);
|
|
110
110
|
setOpen(false);
|
|
111
111
|
}, size: "sm" })] })] })) : null] }) })] }));
|
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,11 +24,12 @@ 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;
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
export { paidPlan };
|
|
31
|
+
/** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
|
|
32
|
+
export declare const house: (family: string | undefined) => boolean;
|
|
25
33
|
/**
|
|
26
34
|
* Whether a model can answer a chat turn: it names no outputs, or names text
|
|
27
35
|
* among them. `zen-scribe` (a transcript) and `zen-voice-mini` (audio) are
|
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,11 +43,12 @@ 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
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
export const spendShown = ({ limits, answered }) => answered && !paidPlan(limits);
|
|
49
|
+
export { paidPlan };
|
|
50
|
+
/** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
|
|
51
|
+
export const house = (family) => family === 'enso' || family === 'zen';
|
|
33
52
|
/**
|
|
34
53
|
* Whether a model can answer a chat turn: it names no outputs, or names text
|
|
35
54
|
* among them. `zen-scribe` (a transcript) and `zen-voice-mini` (audio) are
|
package/dist/lib/plans.d.ts
CHANGED
|
@@ -150,8 +150,6 @@ export declare function charge(plan: SubscriptionPlan, interval: Interval): numb
|
|
|
150
150
|
export declare function quote(plan: SubscriptionPlan, interval: Interval): string | null;
|
|
151
151
|
/** What a year saves against twelve months, as a whole percent: 1 − year ÷ (12 × month). Null where it saves nothing. */
|
|
152
152
|
export declare function saving(plan: SubscriptionPlan): number | null;
|
|
153
|
-
/** Whether the Free plan runs a model: one its row names, or any `:free` route. */
|
|
154
|
-
export declare function onFree(model: string): boolean;
|
|
155
153
|
/**
|
|
156
154
|
* The catalog's own name for a plan slug — "max-20x" reads as "Max 20x" —
|
|
157
155
|
* or null where the slug is empty or the catalog does not carry it.
|
package/dist/lib/plans.js
CHANGED
|
@@ -222,17 +222,6 @@ export function saving(plan) {
|
|
|
222
222
|
return null;
|
|
223
223
|
return Math.round((1 - year / (12 * month)) * 100);
|
|
224
224
|
}
|
|
225
|
-
/**
|
|
226
|
-
* The models the Free plan runs, as @hanzo/plans states them: `ai.models` on
|
|
227
|
-
* the `free` row. Read on first ask and not at import, so a page that only
|
|
228
|
-
* formats money or names the pay page carries none of the catalogue.
|
|
229
|
-
*/
|
|
230
|
-
let freeModels = null;
|
|
231
|
-
/** Whether the Free plan runs a model: one its row names, or any `:free` route. */
|
|
232
|
-
export function onFree(model) {
|
|
233
|
-
freeModels ??= new Set(subscriptionPlans.find((p) => p.id === "free")?.entitlements?.["ai.models"] ?? []);
|
|
234
|
-
return freeModels.has(model) || model.endsWith(":free");
|
|
235
|
-
}
|
|
236
225
|
/**
|
|
237
226
|
* The catalog's own name for a plan slug — "max-20x" reads as "Max 20x" —
|
|
238
227
|
* or null where the slug is empty or the catalog does not carry it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@hanzo/iam": ">=0.21.28",
|
|
57
57
|
"@hanzo/logo": ">=1.0.26",
|
|
58
58
|
"@hanzo/plans": ">=1.8.9",
|
|
59
|
-
"@hanzo/ui": ">=8.27.
|
|
59
|
+
"@hanzo/ui": ">=8.27.67",
|
|
60
60
|
"@hanzo/usage": ">=0.1.18",
|
|
61
61
|
"@hanzo/voice": ">=0.1.16",
|
|
62
62
|
"@hanzogui/shell": "^8.3.20",
|
|
@@ -66,16 +66,16 @@
|
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@hanzo/ai": "0.6.20",
|
|
69
|
-
"@hanzo/appearance": "0.2.
|
|
69
|
+
"@hanzo/appearance": "0.2.8",
|
|
70
70
|
"@hanzo/build": "0.2.34",
|
|
71
71
|
"@hanzo/composer": "0.2.0",
|
|
72
|
-
"@hanzo/design": "0.5.
|
|
72
|
+
"@hanzo/design": "0.5.34",
|
|
73
73
|
"@hanzo/event": "0.3.56",
|
|
74
74
|
"@hanzo/gui": "8.3.5",
|
|
75
75
|
"@hanzo/iam": "0.21.28",
|
|
76
76
|
"@hanzo/logo": "1.0.26",
|
|
77
77
|
"@hanzo/plans": "1.8.9",
|
|
78
|
-
"@hanzo/ui": "8.27.
|
|
78
|
+
"@hanzo/ui": "8.27.67",
|
|
79
79
|
"@hanzo/usage": "0.1.18",
|
|
80
80
|
"@hanzo/voice": "0.1.16",
|
|
81
81
|
"@hanzogui/config": "8.3.5",
|