@hanzo/rooms 0.1.57 → 0.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +13 -1
- package/dist/App.js +117 -36
- package/dist/Chat.js +55 -13
- package/dist/Column.d.ts +10 -6
- package/dist/Column.js +77 -54
- package/dist/Home.js +7 -7
- package/dist/Inbox.js +1 -1
- package/dist/Me.d.ts +10 -2
- package/dist/Me.js +36 -9
- package/dist/Meet.js +1 -1
- package/dist/Prose.js +16 -9
- package/dist/Rail.d.ts +39 -0
- package/dist/Rail.js +85 -0
- package/dist/Recents.js +24 -12
- package/dist/RightPane.d.ts +23 -0
- package/dist/RightPane.js +63 -11
- package/dist/Share.js +2 -2
- package/dist/Shell.js +7 -8
- package/dist/Talk.js +3 -3
- package/dist/recent.js +1 -1
- package/dist/rooms.js +2 -2
- package/dist/thread.d.ts +8 -0
- package/dist/thread.js +15 -0
- package/dist/where.d.ts +2 -0
- package/dist/where.js +2 -0
- package/package.json +2 -2
- package/dist/ModeMark.d.ts +0 -8
- package/dist/ModeMark.js +0 -19
package/dist/App.d.ts
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The window's bar, for a host that is a window of its own: `inset` px kept
|
|
3
|
+
* clear at its left for the window's own controls (macOS paints its traffic
|
|
4
|
+
* lights over the page there), and `drag`, the attributes the host's shell
|
|
5
|
+
* reads as the region that moves the window.
|
|
6
|
+
*/
|
|
7
|
+
export interface Bar {
|
|
8
|
+
inset?: number;
|
|
9
|
+
drag?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare function App({ bar }?: {
|
|
12
|
+
bar?: Bar;
|
|
13
|
+
}): import("react").JSX.Element;
|
package/dist/App.js
CHANGED
|
@@ -2,58 +2,74 @@
|
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The web app as the whole window, in two modes: Chat, a conversation with the
|
|
4
4
|
// product's AI, and Dev, @hanzo/build's `<Builder>` without a rail of its own.
|
|
5
|
-
// One column serves both (Column.tsx): the wordmark and the switch, New, Dev's
|
|
6
|
-
// places, the reader's Recents in both modes, and the account. The address says
|
|
7
|
-
// the mode, as a path (lib/host `talk`, `app`, `place`): `/` and `/chat` are
|
|
8
|
-
// Chat on a new conversation, `/chat/<id>` one of the reader's, and a path under
|
|
9
|
-
// `/dev` is Dev: `/dev/run/<id>` a run, `/dev/projects/<org>/<repo>` a project.
|
|
10
|
-
// Moving between the modes is a history entry (`go`), never a load, so a host
|
|
11
|
-
// serves this one component for every path under each.
|
|
12
5
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
6
|
+
// OUTER the rail (Rail.tsx) and the sidebar beside it (Column.tsx), on the
|
|
7
|
+
// window's floor, with the window's bar above them where the host has
|
|
8
|
+
// no browser around it (`bar`).
|
|
9
|
+
// INNER the content, cut as one rounded panel inset from the window's edges
|
|
10
|
+
// and from the sidebar by the gutter (`--pane-gap`): Chat's
|
|
11
|
+
// conversation with its tabs beside it (Paned), or Dev's builder.
|
|
12
|
+
//
|
|
13
|
+
// The rail moves between the modes; the sidebar holds the mode's name, its
|
|
14
|
+
// search, New, Dev's places and the reader's Recents. The address says the
|
|
15
|
+
// mode, as a path (lib/host `talk`, `app`, `place`): `/` and `/chat` are Chat on
|
|
16
|
+
// a new conversation, `/chat/<id>` one of the reader's, and a path under `/dev`
|
|
17
|
+
// is Dev: `/dev/run/<id>` a run, `/dev/projects/<org>/<repo>` a project. Moving
|
|
18
|
+
// between the modes is a history entry (`go`), never a load, so a host serves
|
|
19
|
+
// this one component for every path under each.
|
|
20
|
+
//
|
|
21
|
+
// Every host draws this: hanzo.ai at `/`, `/chat` and `/dev`, and the desktop
|
|
22
|
+
// app as its main window. The host mounts IamProvider and `<Rooms>` with its
|
|
23
|
+
// router and the path, and draws this only for a signed-in reader; what a
|
|
24
|
+
// stranger sees is the host's.
|
|
25
|
+
//
|
|
26
|
+
// THE SIDEBAR'S TOGGLE IS AT THE WINDOW'S TOP-LEFT. In a browser tab it leads
|
|
27
|
+
// the rail. A host that is a window of its own (`bar`) has no browser around
|
|
28
|
+
// it, so the app draws the window's bar: the toggle, then Back and Forward
|
|
29
|
+
// through the host's router, clear of the window's own controls, and the bar
|
|
30
|
+
// drags the window. Shut, the sidebar leaves the rail alone at the edge; the
|
|
31
|
+
// choice is kept per browser under the key the builder's rail kept it under.
|
|
32
|
+
//
|
|
33
|
+
// THE KEYS: ⌘K / Ctrl+K searches the sidebar (opening it first), ⌘, / Ctrl+,
|
|
34
|
+
// opens Settings, and in a window ⌘[ ⌘] / Alt+← Alt+→ are Back and Forward —
|
|
35
|
+
// never while the reader types.
|
|
16
36
|
//
|
|
17
37
|
// DEV'S PANE HAS NO COLUMN BESIDE IT. The column beside a pane is Chat's (its
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// builder. So Dev's pane is cut on its own and the summary is Chat's alone.
|
|
38
|
+
// tabs, `useOpen().aside`), and that choice outlives a move to Dev, where
|
|
39
|
+
// nothing fills it. So Dev's pane is cut on its own and the tabs are Chat's.
|
|
21
40
|
//
|
|
22
41
|
// EVERY MOVE PUTS THE DRAWER AWAY, the one to where the reader already is
|
|
23
|
-
// included.
|
|
24
|
-
// that closed only on a new address stayed open over the page it had asked for.
|
|
42
|
+
// included.
|
|
25
43
|
//
|
|
26
44
|
// THE PLANS' OWN ADDRESS LEAVES for the pay site in its place (dev.tsx `leave`),
|
|
27
45
|
// so the builder's own plans are never drawn.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// column beside it are three panes floating on a lit floor (ground.ts), one
|
|
31
|
-
// gutter round them and between them, `--pane-gap`, each sized by the edge
|
|
32
|
-
// beside it.
|
|
33
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
34
|
-
import { Menu } from 'lucide-react';
|
|
46
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
47
|
+
import { ArrowLeft, ArrowRight, Menu } from 'lucide-react';
|
|
35
48
|
import { AiProvider } from '@hanzo/ai/react';
|
|
36
49
|
import { Text, XStack, YStack } from '@hanzo/gui';
|
|
37
|
-
import { Builder, HostProvider, route } from '@hanzo/build';
|
|
50
|
+
import { Builder, HostProvider, path as written, route } from '@hanzo/build';
|
|
38
51
|
import { SidebarIconButton } from '@hanzo/ui/chat';
|
|
39
52
|
import { useAi } from './lib/ai.js';
|
|
40
53
|
import { Chat } from './Chat.js';
|
|
41
54
|
import { Column } from './Column.js';
|
|
42
55
|
import { isPlans, leave, useDevHost } from './dev.js';
|
|
43
56
|
import { useRooms } from './host.js';
|
|
44
|
-
import {
|
|
57
|
+
import { useKept } from './kept.js';
|
|
58
|
+
import { Mark, Rail, Toggle } from './Rail.js';
|
|
45
59
|
import { empty, openThread, useOpen } from './open.js';
|
|
46
60
|
import { Paned } from './Shell.js';
|
|
47
61
|
import { floor, pane } from './ground.js';
|
|
62
|
+
import { pick } from './lib/session.js';
|
|
48
63
|
import { app, go, place, talk } from './lib/host.js';
|
|
64
|
+
import { wordmark } from './where.js';
|
|
49
65
|
/** `/` is Chat on a new conversation. */
|
|
50
66
|
const HOME = { mode: 'chat', thread: null };
|
|
67
|
+
/** The bar's height, px: the reference window's title bar. */
|
|
68
|
+
const BAR = 40;
|
|
51
69
|
/**
|
|
52
70
|
* The dim under the phone's drawer: design's scrim at two fifths, so the page
|
|
53
|
-
* still reads through the drawer's glass
|
|
54
|
-
*
|
|
55
|
-
* carries a `color-mix()` to the page unchanged; its types only name custom
|
|
56
|
-
* properties.
|
|
71
|
+
* still reads through the drawer's glass. gui carries a `color-mix()` to the
|
|
72
|
+
* page unchanged; its types only name custom properties.
|
|
57
73
|
*/
|
|
58
74
|
const DIM = 'color-mix(in srgb, var(--surface-scrim) 40%, transparent)';
|
|
59
75
|
/** The phone's drawer stands the gutter off each edge, or the device's inset where that is wider. */
|
|
@@ -62,11 +78,22 @@ const INSET = {
|
|
|
62
78
|
bottom: 'max(var(--pane-gap), var(--safe-bottom))',
|
|
63
79
|
left: 'max(var(--pane-gap), var(--safe-left))',
|
|
64
80
|
};
|
|
65
|
-
/** Where each mode was last, so the
|
|
81
|
+
/** Where each mode was last, so the rail goes back to it. */
|
|
66
82
|
const last = { chat: '', dev: '' };
|
|
67
|
-
|
|
83
|
+
/** Whether a key press is the reader typing, where the app's keys are the field's. */
|
|
84
|
+
const typing = (e) => {
|
|
85
|
+
const el = e.target;
|
|
86
|
+
return !!el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT');
|
|
87
|
+
};
|
|
88
|
+
/** The builder's Settings, general section. */
|
|
89
|
+
const SETTINGS = written({ kind: 'settings', section: 'general' });
|
|
90
|
+
export function App({ bar } = {}) {
|
|
68
91
|
const [drawer, setDrawer] = useState(false);
|
|
69
92
|
const shut = useCallback(() => setDrawer(false), []);
|
|
93
|
+
// The sidebar shut, kept per browser under the key the builder's rail kept it under.
|
|
94
|
+
const [closed, setClosed] = useKept('hanzo.build.rail', false);
|
|
95
|
+
// Each ⌘K: the sidebar's search, asked for again.
|
|
96
|
+
const [seek, setSeek] = useState(0);
|
|
70
97
|
const dev = useDevHost();
|
|
71
98
|
// The builder's moves put the drawer away first.
|
|
72
99
|
const host = useMemo(() => ({
|
|
@@ -77,19 +104,22 @@ export function App() {
|
|
|
77
104
|
},
|
|
78
105
|
}), [dev, shut]);
|
|
79
106
|
const { client } = useAi();
|
|
80
|
-
const
|
|
107
|
+
const { router, path } = useRooms();
|
|
108
|
+
const here = path ?? '/';
|
|
81
109
|
const at = place(here) ?? HOME;
|
|
82
110
|
const mode = at.mode;
|
|
111
|
+
const where = at.mode === 'dev' ? at.at : null;
|
|
83
112
|
// A run floats two panes of its own, so the pane it stands in is not cut.
|
|
84
|
-
const run =
|
|
113
|
+
const run = where !== null && route(where).kind === 'run';
|
|
85
114
|
const asked = at.mode === 'chat' ? at.thread : null;
|
|
86
115
|
const { thread } = useOpen();
|
|
87
|
-
const plans =
|
|
116
|
+
const plans = where !== null && isPlans(where);
|
|
88
117
|
useEffect(() => {
|
|
89
118
|
if (plans)
|
|
90
119
|
leave('app');
|
|
91
120
|
}, [plans]);
|
|
92
|
-
// An address names Chat's conversation,
|
|
121
|
+
// An address names Chat's conversation, a conversation just begun writes its
|
|
122
|
+
// id back, and one emptied (New chat, wherever it was pressed) leaves for /chat.
|
|
93
123
|
useEffect(() => {
|
|
94
124
|
if (mode !== 'chat')
|
|
95
125
|
return;
|
|
@@ -100,9 +130,18 @@ export function App() {
|
|
|
100
130
|
// The address decides; the selection follows it.
|
|
101
131
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
132
|
}, [mode, asked]);
|
|
133
|
+
// The conversation the pane held before this render: an emptied one is the
|
|
134
|
+
// address's own conversation let go of, never a first render before it opens.
|
|
135
|
+
const held = useRef(null);
|
|
103
136
|
useEffect(() => {
|
|
104
|
-
|
|
137
|
+
const was = held.current;
|
|
138
|
+
held.current = thread;
|
|
139
|
+
if (mode !== 'chat')
|
|
140
|
+
return;
|
|
141
|
+
if (thread && thread !== asked)
|
|
105
142
|
go(talk(thread), true);
|
|
143
|
+
else if (!thread && was && asked === was)
|
|
144
|
+
go(talk());
|
|
106
145
|
// The selection writes the address only when it moves.
|
|
107
146
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
147
|
}, [mode, thread]);
|
|
@@ -115,7 +154,49 @@ export function App() {
|
|
|
115
154
|
shut();
|
|
116
155
|
go(address);
|
|
117
156
|
}, [shut]);
|
|
157
|
+
// The app's keys.
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const key = (e) => {
|
|
160
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
161
|
+
if (mod && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k') {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
setClosed(false);
|
|
164
|
+
if (window.matchMedia('(max-width: 767.98px)').matches)
|
|
165
|
+
setDrawer(true);
|
|
166
|
+
setSeek((n) => n + 1);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (typing(e))
|
|
170
|
+
return;
|
|
171
|
+
if (mod && !e.shiftKey && !e.altKey && e.key === ',') {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
move(app(SETTINGS));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!bar)
|
|
177
|
+
return;
|
|
178
|
+
const back = (e.metaKey && e.key === '[') || (e.altKey && e.key === 'ArrowLeft');
|
|
179
|
+
const forth = (e.metaKey && e.key === ']') || (e.altKey && e.key === 'ArrowRight');
|
|
180
|
+
if ((!back && !forth) || e.ctrlKey || e.shiftKey)
|
|
181
|
+
return;
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
if (back)
|
|
184
|
+
router.back();
|
|
185
|
+
else
|
|
186
|
+
router.forward();
|
|
187
|
+
};
|
|
188
|
+
window.addEventListener('keydown', key);
|
|
189
|
+
return () => window.removeEventListener('keydown', key);
|
|
190
|
+
// setClosed is the kept setter of this render; the keys read it fresh each time.
|
|
191
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
192
|
+
}, [bar, router, move]);
|
|
118
193
|
const toChat = () => move(last.chat || talk());
|
|
119
194
|
const toDev = () => move(last.dev || app(''));
|
|
120
|
-
|
|
195
|
+
// Another organization: picked, and the app opened in it at the mode's New.
|
|
196
|
+
const org = (o) => {
|
|
197
|
+
pick(o);
|
|
198
|
+
window.location.assign(mode === 'dev' ? app('') : talk());
|
|
199
|
+
};
|
|
200
|
+
const flip = () => setClosed(!closed);
|
|
201
|
+
return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsxs(YStack, { "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", children: [bar ? (_jsxs(XStack, { "data-slot": "title-bar", height: BAR, shrink: 0, items: "center", gap: 2, pl: (bar.inset ?? 0) + 6, pr: "$2", ...bar.drag, children: [_jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Toggle, { open: !closed, onPress: flip }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Toggle, { open: drawer, onPress: () => setDrawer(true), label: "Open sidebar" }) }), _jsx(Mark, { label: "Back", "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Forward", "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), flex: 1, minW: 0, minH: 0, pr: "var(--pane-gap)", pb: "var(--pane-gap)", pt: bar ? 0 : 'var(--pane-gap)', "$max-md": { p: 0 }, children: [_jsx(YStack, { height: "100%", "$max-md": { display: 'none' }, children: _jsx(Rail, { host: host, mode: mode, at: where, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, go: move, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), seek: seek }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0 }, children: [bar ? null : (_jsxs(XStack, { "data-slot": "app-bar", height: 44, px: "$1", gap: "$1", items: "center", shrink: 0, $md: { display: 'none' }, children: [_jsx(SidebarIconButton, { label: "Open sidebar", onPress: () => setDrawer(true), width: 44, height: 44, rounded: 999, children: _jsx(Menu, { size: 18 }) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) })] })), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, seek: seek, drawer: true, foot: _jsx(Rail, { host: host, mode: mode, at: where, open: true, flat: true, onChat: toChat, onDev: toDev, go: move, onOrg: org }) }) })] })) : null] }) }) }));
|
|
121
202
|
}
|
package/dist/Chat.js
CHANGED
|
@@ -45,6 +45,7 @@ import { useMedia } from "@hanzo/gui";
|
|
|
45
45
|
import { useIam, useIamToken } from "@hanzo/iam/react";
|
|
46
46
|
import { bearer, hasSession, org } from "./lib/session.js";
|
|
47
47
|
import { api } from "./lib/api.js";
|
|
48
|
+
import { dev } from "./lib/host.js";
|
|
48
49
|
import { chats, house, paidPlan, paused, setCreditsAfterAllowance, useLimits } from "./lib/limits.js";
|
|
49
50
|
import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
|
|
50
51
|
import { WEB } from "./lib/web.js";
|
|
@@ -52,14 +53,14 @@ import { useHydrated } from "./lib/hydrated.js";
|
|
|
52
53
|
import { base, served } from "./lib/ai.js";
|
|
53
54
|
import { onServed } from "./lib/served.js";
|
|
54
55
|
import { refused as worded, speech, useDictation, useTalk, useVoice } from "@hanzo/voice";
|
|
55
|
-
import { FileText, Image as ImageMark, Paperclip,
|
|
56
|
+
import { FileText, Image as ImageMark, Paperclip, X } from "lucide-react";
|
|
56
57
|
import { FREE } from "./lib/ai.js";
|
|
57
58
|
import { openThread, useOpen } from "./open.js";
|
|
58
59
|
import { Beside, Framed } from "./Shell.js";
|
|
59
|
-
import { SEED, look,
|
|
60
|
+
import { SEED, Summary, look, useSites } from "./RightPane.js";
|
|
60
61
|
import { Share } from "./Share.js";
|
|
61
|
-
import {
|
|
62
|
-
import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, useDeck, useKey, useMind, usePanel, } from "@hanzo/build";
|
|
62
|
+
import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
|
|
63
|
+
import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, useDeck, useKey, useMind, usePanel, frames, name as tabName, } from "@hanzo/build";
|
|
63
64
|
import { checkoutUrl } from './lib/pay.js';
|
|
64
65
|
import { Upgrade, useFree } from './Upgrade.js';
|
|
65
66
|
import { enter, LOGIN } from './lib/destination.js';
|
|
@@ -238,7 +239,11 @@ export function Ask({ placeholder, idle, }) {
|
|
|
238
239
|
* character (`Message ${persona.name}`). `label` stays "Ask anything", so the
|
|
239
240
|
* accessible name does not move when the placeholder does.
|
|
240
241
|
*/
|
|
241
|
-
const message = () => `
|
|
242
|
+
const message = () => `Ask ${brand()}`;
|
|
243
|
+
/** The line every answer is sent under, above the composer, as the reference sets it. */
|
|
244
|
+
const caution = () => `${brand()} can make mistakes. Check important info.`;
|
|
245
|
+
/** The panel offers no kinds of its own in Chat: its tabs are pages. */
|
|
246
|
+
const NONE = [];
|
|
242
247
|
/** The id the room's system turn is held under in the thread. */
|
|
243
248
|
const ROOM = "room";
|
|
244
249
|
/** The models that route a turn rather than name the one that answers it. */
|
|
@@ -489,13 +494,12 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
489
494
|
const framed = useContext(Framed);
|
|
490
495
|
const opened = useOpen();
|
|
491
496
|
const { thread: open, open: reopen, agents: room, openAgent, seat, emptied, aside, showAside, showSettings } = opened;
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
// archived) reads unpinned.
|
|
497
|
+
// THE HEADER'S ⋯ IS THE ROW'S MENU (thread.tsx): rename, pin, share, its
|
|
498
|
+
// link, a new tab, archive and delete, kept by the server. A conversation not
|
|
499
|
+
// in the list yet (just begun, or archived) has no menu until it is.
|
|
496
500
|
const listing = useThreadList(false, open);
|
|
497
|
-
const acts = useThreadActions();
|
|
498
501
|
const mine = open ? listing.threads.find((t) => t.id === open) : undefined;
|
|
502
|
+
const alone = useMemo(() => new Map(mine ? [[mine.id, mine]] : []), [mine]);
|
|
499
503
|
// The conversation's key in `pane.ts`, the one the column beside it reads.
|
|
500
504
|
const at = channel(opened);
|
|
501
505
|
// The first of them: what a surface that addresses one person reads.
|
|
@@ -506,6 +510,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
506
510
|
const wide = useMedia().lg;
|
|
507
511
|
const [panel, setPanel] = usePanel("chat", false, wide);
|
|
508
512
|
const deck = useDeck(`chat:${at}`, SEED);
|
|
513
|
+
// The org's sites, which an empty tab suggests.
|
|
514
|
+
const sites = useSites();
|
|
509
515
|
useKey(() => setPanel(!panel));
|
|
510
516
|
// On a laptop the frame's column is the panel's; a close from the frame (its
|
|
511
517
|
// edge pulled shut, the shell's own control) comes back into `panel`.
|
|
@@ -1042,8 +1048,16 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1042
1048
|
// record's own picture. Its own answer comes first, which is the order
|
|
1043
1049
|
// `Face` already states for every other surface.
|
|
1044
1050
|
const portrait = mate?.avatar ?? persona?.avatar ?? roleOf(withWhom)?.portrait;
|
|
1045
|
-
//
|
|
1046
|
-
|
|
1051
|
+
// THE PANEL IS TABS OF PAGES, a browser beside the conversation; what the
|
|
1052
|
+
// conversation produced and took in is its Summary (RightPane.tsx), each row
|
|
1053
|
+
// opening here. A deck kept from when Artifacts and Sources were tabs lets them go.
|
|
1054
|
+
useEffect(() => {
|
|
1055
|
+
for (const t of deck.tabs)
|
|
1056
|
+
if (t.kind !== PAGE)
|
|
1057
|
+
deck.shut(t.id);
|
|
1058
|
+
// A sweep of what was kept, once the deck is read.
|
|
1059
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1060
|
+
}, [at]);
|
|
1047
1061
|
/** Whether the field has focus, so the FRAME can wear the ring the field is
|
|
1048
1062
|
* no longer allowed to draw. `:focus-within` would say this in CSS; the
|
|
1049
1063
|
* stylesheet that would hold it cannot reach past gui's own rule, so the
|
|
@@ -1201,7 +1215,34 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1201
1215
|
}, onCancel: () => {
|
|
1202
1216
|
setAsking(null);
|
|
1203
1217
|
} }));
|
|
1218
|
+
/** The tabs' browser, as this conversation offers it: the org's sites to start from, and a page asked about, captured or opened in Dev. */
|
|
1219
|
+
const browsing = {
|
|
1220
|
+
suggested: sites,
|
|
1221
|
+
onAsk: (page) => {
|
|
1222
|
+
const quote = page.selection ? `\n> ${page.selection.trim().split("\n").join("\n> ")}` : "";
|
|
1223
|
+
setDraft((was) => `${was ? `${was}\n\n` : ""}About ${page.title} (${page.url}):${quote}\n`);
|
|
1224
|
+
},
|
|
1225
|
+
onCapture: (file) => void hold(at, [file], via),
|
|
1226
|
+
onDev: (page) => {
|
|
1227
|
+
const slug = new URL(page.url).hostname.match(/^([a-z0-9-]+)\.hanzo\.app$/)?.[1] ?? null;
|
|
1228
|
+
window.location.assign(dev(slug));
|
|
1229
|
+
},
|
|
1230
|
+
};
|
|
1204
1231
|
return (_jsxs(YStack, { flex: 1, minH: 0, width: "100%", position: "relative", "data-slot": "chat-room",
|
|
1232
|
+
// A LINK IN AN ANSWER OPENS IN A TAB beside the conversation when the tab
|
|
1233
|
+
// can show it — a site or preview we host — and in a browser tab of its
|
|
1234
|
+
// own otherwise, as a modified click always does.
|
|
1235
|
+
onClickCapture: (press) => {
|
|
1236
|
+
const e = press;
|
|
1237
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
|
|
1238
|
+
return;
|
|
1239
|
+
const a = e.target?.closest?.("a[href]");
|
|
1240
|
+
if (!a || !/^https?:/.test(a.href) || !frames(a.href))
|
|
1241
|
+
return;
|
|
1242
|
+
e.preventDefault();
|
|
1243
|
+
deck.open({ kind: PAGE, title: tabName(a.href), url: a.href });
|
|
1244
|
+
setPanel(true);
|
|
1245
|
+
},
|
|
1205
1246
|
// Entered and left once for every child crossed, so the veil follows a
|
|
1206
1247
|
// count rather than the last event.
|
|
1207
1248
|
onDragEnter: (e) => {
|
|
@@ -1226,7 +1267,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1226
1267
|
e.preventDefault();
|
|
1227
1268
|
// A dropped folder arrives as entries; each file under it goes up with its path.
|
|
1228
1269
|
void dropped(e.dataTransfer).then((list) => hold(at, list, via));
|
|
1229
|
-
}, children: [dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { height:
|
|
1270
|
+
}, children: [dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: mine.title || "Untitled" })) : null, room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { "data-slot": "conversation-controls", items: "center", gap: 2, shrink: 0, children: [open ? _jsx(Share, { thread: open }) : null, mine ? (_jsx(ConversationMenu, { threads: alone, active: open, children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { at: at, said: said, held: files, deck: deck, onOpen: () => setPanel(true), onCreate: () => window.location.assign(site("/dev")) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs", ...browsing }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs", ...browsing }) : null, isFree(model) ? (_jsxs(Text, { fontSize: "$2", color: "$soft", text: "center", maxW: MEASURE, mx: "auto", px: "$4", py: "$2", children: [freeCopy.notice, " ", _jsx(Anchor, { href: site("/legal/privacy"), children: freeCopy.noticeCta })] })) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
|
|
1230
1271
|
// AN EMPTY ROOM CENTRES ITS COMPOSER. While nothing has been said the
|
|
1231
1272
|
// opening (the heading and the starters) and the composer sit as one
|
|
1232
1273
|
// group in the middle of the height, with or without the plan bars in
|
|
@@ -1304,6 +1345,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1304
1345
|
// `label` keeps the accessible name constant while it does.
|
|
1305
1346
|
placeholder: room.length ? `Message ${join(room)}` : placeholder,
|
|
1306
1347
|
label: ASK,
|
|
1348
|
+
note: caution(),
|
|
1307
1349
|
// In the app's frame the composer floats on the pane as a pane of its own.
|
|
1308
1350
|
framed,
|
|
1309
1351
|
// A file or an image pasted into the field is held like a picked one;
|
package/dist/Column.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { type Host } from '@hanzo/build';
|
|
2
|
-
import type
|
|
3
|
-
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import { type Mode } from './recent.js';
|
|
4
|
+
export declare function Column({ host, mode, thread, go, onShut, seek, drawer, foot, }: {
|
|
4
5
|
host: Host;
|
|
5
6
|
mode: Mode;
|
|
6
7
|
/** The Chat conversation the pane holds, or null. */
|
|
7
8
|
thread: string | null;
|
|
8
|
-
/** The switch: to Chat and to Dev, each where the reader left it. */
|
|
9
|
-
onChat: () => void;
|
|
10
|
-
onDev: () => void;
|
|
11
9
|
/** Moves the app to an address of this page. */
|
|
12
10
|
go: (address: string) => void;
|
|
13
|
-
/**
|
|
11
|
+
/** Shut the sidebar: its edge pulled in past the floor. */
|
|
12
|
+
onShut: () => void;
|
|
13
|
+
/** Each new value asks for the search, focused (⌘K). */
|
|
14
|
+
seek?: number;
|
|
15
|
+
/** Drawn in the phone's drawer: no edge, cut as a floating pane. */
|
|
14
16
|
drawer?: boolean;
|
|
17
|
+
/** What stands at its foot: the drawer's rail, laid flat. */
|
|
18
|
+
foot?: ReactNode;
|
|
15
19
|
}): import("react").JSX.Element;
|