@hanzo/rooms 0.1.38 → 0.1.40
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 +2 -2
- package/dist/Account.js +18 -9
- package/dist/Chat.js +125 -202
- package/dist/Column.js +16 -15
- package/dist/Directory.d.ts +4 -9
- package/dist/Directory.js +60 -155
- package/dist/Me.js +20 -16
- package/dist/Prose.js +5 -4
- package/dist/RightPane.d.ts +20 -28
- package/dist/RightPane.js +107 -327
- package/dist/Settings.js +42 -28
- package/dist/Shell.js +19 -22
- package/dist/dev.js +42 -26
- package/dist/host.d.ts +5 -9
- package/dist/host.js +5 -6
- package/dist/lib/destination.d.ts +6 -6
- package/dist/lib/destination.js +6 -6
- package/dist/lib/host.d.ts +50 -31
- package/dist/lib/host.js +98 -48
- package/dist/lib/limits.d.ts +13 -9
- package/dist/lib/limits.js +20 -0
- package/dist/lib/plans.d.ts +2 -13
- package/dist/lib/plans.js +2 -19
- package/dist/lib/session.d.ts +9 -12
- package/dist/lib/session.js +7 -19
- package/dist/lib/web.d.ts +46 -38
- package/dist/lib/web.js +480 -213
- package/dist/pane.d.ts +4 -57
- package/dist/pane.js +13 -159
- package/package.json +3 -5
- package/dist/Billing.d.ts +0 -1
- package/dist/Billing.js +0 -32
- package/dist/enso.d.ts +0 -72
- package/dist/enso.js +0 -112
- package/dist/model.d.ts +0 -4
- package/dist/model.js +0 -44
package/dist/pane.d.ts
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import { type Api, type WorkFile } from './lib/files.js';
|
|
2
|
-
/** One open tab in the pane's browser. */
|
|
3
|
-
export interface Leaf {
|
|
4
|
-
id: string;
|
|
5
|
-
url: string;
|
|
6
|
-
/** What the strip shows: host and path, which is all we can read across an
|
|
7
|
-
* origin we do not own. A frame's real title is not ours to ask for. */
|
|
8
|
-
title: string;
|
|
9
|
-
}
|
|
10
2
|
/**
|
|
11
3
|
* A file put into this conversation, on its way to the workspace or already
|
|
12
4
|
* there, waiting to go with the next message.
|
|
13
5
|
*
|
|
14
|
-
* `href` is an object URL and is the reason this is never written down
|
|
15
|
-
*
|
|
16
|
-
* workspace file's, never a guess.
|
|
6
|
+
* `href` is an object URL and is the reason this is never written down. A name
|
|
7
|
+
* and a size are the file system's, or the workspace file's, never a guess.
|
|
17
8
|
*/
|
|
18
9
|
export interface Held {
|
|
19
10
|
id: string;
|
|
@@ -37,13 +28,8 @@ export interface Via {
|
|
|
37
28
|
api: Api;
|
|
38
29
|
org: string | null;
|
|
39
30
|
}
|
|
40
|
-
/**
|
|
31
|
+
/** What one conversation holds for its next message. */
|
|
41
32
|
export interface Pane {
|
|
42
|
-
tabs: Leaf[];
|
|
43
|
-
/** The chosen tab, or null when this conversation has opened none. */
|
|
44
|
-
at: string | null;
|
|
45
|
-
/** Whether the summary column stands beside the browser. */
|
|
46
|
-
pinned: boolean;
|
|
47
33
|
held: Held[];
|
|
48
34
|
/** Why the last files offered were refused, in the reader's words, or null. */
|
|
49
35
|
refused: string | null;
|
|
@@ -55,52 +41,13 @@ export interface Pane {
|
|
|
55
41
|
* an inbox room arrives over a transport and is keyed by it, a thread is a
|
|
56
42
|
* conversation the org has recorded, and an agent is who you are talking to
|
|
57
43
|
* before a thread exists. Prefixed so a room called `x` and a thread called `x`
|
|
58
|
-
* cannot land on one entry.
|
|
59
|
-
*
|
|
60
|
-
* `new` is a real answer and not a fallback: a room nobody has spoken in yet is
|
|
61
|
-
* somewhere a reader can open tabs, and they should still be there when the
|
|
62
|
-
* first turn mints a thread. It is the ONE key that several conversations pass
|
|
63
|
-
* through, which is the honest cost of not having an id to key on yet.
|
|
44
|
+
* cannot land on one entry. `new` is a room nobody has spoken in yet.
|
|
64
45
|
*/
|
|
65
46
|
export declare function channel(open: {
|
|
66
47
|
room: string | null;
|
|
67
48
|
thread: string | null;
|
|
68
49
|
agent: string | null;
|
|
69
50
|
}): string;
|
|
70
|
-
/** The host and path of an address, which is as much as a title can honestly be. */
|
|
71
|
-
export declare function name(url: string): string;
|
|
72
|
-
/**
|
|
73
|
-
* An address as typed, made into one that can be opened.
|
|
74
|
-
*
|
|
75
|
-
* A bare host is `https`, because that is the web now; a loopback name is
|
|
76
|
-
* `http`, because nothing is listening on 443 of a laptop and offering a
|
|
77
|
-
* reader a URL that cannot connect is worse than guessing.
|
|
78
|
-
*/
|
|
79
|
-
export declare function address(typed: string): string;
|
|
80
|
-
/**
|
|
81
|
-
* Opens an address in this conversation, and chooses it.
|
|
82
|
-
*
|
|
83
|
-
* `called` is for the things that HAVE a name of their own — an answer's
|
|
84
|
-
* `index.html`, a file off the reader's disk — because their address is a blob
|
|
85
|
-
* and `name()` can read nothing useful out of one. Without it the host and path
|
|
86
|
-
* are the honest title.
|
|
87
|
-
*/
|
|
88
|
-
export declare function openTab(key: string, url: string, called?: string): void;
|
|
89
|
-
/** Points an already-open tab somewhere else. */
|
|
90
|
-
export declare function goTab(key: string, id: string, url: string): void;
|
|
91
|
-
/** Chooses an open tab. */
|
|
92
|
-
export declare function pickTab(key: string, id: string): void;
|
|
93
|
-
/**
|
|
94
|
-
* Shuts a tab.
|
|
95
|
-
*
|
|
96
|
-
* THE NEIGHBOUR IS THE ONE TO ITS RIGHT, falling back to its left — what every
|
|
97
|
-
* browser does, and the only choice that does not send the reader somewhere
|
|
98
|
-
* they were not looking. Closing the last tab leaves none chosen rather than
|
|
99
|
-
* refusing, because a pane with no tabs is a state this panel draws.
|
|
100
|
-
*/
|
|
101
|
-
export declare function shutTab(key: string, id: string): void;
|
|
102
|
-
/** Shows or hides the summary column beside this conversation's browser. */
|
|
103
|
-
export declare function pin(key: string, on: boolean): void;
|
|
104
51
|
/** The workspace file `id` as the index last said, or undefined until it has. */
|
|
105
52
|
export declare function useWorkFile(id: string | undefined): WorkFile | undefined;
|
|
106
53
|
/** The object URL over workspace file `id`'s bytes on this machine, or '' where this document never held them. */
|
package/dist/pane.js
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
// What
|
|
2
|
+
// What a conversation is holding for its next message, PER CONVERSATION: the
|
|
3
|
+
// files on their way to the workspace and why any were refused. The panel's
|
|
4
|
+
// tabs are @hanzo/build's (`useDeck`), kept per conversation under the same key
|
|
5
|
+
// (`channel`).
|
|
3
6
|
//
|
|
4
7
|
// The store lives outside React for the reason `open.ts` gives: the router
|
|
5
8
|
// mounts the rooms, so anything kept inside one is destroyed by a route change.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// losing work rather than losing their place.
|
|
10
|
-
//
|
|
11
|
-
// ONE ENTRY PER CONVERSATION, not one blob holding them all. A blob is read in
|
|
12
|
-
// full at boot and rewritten in full on every change, so ten conversations make
|
|
13
|
-
// nine channels' tabs the cost of moving one tab in the tenth. Keyed
|
|
14
|
-
// separately, both costs are proportional to the conversation you are actually
|
|
15
|
-
// in, and a corrupt entry loses one pane rather than all of them.
|
|
16
|
-
//
|
|
17
|
-
// WHAT IS KEPT AND WHAT IS NOT. Tabs, the chosen tab and the pin survive a
|
|
18
|
-
// reload because they are addresses and flags. Files picked off the reader's
|
|
19
|
-
// own disk DO NOT: a file handle is an object URL, which the browser revokes
|
|
20
|
-
// with the document that minted it, so a persisted row would come back as a
|
|
21
|
-
// name pointing at nothing. A row that looks like a file and opens nothing is
|
|
22
|
-
// worse than no row, so they are held for the session and no longer.
|
|
9
|
+
// It is NOT written down: a file picked off the reader's own disk is an object
|
|
10
|
+
// URL, which the browser revokes with the document that minted it, so a kept
|
|
11
|
+
// row would come back as a name pointing at nothing.
|
|
23
12
|
//
|
|
24
13
|
// ONE WAY IN FOR A FILE. The composer's paperclip, a drop on the room, a paste
|
|
25
|
-
// into the field and the
|
|
26
|
-
// road (`lib/files`: straight to the org's workspace, in parts when it is
|
|
14
|
+
// into the field and the Sources tab's `+` all call `hold`, so a file goes up by
|
|
15
|
+
// one road (`lib/files`: straight to the org's workspace, in parts when it is
|
|
27
16
|
// large), shows as a chip over the field and a row under Sources at once, and
|
|
28
17
|
// goes with the next message by reference. Sending spends what is held; from
|
|
29
18
|
// then on the turn itself is the record Sources reads.
|
|
@@ -31,8 +20,7 @@ import { useSyncExternalStore } from 'react';
|
|
|
31
20
|
import { chatKey, follow, pool, register, settled, upload, workspace } from './lib/files.js';
|
|
32
21
|
/** A pane nobody has touched. Shared, and never mutated — see `EMPTY` in
|
|
33
22
|
* `open.ts` for why a fresh object per read is an infinite render. */
|
|
34
|
-
const BARE = {
|
|
35
|
-
const where = (key) => `hanzo.pane.${key}`;
|
|
23
|
+
const BARE = { held: [], refused: null };
|
|
36
24
|
/**
|
|
37
25
|
* The conversation a pane belongs to.
|
|
38
26
|
*
|
|
@@ -40,12 +28,7 @@ const where = (key) => `hanzo.pane.${key}`;
|
|
|
40
28
|
* an inbox room arrives over a transport and is keyed by it, a thread is a
|
|
41
29
|
* conversation the org has recorded, and an agent is who you are talking to
|
|
42
30
|
* before a thread exists. Prefixed so a room called `x` and a thread called `x`
|
|
43
|
-
* cannot land on one entry.
|
|
44
|
-
*
|
|
45
|
-
* `new` is a real answer and not a fallback: a room nobody has spoken in yet is
|
|
46
|
-
* somewhere a reader can open tabs, and they should still be there when the
|
|
47
|
-
* first turn mints a thread. It is the ONE key that several conversations pass
|
|
48
|
-
* through, which is the honest cost of not having an id to key on yet.
|
|
31
|
+
* cannot land on one entry. `new` is a room nobody has spoken in yet.
|
|
49
32
|
*/
|
|
50
33
|
export function channel(open) {
|
|
51
34
|
if (open.room)
|
|
@@ -56,9 +39,6 @@ export function channel(open) {
|
|
|
56
39
|
return `agent:${open.agent}`;
|
|
57
40
|
return 'new';
|
|
58
41
|
}
|
|
59
|
-
// The panes this document has actually touched. A switch back reads from here
|
|
60
|
-
// rather than re-parsing JSON, so moving between two conversations costs one
|
|
61
|
-
// read each and nothing after that.
|
|
62
42
|
const live = new Map();
|
|
63
43
|
const listeners = new Set();
|
|
64
44
|
function announce() {
|
|
@@ -71,145 +51,19 @@ function subscribe(l) {
|
|
|
71
51
|
listeners.delete(l);
|
|
72
52
|
};
|
|
73
53
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
* A BAD ENTRY IS DROPPED, NOT THROWN. Storage holds whatever an older build
|
|
78
|
-
* wrote, and one unparseable pane must not take the room down with it — the
|
|
79
|
-
* reader loses some tabs, which is what they would have had anyway.
|
|
80
|
-
*/
|
|
81
|
-
function read(key) {
|
|
82
|
-
const found = live.get(key);
|
|
83
|
-
if (found)
|
|
84
|
-
return found;
|
|
85
|
-
let pane = BARE;
|
|
86
|
-
try {
|
|
87
|
-
const raw = typeof window === 'undefined' ? null : window.localStorage.getItem(where(key));
|
|
88
|
-
if (raw) {
|
|
89
|
-
const kept = JSON.parse(raw);
|
|
90
|
-
const tabs = Array.isArray(kept.tabs)
|
|
91
|
-
? kept.tabs.filter((t) => !!t && typeof t.id === 'string' && typeof t.url === 'string' && typeof t.title === 'string')
|
|
92
|
-
: [];
|
|
93
|
-
pane = {
|
|
94
|
-
tabs,
|
|
95
|
-
// A chosen tab that is no longer open is not a choice. Restoring one
|
|
96
|
-
// leaves the strip with nothing marked and the panel drawing a tab that
|
|
97
|
-
// is not in it.
|
|
98
|
-
at: tabs.some((t) => t.id === kept.at) ? kept.at : (tabs[0]?.id ?? null),
|
|
99
|
-
pinned: kept.pinned !== false,
|
|
100
|
-
held: [],
|
|
101
|
-
refused: null,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
pane = BARE;
|
|
107
|
-
}
|
|
108
|
-
live.set(key, pane);
|
|
109
|
-
return pane;
|
|
110
|
-
}
|
|
111
|
-
/** The server renders no storage, so every pane starts bare there. Returning
|
|
112
|
-
* the shared object keeps the snapshot stable across reads. */
|
|
54
|
+
const read = (key) => live.get(key) ?? BARE;
|
|
55
|
+
/** The server renders nothing held, so every pane starts bare there. */
|
|
113
56
|
const onServer = () => BARE;
|
|
114
57
|
function write(key, next) {
|
|
115
58
|
live.set(key, next);
|
|
116
|
-
try {
|
|
117
|
-
if (typeof window === 'undefined')
|
|
118
|
-
return;
|
|
119
|
-
const kept = { tabs: next.tabs, at: next.at, pinned: next.pinned };
|
|
120
|
-
window.localStorage.setItem(where(key), JSON.stringify(kept));
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
// A browser refusing to store — private mode, a full quota — is not a
|
|
124
|
-
// reason to refuse the change on screen. The pane stands for this document.
|
|
125
|
-
}
|
|
126
59
|
announce();
|
|
127
60
|
}
|
|
128
61
|
/** Applies a change to one conversation's pane. */
|
|
129
62
|
function edit(key, change) {
|
|
130
63
|
write(key, change(read(key)));
|
|
131
64
|
}
|
|
132
|
-
/** The host and path of an address, which is as much as a title can honestly be. */
|
|
133
|
-
export function name(url) {
|
|
134
|
-
try {
|
|
135
|
-
const u = new URL(url);
|
|
136
|
-
return u.host + (u.pathname === '/' ? '' : u.pathname);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return url;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* An address as typed, made into one that can be opened.
|
|
144
|
-
*
|
|
145
|
-
* A bare host is `https`, because that is the web now; a loopback name is
|
|
146
|
-
* `http`, because nothing is listening on 443 of a laptop and offering a
|
|
147
|
-
* reader a URL that cannot connect is worse than guessing.
|
|
148
|
-
*/
|
|
149
|
-
export function address(typed) {
|
|
150
|
-
const t = typed.trim();
|
|
151
|
-
if (!t)
|
|
152
|
-
return '';
|
|
153
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(t))
|
|
154
|
-
return t;
|
|
155
|
-
return /^(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(t) ? `http://${t}` : `https://${t}`;
|
|
156
|
-
}
|
|
157
65
|
let minted = 0;
|
|
158
66
|
const mint = (prefix) => `${prefix}${Date.now().toString(36)}${(minted++).toString(36)}`;
|
|
159
|
-
/**
|
|
160
|
-
* Opens an address in this conversation, and chooses it.
|
|
161
|
-
*
|
|
162
|
-
* `called` is for the things that HAVE a name of their own — an answer's
|
|
163
|
-
* `index.html`, a file off the reader's disk — because their address is a blob
|
|
164
|
-
* and `name()` can read nothing useful out of one. Without it the host and path
|
|
165
|
-
* are the honest title.
|
|
166
|
-
*/
|
|
167
|
-
export function openTab(key, url, called) {
|
|
168
|
-
const at = address(url);
|
|
169
|
-
if (!at)
|
|
170
|
-
return;
|
|
171
|
-
edit(key, (was) => {
|
|
172
|
-
const leaf = { id: mint('t'), url: at, title: called || name(at) };
|
|
173
|
-
return { ...was, tabs: [...was.tabs, leaf], at: leaf.id };
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
/** Points an already-open tab somewhere else. */
|
|
177
|
-
export function goTab(key, id, url) {
|
|
178
|
-
const at = address(url);
|
|
179
|
-
if (!at)
|
|
180
|
-
return;
|
|
181
|
-
edit(key, (was) => ({
|
|
182
|
-
...was,
|
|
183
|
-
tabs: was.tabs.map((t) => (t.id === id ? { ...t, url: at, title: name(at) } : t)),
|
|
184
|
-
}));
|
|
185
|
-
}
|
|
186
|
-
/** Chooses an open tab. */
|
|
187
|
-
export function pickTab(key, id) {
|
|
188
|
-
edit(key, (was) => (was.at === id ? was : { ...was, at: id }));
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Shuts a tab.
|
|
192
|
-
*
|
|
193
|
-
* THE NEIGHBOUR IS THE ONE TO ITS RIGHT, falling back to its left — what every
|
|
194
|
-
* browser does, and the only choice that does not send the reader somewhere
|
|
195
|
-
* they were not looking. Closing the last tab leaves none chosen rather than
|
|
196
|
-
* refusing, because a pane with no tabs is a state this panel draws.
|
|
197
|
-
*/
|
|
198
|
-
export function shutTab(key, id) {
|
|
199
|
-
edit(key, (was) => {
|
|
200
|
-
const gone = was.tabs.findIndex((t) => t.id === id);
|
|
201
|
-
if (gone < 0)
|
|
202
|
-
return was;
|
|
203
|
-
const tabs = was.tabs.filter((t) => t.id !== id);
|
|
204
|
-
if (was.at !== id)
|
|
205
|
-
return { ...was, tabs };
|
|
206
|
-
return { ...was, tabs, at: (tabs[gone] ?? tabs[gone - 1])?.id ?? null };
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
/** Shows or hides the summary column beside this conversation's browser. */
|
|
210
|
-
export function pin(key, on) {
|
|
211
|
-
edit(key, (was) => (was.pinned === on ? was : { ...was, pinned: on }));
|
|
212
|
-
}
|
|
213
67
|
// THE WORKSPACE FILES THIS DOCUMENT HAS SEEN, by id, as the index last said.
|
|
214
68
|
// A chip over the field and a chip in a sent turn read one record, so a file
|
|
215
69
|
// that finishes indexing after the message went shows ready in both.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
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",
|
|
@@ -49,13 +49,12 @@
|
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@hanzo/ai": ">=0.6.23",
|
|
51
51
|
"@hanzo/appearance": ">=0.2.6",
|
|
52
|
-
"@hanzo/build": ">=0.2.
|
|
52
|
+
"@hanzo/build": ">=0.2.53",
|
|
53
53
|
"@hanzo/composer": ">=0.2.0",
|
|
54
54
|
"@hanzo/event": ">=0.3.56",
|
|
55
55
|
"@hanzo/gui": "^8.3.5",
|
|
56
56
|
"@hanzo/iam": ">=0.21.28",
|
|
57
57
|
"@hanzo/logo": ">=1.0.26",
|
|
58
|
-
"@hanzo/plans": ">=1.8.9",
|
|
59
58
|
"@hanzo/ui": ">=8.27.67",
|
|
60
59
|
"@hanzo/usage": ">=0.1.18",
|
|
61
60
|
"@hanzo/voice": ">=0.1.16",
|
|
@@ -67,14 +66,13 @@
|
|
|
67
66
|
"devDependencies": {
|
|
68
67
|
"@hanzo/ai": "0.6.23",
|
|
69
68
|
"@hanzo/appearance": "0.2.8",
|
|
70
|
-
"@hanzo/build": "0.2.
|
|
69
|
+
"@hanzo/build": "0.2.53",
|
|
71
70
|
"@hanzo/composer": "0.2.0",
|
|
72
71
|
"@hanzo/design": "0.5.34",
|
|
73
72
|
"@hanzo/event": "0.3.56",
|
|
74
73
|
"@hanzo/gui": "8.3.5",
|
|
75
74
|
"@hanzo/iam": "0.21.28",
|
|
76
75
|
"@hanzo/logo": "1.0.26",
|
|
77
|
-
"@hanzo/plans": "1.8.9",
|
|
78
76
|
"@hanzo/ui": "8.27.67",
|
|
79
77
|
"@hanzo/usage": "0.1.18",
|
|
80
78
|
"@hanzo/voice": "0.1.16",
|
package/dist/Billing.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function Billing(): import("react").JSX.Element;
|
package/dist/Billing.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* WHAT THE WORKSPACE BOUGHT, AND WHAT IT MAY SPEND — as Billing, not as the
|
|
5
|
-
* person's card. Credits belong to the workspace (an org-level pool), so they
|
|
6
|
-
* are read here and pressed through to billing at hanzo.ai/pay for the org this
|
|
7
|
-
* workspace is.
|
|
8
|
-
*
|
|
9
|
-
* NOTHING HERE IS DRAWN FROM A GUESS. A figure shows only when the read
|
|
10
|
-
* SUCCEEDED: a balance that could not be read is unknown, and unknown is not
|
|
11
|
-
* zero, so it renders as the plain word rather than as `$0.00`.
|
|
12
|
-
*/
|
|
13
|
-
import { Wallet } from 'lucide-react';
|
|
14
|
-
import { Box, Text, XStack } from '@hanzo/ui';
|
|
15
|
-
import { useIam } from '@hanzo/iam/react';
|
|
16
|
-
import { formatCents } from '@hanzo/usage';
|
|
17
|
-
import { renewal, useSubscription, useTier } from './lib/tier.js';
|
|
18
|
-
import { payPage } from './lib/pay.js';
|
|
19
|
-
import { planName } from './lib/plans.js';
|
|
20
|
-
export function Billing() {
|
|
21
|
-
const { isAuthenticated } = useIam();
|
|
22
|
-
const { tier } = useTier(isAuthenticated);
|
|
23
|
-
const sub = useSubscription(isAuthenticated);
|
|
24
|
-
if (!isAuthenticated)
|
|
25
|
-
return null;
|
|
26
|
-
// WHAT THE CUSTOMER BOUGHT, not the coarser class it is served under: a
|
|
27
|
-
// Max 20x subscriber runs on `pro` limits, and printing that read as "Pro".
|
|
28
|
-
// A subscription is named with its term; its usage never draws the balance,
|
|
29
|
-
// so the balance stands beside a plan only where there is no subscription.
|
|
30
|
-
const plan = tier ? planName(tier.plan) || tier.tier.displayName || tier.tier.name : null;
|
|
31
|
-
return (_jsx(Box, { render: "button", onClick: () => window.open(payPage(), '_blank', 'noopener'), "aria-label": "Billing", px: "$2.5", py: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Wallet, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", color: "$ink", children: plan ? `${plan} · ${sub ? renewal(sub) : `${formatCents(tier.balance.effectiveAvailable)} available`}` : 'Billing' })] }) }));
|
|
32
|
-
}
|
package/dist/enso.d.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { type ModelCatalogEntry, type PauseSource } from '@hanzo/ui/models';
|
|
2
|
-
/**
|
|
3
|
-
* How hard to think — and, one press inside, what thinks.
|
|
4
|
-
*
|
|
5
|
-
* EFFORT IS ON THE BAR AND THE MODEL IS NOT. That is the product's position
|
|
6
|
-
* rather than a layout preference: Enso routes the ask, so naming a model is an
|
|
7
|
-
* override and belongs inside this panel, not on the row where it asked every
|
|
8
|
-
* reader to hold an opinion about a catalogue before they could type a
|
|
9
|
-
* sentence.
|
|
10
|
-
*
|
|
11
|
-
* Effort earns the bar because it belongs to the ASK. The same person wants a
|
|
12
|
-
* fast answer to one question and a careful one to the next; the model they
|
|
13
|
-
* want rarely changes between two messages, which is what makes it a
|
|
14
|
-
* preference and this a control.
|
|
15
|
-
*
|
|
16
|
-
* `reasoning_effort` is an OpenAI-compatible completion parameter and rides
|
|
17
|
-
* `useChat`'s `params`, so on /chat it reaches the API. A surface that cannot
|
|
18
|
-
* carry it passes no `onEffort` and the chip names the router instead — the
|
|
19
|
-
* coding plane takes a `model` and has no effort field, and a control that
|
|
20
|
-
* reaches nothing is worse than one that is absent.
|
|
21
|
-
*/
|
|
22
|
-
export declare const EFFORTS: readonly [{
|
|
23
|
-
readonly id: 'minimal';
|
|
24
|
-
readonly label: 'Instant';
|
|
25
|
-
readonly note: 'Answer now, no deliberation';
|
|
26
|
-
}, {
|
|
27
|
-
readonly id: 'low';
|
|
28
|
-
readonly label: 'Low';
|
|
29
|
-
readonly note: 'A quick pass';
|
|
30
|
-
}, {
|
|
31
|
-
readonly id: 'medium';
|
|
32
|
-
readonly label: 'Medium';
|
|
33
|
-
readonly note: 'Thinks before it writes';
|
|
34
|
-
}, {
|
|
35
|
-
readonly id: 'high';
|
|
36
|
-
readonly label: 'High';
|
|
37
|
-
readonly note: 'Slow and careful, for hard problems';
|
|
38
|
-
}];
|
|
39
|
-
export type EffortId = (typeof EFFORTS)[number]['id'];
|
|
40
|
-
/**
|
|
41
|
-
* The effort preference, shared by every surface that sends a completion.
|
|
42
|
-
*
|
|
43
|
-
* Same shape as `useModel` — one storage key, one change event, read on mount
|
|
44
|
-
* rather than in the initialiser so the server and the first client render
|
|
45
|
-
* agree and hydration does not warn.
|
|
46
|
-
*/
|
|
47
|
-
export declare function useEffort(): readonly [EffortId, (next: EffortId) => void];
|
|
48
|
-
/** The name a served model goes by: its row's label, its name, or its id made readable. */
|
|
49
|
-
export declare function nameOf(models: readonly {
|
|
50
|
-
id: string;
|
|
51
|
-
}[], id: string): string;
|
|
52
|
-
export interface EnsoProps {
|
|
53
|
-
/** Current effort. Omit `onEffort` where the surface cannot send one. */
|
|
54
|
-
effort?: EffortId;
|
|
55
|
-
onEffort?: (next: EffortId) => void;
|
|
56
|
-
/** The override. `models` empty hides it rather than offering an empty menu. */
|
|
57
|
-
model?: string;
|
|
58
|
-
onModel?: (id: string) => void;
|
|
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;
|
|
63
|
-
disabled?: boolean;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* The chip that sits beside send, and the panel it opens.
|
|
67
|
-
*
|
|
68
|
-
* The chip names what answers the next message: Enso while Enso routes it
|
|
69
|
-
* (`ENSO`, or `FREE` on the free route), and the model itself once one is
|
|
70
|
-
* picked. Beside the name it states the effort where the surface sends one.
|
|
71
|
-
*/
|
|
72
|
-
export declare function Enso({ effort, onEffort, model, onModel, models, limits, disabled }: EnsoProps): import("react").JSX.Element;
|
package/dist/enso.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { Check, ChevronDown } from 'lucide-react';
|
|
5
|
-
import { Box, Popover, PopoverContent, PopoverTrigger, Separator, Text, XStack, YStack, } from '@hanzo/ui';
|
|
6
|
-
import { ModelPicker } from '@hanzo/ui/models';
|
|
7
|
-
import { ProviderMark } from './ProviderMark.js';
|
|
8
|
-
import { ENSO, FREE } from './lib/ai.js';
|
|
9
|
-
import { modelName } from './lib/models.js';
|
|
10
|
-
/**
|
|
11
|
-
* How hard to think — and, one press inside, what thinks.
|
|
12
|
-
*
|
|
13
|
-
* EFFORT IS ON THE BAR AND THE MODEL IS NOT. That is the product's position
|
|
14
|
-
* rather than a layout preference: Enso routes the ask, so naming a model is an
|
|
15
|
-
* override and belongs inside this panel, not on the row where it asked every
|
|
16
|
-
* reader to hold an opinion about a catalogue before they could type a
|
|
17
|
-
* sentence.
|
|
18
|
-
*
|
|
19
|
-
* Effort earns the bar because it belongs to the ASK. The same person wants a
|
|
20
|
-
* fast answer to one question and a careful one to the next; the model they
|
|
21
|
-
* want rarely changes between two messages, which is what makes it a
|
|
22
|
-
* preference and this a control.
|
|
23
|
-
*
|
|
24
|
-
* `reasoning_effort` is an OpenAI-compatible completion parameter and rides
|
|
25
|
-
* `useChat`'s `params`, so on /chat it reaches the API. A surface that cannot
|
|
26
|
-
* carry it passes no `onEffort` and the chip names the router instead — the
|
|
27
|
-
* coding plane takes a `model` and has no effort field, and a control that
|
|
28
|
-
* reaches nothing is worse than one that is absent.
|
|
29
|
-
*/
|
|
30
|
-
export const EFFORTS = [
|
|
31
|
-
{ id: 'minimal', label: 'Instant', note: 'Answer now, no deliberation' },
|
|
32
|
-
{ id: 'low', label: 'Low', note: 'A quick pass' },
|
|
33
|
-
{ id: 'medium', label: 'Medium', note: 'Thinks before it writes' },
|
|
34
|
-
{ id: 'high', label: 'High', note: 'Slow and careful, for hard problems' },
|
|
35
|
-
];
|
|
36
|
-
const DEFAULT = 'medium';
|
|
37
|
-
const KEY = 'hanzo.chat.effort';
|
|
38
|
-
const CHANGE = 'hanzo:effort';
|
|
39
|
-
const read = () => {
|
|
40
|
-
if (typeof window === 'undefined')
|
|
41
|
-
return DEFAULT;
|
|
42
|
-
try {
|
|
43
|
-
const held = window.localStorage.getItem(KEY);
|
|
44
|
-
return EFFORTS.some((e) => e.id === held) ? held : DEFAULT;
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return DEFAULT;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* The effort preference, shared by every surface that sends a completion.
|
|
52
|
-
*
|
|
53
|
-
* Same shape as `useModel` — one storage key, one change event, read on mount
|
|
54
|
-
* rather than in the initialiser so the server and the first client render
|
|
55
|
-
* agree and hydration does not warn.
|
|
56
|
-
*/
|
|
57
|
-
export function useEffort() {
|
|
58
|
-
const [effort, setEffort] = useState(DEFAULT);
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
setEffort(read());
|
|
61
|
-
const sync = () => setEffort(read());
|
|
62
|
-
window.addEventListener('storage', sync);
|
|
63
|
-
window.addEventListener(CHANGE, sync);
|
|
64
|
-
return () => {
|
|
65
|
-
window.removeEventListener('storage', sync);
|
|
66
|
-
window.removeEventListener(CHANGE, sync);
|
|
67
|
-
};
|
|
68
|
-
}, []);
|
|
69
|
-
const choose = (next) => {
|
|
70
|
-
setEffort(next);
|
|
71
|
-
try {
|
|
72
|
-
window.localStorage.setItem(KEY, next);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// A private browser may refuse persistence; this tab still keeps the choice.
|
|
76
|
-
}
|
|
77
|
-
window.dispatchEvent(new Event(CHANGE));
|
|
78
|
-
};
|
|
79
|
-
return [effort, choose];
|
|
80
|
-
}
|
|
81
|
-
/** The name a served model goes by: its row's label, its name, or its id made readable. */
|
|
82
|
-
export function nameOf(models, id) {
|
|
83
|
-
const row = models.find((m) => m.id === id);
|
|
84
|
-
return modelName({ id, name: String(row?.label ?? row?.name ?? '') });
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* The chip that sits beside send, and the panel it opens.
|
|
88
|
-
*
|
|
89
|
-
* The chip names what answers the next message: Enso while Enso routes it
|
|
90
|
-
* (`ENSO`, or `FREE` on the free route), and the model itself once one is
|
|
91
|
-
* picked. Beside the name it states the effort where the surface sends one.
|
|
92
|
-
*/
|
|
93
|
-
export function Enso({ effort, onEffort, model, onModel, models = [], limits, disabled }) {
|
|
94
|
-
const [open, setOpen] = useState(false);
|
|
95
|
-
const chosen = EFFORTS.find((e) => e.id === effort);
|
|
96
|
-
const pace = onEffort && chosen ? chosen.label : null;
|
|
97
|
-
const picked = model && model !== ENSO && model !== FREE ? model : undefined;
|
|
98
|
-
const name = picked ? nameOf(models, picked) : 'Enso';
|
|
99
|
-
// The panel opens above the chip when there is no room below it, so the model
|
|
100
|
-
// picker at its foot stays on screen on a laptop-height window. It is SOLID:
|
|
101
|
-
// it stands over a transcript that streams, which would read through glass,
|
|
102
|
-
// and the model list it opens inherits the same ground.
|
|
103
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, children: [_jsx(PopoverTrigger, { disabled: disabled, children: _jsxs(XStack, { "data-slot": "enso", items: "center", gap: "$1.5", px: "$2.5", height: 32, maxW: 220, rounded: "$10", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, opacity: disabled ? 0.4 : 1, "aria-label": pace ? `${name}: ${pace}` : name, children: [_jsx(ProviderMark, { provider: "enso", model: picked, size: 14 }), _jsx(Text, { fontSize: "$1", color: "$ink", fontWeight: "600", numberOfLines: 1, children: name }), pace ? (_jsx(Text, { fontSize: "$1", color: "$soft", fontWeight: "500", children: pace })) : null, _jsx(ChevronDown, { size: 12, "aria-hidden": true })] }) }), _jsx(PopoverContent, { solid: true, align: "end", p: 6, width: 268, children: _jsxs(YStack, { gap: "$1", children: [onEffort
|
|
104
|
-
? EFFORTS.map((e) => (_jsx(Box, { render: "button", onClick: () => {
|
|
105
|
-
onEffort(e.id);
|
|
106
|
-
setOpen(false);
|
|
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(ModelPicker, { models: models, limits: limits, value: model, onChange: (id) => {
|
|
109
|
-
onModel(id);
|
|
110
|
-
setOpen(false);
|
|
111
|
-
}, size: "sm" })] })] })) : null] }) })] }));
|
|
112
|
-
}
|
package/dist/model.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** Make `model` the chat default, in this browser, for every open chat. */
|
|
2
|
-
export declare function remember(model: string): void;
|
|
3
|
-
/** The signed-in chat default, shared by Settings and every new conversation. */
|
|
4
|
-
export declare function useModel(): readonly [string, (model: string) => void];
|
package/dist/model.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
|
-
import { ENSO } from "./lib/ai.js";
|
|
4
|
-
const KEY = "hanzo.chat.model";
|
|
5
|
-
const CHANGE = "hanzo:model";
|
|
6
|
-
function read() {
|
|
7
|
-
if (typeof window === "undefined")
|
|
8
|
-
return ENSO;
|
|
9
|
-
try {
|
|
10
|
-
return window.localStorage.getItem(KEY) || ENSO;
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return ENSO;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/** Make `model` the chat default, in this browser, for every open chat. */
|
|
17
|
-
export function remember(model) {
|
|
18
|
-
try {
|
|
19
|
-
window.localStorage.setItem(KEY, model);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
// A private browser may refuse persistence; this tab still keeps the choice.
|
|
23
|
-
}
|
|
24
|
-
window.dispatchEvent(new Event(CHANGE));
|
|
25
|
-
}
|
|
26
|
-
/** The signed-in chat default, shared by Settings and every new conversation. */
|
|
27
|
-
export function useModel() {
|
|
28
|
-
const [model, setModel] = useState(ENSO);
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
setModel(read());
|
|
31
|
-
const sync = () => setModel(read());
|
|
32
|
-
window.addEventListener("storage", sync);
|
|
33
|
-
window.addEventListener(CHANGE, sync);
|
|
34
|
-
return () => {
|
|
35
|
-
window.removeEventListener("storage", sync);
|
|
36
|
-
window.removeEventListener(CHANGE, sync);
|
|
37
|
-
};
|
|
38
|
-
}, []);
|
|
39
|
-
const choose = (next) => {
|
|
40
|
-
setModel(next);
|
|
41
|
-
remember(next);
|
|
42
|
-
};
|
|
43
|
-
return [model, choose];
|
|
44
|
-
}
|