@hanzo/rooms 0.1.0
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/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
package/dist/Shell.js
ADDED
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The rooms' shell. ONE of them, worn by /chat and every room. (The web app is
|
|
4
|
+
// `/`, whole, with a rail of its own: app/page.tsx. Dev is its door.)
|
|
5
|
+
//
|
|
6
|
+
// The two routes are the same room with different furniture: a rail on the left,
|
|
7
|
+
// a pane on the right. What differs is the rail's title, its nav, and which list
|
|
8
|
+
// it shows — three props, not two components. Building a second shell is how the
|
|
9
|
+
// two surfaces drift, and they are meant to be one product.
|
|
10
|
+
//
|
|
11
|
+
// Everything here is composition. The parts come from `@hanzo/ui/chat`, which is
|
|
12
|
+
// where the Hanzo chat shell lives for every surface that wears one; this file
|
|
13
|
+
// holds arrangement and words and owns no pixels of its own. That is the whole
|
|
14
|
+
// architecture: a stranger with `@hanzo/ui`, `@hanzo/ai` and `@hanzo/iam` can
|
|
15
|
+
// assemble this room, because nothing in it is private to hanzo.ai.
|
|
16
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
17
|
+
import { createPortal } from "react-dom";
|
|
18
|
+
import { MessageSquare, Code2, Inbox, Bot, ListChecks, HardDrive, Settings2, UserPlus, Building2, Search, Hash, AtSign, Lock, Workflow, Plus, ArrowLeft, ArrowRight, Send, Minus, Blocks, Star, FolderOpen, Activity, BookOpen, SquareTerminal, Share2, Sparkles, PanelLeft, PanelRight, Users, Globe, Cpu, Boxes, Wallet, Video, Calendar, LayoutGrid, ArrowUpLeft, } from "lucide-react";
|
|
19
|
+
import { AiProvider, useAgents, useInbox, useModels, usePeople, useThreads, } from "@hanzo/ai/react";
|
|
20
|
+
import { Box, XStack, YStack, Text } from "@hanzo/ui";
|
|
21
|
+
// THE PAPER LADDER. `sheet(n)` is a fill AND the light on it, published as a
|
|
22
|
+
// pair by @hanzo/design, so a surface asks for a HEIGHT instead of composing a
|
|
23
|
+
// shadow — which is the only way three panels on one screen end up lit from one
|
|
24
|
+
// direction. `scrim` is the dim under anything that lifts off the page.
|
|
25
|
+
import { scrim, sheet } from "@hanzo/ui/glass";
|
|
26
|
+
import { Sidebar, SidebarIconButton, SidebarNewChat, SidebarScroll, SidebarSection, SidebarItem, SidebarFolder, } from "@hanzo/ui/chat";
|
|
27
|
+
import { OrgProjectSwitcher, useIam, useOrganizations, } from "@hanzo/iam/react";
|
|
28
|
+
import { Grip } from "@hanzo/build";
|
|
29
|
+
import { HanzoMark, } from "@hanzogui/shell";
|
|
30
|
+
import { useHydrated } from "./lib/hydrated.js";
|
|
31
|
+
import { useHost } from "./lib/hostname.js";
|
|
32
|
+
import { useAi } from "./lib/ai.js";
|
|
33
|
+
import { useTiers, useHidden } from "./lib/tiers.js";
|
|
34
|
+
import { useAccount } from "./lib/account.js";
|
|
35
|
+
import { hasSession, superAdmin } from "./lib/session.js";
|
|
36
|
+
import { enter, LOGIN } from "./lib/destination.js";
|
|
37
|
+
import { address, sharedWithMe } from "./lib/share.js";
|
|
38
|
+
import { list } from "./lib/list.js";
|
|
39
|
+
import { sku } from "./lib/limits.js";
|
|
40
|
+
import { mark } from "./lib/mark.js";
|
|
41
|
+
import { useOpen } from "./open.js";
|
|
42
|
+
import { useSpan } from "./span.js";
|
|
43
|
+
import { pane } from "./ground.js";
|
|
44
|
+
/** The new-agent row's fields: an outlined box in the room's own ink. */
|
|
45
|
+
const FIELD = {
|
|
46
|
+
bg: "transparent",
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderColor: "var(--border)",
|
|
49
|
+
rounded: 6,
|
|
50
|
+
py: 6,
|
|
51
|
+
outlineStyle: "none",
|
|
52
|
+
color: "inherit",
|
|
53
|
+
fontSize: "$2",
|
|
54
|
+
};
|
|
55
|
+
/** The side column's grip stands in the gutter the frame leaves, or 8px where there is none; the rail clears the
|
|
56
|
+
* home indicator on a phone. gui carries a `calc()`, `max()` or `env()` to the page unchanged; its types only name
|
|
57
|
+
* custom properties. */
|
|
58
|
+
const GUTTER_START = "calc(-1 * var(--pane-gap, 0px))";
|
|
59
|
+
const GUTTER = "max(8px, var(--pane-gap, 8px))";
|
|
60
|
+
const HOME_INDICATOR = "env(safe-area-inset-bottom)";
|
|
61
|
+
import { invite } from "./lib/home.js";
|
|
62
|
+
import { reach } from "./lib/reach.js";
|
|
63
|
+
import { say } from "./failure.js";
|
|
64
|
+
import { Face } from "./cast.js";
|
|
65
|
+
import { useStarred, titleOf } from "./stars.js";
|
|
66
|
+
import { roomName } from "./network.js";
|
|
67
|
+
import { useTeamRooms, teamKey, roomHref } from "./rooms.js";
|
|
68
|
+
import { apex, dev, ENTRY, room as moves, WORKSPACE } from "./lib/host.js";
|
|
69
|
+
import { netKey } from "./conversations.js";
|
|
70
|
+
import { FIRST, SettingsList, SettingsPane, } from "./Settings.js";
|
|
71
|
+
import { Profile } from "./Profile.js";
|
|
72
|
+
import { SupportPicker } from "./Support.js";
|
|
73
|
+
import { Find } from "./Find.js";
|
|
74
|
+
import { Account } from "./Account.js";
|
|
75
|
+
import { ModeMark } from "./ModeMark.js";
|
|
76
|
+
import { called } from "./team.js";
|
|
77
|
+
import { checkoutUrl } from './lib/plans.js';
|
|
78
|
+
/** Tabs: the terminals, at their own host. */
|
|
79
|
+
const TABS = "https://tabs.hanzo.ai/app";
|
|
80
|
+
/** Where a plan is chosen. `lib/plans` owns the address; this is the door. */
|
|
81
|
+
const BILLING = checkoutUrl();
|
|
82
|
+
/**
|
|
83
|
+
* THE FLOOR UNDER ANYTHING A FINGER PRESSES.
|
|
84
|
+
*
|
|
85
|
+
* 44 CSS px each way, which is the width at which a target stops being aimed at
|
|
86
|
+
* and starts being pressed — the number WCAG 2.5.5 and Apple's own guidance
|
|
87
|
+
* arrive at separately, and the one that does not move with the pointer, since a
|
|
88
|
+
* mouse on a laptop and a thumb on the same page get the same control. Every
|
|
89
|
+
* icon control in this shell's own chrome had been drawn at the size of its
|
|
90
|
+
* GLYPH instead: @hanzo/ui gives its icon button 28, the mark's rule in
|
|
91
|
+
* globals.css says 30, and compose padded a 16px pen out to 32.
|
|
92
|
+
*
|
|
93
|
+
* IT IS NOT SPREAD EVERYWHERE, and the exceptions are worth knowing before you
|
|
94
|
+
* grep for why a neighbour is still small: the roster's own '+' and its
|
|
95
|
+
* per-agent remove sit at ~22, and the sidebar filter input is 30. Those are a
|
|
96
|
+
* list's controls rather than the shell's, and they move as that list's own
|
|
97
|
+
* change — but the shell is not uniformly 44 until they do.
|
|
98
|
+
*
|
|
99
|
+
* A MINIMUM and not a size, which is what lets it be applied from here without
|
|
100
|
+
* touching either of those. The used width is the larger of `width` and
|
|
101
|
+
* `min-width`, so the mark reaches 44 while its stylesheet still says 30 and
|
|
102
|
+
* nothing already bigger is disturbed — and the glyphs keep the sizes they were
|
|
103
|
+
* chosen at. A larger icon would be a different drawing; this is the same
|
|
104
|
+
* drawing you can hit.
|
|
105
|
+
*/
|
|
106
|
+
const TAP = { minWidth: 44, minHeight: 44 };
|
|
107
|
+
/** The height of a control you can SEE in the top row — the search field, the
|
|
108
|
+
* sign-in pill. It is not TAP: a target is 44 so a thumb can find it, and a
|
|
109
|
+
* control is 28 so a row of them reads as one line. Stating the target on the
|
|
110
|
+
* element that also carries the border makes the target visible, which is how
|
|
111
|
+
* the sign-in button came to stand half again as tall as the field beside it. */
|
|
112
|
+
const CONTROL = 28;
|
|
113
|
+
import { getStoredPeople, getStoredAgents, getCustomThreads, createDirectChat, } from "./contacts-store.js";
|
|
114
|
+
import { bare } from './lib/bare.js';
|
|
115
|
+
import { BAD } from './lib/mix.js';
|
|
116
|
+
import { site, where } from './where.js';
|
|
117
|
+
/**
|
|
118
|
+
* The destinations, in rail order.
|
|
119
|
+
*
|
|
120
|
+
* ONE table. Route, title and icon were three `Record<Mode, …>` standing side by
|
|
121
|
+
* side, which is a shape that lets a sixth destination arrive with two of the
|
|
122
|
+
* three filled in and no error until someone navigates to it.
|
|
123
|
+
*/
|
|
124
|
+
const HOME = {
|
|
125
|
+
id: "home",
|
|
126
|
+
// The rooms' Home is the host's: /home beside the app, / on hanzo.team.
|
|
127
|
+
get route() {
|
|
128
|
+
return where().home;
|
|
129
|
+
},
|
|
130
|
+
title: "Hanzo",
|
|
131
|
+
label: "Home",
|
|
132
|
+
// The chat bubble. Home IS the conversation with the org — the rooms it talks
|
|
133
|
+
// in and the people in them — so it is drawn as one. It shared this glyph with
|
|
134
|
+
// Chat once and was given a house to separate them; the two never sit in the
|
|
135
|
+
// rail together (CHAT is a tile, HOME is not), so there is nothing to tell
|
|
136
|
+
// apart, and a house says "a place" where the room is a conversation.
|
|
137
|
+
icon: MessageSquare,
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* CHAT LEADS THE RAIL, and Dev follows it.
|
|
141
|
+
*
|
|
142
|
+
* These are the two rooms a person lives in — one to ask and one to build — so
|
|
143
|
+
* they are the first two tiles and the switch between them is one press. The
|
|
144
|
+
* rest of the rail is where you go from there.
|
|
145
|
+
*
|
|
146
|
+
* It was off the rail while /chat answered `invalid redirect_uri`: a tile that
|
|
147
|
+
* leads to an error page offers a door that does not open. The room is this
|
|
148
|
+
* export's own now, so the door opens.
|
|
149
|
+
*/
|
|
150
|
+
const CHAT = {
|
|
151
|
+
id: "chat",
|
|
152
|
+
route: "/chat",
|
|
153
|
+
title: "Hanzo AI",
|
|
154
|
+
label: "Chat",
|
|
155
|
+
icon: MessageSquare,
|
|
156
|
+
create: "New chat",
|
|
157
|
+
};
|
|
158
|
+
const PLACES = [
|
|
159
|
+
{
|
|
160
|
+
id: "dev",
|
|
161
|
+
get route() {
|
|
162
|
+
return dev();
|
|
163
|
+
},
|
|
164
|
+
title: "Hanzo Dev",
|
|
165
|
+
label: "Dev",
|
|
166
|
+
icon: Code2,
|
|
167
|
+
create: "New run",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "agents",
|
|
171
|
+
route: "/work",
|
|
172
|
+
title: "Agents",
|
|
173
|
+
label: "Agents",
|
|
174
|
+
icon: Bot,
|
|
175
|
+
tier: "alpha",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "bots",
|
|
179
|
+
route: "/bots",
|
|
180
|
+
title: "Bots",
|
|
181
|
+
label: "Bots",
|
|
182
|
+
icon: Cpu,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "meet",
|
|
186
|
+
route: "/meet",
|
|
187
|
+
title: "Hanzo Meet",
|
|
188
|
+
label: "Meet",
|
|
189
|
+
icon: Video,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "cal",
|
|
193
|
+
route: "/cal",
|
|
194
|
+
title: "Calendar",
|
|
195
|
+
label: "Calendar",
|
|
196
|
+
icon: Calendar,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "contacts",
|
|
200
|
+
route: "/contacts",
|
|
201
|
+
title: "People & Agents",
|
|
202
|
+
label: "Contacts",
|
|
203
|
+
icon: Users,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "inbox",
|
|
207
|
+
route: "/inbox",
|
|
208
|
+
title: "Inbox",
|
|
209
|
+
label: "Inbox",
|
|
210
|
+
icon: Inbox,
|
|
211
|
+
tier: "beta",
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "tasks",
|
|
215
|
+
route: "/board",
|
|
216
|
+
title: "Board",
|
|
217
|
+
label: "Board",
|
|
218
|
+
icon: ListChecks,
|
|
219
|
+
tier: "alpha",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "drive",
|
|
223
|
+
route: "/drive",
|
|
224
|
+
title: "Drive",
|
|
225
|
+
label: "Drive",
|
|
226
|
+
icon: HardDrive,
|
|
227
|
+
tier: "beta",
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
export const CONSOLE_PRODUCTS = [
|
|
231
|
+
{ id: "cloud", label: "Compute & VMs", icon: Cpu, href: "https://platform.hanzo.ai/cloud" },
|
|
232
|
+
{ id: "storage", label: "S3 Object Storage", icon: HardDrive, href: "https://platform.hanzo.ai/storage" },
|
|
233
|
+
{ id: "datastore", label: "Datastore", icon: Boxes, href: "https://platform.hanzo.ai/datastore" },
|
|
234
|
+
{ id: "iam", label: "Identity & Access", icon: Lock, href: "https://identity.hanzo.ai" },
|
|
235
|
+
{ id: "observe", label: "Observability", icon: Activity, href: "https://platform.hanzo.ai/observability" },
|
|
236
|
+
{ id: "network", label: "Zero Trust & Zap", icon: Globe, href: "https://platform.hanzo.ai/network" },
|
|
237
|
+
{ id: "flow", label: "Agent Flow", icon: Workflow, href: "https://flow.hanzo.ai" },
|
|
238
|
+
{ id: "models", label: "Foundation Models", icon: Blocks, href: "https://models.hanzo.ai" },
|
|
239
|
+
{ id: "webhooks", label: "Webhooks & Events", icon: Send, href: "https://platform.hanzo.ai/webhooks" },
|
|
240
|
+
{ id: "billing", label: "Billing & Wallet", icon: Wallet, href: BILLING },
|
|
241
|
+
];
|
|
242
|
+
/**
|
|
243
|
+
* Whether a place belongs in THIS reader's rail.
|
|
244
|
+
*
|
|
245
|
+
* A place with no tier is the landing's offer — Chat and Dev — and everyone gets
|
|
246
|
+
* it. A tiered room shows only where two things are true: the reader's identity
|
|
247
|
+
* OPENS the tier (`useTiers`: a SuperAdmin gets every tier, an alpha/beta org its
|
|
248
|
+
* own, a stranger none), and the reader has not HIDDEN it in Settings. The rooms
|
|
249
|
+
* stay in PLACES either way — the structure is whole — so opening a tier is a
|
|
250
|
+
* flag flip, not a code change. Hide, never delete.
|
|
251
|
+
*
|
|
252
|
+
* A rail of two is what a stranger meets, which is the landing's offer exactly.
|
|
253
|
+
* The other four are dark until each ships and its tier is turned on.
|
|
254
|
+
*
|
|
255
|
+
* ON THE APEX THE ROOMS ARE hanzo.team'S. There a place shows only when its
|
|
256
|
+
* route stays on the apex, which is Chat and Dev, and the rail ends in one tile
|
|
257
|
+
* that opens the workspace instead.
|
|
258
|
+
*/
|
|
259
|
+
const shows = (place, tiers, hidden, onApex) => !(onApex && moves(place.route)) &&
|
|
260
|
+
(!place.tier || (tiers.has(place.tier) && !hidden.has(place.tier)));
|
|
261
|
+
/**
|
|
262
|
+
* WHERE "GO THERE" COMES FROM, and why it is a prop.
|
|
263
|
+
*
|
|
264
|
+
* The rail moves between rooms, which is the one thing in this shell only the
|
|
265
|
+
* HOST can do: a Next route pushes through its router, a Tauri window swaps a
|
|
266
|
+
* view, an embedded surface may refuse to navigate at all. Taking `navigate`
|
|
267
|
+
* as a prop is what leaves this file — and every component under it — with no
|
|
268
|
+
* import from any framework, so the same shell mounts under Next, under Vite
|
|
269
|
+
* in a desktop webview, or under a test that just records where it was asked
|
|
270
|
+
* to go.
|
|
271
|
+
*
|
|
272
|
+
* Measured before the change: `next/navigation` here and `next/link` in
|
|
273
|
+
* `Chat.tsx` were the ONLY two framework imports in the whole /chat + /dev
|
|
274
|
+
* surface — 13 files, all `'use client'`, no RSC. Those two were the entire
|
|
275
|
+
* cost of the port, and this removes one of them.
|
|
276
|
+
*/
|
|
277
|
+
export function Workspace({ mode, children, navigate, back, forward, }) {
|
|
278
|
+
const { client } = useAi();
|
|
279
|
+
return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(Frame, { mode: mode, navigate: navigate, back: back, forward: forward, children: children }) }));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The conversations, for /chat only.
|
|
283
|
+
*
|
|
284
|
+
* It owns its hook so the request is made by the surface that draws it —
|
|
285
|
+
* mounted from `Frame` this ran on /dev too, one authenticated read per page
|
|
286
|
+
* load whose answer was never rendered. Remounting it (a changed `key`) is the
|
|
287
|
+
* refresh, so nothing has to hand a `reload` back up the tree.
|
|
288
|
+
*/
|
|
289
|
+
/**
|
|
290
|
+
* WHEN a conversation was last spoken in, as a heading.
|
|
291
|
+
*
|
|
292
|
+
* Time is the only thing the platform says about a thread besides its title —
|
|
293
|
+
* `Thread` is `{id, title, updatedAt}` — so it is the only grouping that can be
|
|
294
|
+
* true. A folder per project would be an invention: nothing files a thread
|
|
295
|
+
* under anything, and headings a reader could not have caused are worse than no
|
|
296
|
+
* headings.
|
|
297
|
+
*
|
|
298
|
+
* The bands are the ones a person actually reasons in. "Today" and "Yesterday"
|
|
299
|
+
* are named because someone remembers those; past that a date is a better
|
|
300
|
+
* answer than a count of days, and past a month the month alone is enough.
|
|
301
|
+
*/
|
|
302
|
+
function when(iso, now) {
|
|
303
|
+
if (!iso)
|
|
304
|
+
return "Earlier";
|
|
305
|
+
const at = new Date(iso);
|
|
306
|
+
if (Number.isNaN(at.getTime()))
|
|
307
|
+
return "Earlier";
|
|
308
|
+
const day = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
309
|
+
const days = Math.round((day(now) - day(at)) / 86400000);
|
|
310
|
+
if (days <= 0)
|
|
311
|
+
return "Today";
|
|
312
|
+
if (days === 1)
|
|
313
|
+
return "Yesterday";
|
|
314
|
+
if (days < 7)
|
|
315
|
+
return "Previous 7 days";
|
|
316
|
+
if (days < 30)
|
|
317
|
+
return "Previous 30 days";
|
|
318
|
+
if (at.getFullYear() === now.getFullYear())
|
|
319
|
+
return at.toLocaleDateString(undefined, { month: "long" });
|
|
320
|
+
return at.toLocaleDateString(undefined, { month: "long", year: "numeric" });
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* WHEN A CONVERSATION STARTED, from its own id.
|
|
324
|
+
*
|
|
325
|
+
* `Thread` publishes a title and `updatedAt` and no created time — but the id
|
|
326
|
+
* is `<nanoseconds>-<seq>`, and that leading number IS the instant it was
|
|
327
|
+
* minted. Verified against the live store: id 1787970910036802961 and
|
|
328
|
+
* updatedAt 2026-08-28T19:35:10.036806065 name the same moment, to the
|
|
329
|
+
* nanosecond. So the pair a reader wants — when it began and when it was last
|
|
330
|
+
* touched — is already on the wire and needs no new field.
|
|
331
|
+
*/
|
|
332
|
+
function born(id) {
|
|
333
|
+
const ns = Number(id.split("-")[0]);
|
|
334
|
+
if (!Number.isFinite(ns) || ns <= 0)
|
|
335
|
+
return null;
|
|
336
|
+
const at = new Date(ns / 1e6);
|
|
337
|
+
return Number.isNaN(at.getTime()) ? null : at;
|
|
338
|
+
}
|
|
339
|
+
/** A day and a clock, in the reader's own locale. */
|
|
340
|
+
const moment = (at) => at.toLocaleString(undefined, {
|
|
341
|
+
month: "short",
|
|
342
|
+
day: "numeric",
|
|
343
|
+
hour: "numeric",
|
|
344
|
+
minute: "2-digit",
|
|
345
|
+
});
|
|
346
|
+
/**
|
|
347
|
+
* The two ends of a conversation, on one line.
|
|
348
|
+
*
|
|
349
|
+
* Started AND last used, because they answer different questions — which one is
|
|
350
|
+
* the old thread, and which one is still alive. Where a conversation was used
|
|
351
|
+
* on the day it began, the second half would repeat the first, so it says the
|
|
352
|
+
* one moment instead of the same moment twice.
|
|
353
|
+
*/
|
|
354
|
+
function Span({ id, updatedAt }) {
|
|
355
|
+
const from = born(id);
|
|
356
|
+
const to = updatedAt ? new Date(updatedAt) : null;
|
|
357
|
+
if (!from)
|
|
358
|
+
return null;
|
|
359
|
+
const moved = to && !Number.isNaN(to.getTime()) && to.getTime() - from.getTime() > 60_000;
|
|
360
|
+
return (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: moved ? `${moment(from)} — ${moment(to)}` : moment(from) }));
|
|
361
|
+
}
|
|
362
|
+
function Chats({ filter = "", onPick, }) {
|
|
363
|
+
const { thread, open } = useOpen();
|
|
364
|
+
// RECENTS ARE THE READER'S OWN, or there are none.
|
|
365
|
+
//
|
|
366
|
+
// An empty list was replaced with ten invented conversation titles carrying
|
|
367
|
+
// manufactured timestamps — "Composer display and UI styling fixes", "Office-
|
|
368
|
+
// lease-agreement file search" — banded under Today and Yesterday like a
|
|
369
|
+
// history somebody had. A visitor's first sight of the app was ten chats they
|
|
370
|
+
// had never had, and every one of them opened nothing.
|
|
371
|
+
const { threads, loading, error: wrong } = useThreads();
|
|
372
|
+
const [customThreads, setCustomThreads] = useState([]);
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
setCustomThreads(getCustomThreads());
|
|
375
|
+
const handleThreadChange = () => setCustomThreads(getCustomThreads());
|
|
376
|
+
window.addEventListener("hanzo_threads_changed", handleThreadChange);
|
|
377
|
+
return () => window.removeEventListener("hanzo_threads_changed", handleThreadChange);
|
|
378
|
+
}, []);
|
|
379
|
+
const { isAuthenticated, user } = useIam();
|
|
380
|
+
const { user: account } = useAccount();
|
|
381
|
+
const signedIn = isAuthenticated || Boolean(user) || Boolean(account) || hasSession();
|
|
382
|
+
const [starred] = useStarred("threads");
|
|
383
|
+
const mergedThreads = useMemo(() => {
|
|
384
|
+
const list = threads.map((t) => ({
|
|
385
|
+
id: t.id,
|
|
386
|
+
title: t.title || "Untitled",
|
|
387
|
+
updatedAt: t.updatedAt,
|
|
388
|
+
}));
|
|
389
|
+
for (const ct of customThreads) {
|
|
390
|
+
if (!list.some((t) => t.id === ct.id)) {
|
|
391
|
+
list.push({
|
|
392
|
+
id: ct.id,
|
|
393
|
+
title: ct.title,
|
|
394
|
+
updatedAt: ct.updatedAt,
|
|
395
|
+
...(ct.type === "call" || ct.type === "group" ? { kind: ct.type } : {}),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return list;
|
|
400
|
+
}, [threads, customThreads]);
|
|
401
|
+
const found = mergedThreads.filter((t) => matches(t.title || "Untitled", filter));
|
|
402
|
+
const shown = found.filter((t) => !starred.has(t.id));
|
|
403
|
+
const kept = found.filter((t) => starred.has(t.id));
|
|
404
|
+
const bands = [];
|
|
405
|
+
if (kept.length)
|
|
406
|
+
bands.push({ label: "Starred", rows: kept });
|
|
407
|
+
const now = new Date();
|
|
408
|
+
for (const t of shown) {
|
|
409
|
+
const label = when(t.updatedAt, now);
|
|
410
|
+
const last = bands[bands.length - 1];
|
|
411
|
+
if (last && last.label === label && last.label !== "Starred")
|
|
412
|
+
last.rows.push(t);
|
|
413
|
+
else
|
|
414
|
+
bands.push({ label, rows: [t] });
|
|
415
|
+
}
|
|
416
|
+
if (!signedIn)
|
|
417
|
+
return _jsx(Note, { children: "Sign in to see your recent chats." });
|
|
418
|
+
const mine = wrong ? (_jsx(Note, { children: say(wrong, "your chats") })) : loading && threads.length === 0 ? (_jsx(Note, { children: "Reading your chats." })) : found.length === 0 && filter ? (_jsxs(Note, { children: ["No chat matches \u201C", filter, "\u201D."] })) : threads.length === 0 ? (_jsx(Note, { children: "No chats yet. Say something and it lands here." })) : null;
|
|
419
|
+
return (_jsxs(_Fragment, { children: [mine, mine ? null : bands.map((band) => (_jsx(SidebarSection, { label: band.label, children: band.rows.map((t) => (_jsx(SidebarItem, { active: t.id === thread, onPress: () => {
|
|
420
|
+
open(t.id);
|
|
421
|
+
onPick?.();
|
|
422
|
+
}, children: _jsxs(YStack, { minW: 0, children: [_jsxs(XStack, { items: "center", gap: "$1.5", minW: 0, children: [t.kind === "call" ? _jsx(Video, { size: 13, color: "var(--muted-foreground)", "aria-label": "Call" }) : null, t.kind === "group" ? _jsx(Users, { size: 13, color: "var(--muted-foreground)", "aria-label": "Group" }) : null, _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: t.title || "Untitled" })] }), _jsx(Span, { id: t.id, updatedAt: t.updatedAt })] }) }, t.id))) }, band.label))), _jsx(SharedWithYou, { filter: filter, onPick: onPick })] }));
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The chats other people shared with this reader, and who shared each: every
|
|
426
|
+
* link they opened signed in, still live and still open to them (lib/share.ts).
|
|
427
|
+
* Pressing one reads it at /chat/shared, by its share id — no token is held for
|
|
428
|
+
* it.
|
|
429
|
+
*/
|
|
430
|
+
function SharedWithYou({ filter, onPick }) {
|
|
431
|
+
const [items, setItems] = useState(null);
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
let on = true;
|
|
434
|
+
sharedWithMe()
|
|
435
|
+
.then((got) => on && setItems(got))
|
|
436
|
+
.catch(() => on && setItems([]));
|
|
437
|
+
return () => {
|
|
438
|
+
on = false;
|
|
439
|
+
};
|
|
440
|
+
}, []);
|
|
441
|
+
const shown = (items ?? []).filter((one) => matches(one.title || "Untitled", filter));
|
|
442
|
+
if (shown.length === 0)
|
|
443
|
+
return null;
|
|
444
|
+
return (_jsx(SidebarSection, { label: "Shared with you", children: shown.map((one) => (_jsx(SidebarItem, { onPress: () => {
|
|
445
|
+
onPick?.();
|
|
446
|
+
window.location.assign(address(one.share));
|
|
447
|
+
}, children: _jsxs(YStack, { minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: one.title || "Untitled" }), one.by ? (_jsxs(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: ["From ", one.by] })) : null] }) }, one.share))) }));
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The projects. A run is something that happened, a project is the thing it
|
|
451
|
+
* happened TO, and the reader picks the second to see the first — pressing a
|
|
452
|
+
* name opens that project's workspace in the app, at `/`.
|
|
453
|
+
*
|
|
454
|
+
* IT OWNS ITS READ, like every other list in this column — mounted from the
|
|
455
|
+
* frame it would fetch on every page load for every reader, including one whose
|
|
456
|
+
* org answers 403.
|
|
457
|
+
*/
|
|
458
|
+
function Projects({ filter, onPick, }) {
|
|
459
|
+
const { client } = useAi();
|
|
460
|
+
const [starred] = useStarred('projects');
|
|
461
|
+
const [rows, setRows] = useState(null);
|
|
462
|
+
// A REFUSAL IS NOT A PROJECT LIST. The catch below wrote `hanzo`, `zoolabs`
|
|
463
|
+
// and `lux` into the rail — three orgs this reader may have nothing to do
|
|
464
|
+
// with — and the same three covered "not asked yet" and "genuinely none". So
|
|
465
|
+
// a 403 drew the same column as a healthy org, and pressing one of those rows
|
|
466
|
+
// opened a project that does not exist for the person who pressed it.
|
|
467
|
+
const [wrong, setWrong] = useState(null);
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
if (!client)
|
|
470
|
+
return;
|
|
471
|
+
let live = true;
|
|
472
|
+
setWrong(null);
|
|
473
|
+
client.http
|
|
474
|
+
.json({ path: "/v1/projects" })
|
|
475
|
+
.then((found) => live && setRows(Array.isArray(found) ? found : []))
|
|
476
|
+
.catch((e) => {
|
|
477
|
+
if (!live)
|
|
478
|
+
return;
|
|
479
|
+
setRows(null);
|
|
480
|
+
setWrong(e);
|
|
481
|
+
});
|
|
482
|
+
return () => {
|
|
483
|
+
live = false;
|
|
484
|
+
};
|
|
485
|
+
}, [client]);
|
|
486
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
487
|
+
const found = list.filter((r) => matches(r.name, filter));
|
|
488
|
+
const shown = [
|
|
489
|
+
...found.filter((r) => starred.has(r.id)),
|
|
490
|
+
...found.filter((r) => !starred.has(r.id)),
|
|
491
|
+
];
|
|
492
|
+
return (_jsxs(_Fragment, { children: [wrong ? (_jsx(Note, { children: say(wrong, "your projects") })) : rows === null ? (_jsx(Note, { children: "Reading your projects." })) : list.length === 0 ? (_jsx(Note, { children: "Nothing deployed yet." })) : shown.length === 0 ? (_jsxs(Note, { children: ["No project matches \u201C", filter, "\u201D."] })) : null, shown.map((one) => (_jsx(YStack, { gap: "$0.5", children: _jsx(SidebarItem, { icon: starred.has(one.id) ? (_jsx(Star, { size: 13, fill: "currentColor", "aria-hidden": true })) : (_jsx(FolderOpen, { size: 13, "aria-hidden": true })), onPress: () => onPick?.(one.slug || one.name), children: one.name }) }, one.id)))] }));
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The rooms the inbox holds, under the transport that carried each one.
|
|
496
|
+
*
|
|
497
|
+
* The transport is the section because that is how a person holds it: Slack
|
|
498
|
+
* here, iMessage there. The same display name can appear under both without
|
|
499
|
+
* being one person — `senderUser` is declared by the schema and omitted by every
|
|
500
|
+
* measured message, so a handle is a handle and a name would be a guess.
|
|
501
|
+
*
|
|
502
|
+
* `useInbox` groups the page it fetched; the server has no rooms route. So this
|
|
503
|
+
* is the rooms in what has ARRIVED, and it grows as the page does.
|
|
504
|
+
*/
|
|
505
|
+
function Rooms({ onPick, navigate, }) {
|
|
506
|
+
const { room, openRoom } = useOpen();
|
|
507
|
+
const { rooms } = useInbox({ limit: 100 });
|
|
508
|
+
if (rooms.length === 0)
|
|
509
|
+
return null;
|
|
510
|
+
const carried = new Map();
|
|
511
|
+
for (const one of rooms) {
|
|
512
|
+
const held = carried.get(one.channel);
|
|
513
|
+
if (held)
|
|
514
|
+
held.push(one);
|
|
515
|
+
else
|
|
516
|
+
carried.set(one.channel, [one]);
|
|
517
|
+
}
|
|
518
|
+
return (_jsx(_Fragment, { children: [...carried].map(([channel, held]) => (_jsx(SidebarFolder, { name: channel, defaultOpen: true, children: held.map((one) => (_jsx(SidebarItem, { active: netKey(one) === room, onPress: () => {
|
|
519
|
+
// ONE NAMESPACE. This wrote the bare inbox key while the team
|
|
520
|
+
// rows wrote a bare room id, into the SAME slot — so whichever
|
|
521
|
+
// pane was open looked the value up in its own list, missed,
|
|
522
|
+
// and drew nothing. `conversations.ts` holds the one key.
|
|
523
|
+
openRoom(netKey(one));
|
|
524
|
+
navigate?.("/inbox");
|
|
525
|
+
onPick?.();
|
|
526
|
+
}, children: roomName(one) }, one.key))) }, channel))) }));
|
|
527
|
+
}
|
|
528
|
+
/** Whether a name answers what was typed. Case-folded substring: three letters
|
|
529
|
+
* of a half-remembered name is not a pattern, and should not have to be. */
|
|
530
|
+
const matches = (name, filter) => !filter || name.toLowerCase().includes(filter.toLowerCase());
|
|
531
|
+
/**
|
|
532
|
+
* WHAT A SECTION SAYS WHEN IT HAS NO ROWS.
|
|
533
|
+
*
|
|
534
|
+
* Every folder in this rail drew only when it had something, so a list that was
|
|
535
|
+
* still loading, a list that was empty and a list whose service had refused all
|
|
536
|
+
* came out as the same blank — and blank was indistinguishable from an org that
|
|
537
|
+
* owns nothing. That is precisely the gap the invented rosters were filling.
|
|
538
|
+
* One line, in the sidebar's own quiet type, and the three states read apart.
|
|
539
|
+
*/
|
|
540
|
+
function Note({ children }) {
|
|
541
|
+
return (_jsx(YStack, { pl: "$4", pr: "$3", py: "$1.5", children: _jsx(Text, { fontSize: "$1", color: "$soft", children: children }) }));
|
|
542
|
+
}
|
|
543
|
+
function Team({ filter = "", onPick, navigate, }) {
|
|
544
|
+
const { room, openRoom } = useOpen();
|
|
545
|
+
const { rooms, wrong } = useTeamRooms();
|
|
546
|
+
const { isAuthenticated, user } = useIam();
|
|
547
|
+
const { user: account } = useAccount();
|
|
548
|
+
const signedIn = isAuthenticated || Boolean(user) || Boolean(account) || hasSession();
|
|
549
|
+
const [starred] = useStarred();
|
|
550
|
+
const shown = list(rooms).filter((r) => matches(r.name, filter));
|
|
551
|
+
const favourites = shown.filter((r) => starred.has(titleOf(r.name, r.direct)));
|
|
552
|
+
const publicChannels = shown.filter((r) => !r.direct && r.visibility === "public");
|
|
553
|
+
const orgChannels = shown.filter((r) => !r.direct && (r.visibility === "org" || (!r.visibility && !r.private)));
|
|
554
|
+
const privateChannels = shown.filter((r) => !r.direct && (r.visibility === "private" || r.private));
|
|
555
|
+
const directs = shown.filter((r) => r.direct);
|
|
556
|
+
// THE ORDER IS SESSION, THEN REFUSAL, THEN SILENCE — the ladder every room in
|
|
557
|
+
// this app climbs (see Home's `Waiting`). "Reading." never resolves for a
|
|
558
|
+
// reader nobody is going to answer, so the session is asked first.
|
|
559
|
+
if (!signedIn)
|
|
560
|
+
return _jsx(Note, { children: "Sign in to see your org's rooms." });
|
|
561
|
+
if (wrong)
|
|
562
|
+
return _jsx(Note, { children: say(wrong, "your rooms") });
|
|
563
|
+
if (rooms === null)
|
|
564
|
+
return _jsx(Note, { children: "Reading your rooms." });
|
|
565
|
+
if (rooms.length === 0)
|
|
566
|
+
return _jsx(Note, { children: "No rooms yet. Open one and the conversation has somewhere to live." });
|
|
567
|
+
if (shown.length === 0)
|
|
568
|
+
return _jsxs(Note, { children: ["No room matches \u201C", filter, "\u201D."] });
|
|
569
|
+
return (_jsxs(_Fragment, { children: [favourites.length > 0 ? (_jsx(SidebarFolder, { name: "Favorites", defaultOpen: true, children: favourites.map((one) => (_jsx(SidebarItem, { active: teamKey(one) === room, icon: _jsx(Star, { size: 13, "aria-hidden": true }), onPress: () => {
|
|
570
|
+
openRoom(teamKey(one));
|
|
571
|
+
navigate?.(roomHref(teamKey(one)));
|
|
572
|
+
onPick?.();
|
|
573
|
+
}, children: one.name }, one.id))) })) : null, publicChannels.length > 0 ? (_jsx(SidebarFolder, { name: "Public Channels", defaultOpen: true, children: publicChannels.map((one) => (_jsx(SidebarItem, { active: teamKey(one) === room, icon: _jsx(Globe, { size: 13, "aria-hidden": true }), onPress: () => {
|
|
574
|
+
openRoom(teamKey(one));
|
|
575
|
+
navigate?.(roomHref(teamKey(one)));
|
|
576
|
+
onPick?.();
|
|
577
|
+
}, children: one.name }, one.id))) })) : null, orgChannels.length > 0 ? (_jsx(SidebarFolder, { name: "Org Channels", defaultOpen: true, children: orgChannels.map((one) => (_jsx(SidebarItem, { active: teamKey(one) === room, icon: _jsx(Hash, { size: 13, "aria-hidden": true }), onPress: () => {
|
|
578
|
+
openRoom(teamKey(one));
|
|
579
|
+
navigate?.(roomHref(teamKey(one)));
|
|
580
|
+
onPick?.();
|
|
581
|
+
}, children: one.name }, one.id))) })) : null, privateChannels.length > 0 ? (_jsx(SidebarFolder, { name: "Private Channels", defaultOpen: true, children: privateChannels.map((one) => (_jsx(SidebarItem, { active: teamKey(one) === room, icon: _jsx(Lock, { size: 13, "aria-hidden": true }), onPress: () => {
|
|
582
|
+
openRoom(teamKey(one));
|
|
583
|
+
navigate?.(roomHref(teamKey(one)));
|
|
584
|
+
onPick?.();
|
|
585
|
+
}, children: one.name }, one.id))) })) : null, directs.length > 0 ? (_jsx(SidebarFolder, { name: "Direct messages", defaultOpen: true, children: directs.map((one) => (_jsx(SidebarItem, { active: teamKey(one) === room, icon: _jsx(AtSign, { size: 13, "aria-hidden": true }), onPress: () => {
|
|
586
|
+
openRoom(teamKey(one));
|
|
587
|
+
navigate?.(roomHref(teamKey(one)));
|
|
588
|
+
onPick?.();
|
|
589
|
+
}, children: one.name }, one.id))) })) : null] }));
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* The teammates, as a directory.
|
|
593
|
+
*
|
|
594
|
+
* IAM knows who is in the org, which is why these rows are real. It publishes no
|
|
595
|
+
* route that sends one of them a message, so a row here IDENTIFIES rather than
|
|
596
|
+
* opens — a name that looks like a conversation and starts nothing is worse than
|
|
597
|
+
* a name that is plainly a name.
|
|
598
|
+
*/
|
|
599
|
+
/**
|
|
600
|
+
* The bots, which ARE openable: an agent has runs, and a run is a thing to
|
|
601
|
+
* watch. Picking one selects it; Agents is where it works.
|
|
602
|
+
*/
|
|
603
|
+
/**
|
|
604
|
+
* AGENTS & APPS — the org's agents, and the two things you can do to one.
|
|
605
|
+
*
|
|
606
|
+
* IT IS DRAWN EVEN WHEN EMPTY. It used to return null on an empty list, which
|
|
607
|
+
* is the state a new org is in — so the one section that can ADD an agent was
|
|
608
|
+
* absent exactly while there were none, and the way in existed only after
|
|
609
|
+
* somebody had already found another way in.
|
|
610
|
+
*
|
|
611
|
+
* The header carries the `+` because that is where the section's own verb
|
|
612
|
+
* belongs; `@hanzo/ui`'s `SidebarFolder` publishes no slot for one, so the
|
|
613
|
+
* header is composed here against the same tokens. When that prop exists this
|
|
614
|
+
* goes back to being a folder — Channels wants the same button.
|
|
615
|
+
*
|
|
616
|
+
* REMOVING ASKS ONCE. An agent is a definition somebody wrote and there is no
|
|
617
|
+
* undo, so the minus arms and the second press does it. Not a dialog: a dialog
|
|
618
|
+
* over a sidebar row is a bigger interruption than the thing it guards.
|
|
619
|
+
*/
|
|
620
|
+
/**
|
|
621
|
+
* PEOPLE & AGENTS — one list, because it is one question.
|
|
622
|
+
*
|
|
623
|
+
* They were two sections, Direct messages and Agents & apps, and the split was
|
|
624
|
+
* about what a name IS rather than about what a reader is looking for: you come
|
|
625
|
+
* to this column to find whoever you are about to talk to, and whether they run
|
|
626
|
+
* on a model is a property of the row, not a reason to look somewhere else. The
|
|
627
|
+
* count in the header is the whole roster, which is the number a person means
|
|
628
|
+
* when they ask how big the team is.
|
|
629
|
+
*
|
|
630
|
+
* The order is people, then agents. Not a ranking — the org's people are the
|
|
631
|
+
* shorter and more stable list, so putting them first keeps the rows a reader
|
|
632
|
+
* already knows in the same place as agents come and go.
|
|
633
|
+
*/
|
|
634
|
+
function Bots({ show, owner, filter = "", onPick, navigate, }) {
|
|
635
|
+
const { agents: room, openAgent, withAgents, openSession, open: openThread } = useOpen();
|
|
636
|
+
const { agents, create, remove, loading: agentsLoading, error: agentsWrong } = useAgents();
|
|
637
|
+
const { people, loading: peopleLoading, error: peopleWrong, } = usePeople({ owner: owner ?? "", service: false });
|
|
638
|
+
const [open, setOpen] = useState(true);
|
|
639
|
+
const [busy, setBusy] = useState(false);
|
|
640
|
+
const [sortMode, setSortMode] = useState("alpha");
|
|
641
|
+
const [arming, setArming] = useState(null);
|
|
642
|
+
const [storedPeople, setStoredPeople] = useState([]);
|
|
643
|
+
const [storedAgents, setStoredAgents] = useState([]);
|
|
644
|
+
useEffect(() => {
|
|
645
|
+
setStoredPeople(getStoredPeople());
|
|
646
|
+
setStoredAgents(getStoredAgents());
|
|
647
|
+
const onChange = () => {
|
|
648
|
+
setStoredPeople(getStoredPeople());
|
|
649
|
+
setStoredAgents(getStoredAgents());
|
|
650
|
+
};
|
|
651
|
+
window.addEventListener("hanzo_contacts_changed", onChange);
|
|
652
|
+
window.addEventListener("hanzo_agents_changed", onChange);
|
|
653
|
+
return () => {
|
|
654
|
+
window.removeEventListener("hanzo_contacts_changed", onChange);
|
|
655
|
+
window.removeEventListener("hanzo_agents_changed", onChange);
|
|
656
|
+
};
|
|
657
|
+
}, []);
|
|
658
|
+
const [making, setMaking] = useState(false);
|
|
659
|
+
const [name, setName] = useState("");
|
|
660
|
+
const [model, setModel] = useState("enso");
|
|
661
|
+
const [emoji, setEmoji] = useState("");
|
|
662
|
+
const [avatar, setAvatar] = useState("");
|
|
663
|
+
const [wrong, setWrong] = useState(null);
|
|
664
|
+
const picking = useRef(null);
|
|
665
|
+
const { models } = useModels();
|
|
666
|
+
const allPeopleMap = new Map();
|
|
667
|
+
for (const p of (Array.isArray(storedPeople) ? storedPeople : []))
|
|
668
|
+
if (p?.id)
|
|
669
|
+
allPeopleMap.set(p.id, p);
|
|
670
|
+
for (const p of (Array.isArray(people) ? people : []))
|
|
671
|
+
if (p?.id)
|
|
672
|
+
allPeopleMap.set(p.id, p);
|
|
673
|
+
const allPeople = Array.from(allPeopleMap.values());
|
|
674
|
+
const allAgentsMap = new Map();
|
|
675
|
+
for (const a of (Array.isArray(storedAgents) ? storedAgents : []))
|
|
676
|
+
if (a?.id)
|
|
677
|
+
allAgentsMap.set(a.id, a);
|
|
678
|
+
for (const a of (Array.isArray(agents) ? agents : []))
|
|
679
|
+
if (a?.id)
|
|
680
|
+
allAgentsMap.set(a.id, a);
|
|
681
|
+
const allAgents = Array.from(allAgentsMap.values());
|
|
682
|
+
let shown = allAgents.filter((a) => a && a.name && matches(a.name, filter));
|
|
683
|
+
let folk = allPeople.filter((one) => one && matches(one.displayName || one.name, filter));
|
|
684
|
+
if (show === "agents")
|
|
685
|
+
folk = [];
|
|
686
|
+
else
|
|
687
|
+
shown = [];
|
|
688
|
+
if (sortMode === "alpha") {
|
|
689
|
+
shown = [...shown].sort((a, b) => a.name.localeCompare(b.name));
|
|
690
|
+
folk = [...folk].sort((a, b) => (a.displayName || a.name).localeCompare(b.displayName || b.name));
|
|
691
|
+
}
|
|
692
|
+
const empty = folk.length + shown.length === 0;
|
|
693
|
+
const waiting = agentsLoading || peopleLoading;
|
|
694
|
+
const broke = agentsWrong ?? peopleWrong;
|
|
695
|
+
const pick = async (file) => {
|
|
696
|
+
if (!file)
|
|
697
|
+
return;
|
|
698
|
+
setWrong(null);
|
|
699
|
+
try {
|
|
700
|
+
// The same crop-and-encode a person's photo goes through: square, 256px,
|
|
701
|
+
// quality lowered until it is inside the store's bound. One contract for
|
|
702
|
+
// every face, so an agent's picture is not a second kind of picture.
|
|
703
|
+
setAvatar(await mark(file));
|
|
704
|
+
setEmoji("");
|
|
705
|
+
}
|
|
706
|
+
catch (e) {
|
|
707
|
+
setWrong(reach(e));
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
const add = async () => {
|
|
711
|
+
const called = name.trim();
|
|
712
|
+
if (!called)
|
|
713
|
+
return;
|
|
714
|
+
setBusy(true);
|
|
715
|
+
setWrong(null);
|
|
716
|
+
try {
|
|
717
|
+
const made = await create({
|
|
718
|
+
name: called,
|
|
719
|
+
model: model || "enso",
|
|
720
|
+
// At most one half, which is what the store keeps anyway — sending both
|
|
721
|
+
// would leave the server to break a tie this side already decided.
|
|
722
|
+
...(avatar ? { avatar } : emoji.trim() ? { emoji: emoji.trim() } : {}),
|
|
723
|
+
});
|
|
724
|
+
setMaking(false);
|
|
725
|
+
setName("");
|
|
726
|
+
setEmoji("");
|
|
727
|
+
setAvatar("");
|
|
728
|
+
openSession(made.id);
|
|
729
|
+
onPick?.();
|
|
730
|
+
}
|
|
731
|
+
catch (e) {
|
|
732
|
+
// The form stays open holding what was typed, which is the only state
|
|
733
|
+
// from which trying again is one press rather than three.
|
|
734
|
+
setWrong(reach(e));
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
setBusy(false);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
const drop = async (id, name) => {
|
|
741
|
+
setBusy(true);
|
|
742
|
+
try {
|
|
743
|
+
await remove(id);
|
|
744
|
+
if (room.includes(name))
|
|
745
|
+
openAgent(name);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
// Left in the list, which is the truth if the delete did not land.
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
setArming(null);
|
|
752
|
+
setBusy(false);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
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) => {
|
|
756
|
+
e.stopPropagation();
|
|
757
|
+
setSortMode((m) => (m === "alpha" ? "recent" : "alpha"));
|
|
758
|
+
}, 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: 10, fontWeight: "600", color: "$soft", children: sortMode === "alpha" ? "A-Z" : "Recent" }) }), show === "agents" ? (_jsx(Box, { render: "button", onClick: () => {
|
|
759
|
+
if (busy)
|
|
760
|
+
return;
|
|
761
|
+
setOpen(true);
|
|
762
|
+
setMaking((v) => !v);
|
|
763
|
+
}, "aria-disabled": busy, "aria-expanded": making, "aria-label": "New agent", p: "$1", rounded: "$2", hoverStyle: { bg: "$raised" }, children: _jsx(Plus, { size: 14, "aria-hidden": true }) })) : null] }), open && making ? (_jsxs(YStack, { pl: "$4", pr: "$3", py: "$2", gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Box, { render: "button", onClick: () => picking.current?.click(), "aria-label": "Pick a picture", rounded: 9999, children: _jsx(Face, { src: avatar, emoji: emoji, name: name || "?", size: 32, person: true }) }), _jsx("input", { ref: picking, type: "file", hidden: true, accept: "image/png,image/jpeg,image/gif,image/webp", onChange: (e) => {
|
|
764
|
+
void pick(e.target.files?.[0]);
|
|
765
|
+
e.target.value = "";
|
|
766
|
+
} }), _jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
|
|
767
|
+
if (e.key === "Enter" && !busy)
|
|
768
|
+
void add();
|
|
769
|
+
if (e.key === "Escape")
|
|
770
|
+
setMaking(false);
|
|
771
|
+
}, placeholder: "Name", "aria-label": "Agent name", autoFocus: true }), ...FIELD, flex: 1, minW: 0, px: 8 })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { render: _jsx("input", { value: emoji, onChange: (e) => {
|
|
772
|
+
setEmoji(e.target.value);
|
|
773
|
+
if (e.target.value)
|
|
774
|
+
setAvatar("");
|
|
775
|
+
}, placeholder: "\uD83D\uDE42", "aria-label": "Emoji" }), ...FIELD, width: 44, text: "center", px: 4 }), _jsxs(Text, { render: _jsx("select", { value: model, onChange: (e) => setModel(e.target.value), "aria-label": "Model" }), ...FIELD, outlineStyle: undefined, flex: 1, minW: 0, px: 8, children: [_jsx("option", { value: "enso", children: "enso" }), models.filter((one) => sku(one.id)).map((one) => (_jsx("option", { value: one.id, children: one.id }, one.id)))] }), _jsx(Box, { render: "button", onClick: () => {
|
|
776
|
+
if (!busy)
|
|
777
|
+
void add();
|
|
778
|
+
}, "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) => {
|
|
779
|
+
const called = person.displayName || person.name;
|
|
780
|
+
return (_jsx(SidebarItem, { icon: _jsx(Face, { name: called, src: person.avatar, size: 20, person: true }), onPress: () => {
|
|
781
|
+
const threadId = createDirectChat({
|
|
782
|
+
id: person.id,
|
|
783
|
+
name: called,
|
|
784
|
+
avatar: person.avatar,
|
|
785
|
+
});
|
|
786
|
+
openThread(threadId);
|
|
787
|
+
navigate?.("/chat");
|
|
788
|
+
onPick?.();
|
|
789
|
+
}, children: _jsxs(YStack, { minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: called }), person.email ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: person.email })) : null] }) }, person.id));
|
|
790
|
+
}), shown.map((agent) => (_jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(SidebarItem, { active: room.includes(called(agent.name)), icon: _jsx(Face, { src: agent.avatar, emoji: agent.emoji, name: agent.name, size: 18 }), onPress: () => {
|
|
791
|
+
setArming(null);
|
|
792
|
+
const threadId = createDirectChat({
|
|
793
|
+
id: agent.id,
|
|
794
|
+
name: called(agent.name),
|
|
795
|
+
isAgent: true,
|
|
796
|
+
avatar: agent.avatar || agent.emoji,
|
|
797
|
+
});
|
|
798
|
+
openThread(threadId);
|
|
799
|
+
withAgents([called(agent.name)]);
|
|
800
|
+
navigate?.("/chat");
|
|
801
|
+
onPick?.();
|
|
802
|
+
}, children: called(agent.name) }) }), _jsx(Box, { render: "button", onClick: () => {
|
|
803
|
+
if (busy)
|
|
804
|
+
return;
|
|
805
|
+
if (arming === agent.id)
|
|
806
|
+
void drop(agent.id, called(agent.name));
|
|
807
|
+
else
|
|
808
|
+
setArming(agent.id);
|
|
809
|
+
}, "aria-label": arming === agent.id
|
|
810
|
+
? `Remove ${called(agent.name)}`
|
|
811
|
+
: `Remove ${called(agent.name)}?`, p: "$1", rounded: "$2", opacity: arming === agent.id ? 1 : 0.45, hoverStyle: { opacity: 1, bg: "$raised" }, children: arming === agent.id ? (_jsx(Text, { fontSize: "$1", color: BAD, children: "Remove" })) : (_jsx(Minus, { size: 13, "aria-hidden": true })) })] }, agent.id))), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 13, "aria-hidden": true }), onPress: () => {
|
|
812
|
+
setArming(null);
|
|
813
|
+
onPick?.();
|
|
814
|
+
navigate?.("/contacts");
|
|
815
|
+
}, children: "Browse the directory" })] })) : null] }));
|
|
816
|
+
}
|
|
817
|
+
/** A phone's tab bar: the rail along the foot, above the home indicator. */
|
|
818
|
+
const TABS_HEIGHT = "calc(56px + env(safe-area-inset-bottom))";
|
|
819
|
+
/** A column's width, px: the floor a drag holds, the ceiling, and the default a double-click returns to. */
|
|
820
|
+
const FLOOR = 200;
|
|
821
|
+
const CEIL = 480;
|
|
822
|
+
/** Per-reader, per-browser. A width that resets is a width you set every load. */
|
|
823
|
+
const SPAN = "hanzo.sidebar.span";
|
|
824
|
+
const BESIDE = "hanzo.aside.span";
|
|
825
|
+
/** The column beside a room: its default width, px. */
|
|
826
|
+
const BESIDE_WIDTH = 380;
|
|
827
|
+
/** The rooms' sidebar: its default width, px. */
|
|
828
|
+
const SIDEBAR_WIDTH = 244;
|
|
829
|
+
/**
|
|
830
|
+
* Where a room's own column goes, when it has one.
|
|
831
|
+
*
|
|
832
|
+
* The frame owns the COLUMN — its width, its edge, whether it is open — and the
|
|
833
|
+
* room owns what is IN it, which is the only split that lets the shell stay
|
|
834
|
+
* ignorant of sources and previews and whatever a room grows next. A slot rather
|
|
835
|
+
* than a prop because the content belongs to the room's own state: passed up
|
|
836
|
+
* through the router as a prop, the sources of an answer would have to live
|
|
837
|
+
* somewhere both the route and the pane can see, and that is the conversation
|
|
838
|
+
* itself moved out of the component holding it.
|
|
839
|
+
*/
|
|
840
|
+
const Slot = createContext(null);
|
|
841
|
+
/** Whether the room is drawn in the app's frame (`Paned framed`), where what
|
|
842
|
+
* floats on the frame's ground is cut as a pane. */
|
|
843
|
+
export const Framed = createContext(false);
|
|
844
|
+
/** Puts its children in the column beside the room. Draws nothing where the
|
|
845
|
+
* frame has no column open. */
|
|
846
|
+
export function Beside({ children }) {
|
|
847
|
+
const slot = useContext(Slot);
|
|
848
|
+
if (!slot)
|
|
849
|
+
return null;
|
|
850
|
+
// WHAT IS BESIDE THE ROOM CANNOT PUT SOMETHING BESIDE THE ROOM. A portal
|
|
851
|
+
// keeps the REACT tree it was written in, so a room rendered in this column
|
|
852
|
+
// still sees this slot and would draw its own column into the column it is
|
|
853
|
+
// already in. Handing its contents an empty slot ends that by construction
|
|
854
|
+
// rather than by a rule somebody has to remember.
|
|
855
|
+
return createPortal(_jsx(Slot.Provider, { value: null, children: children }), slot);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* A PANE AND THE COLUMN BESIDE IT. The room draws the pane; the column is open
|
|
859
|
+
* while `useOpen().aside` names one, and `Beside` fills it. The rooms' frame and
|
|
860
|
+
* the app at `/` lay a room out with this, so a room's column is one mechanism.
|
|
861
|
+
* `swap` stands in the pane in the room's place (a chosen Settings row).
|
|
862
|
+
*
|
|
863
|
+
* `framed` is the app at `/`: both are panes floating on the frame's ground
|
|
864
|
+
* (app/_web.tsx), filled with `--pane-fill` and cut by `pane()`, and the edge
|
|
865
|
+
* that sizes the column sits in the gutter between them. In a room they lie on
|
|
866
|
+
* the paper ladder instead. In the rooms' shell the two are its `pane` and
|
|
867
|
+
* `side` grid areas. `bare` leaves the framed pane uncut, for a room that floats
|
|
868
|
+
* panes of its own: a Dev run, which @hanzo/build draws as two.
|
|
869
|
+
*/
|
|
870
|
+
export function Paned({ children, swap, framed = false, bare = false, }) {
|
|
871
|
+
const { aside, showAside } = useOpen();
|
|
872
|
+
const beside = useSpan(BESIDE, BESIDE_WIDTH, FLOOR, CEIL);
|
|
873
|
+
// State and not a ref: a ref set during commit does not re-render, so the
|
|
874
|
+
// portal would have nowhere to go on the pass that created its target.
|
|
875
|
+
const [slot, setSlot] = useState(null);
|
|
876
|
+
useEffect(() => {
|
|
877
|
+
if (!aside)
|
|
878
|
+
setSlot(null);
|
|
879
|
+
}, [aside]);
|
|
880
|
+
return (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "main", "data-slot": "pane", gridArea: "pane", ...(framed
|
|
881
|
+
? bare
|
|
882
|
+
? null
|
|
883
|
+
: { ...pane(), overflow: "hidden" }
|
|
884
|
+
: { ...sheet(0), borderLeftWidth: 1, borderColor: "var(--edge, var(--white-06))" }), flex: 1, minW: 0, minH: 0, children: swap ?? (_jsx(Framed.Provider, { value: framed, children: _jsx(Slot.Provider, { value: slot, children: children }) })) }), aside ? (_jsxs(YStack, { render: "aside", "data-slot": "side", gridArea: "side", ...pane(framed ? undefined : sheet(1).backgroundColor),
|
|
885
|
+
// In the frame the gutter stands between the pane and this column.
|
|
886
|
+
ml: framed ? "var(--pane-gap)" : undefined, ref: beside.ref,
|
|
887
|
+
// inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
|
|
888
|
+
style: beside.style, position: "relative", width: "var(--span)", maxW: "50%", shrink: 0, minH: 0, display: "none", $lg: { display: "flex" }, children: [_jsx(Grip, { side: "right", span: beside.span, floor: FLOOR, ceil: CEIL, reset: BESIDE_WIDTH, onSpan: beside.move, onKeep: beside.keep, onShut: () => showAside(null), label: "Resize side panel",
|
|
889
|
+
// In the gutter the frame leaves between the panes; a room has none.
|
|
890
|
+
l: GUTTER_START, width: GUTTER }), _jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", rounded: "var(--pane-round)", ref: (el) => setSlot(el instanceof HTMLElement ? el : null) })] })) : null] }));
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* THE RAIL: where you can go, as marks with their names under them. It is the
|
|
894
|
+
* sidebar's collapsed form on a laptop — drawn only while the sidebar is shut,
|
|
895
|
+
* because an open sidebar lists the same places at its top — and the tab bar
|
|
896
|
+
* along the bottom of a phone. It carries no organization: the workspace
|
|
897
|
+
* switcher is the one at the top of the sidebar.
|
|
898
|
+
*/
|
|
899
|
+
/**
|
|
900
|
+
* A row that LEAVES this app for another address, in this tab.
|
|
901
|
+
*
|
|
902
|
+
* Its arrow points up and to the left at rest and turns up and to the right —
|
|
903
|
+
* the way every row that opens somewhere else points — as the reader reaches for
|
|
904
|
+
* it: hovered, focused, or pressed. A press also holds the turn, so the arrow
|
|
905
|
+
* stays pointed while the page leaves. Reduced motion turns it at once, by the
|
|
906
|
+
* site-wide rule that shortens every transition.
|
|
907
|
+
*/
|
|
908
|
+
function Leave({ href, children }) {
|
|
909
|
+
const [turned, setTurned] = useState(false);
|
|
910
|
+
const [near, setNear] = useState(false);
|
|
911
|
+
return (_jsx(SidebarItem, { "data-leave": "", onMouseEnter: () => setNear(true), onMouseLeave: () => setNear(false), onFocus: () => setNear(true), onBlur: () => setNear(false), onPressIn: () => setNear(true), icon: _jsx(YStack, { "data-arrow": "", transition: "quickest", rotate: turned || near ? "90deg" : "0deg", children: _jsx(ArrowUpLeft, { size: 15, "aria-hidden": true }) }), onPress: () => {
|
|
912
|
+
setTurned(true);
|
|
913
|
+
window.location.assign(href);
|
|
914
|
+
}, children: children }));
|
|
915
|
+
}
|
|
916
|
+
function Rail({ mode, places, team, open, navigate, onSettings, onMore, }) {
|
|
917
|
+
return (_jsxs(YStack, { "data-slot": "rail", render: "nav", "aria-label": "Places", ...sheet(0), gridArea: "rail", minH: 0, borderRightWidth: 1, borderColor: "var(--edge, var(--white-06))", $md: open ? { display: "none" } : undefined, "$max-md": {
|
|
918
|
+
flexDirection: "row",
|
|
919
|
+
items: "center",
|
|
920
|
+
borderRightWidth: 0,
|
|
921
|
+
borderTopWidth: 1,
|
|
922
|
+
borderTopColor: "var(--border)",
|
|
923
|
+
},
|
|
924
|
+
// Clear of the home indicator on a phone; zero on a flat screen.
|
|
925
|
+
pb: HOME_INDICATOR, children: [_jsxs(YStack, { "data-slot": "places", ...bare, items: "center", gap: "$0.5", flex: 1, minW: 0, minH: 0, overflowY: "auto", pt: "$2", "$max-md": {
|
|
926
|
+
display: "grid",
|
|
927
|
+
gridAutoFlow: "column",
|
|
928
|
+
gridAutoColumns: "minmax(0, 1fr)",
|
|
929
|
+
justifyItems: "stretch",
|
|
930
|
+
overflow: "hidden",
|
|
931
|
+
}, children: [places.map((place, i) => {
|
|
932
|
+
const here = place.id === mode;
|
|
933
|
+
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: 10, lineHeight: 12, color: here ? "$ink" : "$quiet", numberOfLines: 1, children: place.label })] }, place.id));
|
|
934
|
+
}), 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: 10, lineHeight: 12, 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: 10, lineHeight: 12, 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: 10, lineHeight: 12, color: "$quiet", children: "Settings" })] }) })] }));
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Chat's column: start a chat, the projects, then every conversation. The app
|
|
938
|
+
* is `/`, whole, with a rail of its own (app/page.tsx), so a project pressed
|
|
939
|
+
* here opens there, through Dev's door.
|
|
940
|
+
*/
|
|
941
|
+
function App({ filter, onPick, navigate, }) {
|
|
942
|
+
const { empty } = useOpen();
|
|
943
|
+
const open = (slug) => {
|
|
944
|
+
navigate?.(dev(slug));
|
|
945
|
+
onPick?.();
|
|
946
|
+
};
|
|
947
|
+
return (_jsxs(_Fragment, { children: [_jsx(SidebarNewChat, { onPress: () => {
|
|
948
|
+
empty();
|
|
949
|
+
onPick?.();
|
|
950
|
+
} }), _jsx(SidebarFolder, { name: "Projects", open: filter ? true : undefined, children: _jsx(Projects, { filter: filter, onPick: open }) }), _jsx(Chats, { filter: filter, onPick: onPick })] }));
|
|
951
|
+
}
|
|
952
|
+
/** What a place holds, for the list column. */
|
|
953
|
+
function Index({ mode, owner, filter, onPick, navigate, }) {
|
|
954
|
+
const rooms = (_jsxs(_Fragment, { children: [_jsx(Rooms, { onPick: onPick, navigate: navigate }), _jsx(Team, { filter: filter, onPick: onPick, navigate: navigate })] }));
|
|
955
|
+
if (mode === "chat")
|
|
956
|
+
return _jsx(App, { filter: filter, onPick: onPick, navigate: navigate });
|
|
957
|
+
if (mode === "home")
|
|
958
|
+
return (_jsxs(_Fragment, { children: [_jsx(Chats, { filter: filter, onPick: onPick }), rooms, _jsx(Bots, { show: "people", owner: owner, filter: filter, onPick: onPick, navigate: navigate }), _jsx(Bots, { show: "agents", owner: owner, filter: filter, onPick: onPick, navigate: navigate })] }));
|
|
959
|
+
if (mode === "bots")
|
|
960
|
+
return _jsx(Bots, { show: "agents", owner: owner, filter: filter, onPick: onPick, navigate: navigate });
|
|
961
|
+
if (mode === "contacts")
|
|
962
|
+
return _jsx(Bots, { show: "people", owner: owner, filter: filter, onPick: onPick, navigate: navigate });
|
|
963
|
+
if (mode === "inbox")
|
|
964
|
+
return rooms;
|
|
965
|
+
if (mode === "tasks")
|
|
966
|
+
return _jsx(Projects, { filter: filter, onPick: onPick });
|
|
967
|
+
return _jsx(Chats, { filter: filter, onPick: onPick });
|
|
968
|
+
}
|
|
969
|
+
function Frame({ mode, children, navigate, back, forward, }) {
|
|
970
|
+
const { user, isAuthenticated, isLoading } = useIam();
|
|
971
|
+
// Bumping this REMOUNTS whichever list is drawn, and a fresh mount re-reads.
|
|
972
|
+
// It is how "New" refreshes without this component holding a `reload` for a
|
|
973
|
+
// hook it no longer runs.
|
|
974
|
+
const hydrated = useHydrated();
|
|
975
|
+
const [finding, setFinding] = useState(false);
|
|
976
|
+
/**
|
|
977
|
+
* WHICH SETTINGS ROW IS OPEN, and null for none. ONE value, not two.
|
|
978
|
+
*
|
|
979
|
+
* The rail swaps its rooms for the settings list while this holds a row, and
|
|
980
|
+
* the room draws that row's pane. Both read the same answer, so a pane deep in
|
|
981
|
+
* the tree asking for Channels and the column's own Settings row are the same
|
|
982
|
+
* act — which is why there is no second `panel` state beside this one.
|
|
983
|
+
*/
|
|
984
|
+
const { settings: settingsPane, showSettings } = useOpen();
|
|
985
|
+
// The profile is a COLUMN, not a modal, so it is not one of the panels above:
|
|
986
|
+
// those lift off the window and dim it, this one stands in the row beside the
|
|
987
|
+
// room. It is the slot the thread view will share.
|
|
988
|
+
const [profiling, setProfiling] = useState(false);
|
|
989
|
+
// Every organization, for a SuperAdmin to step into (components/workspace/Support.tsx).
|
|
990
|
+
const [supporting, setSupporting] = useState(false);
|
|
991
|
+
/**
|
|
992
|
+
* The sidebar is open by default on desktop and stays open.
|
|
993
|
+
*
|
|
994
|
+
* WHAT THE READER KEPT ARRIVES AFTER MOUNT, for the reason `useSpan` reads
|
|
995
|
+
* its width there: this page is prerendered, so an initializer that reads
|
|
996
|
+
* localStorage or the window's width renders one tree on the server and a
|
|
997
|
+
* different one on the first client pass. React answered that by throwing the
|
|
998
|
+
* server's tree away — hydration error #418, measured on /dev at 390 where
|
|
999
|
+
* the server sent "Close sidebar" and the client drew "Open sidebar" — and a
|
|
1000
|
+
* regenerated tree came back with the rail at zero width.
|
|
1001
|
+
*/
|
|
1002
|
+
const [asked, setAsked] = useState(true);
|
|
1003
|
+
useEffect(() => {
|
|
1004
|
+
let kept = null;
|
|
1005
|
+
try {
|
|
1006
|
+
kept = localStorage.getItem("hanzo:sidebar:open");
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
// No store is the width of the window, which is a state and not a failure.
|
|
1010
|
+
}
|
|
1011
|
+
setAsked(kept !== null ? kept === "true" : window.innerWidth >= 768);
|
|
1012
|
+
}, []);
|
|
1013
|
+
const [peeking, setPeeking] = useState(false);
|
|
1014
|
+
/**
|
|
1015
|
+
* How wide the reader keeps the sidebar.
|
|
1016
|
+
*
|
|
1017
|
+
* The stored value is read after mount and not during render, for the reason
|
|
1018
|
+
* `asked` starts null: this page is prerendered, and a render that reads
|
|
1019
|
+
* localStorage produces different HTML on the server and the client.
|
|
1020
|
+
*/
|
|
1021
|
+
const side = useSpan(SPAN, SIDEBAR_WIDTH, FLOOR, CEIL);
|
|
1022
|
+
const { aside, showAside } = useOpen();
|
|
1023
|
+
// The whole org state, because the account control at the foot takes it as
|
|
1024
|
+
// one value — the switcher is @hanzo/iam's and it reads what it needs.
|
|
1025
|
+
const orgState = useOrganizations();
|
|
1026
|
+
const { currentOrg, currentOrgId } = orgState;
|
|
1027
|
+
const [filter, setFilter] = useState("");
|
|
1028
|
+
useEffect(() => {
|
|
1029
|
+
if (typeof window !== "undefined") {
|
|
1030
|
+
const a = new URLSearchParams(window.location.search).get("aside");
|
|
1031
|
+
if (a && a !== aside)
|
|
1032
|
+
showAside(a);
|
|
1033
|
+
}
|
|
1034
|
+
}, [aside, showAside]);
|
|
1035
|
+
// WHAT THE SEARCH OPENS ONTO. The threads this reader has and the rooms the
|
|
1036
|
+
// org keeps, in one list, because "recent" is a question about where you have
|
|
1037
|
+
// been rather than about which store the answer came from. Both hooks already
|
|
1038
|
+
// run for the sidebar, so this costs no request of its own.
|
|
1039
|
+
// Which rooms this reader's rail carries: Chat and Dev for everyone, the tiered
|
|
1040
|
+
// rooms only where the identity opens the tier and the reader has not hidden it.
|
|
1041
|
+
const tiers = useTiers();
|
|
1042
|
+
const [hidden] = useHidden();
|
|
1043
|
+
const shown = asked || peeking;
|
|
1044
|
+
const toggle = () => {
|
|
1045
|
+
setPeeking(false);
|
|
1046
|
+
setAsked((open) => !open);
|
|
1047
|
+
};
|
|
1048
|
+
const dismiss = () => {
|
|
1049
|
+
setPeeking(false);
|
|
1050
|
+
if (typeof window !== "undefined" && window.innerWidth < 1024) {
|
|
1051
|
+
setAsked(false);
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
const onPick = typeof window !== "undefined" && window.innerWidth < 1024 ? dismiss : undefined;
|
|
1055
|
+
useEffect(() => {
|
|
1056
|
+
const shortcut = (event) => {
|
|
1057
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
1058
|
+
event.preventDefault();
|
|
1059
|
+
setFinding(true);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
window.addEventListener("keydown", shortcut);
|
|
1063
|
+
return () => window.removeEventListener("keydown", shortcut);
|
|
1064
|
+
}, []);
|
|
1065
|
+
/**
|
|
1066
|
+
* THE SURFACES THAT ARE NOT ROOMS, said once.
|
|
1067
|
+
*
|
|
1068
|
+
* Connectors is a page, Skills and Usage are panes of Settings, Terminals is
|
|
1069
|
+
* another product at its own host, Docs is a page. The column groups them
|
|
1070
|
+
* under "More" — where a reader coming from hanzo.app looks — and the palette
|
|
1071
|
+
* offers the same set, so this says WHAT they are and each surface says only
|
|
1072
|
+
* how it draws them. Written twice they had already parted: Settings was in
|
|
1073
|
+
* one list and not the other.
|
|
1074
|
+
*
|
|
1075
|
+
* ONE VERB PER ROW, and no href beside it. A row that carried both would be
|
|
1076
|
+
* two ways to reach one place, and the two would answer differently the day
|
|
1077
|
+
* one of them learned to close the sheet behind it.
|
|
1078
|
+
*/
|
|
1079
|
+
const tools = useMemo(() => [
|
|
1080
|
+
{
|
|
1081
|
+
id: "connectors",
|
|
1082
|
+
label: "Connectors",
|
|
1083
|
+
icon: Blocks,
|
|
1084
|
+
go: () => {
|
|
1085
|
+
navigate?.(site("/integrations"));
|
|
1086
|
+
dismiss();
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
id: "skills",
|
|
1091
|
+
label: "Skills",
|
|
1092
|
+
icon: Workflow,
|
|
1093
|
+
go: () => showSettings("skills"),
|
|
1094
|
+
},
|
|
1095
|
+
// TERMINALS ARE TABS, and Tabs is its own product at its own host —
|
|
1096
|
+
// hanzo.app's row pointed there too. A new tab for the reason that row
|
|
1097
|
+
// used one: a reader who follows it inside this one loses the project
|
|
1098
|
+
// they were building to reach a shell.
|
|
1099
|
+
{
|
|
1100
|
+
id: "terminals",
|
|
1101
|
+
label: "Terminals",
|
|
1102
|
+
icon: SquareTerminal,
|
|
1103
|
+
go: () => window.open(TABS, "_blank", "noopener"),
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
id: "usage",
|
|
1107
|
+
label: "Usage",
|
|
1108
|
+
icon: Activity,
|
|
1109
|
+
go: () => showSettings("usage"),
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
id: "docs",
|
|
1113
|
+
label: "Docs",
|
|
1114
|
+
icon: BookOpen,
|
|
1115
|
+
go: () => {
|
|
1116
|
+
navigate?.(site("/docs"));
|
|
1117
|
+
dismiss();
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
id: "settings",
|
|
1122
|
+
label: "Settings",
|
|
1123
|
+
icon: Settings2,
|
|
1124
|
+
go: () => showSettings(FIRST),
|
|
1125
|
+
},
|
|
1126
|
+
], [navigate, dismiss, showSettings]);
|
|
1127
|
+
/**
|
|
1128
|
+
* A SETTINGS ROW, PRESSED.
|
|
1129
|
+
*
|
|
1130
|
+
* A row that names a destination goes there and closes the list behind it; a
|
|
1131
|
+
* row that names a pane opens it and, on a phone, puts the drawer away —
|
|
1132
|
+
* the sheet covers the room the pane is about to fill.
|
|
1133
|
+
*/
|
|
1134
|
+
const pick = (row) => {
|
|
1135
|
+
if (!row.href) {
|
|
1136
|
+
showSettings(row.id);
|
|
1137
|
+
dismiss();
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (row.href.startsWith("http")) {
|
|
1141
|
+
window.open(row.href, "_blank", "noopener");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
showSettings(null);
|
|
1145
|
+
navigate?.(row.href);
|
|
1146
|
+
dismiss();
|
|
1147
|
+
};
|
|
1148
|
+
/**
|
|
1149
|
+
* WHERE THE PALETTE CAN GO WITHOUT ASKING ANYONE.
|
|
1150
|
+
*
|
|
1151
|
+
* The rooms this identity may open, and the surfaces above. The lists that
|
|
1152
|
+
* have to be read — conversations, projects — are `Find`'s, which is why they
|
|
1153
|
+
* are not here: this half is known before anyone types.
|
|
1154
|
+
*/
|
|
1155
|
+
const onApex = apex(useHost());
|
|
1156
|
+
const places = useMemo(() => [CHAT, ...PLACES].filter((place) => shows(place, tiers, hidden, onApex)), [tiers, hidden, onApex]);
|
|
1157
|
+
const doors = useMemo(() => [
|
|
1158
|
+
// HOME LEADS, and the palette is the only way to it: it is off the rail
|
|
1159
|
+
// by design and no control anywhere else points at it, which left 400
|
|
1160
|
+
// lines of dashboard reading live data that nobody could reach.
|
|
1161
|
+
...[HOME, CHAT, ...PLACES]
|
|
1162
|
+
.filter((place) => shows(place, tiers, hidden, onApex))
|
|
1163
|
+
.map((place) => ({
|
|
1164
|
+
// The LABEL, not the title: `title` is what a browser tab says, and
|
|
1165
|
+
// Home's is the brand — one row reading "Hanzo" among Chat, Dev and
|
|
1166
|
+
// Drive names the company rather than the room.
|
|
1167
|
+
id: place.id,
|
|
1168
|
+
title: place.label,
|
|
1169
|
+
category: "Rooms",
|
|
1170
|
+
icon: _jsx(place.icon, { size: 15, "aria-hidden": true }),
|
|
1171
|
+
href: place.route,
|
|
1172
|
+
})),
|
|
1173
|
+
...tools.map((tool) => ({
|
|
1174
|
+
id: tool.id,
|
|
1175
|
+
title: tool.label,
|
|
1176
|
+
category: "Workspace",
|
|
1177
|
+
icon: _jsx(tool.icon, { size: 15, "aria-hidden": true }),
|
|
1178
|
+
action: tool.go,
|
|
1179
|
+
})),
|
|
1180
|
+
], [tiers, hidden, onApex, tools]);
|
|
1181
|
+
// The org IS the workspace. Naming it twice is how the bar and the rail come
|
|
1182
|
+
// to disagree about which one you are standing in.
|
|
1183
|
+
const workspace = currentOrg?.displayName || currentOrg?.name || "Hanzo";
|
|
1184
|
+
const owner = currentOrgId ?? "";
|
|
1185
|
+
// The two folders' rows, narrowed by the column's filter like every other list.
|
|
1186
|
+
const products = CONSOLE_PRODUCTS.filter((prod) => matches(prod.label, filter));
|
|
1187
|
+
const more = tools.filter((tool) => matches(tool.label, filter));
|
|
1188
|
+
// The static export prerenders the signed-out room, so the rail's identity
|
|
1189
|
+
// half stays quiet until React owns the page. `isLoading` alone is not enough:
|
|
1190
|
+
// during prerender it is already false and `user` is already null, which would
|
|
1191
|
+
// ship "Sign in" baked into HTML a signed-in reader then sees flash.
|
|
1192
|
+
const settled = hydrated && !isLoading;
|
|
1193
|
+
const { user: account } = useAccount();
|
|
1194
|
+
const signedIn = isAuthenticated || Boolean(user) || Boolean(account) || hasSession();
|
|
1195
|
+
const list = (_jsxs(Sidebar, { ref: side.ref,
|
|
1196
|
+
// A phone's sheet is never wider than 280, whatever the laptop kept.
|
|
1197
|
+
// inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
|
|
1198
|
+
style: side.style, $md: { width: "var(--span)" }, "$max-md": { width: "100%", maxW: 280 }, ...sheet(0), borderRightWidth: 1, borderColor: "var(--edge, var(--white-06))", position: "relative", children: [_jsx(Grip, { side: "left", span: side.span, floor: FLOOR, ceil: CEIL, reset: SIDEBAR_WIDTH, onSpan: side.move, onKeep: side.keep, onShut: () => setAsked(false), label: "Resize sidebar" }), _jsx(XStack, { "data-slot": "mode-switch", items: "center", px: "$2", pt: "$2", minW: 0, children: _jsx(ModeMark, { mode: mode === "dev" ? "dev" : "chat", onChat: () => navigate?.("/chat"), onDev: () => navigate?.(dev()) }) }), _jsxs(XStack, { "data-slot": "workspace-switcher", items: "center", gap: "$2", px: "$2", pt: "$1", pb: "$1", minW: 0, children: [_jsx(Text, { render: _jsx("a", { href: "/", onClick: (e) => {
|
|
1199
|
+
if (!navigate || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
|
|
1200
|
+
return;
|
|
1201
|
+
e.preventDefault();
|
|
1202
|
+
navigate("/");
|
|
1203
|
+
}, "aria-label": "Hanzo 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: () => {
|
|
1204
|
+
navigate?.(place.route);
|
|
1205
|
+
onPick?.();
|
|
1206
|
+
}, 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) => {
|
|
1207
|
+
if (e.key === "Escape")
|
|
1208
|
+
setFilter("");
|
|
1209
|
+
},
|
|
1210
|
+
// FILTER, not Search. Two controls that both said Search did two
|
|
1211
|
+
// different things: this one narrows the list beside you, and the
|
|
1212
|
+
// bar above opens the palette that finds anything anywhere.
|
|
1213
|
+
placeholder: "Filter", "aria-label": "Filter sidebar" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }) }), settingsPane ? (_jsx(SidebarScroll, { children: _jsx(SettingsList, { active: settingsPane, filter: filter, onPick: pick, onBack: () => showSettings(null) }) })) : (_jsx(_Fragment, { children: _jsx(SidebarScroll, { children: _jsxs(_Fragment, { children: [settled ? (_jsx(Index, { mode: mode, owner: owner, filter: filter, onPick: onPick, navigate: navigate })) : null, products.length ? (_jsx(SidebarFolder, { name: "Cloud & Platform", open: filter ? true : undefined, children: products.map((prod) => (_jsx(SidebarItem, { icon: _jsx(prod.icon, { size: 13, "aria-hidden": true }), onPress: () => window.open(prod.href, "_blank", "noopener"), children: prod.label }, prod.id))) })) : null, more.length ? (_jsx(SidebarFolder, { name: "More", open: filter ? true : undefined, children: more.map((tool) => (_jsx(SidebarItem, { icon: _jsx(tool.icon, { size: 13, "aria-hidden": true }), onPress: tool.go, children: tool.label }, tool.id))) })) : null] }) }) })), settled && onApex ? _jsx(Leave, { href: signedIn ? ENTRY : WORKSPACE, children: "Workspace" }) : null, settled ? (signedIn ? (_jsxs(_Fragment, { children: [superAdmin() ? (_jsx(SidebarItem, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), onPress: () => setSupporting(true), children: "All organizations" })) : null, _jsx(SidebarItem, { icon: _jsx(UserPlus, { size: 15, "aria-hidden": true }), onPress: () => window.open(invite(), "_blank", "noopener"), children: "Invite Teammates" }), _jsx(SidebarItem, { icon: _jsx(Share2, { size: 15, "aria-hidden": true }), onPress: () => {
|
|
1214
|
+
navigate?.(site("/referral"));
|
|
1215
|
+
dismiss();
|
|
1216
|
+
}, children: "Share Hanzo" }), _jsx(SidebarItem, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), onPress: () => window.open(BILLING, "_blank", "noopener"), children: "Upgrade" }), _jsx(Account, { onToggleCollapse: toggle, onProfile: () => setProfiling(true), onAppearance: () => showSettings("general") })] })) : null) : null] }));
|
|
1217
|
+
return (_jsxs(YStack, { "data-slot": "workspace", "data-hydrated": settled ? "true" : undefined, "data-sidebar": asked ? "open" : "shut", display: "grid", gridTemplateColumns: `${asked ? 0 : 64}px auto minmax(0, 1fr) auto`, gridTemplateRows: "44px minmax(0, 1fr)", gridTemplateAreas: '"rail bar bar bar" "rail list pane side"', "$max-md": {
|
|
1218
|
+
gridTemplateColumns: "minmax(0, 1fr)",
|
|
1219
|
+
gridTemplateRows: `44px minmax(0, 1fr) ${TABS_HEIGHT}`,
|
|
1220
|
+
gridTemplateAreas: '"bar" "pane" "rail"',
|
|
1221
|
+
}, position: "relative", width: "100%", height: "100dvh", overflow: "hidden", bg: "$background", backgroundImage: "var(--pane-ground)", children: [_jsx(Rail, { mode: mode, places: places, open: asked, team: onApex ? (signedIn ? ENTRY : WORKSPACE) : undefined, navigate: navigate, onSettings: () => showSettings(FIRST), onMore: () => setFinding(true) }), _jsxs(XStack, { render: "header", "data-slot": "bar", gridArea: "bar", ...sheet(0), height: 44, shrink: 0, items: "center", px: "$3", gap: "$3", borderBottomWidth: 1, borderColor: "var(--edge, var(--white-06))", children: [_jsxs(XStack, { items: "center", gap: "$1", shrink: 0, children: [_jsx(SidebarIconButton, { ...TAP, label: shown ? "Close sidebar" : "Open sidebar", onPress: toggle, onMouseEnter: () => setPeeking(true), children: _jsx(PanelLeft, { size: 16, "aria-hidden": true }) }), back || forward ? (_jsxs(XStack, { display: "none", $md: { display: "flex" }, items: "center", gap: "$1", shrink: 0, children: [_jsx(SidebarIconButton, { ...TAP, label: "Back", onPress: () => back?.(), children: _jsx(ArrowLeft, { size: 16, "aria-hidden": true }) }), _jsx(SidebarIconButton, { ...TAP, label: "Forward", onPress: () => forward?.(), children: _jsx(ArrowRight, { size: 16, "aria-hidden": true }) })] })) : null] }), _jsx(XStack, { flex: 1, justify: "center", children: _jsx(YStack, { width: 440, maxW: "100%", position: "relative", children: _jsxs(Box, { render: "button", onClick: () => setFinding(true), display: "flex", flexDirection: "row", "data-slot": "find", items: "center", "$max-md": { justify: "center" }, gap: "$2", width: "100%", height: CONTROL, px: "$2.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Search, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsxs(Text, { fontSize: "$1", color: "$soft", "$max-md": { display: "none" }, children: ["Search ", workspace] }), _jsx(XStack, { ml: "auto", items: "center", justify: "center", height: 18, px: "$1", rounded: "$1", bg: "$edge", "$max-md": { display: "none" }, children: _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "$soft", children: "\u2318K" }) })] }) }) }), _jsxs(XStack, { shrink: 0, minW: 0, justify: "flex-end", items: "center", gap: "$2", children: [settled && !signedIn ? (_jsx(Box, { ...TAP, render: "button", onClick: () => enter(LOGIN), display: "flex", items: "center", justify: "center", cursor: "pointer", children: _jsx(XStack, { items: "center", justify: "center", height: CONTROL, px: "$2.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "$ink", children: "Sign in" }) }) })) : null, _jsxs(XStack, { display: "none", $lg: { display: "flex" }, items: "center", gap: "$1", children: [_jsx(SidebarIconButton, { ...TAP, label: aside === "roster" ? "Hide directory & roster" : "Show directory & roster", onPress: () => showAside(aside === "roster" ? null : "roster"), children: _jsx(Users, { size: 16, "aria-hidden": true }) }), _jsx(SidebarIconButton, { ...TAP, label: aside === "tasks" ? "Close side panel" : "Open side panel", onPress: () => showAside(aside === "tasks" ? null : "tasks"), children: _jsx(PanelRight, { size: 16, "aria-hidden": true }) })] })] })] }), shown ? (_jsx(Box, { render: "button", "aria-label": "Close sidebar", onClick: dismiss, ...scrim, "data-slot": "scrim", position: "absolute", t: 44, r: 0, b: TABS_HEIGHT, l: 0, z: "var(--z-scrim)", $md: { display: "none" } })) : null, shown ? (_jsx(XStack, { "data-slot": "list", gridArea: "list", position: asked ? "relative" : "absolute", ...(asked ? {} : { top: 44, bottom: 0, left: 64 }), "$max-md": { position: "fixed", t: 44, r: 0, b: TABS_HEIGHT, l: 0, width: "100%" }, z: "var(--z-drawer)", bg: "$background", onMouseEnter: () => setPeeking(true), onMouseLeave: () => {
|
|
1222
|
+
if (!asked)
|
|
1223
|
+
setPeeking(false);
|
|
1224
|
+
}, children: list })) : null, _jsx(Paned, { swap: settingsPane ? _jsx(SettingsPane, { id: settingsPane }) : undefined, children: children }), _jsx(Profile, { open: profiling, onClose: () => setProfiling(false) }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null, finding ? (_jsx(Find, { doors: doors, onClose: () => setFinding(false), navigate: navigate })) : null] }));
|
|
1225
|
+
}
|