@hanzo/build 0.2.80 → 0.2.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/lib/activity.d.ts +3 -7
- package/lib/activity.js +18 -5
- package/lib/api/agents.d.ts +1 -20
- package/lib/api/agents.js +16 -8
- package/lib/api/auto.d.ts +3 -23
- package/lib/api/auto.js +39 -22
- package/lib/api/billing.d.ts +0 -35
- package/lib/api/billing.js +6 -5
- package/lib/api/call.d.ts +16 -8
- package/lib/api/call.js +32 -1
- package/lib/api/capabilities.d.ts +0 -11
- package/lib/api/capabilities.js +2 -1
- package/lib/api/codebases.d.ts +0 -10
- package/lib/api/codebases.js +3 -2
- package/lib/api/coding.d.ts +3 -32
- package/lib/api/coding.js +13 -7
- package/lib/api/connectors.d.ts +1 -17
- package/lib/api/connectors.js +5 -4
- package/lib/api/environment.d.ts +6 -20
- package/lib/api/environment.js +22 -19
- package/lib/api/github.d.ts +0 -22
- package/lib/api/github.js +3 -2
- package/lib/api/keys.d.ts +0 -14
- package/lib/api/keys.js +4 -3
- package/lib/api/library.d.ts +3 -17
- package/lib/api/library.js +23 -22
- package/lib/api/links.d.ts +1 -1
- package/lib/api/links.js +1 -1
- package/lib/api/machines.d.ts +0 -15
- package/lib/api/machines.js +6 -5
- package/lib/api/mcp.d.ts +0 -9
- package/lib/api/mcp.js +3 -2
- package/lib/api/members.d.ts +0 -15
- package/lib/api/members.js +4 -3
- package/lib/api/memory.d.ts +0 -11
- package/lib/api/memory.js +2 -1
- package/lib/api/places.d.ts +1 -9
- package/lib/api/places.js +12 -1
- package/lib/api/plugins.d.ts +1 -16
- package/lib/api/plugins.js +4 -3
- package/lib/api/profile.d.ts +0 -10
- package/lib/api/profile.js +5 -4
- package/lib/api/projects.d.ts +0 -14
- package/lib/api/projects.js +11 -10
- package/lib/api/provider.d.ts +2 -14
- package/lib/api/provider.js +12 -10
- package/lib/api/sandbox.d.ts +0 -17
- package/lib/api/sandbox.js +5 -4
- package/lib/api/sessions.d.ts +0 -17
- package/lib/api/sessions.js +5 -4
- package/lib/api/skills.d.ts +1 -15
- package/lib/api/skills.js +5 -4
- package/lib/api/studio.d.ts +0 -14
- package/lib/api/studio.js +6 -5
- package/lib/api/turn.d.ts +6 -28
- package/lib/api/turn.js +105 -49
- package/lib/api/verdict.d.ts +0 -9
- package/lib/api/verdict.js +2 -1
- package/lib/api/webhooks.d.ts +1 -14
- package/lib/api/webhooks.js +5 -4
- package/lib/ask.js +7 -4
- package/lib/automations.d.ts +5 -4
- package/lib/automations.js +95 -74
- package/lib/back.js +10 -4
- package/lib/builder.js +7 -5
- package/lib/carry.js +20 -16
- package/lib/customize/agents.js +78 -31
- package/lib/customize/connectors.js +62 -48
- package/lib/customize/index.js +43 -10
- package/lib/customize/plugins.js +34 -22
- package/lib/customize/say.d.ts +13 -1
- package/lib/customize/say.js +15 -21
- package/lib/customize/skills.js +31 -22
- package/lib/customize/ui.d.ts +5 -9
- package/lib/customize/ui.js +26 -16
- package/lib/data.d.ts +1 -1
- package/lib/data.js +3 -2
- package/lib/desk.js +45 -33
- package/lib/door.js +35 -10
- package/lib/environment.js +25 -19
- package/lib/files.js +43 -34
- package/lib/find.js +12 -9
- package/lib/foot.js +7 -4
- package/lib/forge.js +61 -40
- package/lib/gear.d.ts +2 -1
- package/lib/gear.js +6 -4
- package/lib/git.js +29 -19
- package/lib/landing.d.ts +5 -5
- package/lib/landing.js +52 -44
- package/lib/library.js +73 -51
- package/lib/merge.d.ts +2 -2
- package/lib/meter.d.ts +1 -1
- package/lib/meter.js +14 -5
- package/lib/mic.js +12 -5
- package/lib/mind.d.ts +1 -0
- package/lib/mind.js +5 -3
- package/lib/nav.d.ts +6 -5
- package/lib/nav.js +37 -30
- package/lib/panel.d.ts +2 -1
- package/lib/panel.js +52 -36
- package/lib/plan.d.ts +4 -20
- package/lib/plan.js +26 -10
- package/lib/plans.js +49 -30
- package/lib/plugins/design.js +3 -2
- package/lib/plugins/index.d.ts +23 -4
- package/lib/plugins/index.js +11 -26
- package/lib/plugins/pdf.js +2 -1
- package/lib/plugins/project.js +7 -4
- package/lib/plugins/research.js +3 -2
- package/lib/plugins/sketch.js +6 -3
- package/lib/plugins/slides.js +3 -2
- package/lib/plugins/template.js +6 -3
- package/lib/plus.js +25 -13
- package/lib/prefs.d.ts +0 -14
- package/lib/prefs.js +2 -1
- package/lib/project.js +69 -62
- package/lib/prompt.d.ts +12 -11
- package/lib/prompt.js +38 -20
- package/lib/publish.js +17 -12
- package/lib/reply.js +6 -3
- package/lib/run.js +72 -54
- package/lib/say.d.ts +2 -0
- package/lib/say.js +7 -0
- package/lib/scheduled.js +54 -43
- package/lib/section.d.ts +1 -0
- package/lib/section.js +8 -2
- package/lib/settings/account.d.ts +1 -0
- package/lib/settings/account.js +31 -21
- package/lib/settings/billing.js +55 -28
- package/lib/settings/capabilities.js +26 -15
- package/lib/settings/card.js +19 -12
- package/lib/settings/code.js +22 -11
- package/lib/settings/credits.d.ts +4 -4
- package/lib/settings/credits.js +34 -21
- package/lib/settings/environments.js +21 -14
- package/lib/settings/general.js +26 -14
- package/lib/settings/index.js +6 -3
- package/lib/settings/integrations.js +42 -28
- package/lib/settings/keys.js +37 -19
- package/lib/settings/list.d.ts +0 -5
- package/lib/settings/list.js +24 -15
- package/lib/settings/machines.d.ts +3 -0
- package/lib/settings/machines.js +63 -38
- package/lib/settings/members.js +23 -14
- package/lib/settings/memory.js +12 -6
- package/lib/settings/notifications.js +32 -19
- package/lib/settings/plan.d.ts +1 -1
- package/lib/settings/plan.js +40 -17
- package/lib/settings/privacy.js +26 -13
- package/lib/settings/ui.d.ts +3 -2
- package/lib/settings/ui.js +9 -3
- package/lib/settings/usage.js +7 -3
- package/lib/shelf.js +20 -17
- package/lib/side.d.ts +1 -26
- package/lib/side.js +11 -5
- package/lib/standing.d.ts +2 -10
- package/lib/standing.js +5 -3
- package/lib/suggest.d.ts +8 -28
- package/lib/suggest.js +39 -4
- package/lib/suggestions.js +14 -5
- package/lib/summary.d.ts +1 -1
- package/lib/summary.js +4 -1
- package/lib/sync.js +22 -12
- package/lib/tools.d.ts +4 -3
- package/lib/tools.js +10 -7
- package/lib/transcript.js +10 -8
- package/package.json +4 -2
package/lib/landing.js
CHANGED
|
@@ -12,8 +12,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* names one, and an ask with none chosen opens the picker rather than making a
|
|
13
13
|
* project up from its words. The choices a person made here are kept in this
|
|
14
14
|
* browser, per org, so the next New starts where the last one did.
|
|
15
|
+
*
|
|
16
|
+
* Its words are the reader's language (@hanzo/i18n `t`, the target is
|
|
17
|
+
* `target`); a codebase's, a machine's and a branch's names are drawn as given.
|
|
15
18
|
*/
|
|
16
19
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
20
|
+
import { phrase } from '@hanzo/i18n';
|
|
21
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
17
22
|
import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
|
|
18
23
|
import { Button } from '@hanzo/ui';
|
|
19
24
|
import { BranchSelect, ChipSelect, FooterLink, RepoSelect } from '@hanzo/ui/product';
|
|
@@ -43,10 +48,10 @@ import { MEASURE, Prompt, useEar } from './prompt.js';
|
|
|
43
48
|
import { Monitor as Device } from '@hanzogui/lucide-icons-2';
|
|
44
49
|
import { compose, Files, pick, read as readFiles } from './tools.js';
|
|
45
50
|
import { useSpoken } from './voice.js';
|
|
46
|
-
/** One vocabulary for the mode, on New and in a workspace
|
|
51
|
+
/** One vocabulary for the mode, on New and in a workspace: English, marked (`phrase`), drawn through `t`. */
|
|
47
52
|
export const MODES = [
|
|
48
|
-
{ id: 'build', label: 'Build', hint: 'Edits, commits and pushes a branch' },
|
|
49
|
-
{ id: 'plan', label: 'Plan', hint: 'Plans the change and writes nothing' },
|
|
53
|
+
{ id: 'build', label: phrase('Build'), hint: phrase('Edits, commits and pushes a branch') },
|
|
54
|
+
{ id: 'plan', label: phrase('Plan'), hint: phrase('Plans the change and writes nothing') },
|
|
50
55
|
];
|
|
51
56
|
/** The org's repositories before they have been read: no answer, so nothing is judged from it. */
|
|
52
57
|
const NOHOMES = { homes: [], whole: false };
|
|
@@ -79,7 +84,8 @@ export function hold(u) {
|
|
|
79
84
|
}
|
|
80
85
|
export function Landing({ onStarted }) {
|
|
81
86
|
const host = useHost();
|
|
82
|
-
const
|
|
87
|
+
const target = useTarget();
|
|
88
|
+
const { t } = useI18n();
|
|
83
89
|
const signed = Boolean(host.person);
|
|
84
90
|
const { prefs, save: savePrefs } = usePrefs();
|
|
85
91
|
const [stored, keep] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, null);
|
|
@@ -109,21 +115,21 @@ export function Landing({ onStarted }) {
|
|
|
109
115
|
// The handover is read on this mount only.
|
|
110
116
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
111
117
|
}, []);
|
|
112
|
-
const places = usePlaces(
|
|
118
|
+
const places = usePlaces(target, signed);
|
|
113
119
|
// The model and effort Chat shares, and dictation into the ask.
|
|
114
|
-
const { limits } = useLimits(signed ? (signal) => readLimits(
|
|
115
|
-
const mind = useMind(
|
|
116
|
-
const voice = useVoice({ speech: useEar(
|
|
117
|
-
const talk = useTalk({ baseUrl:
|
|
120
|
+
const { limits } = useLimits(signed ? (signal) => readLimits(target, signal) : null, target);
|
|
121
|
+
const mind = useMind(target, limits?.plan);
|
|
122
|
+
const voice = useVoice({ speech: useEar(target), language: useSpoken(), ...useDictation(draft, setDraft) });
|
|
123
|
+
const talk = useTalk({ baseUrl: target.api, token: () => target.token(), ...(target.org ? { org: target.org } : {}) });
|
|
118
124
|
// What the run carries beside the ask: skills, memory and the mode, as Chat's turns do.
|
|
119
|
-
const carry = useCarry(
|
|
125
|
+
const carry = useCarry(target, '', (tab) => host.go(path({ kind: 'customize', tab })), {
|
|
120
126
|
draft,
|
|
121
127
|
setDraft,
|
|
122
128
|
// A run reads text: a file that is one rides the ask, a picture goes with a chat.
|
|
123
129
|
attach: (list) => {
|
|
124
130
|
const text = list.filter((f) => !f.type.startsWith('image/'));
|
|
125
131
|
if (text.length < list.length)
|
|
126
|
-
setNote('A run reads text files; send a picture in Chat.');
|
|
132
|
+
setNote(t('A run reads text files; send a picture in Chat.'));
|
|
127
133
|
if (text.length) {
|
|
128
134
|
const dt = new DataTransfer();
|
|
129
135
|
for (const f of text)
|
|
@@ -138,16 +144,16 @@ export function Landing({ onStarted }) {
|
|
|
138
144
|
go: (href) => (href.startsWith('http') ? host.open(href) : host.go(href)),
|
|
139
145
|
}, DEV_MODES);
|
|
140
146
|
const mode = carry.mode.value === 'plan' ? 'plan' : 'build';
|
|
141
|
-
const hints = useSuggestions(
|
|
147
|
+
const hints = useSuggestions(target);
|
|
142
148
|
// The chosen codebase's environment, so New can say when it has none yet.
|
|
143
149
|
const codebase = isForge(kept.repo) ? kept.repo.name : '';
|
|
144
150
|
// The org's repositories on the forge: what a run and an environment can name.
|
|
145
|
-
const homes = useCached(signed ? `${host.org}:homes` : null, (signal) => readHomes(
|
|
151
|
+
const homes = useCached(signed ? `${host.org}:homes` : null, (signal) => readHomes(target, signal), NOHOMES);
|
|
146
152
|
// A kept codebase the org's whole list does not hold — a board's key once handed over as one — is not offered as one.
|
|
147
153
|
const stranger = codebase !== '' && homes.value.whole && !homes.value.homes.some((h) => h.name.toLowerCase() === codebase.toLowerCase());
|
|
148
154
|
// Read once the list has answered (or failed), so a name that is not the org's is never asked for.
|
|
149
155
|
const settled = homes.value !== NOHOMES || homes.error !== null;
|
|
150
|
-
const found = useRead(signed && codebase && settled && !stranger ? () => read(
|
|
156
|
+
const found = useRead(signed && codebase && settled && !stranger ? () => read(target, codebase) : null, null, [target, signed, codebase, settled, stranger]);
|
|
151
157
|
// A read kept from the codebase before is not this one's.
|
|
152
158
|
const env = { ...found, value: found.value && found.value.repo === codebase && !stranger ? found.value : null };
|
|
153
159
|
const place = places.value.find((p) => p.id === kept.place) ?? SANDBOX;
|
|
@@ -158,7 +164,7 @@ export function Landing({ onStarted }) {
|
|
|
158
164
|
// pushes and proposes on GitHub, where its people work. A GitHub read that
|
|
159
165
|
// fails leaves the forge's list standing.
|
|
160
166
|
const loadRepos = useCallback(async (q) => {
|
|
161
|
-
const [page, hub] = await Promise.all([codebases(
|
|
167
|
+
const [page, hub] = await Promise.all([codebases(target, q), githubRepos(target, { q }).catch(() => null)]);
|
|
162
168
|
const repos = page.map(asRepo);
|
|
163
169
|
for (const r of repos)
|
|
164
170
|
known.current.set(r.name, r);
|
|
@@ -166,15 +172,15 @@ export function Landing({ onStarted }) {
|
|
|
166
172
|
for (const r of github)
|
|
167
173
|
hubs.current.set(r.full_name.toLowerCase(), r);
|
|
168
174
|
return { repos: [...repos, ...github], next: null };
|
|
169
|
-
}, [
|
|
175
|
+
}, [target]);
|
|
170
176
|
// Drawn only once a forge codebase is chosen; one the forge no longer lists offers its default branch.
|
|
171
177
|
const home = kept.repo?.default_branch || 'main';
|
|
172
178
|
const loadBranches = useCallback(async (q) => {
|
|
173
|
-
const got = await one(
|
|
179
|
+
const got = await one(target, codebase);
|
|
174
180
|
const needle = q.trim().toLowerCase();
|
|
175
181
|
const names = (got?.branches ?? [home]).filter((b) => !needle || b.toLowerCase().includes(needle));
|
|
176
182
|
return { branches: names.map((name) => ({ name })), next: null };
|
|
177
|
-
}, [
|
|
183
|
+
}, [target, codebase, home]);
|
|
178
184
|
// The composer sends only a draft with words in it, and not while a send is out.
|
|
179
185
|
const send = async (said = draft) => {
|
|
180
186
|
if (!signed) {
|
|
@@ -185,18 +191,18 @@ export function Landing({ onStarted }) {
|
|
|
185
191
|
const repo = isForge(kept.repo) ? kept.repo : null;
|
|
186
192
|
const hub = isHub(kept.repo) ? kept.repo : null;
|
|
187
193
|
if (!repo && !hub) {
|
|
188
|
-
setNote('Choose the project this run works on: a repository of this organization, or one on its GitHub.');
|
|
194
|
+
setNote(t('Choose the project this run works on: a repository of this organization, or one on its GitHub.'));
|
|
189
195
|
setPicking(true);
|
|
190
196
|
return;
|
|
191
197
|
}
|
|
192
198
|
// A plan or a setup routed to a machine is refused here, in its own words, before it leaves the page.
|
|
193
|
-
const why = unhonoured(mode, place.id);
|
|
199
|
+
const why = unhonoured(mode, place.id, t);
|
|
194
200
|
if (why) {
|
|
195
201
|
setNote(why);
|
|
196
202
|
return;
|
|
197
203
|
}
|
|
198
204
|
if (stranger) {
|
|
199
|
-
setNote(
|
|
205
|
+
setNote(t('{repo} is not one of this organization’s repositories. Choose one, or leave it empty to start a new project.', { repo: codebase }));
|
|
200
206
|
setPicking(true);
|
|
201
207
|
return;
|
|
202
208
|
}
|
|
@@ -212,11 +218,11 @@ export function Landing({ onStarted }) {
|
|
|
212
218
|
return;
|
|
213
219
|
}
|
|
214
220
|
// Manual: the run is asked for before it starts; it edits, commits and pushes on its own once it has.
|
|
215
|
-
if (!(await carry.allow({ id: 'run', name: 'Dev to start this run', note: 'It edits, tests, commits and pushes its own branch' }))) {
|
|
216
|
-
setNote('Not started.');
|
|
221
|
+
if (!(await carry.allow({ id: 'run', name: t('Dev to start this run'), note: t('It edits, tests, commits and pushes its own branch') }))) {
|
|
222
|
+
setNote(t('Not started.'));
|
|
217
223
|
return;
|
|
218
224
|
}
|
|
219
|
-
const run = await start(
|
|
225
|
+
const run = await start(target, {
|
|
220
226
|
prompt: compose([said.trim(), carry.text, prepped.system ?? ''].filter(Boolean).join('\n\n---\n'), files),
|
|
221
227
|
// The name alone. The org rides the request, and a slash is not a repo name.
|
|
222
228
|
// An empty base or place is not sent: the default branch, and the sandbox.
|
|
@@ -234,7 +240,7 @@ export function Landing({ onStarted }) {
|
|
|
234
240
|
onStarted(run.session);
|
|
235
241
|
}
|
|
236
242
|
catch (e) {
|
|
237
|
-
setNote(plain(e, repo?.name ?? hub?.full_name ?? 'a new project', 'run'));
|
|
243
|
+
setNote(plain(e, repo?.name ?? hub?.full_name ?? t('a new project'), 'run', t));
|
|
238
244
|
}
|
|
239
245
|
finally {
|
|
240
246
|
setBusy(false);
|
|
@@ -285,13 +291,13 @@ export function Landing({ onStarted }) {
|
|
|
285
291
|
return;
|
|
286
292
|
case 'schedule':
|
|
287
293
|
setNote('');
|
|
288
|
-
void book(
|
|
294
|
+
void book(target, a, 'dev').then((made) => host.go(path({ kind: 'automation', id: made.id })), (e) => setNote(e instanceof Error && e.message ? e.message : t('The scheduled task could not be made.')));
|
|
289
295
|
return;
|
|
290
296
|
}
|
|
291
297
|
};
|
|
292
298
|
/** Start the agent that finds a codebase's environment, and open it: its run is the progress. A refusal is the dialog's to say. */
|
|
293
299
|
const setup = async (repo) => {
|
|
294
|
-
const run = await start(
|
|
300
|
+
const run = await start(target, { prompt: SETUP, repo, mode: 'setup', model: mind.model, effort: mind.effort });
|
|
295
301
|
onStarted(run.session);
|
|
296
302
|
};
|
|
297
303
|
/** The codebase a dialog chose, chosen here too. */
|
|
@@ -303,22 +309,24 @@ export function Landing({ onStarted }) {
|
|
|
303
309
|
// Cloud is the sandbox, set up by the chosen codebase's environment.
|
|
304
310
|
const setUp = codebase && env.value
|
|
305
311
|
? env.value.state === 'ready'
|
|
306
|
-
?
|
|
312
|
+
? t('{repo} environment', { repo: codebase })
|
|
307
313
|
: env.value.state === 'proposed'
|
|
308
|
-
?
|
|
314
|
+
? t('{repo} environment proposed', { repo: codebase })
|
|
309
315
|
: unset(env.value)
|
|
310
|
-
?
|
|
311
|
-
:
|
|
316
|
+
? t('no {repo} environment yet', { repo: codebase })
|
|
317
|
+
: t('{repo} environment, empty', { repo: codebase })
|
|
312
318
|
: '';
|
|
313
319
|
const placeItems = useMemo(() => (places.value.length ? places.value : [SANDBOX]).map((p) => ({
|
|
314
320
|
id: p.id || 'sandbox',
|
|
315
|
-
label: p.id ? p.label : 'Cloud',
|
|
316
|
-
hint: p.id ? ['Remote control', p.status, p.capacity].filter(Boolean).join(' · ') : ['Hanzo sandbox', setUp].filter(Boolean).join(' · '),
|
|
321
|
+
label: p.id ? p.label : t('Cloud'),
|
|
322
|
+
hint: p.id ? [t('Remote control'), p.status, p.capacity].filter(Boolean).join(' · ') : [t('Hanzo sandbox'), setUp].filter(Boolean).join(' · '),
|
|
317
323
|
disabled: !ready(p),
|
|
318
324
|
place: p,
|
|
319
|
-
})), [places.value, setUp]);
|
|
325
|
+
})), [places.value, setUp, t]);
|
|
320
326
|
const branch = kept.branch || kept.repo?.default_branch || 'main';
|
|
321
|
-
const where = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name:
|
|
327
|
+
const where = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: t('Where the run runs'), icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : t('Cloud'), chosen: placeItems.find((i) => i.place.id === place.id), items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: t('Search machines…'), loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: t('Set up remote control') }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Run {command} on a machine: it joins this list, and a run there uses its checkout and credentials.', {
|
|
328
|
+
command: (_jsx(SizableText, { size: "$1", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "hanzo link" })),
|
|
329
|
+
}) })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) || isHub(kept.repo) ? kept.repo : null, onChange: (r) => {
|
|
322
330
|
const hub = r.github ? hubs.current.get((r.full_name ?? "").toLowerCase()) : undefined;
|
|
323
331
|
setIssue(0);
|
|
324
332
|
if (hub) {
|
|
@@ -328,20 +336,20 @@ export function Landing({ onStarted }) {
|
|
|
328
336
|
const row = known.current.get(r.name) ?? chosen(r);
|
|
329
337
|
set({ repo: row, branch: row.default_branch });
|
|
330
338
|
}, load: loadRepos, troubleshoot: {
|
|
331
|
-
label: 'Missing a GitHub repository? Bring it onto the forge',
|
|
339
|
+
label: t('Missing a GitHub repository? Bring it onto the forge'),
|
|
332
340
|
onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })),
|
|
333
|
-
}, connect: _jsx(XStack, { justify: "flex-end", children: _jsx(FooterLink, { label:
|
|
334
|
-
label: 'Add to project',
|
|
341
|
+
}, connect: _jsx(XStack, { justify: "flex-end", children: _jsx(FooterLink, { label: t('Refresh list'), onPress: () => setRound((n) => n + 1) }) }), note: t("Repositories on the forge and on your organization's GitHub. Type to search."), action: {
|
|
342
|
+
label: t('Add to project'),
|
|
335
343
|
onPress: (r) => {
|
|
336
344
|
const row = known.current.get(r.name);
|
|
337
345
|
setAdding({
|
|
338
|
-
repo: row?.clone || `${
|
|
346
|
+
repo: row?.clone || `${target.api}/v1/git/${r.owner}/${r.name}.git`,
|
|
339
347
|
title: row?.full_name || `${r.owner}/${r.name}`,
|
|
340
348
|
ref: r.default_branch || 'main',
|
|
341
349
|
name: r.name,
|
|
342
350
|
});
|
|
343
351
|
},
|
|
344
|
-
}, placeholder:
|
|
352
|
+
}, placeholder: t('Choose a project'), disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null] }));
|
|
345
353
|
const head = files.length ? (_jsx(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: _jsx(Files, { files: files, onFiles: setFiles }) })) : undefined;
|
|
346
354
|
// The machines a run can go to, as the `+` menu's Devices: the same choice as `where`'s.
|
|
347
355
|
const devices = {
|
|
@@ -350,10 +358,10 @@ export function Landing({ onStarted }) {
|
|
|
350
358
|
loading: places.loading,
|
|
351
359
|
error: places.error?.message ?? null,
|
|
352
360
|
};
|
|
353
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", pb: "$10", overflowY: "auto", overflowX: "hidden", children: [_jsxs(YStack, { width: "100%", maxW: MEASURE, gap: "$3", children: [_jsx(SizableText, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", style: { textAlign: 'center' }, children:
|
|
354
|
-
?
|
|
355
|
-
:
|
|
356
|
-
upload: () => pick(files, setFiles, setNote),
|
|
361
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", pb: "$10", overflowY: "auto", overflowX: "hidden", children: [_jsxs(YStack, { width: "100%", maxW: MEASURE, gap: "$3", children: [_jsx(SizableText, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", style: { textAlign: 'center' }, children: t('Build what’s next.') }), _jsxs(YStack, { gap: "$2", children: [stranger ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: t('{repo} is not one of this organization’s repositories.', { repo: codebase }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setPicking(true), children: t('Choose a repository') })] })) : env.value && (env.value.state === 'proposed' || unset(env.value)) && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
|
|
362
|
+
? t('A proposed environment for {repo} is waiting for review.', { repo: codebase })
|
|
363
|
+
: t('{repo} has no environment yet, so every run starts it bare.', { repo: codebase }) }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: t('Review') })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: t('Set up environment') }))] })) : null, note || unhonoured(mode, place.id, t) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode, place.id, t) || mind.note })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, label: t('Describe a task or ask a question'), head: head, plus: {
|
|
364
|
+
upload: () => pick(files, setFiles, setNote, t),
|
|
357
365
|
skills: carry.skills,
|
|
358
366
|
design: carry.design,
|
|
359
367
|
plugins: carry.plugins,
|
package/lib/library.js
CHANGED
|
@@ -11,13 +11,19 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* with the org, copy its link, rename, move to a project, use in Studio or in
|
|
12
12
|
* Chat, and delete. What a colleague shared is drawn with their name and offers
|
|
13
13
|
* only what a reader may do with it.
|
|
14
|
+
*
|
|
15
|
+
* Its words are the reader's language (@hanzo/i18n `t`); an item's name, a
|
|
16
|
+
* colleague's, a project's and what an item holds are drawn as given.
|
|
14
17
|
*/
|
|
15
18
|
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
16
19
|
import { AudioLines, Box, Code2, Copy, Download, ExternalLink, FileText, FolderInput, Image as Picture, LayoutGrid, List, MessageSquare, MoreHorizontal, Pencil, Presentation, Search, Share2, Sparkles, Trash2, Type, Video, } from '@hanzogui/lucide-icons-2';
|
|
20
|
+
import { phrase } from '@hanzo/i18n';
|
|
21
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
17
22
|
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
|
|
18
23
|
import { ChipSelect } from '@hanzo/ui/product';
|
|
19
24
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
20
25
|
import { ago } from './ago.js';
|
|
26
|
+
import { told } from './api/call.js';
|
|
21
27
|
import { change, days, get, items as list, KINDS, remove, size, SOURCE } from './api/library.js';
|
|
22
28
|
import { Confirm, Rename } from './ask.js';
|
|
23
29
|
import { useKept, useProjects, useRead } from './data.js';
|
|
@@ -28,18 +34,22 @@ import { Panel } from './panel.js';
|
|
|
28
34
|
import { gap, pane } from './pane.js';
|
|
29
35
|
import { Sites } from './shelf.js';
|
|
30
36
|
import { useDeck } from './tabs.js';
|
|
37
|
+
/** Where an item was made: Chat, Dev and Studio by their names, the rest in words, marked (`phrase`) and drawn through `t`. */
|
|
31
38
|
const FROM = [
|
|
32
|
-
{ id: '', label: 'Anywhere' },
|
|
39
|
+
{ id: '', label: phrase('Anywhere') },
|
|
33
40
|
{ id: 'chat', label: 'Chat' },
|
|
34
41
|
{ id: 'dev', label: 'Dev' },
|
|
35
42
|
{ id: 'studio', label: 'Studio' },
|
|
36
|
-
{ id: 'workflow', label: 'Workflows' },
|
|
43
|
+
{ id: 'workflow', label: phrase('Workflows') },
|
|
37
44
|
];
|
|
38
45
|
const WHOSE = [
|
|
39
|
-
{ id: 'all', label: 'Mine and shared' },
|
|
40
|
-
{ id: 'mine', label: 'Mine' },
|
|
41
|
-
{ id: 'shared', label: 'Shared with me' },
|
|
46
|
+
{ id: 'all', label: phrase('Mine and shared') },
|
|
47
|
+
{ id: 'mine', label: phrase('Mine') },
|
|
48
|
+
{ id: 'shared', label: phrase('Shared with me') },
|
|
42
49
|
];
|
|
50
|
+
/** A day's list, named: the days the Library says in words each a sentence, a month as given. */
|
|
51
|
+
const MADE = { Today: phrase('Made today'), Yesterday: phrase('Made yesterday'), Earlier: phrase('Made earlier') };
|
|
52
|
+
const made = (label, t) => (MADE[label] ? t(MADE[label]) : t('Made {month}', { month: label.toLowerCase() }));
|
|
43
53
|
/** How many items a page reads. */
|
|
44
54
|
const PAGE = 60;
|
|
45
55
|
/** The side panel's scope: the items a reader opened, kept as they left them. */
|
|
@@ -77,8 +87,8 @@ export function Thumb({ x, size: n }) {
|
|
|
77
87
|
return (_jsx(YStack, { width: n ?? '100%', height: n, style: n ? undefined : { aspectRatio: '1 / 1' }, rounded: "$3", overflow: "hidden", bg: "$raised", items: "center", justify: "center", shrink: 0, "aria-hidden": true, children: src && !broken ? (_jsx("img", { src: src, alt: "", loading: "lazy", onError: () => setBroken(true), style: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' } })) : (_jsx(YStack, { opacity: 0.7, children: glyph(x.kind, n ? Math.max(14, Math.round(n / 2.6)) : 28) })) }));
|
|
78
88
|
}
|
|
79
89
|
/** What a tile says under its name: where it came from, whose, and when. */
|
|
80
|
-
function meta(x) {
|
|
81
|
-
return [SOURCE[x.source], x.mine ? '' :
|
|
90
|
+
function meta(x, t) {
|
|
91
|
+
return [t(SOURCE[x.source]), x.mine ? '' : t('shared by {owner}', { owner: x.owner }), ago(x.created)].filter(Boolean).join(' · ');
|
|
82
92
|
}
|
|
83
93
|
/** An item's link, to paste: this page with the item named. */
|
|
84
94
|
function linkTo(x) {
|
|
@@ -117,8 +127,9 @@ function named() {
|
|
|
117
127
|
}
|
|
118
128
|
/** What can be done with one item, as menu rows. */
|
|
119
129
|
function useActs(onChanged, onGone) {
|
|
130
|
+
const { t } = useI18n();
|
|
120
131
|
const host = useHost();
|
|
121
|
-
const
|
|
132
|
+
const target = useTarget();
|
|
122
133
|
const [renaming, setRenaming] = useState(null);
|
|
123
134
|
const [deleting, setDeleting] = useState(null);
|
|
124
135
|
const [moving, setMoving] = useState(null);
|
|
@@ -129,7 +140,7 @@ function useActs(onChanged, onGone) {
|
|
|
129
140
|
await work();
|
|
130
141
|
}
|
|
131
142
|
catch (e) {
|
|
132
|
-
setNote(e
|
|
143
|
+
setNote(told(e, t));
|
|
133
144
|
}
|
|
134
145
|
};
|
|
135
146
|
const copy = (text, said) => act(async () => {
|
|
@@ -137,51 +148,52 @@ function useActs(onChanged, onGone) {
|
|
|
137
148
|
setNote(said);
|
|
138
149
|
});
|
|
139
150
|
const rows = (x) => [
|
|
140
|
-
...(x.origin ? [{ key: 'open', label: 'Open where it was made', icon: _jsx(ExternalLink, { size: 16 }), onSelect: () => host.open(x.origin) }] : []),
|
|
141
|
-
...(x.download ? [{ key: 'download', label: 'Download', icon: _jsx(Download, { size: 16 }), onSelect: () => window.open(x.download, '_blank', 'noopener,noreferrer') }] : []),
|
|
151
|
+
...(x.origin ? [{ key: 'open', label: t('Open where it was made'), icon: _jsx(ExternalLink, { size: 16 }), onSelect: () => host.open(x.origin) }] : []),
|
|
152
|
+
...(x.download ? [{ key: 'download', label: t('Download'), icon: _jsx(Download, { size: 16 }), onSelect: () => window.open(x.download, '_blank', 'noopener,noreferrer') }] : []),
|
|
142
153
|
...(x.mine
|
|
143
154
|
? [
|
|
144
155
|
{
|
|
145
156
|
key: 'share',
|
|
146
|
-
label: x.shared ? 'Stop sharing' : 'Share with organization',
|
|
157
|
+
label: x.shared ? t('Stop sharing') : t('Share with organization'),
|
|
147
158
|
icon: _jsx(Share2, { size: 16 }),
|
|
148
|
-
description: x.shared ? 'Only you see it' : 'Everyone in it can open it',
|
|
159
|
+
description: x.shared ? t('Only you see it') : t('Everyone in it can open it'),
|
|
149
160
|
onSelect: () => void act(async () => {
|
|
150
|
-
const next = await change(
|
|
161
|
+
const next = await change(target, x.id, { shared: !x.shared });
|
|
151
162
|
onChanged(next);
|
|
152
|
-
setNote(next.shared ?
|
|
163
|
+
setNote(next.shared ? t('{name} is shared with the organization', { name: next.name }) : t('{name} is yours alone', { name: next.name }));
|
|
153
164
|
}),
|
|
154
165
|
},
|
|
155
166
|
]
|
|
156
167
|
: []),
|
|
157
|
-
{ key: 'link', label: 'Copy link', icon: _jsx(Copy, { size: 16 }), onSelect: () => void copy(linkTo(x), 'Link copied') },
|
|
168
|
+
{ key: 'link', label: t('Copy link'), icon: _jsx(Copy, { size: 16 }), onSelect: () => void copy(linkTo(x), t('Link copied')) },
|
|
158
169
|
...(x.mine
|
|
159
170
|
? [
|
|
160
|
-
{ key: 'rename', label: 'Rename', icon: _jsx(Pencil, { size: 16 }), onSelect: () => setRenaming(x) },
|
|
161
|
-
{ key: 'move', label: 'Move to project', icon: _jsx(FolderInput, { size: 16 }), onSelect: () => setMoving(x) },
|
|
171
|
+
{ key: 'rename', label: t('Rename'), icon: _jsx(Pencil, { size: 16 }), onSelect: () => setRenaming(x) },
|
|
172
|
+
{ key: 'move', label: t('Move to project'), icon: _jsx(FolderInput, { size: 16 }), onSelect: () => setMoving(x) },
|
|
162
173
|
]
|
|
163
174
|
: []),
|
|
164
|
-
{ key: 'studio', label: 'Use in Studio', icon: _jsx(Sparkles, { size: 16 }), onSelect: () => host.open(`/studio?asset=${encodeURIComponent(x.id)}`) },
|
|
165
|
-
{ key: 'chat', label: 'Use in Chat', icon: _jsx(MessageSquare, { size: 16 }), onSelect: () => host.open(`/chat?attach=${encodeURIComponent(x.id)}`) },
|
|
166
|
-
...(x.mine ? [{ key: 'delete', label: 'Delete', icon: _jsx(Trash2, { size: 16 }), onSelect: () => setDeleting(x) }] : []),
|
|
175
|
+
{ key: 'studio', label: t('Use in Studio'), icon: _jsx(Sparkles, { size: 16 }), onSelect: () => host.open(`/studio?asset=${encodeURIComponent(x.id)}`) },
|
|
176
|
+
{ key: 'chat', label: t('Use in Chat'), icon: _jsx(MessageSquare, { size: 16 }), onSelect: () => host.open(`/chat?attach=${encodeURIComponent(x.id)}`) },
|
|
177
|
+
...(x.mine ? [{ key: 'delete', label: t('Delete'), icon: _jsx(Trash2, { size: 16 }), onSelect: () => setDeleting(x) }] : []),
|
|
167
178
|
];
|
|
168
|
-
const dialogs = (_jsxs(_Fragment, { children: [_jsx(Rename, { open: Boolean(renaming), onOpenChange: (o) => !o && setRenaming(null), title:
|
|
169
|
-
const next = await change(
|
|
179
|
+
const dialogs = (_jsxs(_Fragment, { children: [_jsx(Rename, { open: Boolean(renaming), onOpenChange: (o) => !o && setRenaming(null), title: t('Rename'), name: renaming?.name ?? '', run: async (name) => {
|
|
180
|
+
const next = await change(target, renaming.id, { name });
|
|
170
181
|
onChanged(next);
|
|
171
|
-
} }), _jsx(Confirm, { open: Boolean(deleting), onOpenChange: (o) => !o && setDeleting(null), title:
|
|
172
|
-
await remove(
|
|
182
|
+
} }), _jsx(Confirm, { open: Boolean(deleting), onOpenChange: (o) => !o && setDeleting(null), title: t('Delete {name}?', { name: deleting?.name ?? '' }), says: t('It leaves the Library and its file is deleted. Anyone it was shared with loses it too.'), act: t('Delete'), run: async () => {
|
|
183
|
+
await remove(target, deleting.id);
|
|
173
184
|
onGone(deleting.id);
|
|
174
185
|
} }), _jsx(Move, { item: moving, onClose: () => setMoving(null), onMoved: (next) => {
|
|
175
186
|
onChanged(next);
|
|
176
|
-
setNote(next.project ?
|
|
187
|
+
setNote(next.project ? t('{name} is in {project}', { name: next.name, project: next.project }) : t('{name} is in no project', { name: next.name }));
|
|
177
188
|
} })] }));
|
|
178
189
|
return { rows, dialogs, note };
|
|
179
190
|
}
|
|
180
191
|
/** Choosing the project an item belongs to, or none. */
|
|
181
192
|
function Move({ item: x, onClose, onMoved }) {
|
|
193
|
+
const { t } = useI18n();
|
|
182
194
|
const host = useHost();
|
|
183
|
-
const
|
|
184
|
-
const projects = useProjects(
|
|
195
|
+
const target = useTarget();
|
|
196
|
+
const projects = useProjects(target, Boolean(host.person) && Boolean(x));
|
|
185
197
|
const [busy, setBusy] = useState(false);
|
|
186
198
|
const [note, setNote] = useState('');
|
|
187
199
|
const pick = async (project) => {
|
|
@@ -190,29 +202,32 @@ function Move({ item: x, onClose, onMoved }) {
|
|
|
190
202
|
setBusy(true);
|
|
191
203
|
setNote('');
|
|
192
204
|
try {
|
|
193
|
-
onMoved(await change(
|
|
205
|
+
onMoved(await change(target, x.id, { project }));
|
|
194
206
|
onClose();
|
|
195
207
|
}
|
|
196
208
|
catch (e) {
|
|
197
|
-
setNote(e
|
|
209
|
+
setNote(told(e, t));
|
|
198
210
|
}
|
|
199
211
|
finally {
|
|
200
212
|
setBusy(false);
|
|
201
213
|
}
|
|
202
214
|
};
|
|
203
|
-
return (_jsx(Dialog, { open: Boolean(x), onOpenChange: (o) => !o && !busy && onClose(), children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children:
|
|
215
|
+
return (_jsx(Dialog, { open: Boolean(x), onOpenChange: (o) => !o && !busy && onClose(), children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: t('Move to project') }), _jsx(YStack, { gap: "$1", maxH: 320, overflow: "scroll", children: projects.loading && !projects.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: t('Reading projects…') })) : projects.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: told(projects.error, t) })) : ([{ slug: '', name: t('No project') }, ...projects.value].map((p) => (_jsx(XStack, { render: "button", "aria-label": p.slug ? t('Move to {project}', { project: p.name || p.slug }) : t('Move to no project'), "aria-current": x?.project === p.slug ? 'true' : undefined, disabled: busy, onPress: () => void pick(p.slug), px: "$3", py: "$2", rounded: "$3", bg: x?.project === p.slug ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(SizableText, { size: "$2", color: "$ink", children: p.name || p.slug }) }, p.slug || 'none')))) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null] }) }));
|
|
204
216
|
}
|
|
205
217
|
/** One item's menu, as a quiet control. */
|
|
206
218
|
function Acts({ x, rows }) {
|
|
207
|
-
|
|
219
|
+
const { t } = useI18n();
|
|
220
|
+
return (_jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": t('Actions for {name}', { name: x.name }), width: 30, height: 30, items: "center", justify: "center", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", shrink: 0, children: _jsx(MoreHorizontal, { size: 16 }) }), items: rows }));
|
|
208
221
|
}
|
|
209
222
|
/** One tile of the grid: the still, then its name and where it came from; its menu beside the name. */
|
|
210
223
|
function Tile({ x, onOpen, rows }) {
|
|
211
|
-
|
|
224
|
+
const { t } = useI18n();
|
|
225
|
+
return (_jsxs(YStack, { role: "listitem", gap: "$2", minW: 0, children: [_jsx(YStack, { render: "button", "aria-label": t('Open {name}', { name: x.name }), onPress: onOpen, rounded: "$4", overflow: "hidden", cursor: "pointer", hoverStyle: { opacity: 0.88 }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(Thumb, { x: x }) }), _jsxs(XStack, { items: "flex-start", gap: "$1", minW: 0, children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta(x, t) })] }), _jsx(Acts, { x: x, rows: rows })] })] }));
|
|
212
226
|
}
|
|
213
227
|
/** One row of the list: a small still, its name, where it came from, how large, and its menu. */
|
|
214
228
|
function Row({ x, onOpen, rows }) {
|
|
215
|
-
|
|
229
|
+
const { t } = useI18n();
|
|
230
|
+
return (_jsxs(XStack, { role: "listitem", items: "center", gap: "$3", px: "$2", py: "$2", rounded: "$3", hoverStyle: { bg: '$hover' }, minW: 0, children: [_jsxs(XStack, { render: "button", "aria-label": t('Open {name}', { name: x.name }), onPress: onOpen, flex: 1, minW: 0, items: "center", gap: "$3", cursor: "pointer", "$platform-web": { textAlign: 'start' }, children: [_jsx(Thumb, { x: x, size: 40 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: meta(x, t) })] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 72, text: "end", "$max-md": { display: 'none' }, children: size(x.bytes) })] }), _jsx(Acts, { x: x, rows: rows })] }));
|
|
216
231
|
}
|
|
217
232
|
/** A chip: one choice among a few, lit when chosen. */
|
|
218
233
|
function Chip({ label, on, onPress }) {
|
|
@@ -220,16 +235,17 @@ function Chip({ label, on, onPress }) {
|
|
|
220
235
|
}
|
|
221
236
|
/** One item, read fresh so its signed URLs are live: the thing itself, then what it is. */
|
|
222
237
|
export function Preview({ id, rows }) {
|
|
223
|
-
const t =
|
|
238
|
+
const { t } = useI18n();
|
|
239
|
+
const target = useTarget();
|
|
224
240
|
const host = useHost();
|
|
225
|
-
const one = useRead(host.person ? () => get(
|
|
241
|
+
const one = useRead(host.person ? () => get(target, id) : null, null, [target, id, host.person]);
|
|
226
242
|
const x = one.value;
|
|
227
243
|
if (!x) {
|
|
228
|
-
return (_jsx(SizableText, { size: "$2", color: "$soft", children: !host.person ? 'Sign in to open it.' : one.error ? one.error
|
|
244
|
+
return (_jsx(SizableText, { size: "$2", color: "$soft", children: !host.person ? t('Sign in to open it.') : one.error ? told(one.error, t) : t('Reading…') }));
|
|
229
245
|
}
|
|
230
|
-
return (_jsxs(YStack, { gap: "$3", minH: 0, children: [_jsx(Shown, { x: x }), _jsxs(XStack, { items: "flex-start", gap: "$2", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", children: [meta(x), size(x.bytes), x.width && x.height ? `${x.width}×${x.height}` : '', x.project ?
|
|
246
|
+
return (_jsxs(YStack, { gap: "$3", minH: 0, children: [_jsx(Shown, { x: x }), _jsxs(XStack, { items: "flex-start", gap: "$2", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", children: [meta(x, t), size(x.bytes), x.width && x.height ? `${x.width}×${x.height}` : '', x.project ? t('in {project}', { project: x.project }) : '', x.shared ? t('shared with the organization') : '']
|
|
231
247
|
.filter(Boolean)
|
|
232
|
-
.join(' · ') })] }), rows ? _jsx(Acts, { x: x, rows: rows(x) }) : null] }), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [x.origin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.open(x.origin), children: [_jsx(ExternalLink, { size: 14 }),
|
|
248
|
+
.join(' · ') })] }), rows ? _jsx(Acts, { x: x, rows: rows(x) }) : null] }), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [x.origin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.open(x.origin), children: [_jsx(ExternalLink, { size: 14 }), t('Open where it was made')] })) : null, x.download ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => window.open(x.download, '_blank', 'noopener,noreferrer'), children: [_jsx(Download, { size: 14 }), t('Download')] })) : null] })] }));
|
|
233
249
|
}
|
|
234
250
|
/** The thing itself, drawn the way its kind is read. */
|
|
235
251
|
function Shown({ x }) {
|
|
@@ -250,6 +266,7 @@ function Shown({ x }) {
|
|
|
250
266
|
}
|
|
251
267
|
/** A text item's first 200 KB, as written; in a frame where the store does not answer this page's read. */
|
|
252
268
|
function Text({ x }) {
|
|
269
|
+
const { t } = useI18n();
|
|
253
270
|
const read = useRead(async () => {
|
|
254
271
|
const res = await fetch(x.url, { cache: 'no-store' });
|
|
255
272
|
if (!res.ok)
|
|
@@ -258,21 +275,22 @@ function Text({ x }) {
|
|
|
258
275
|
}, '', [x.url]);
|
|
259
276
|
if (read.error)
|
|
260
277
|
return _jsx("iframe", { src: x.url, title: x.name, style: { width: '100%', height: '60vh', border: 0, borderRadius: 12, background: 'white' } });
|
|
261
|
-
return (_jsx(YStack, { rounded: "$3", bg: "$raised", p: "$3", maxH: "60vh", overflow: "scroll", children: _jsx(SizableText, { size: "$1", color: "$ink", style: { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: read.loading && !read.value ? 'Reading…' : read.value }) }));
|
|
278
|
+
return (_jsx(YStack, { rounded: "$3", bg: "$raised", p: "$3", maxH: "60vh", overflow: "scroll", children: _jsx(SizableText, { size: "$1", color: "$ink", style: { whiteSpace: 'pre-wrap', fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: read.loading && !read.value ? t('Reading…') : read.value }) }));
|
|
262
279
|
}
|
|
263
280
|
/** A run's outputs as small tiles: each opens in the Library. */
|
|
264
281
|
export function Outputs({ ids }) {
|
|
282
|
+
const { t } = useI18n();
|
|
265
283
|
const host = useHost();
|
|
266
|
-
const
|
|
284
|
+
const target = useTarget();
|
|
267
285
|
const key = ids.join(',');
|
|
268
|
-
const got = useRead(host.person && ids.length ? () => Promise.all(ids.slice(0, 12).map((id) => get(
|
|
286
|
+
const got = useRead(host.person && ids.length ? () => Promise.all(ids.slice(0, 12).map((id) => get(target, id).catch(() => null))) : null, [], [target, key, host.person]);
|
|
269
287
|
const shown = got.value.filter((x) => x !== null);
|
|
270
288
|
if (!ids.length)
|
|
271
289
|
return null;
|
|
272
|
-
return (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", pt: "$1.5", role: "list", "aria-label":
|
|
290
|
+
return (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", pt: "$1.5", role: "list", "aria-label": t('What it made'), children: [shown.map((x) => (_jsx(XStack, { role: "listitem", children: _jsxs(XStack, { render: "button", "aria-label": t('Open {name} in the Library', { name: x.name }), onPress: () => {
|
|
273
291
|
want(x.id);
|
|
274
292
|
host.go('-/artifacts');
|
|
275
|
-
}, items: "center", gap: "$2",
|
|
293
|
+
}, items: "center", gap: "$2", paddingInlineEnd: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", overflow: "hidden", hoverStyle: { bg: '$hover' }, cursor: "pointer", maxW: 240, children: [_jsx(Thumb, { x: x, size: 36 }), _jsx(SizableText, { size: "$1", color: "$ink", numberOfLines: 1, children: x.name })] }) }, x.id))), got.loading && !shown.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: ids.length === 1 ? t('Reading what it made…') : t('Reading the {n} things it made…', { n: ids.length }) })) : null] }));
|
|
276
294
|
}
|
|
277
295
|
/** A list read page by page: the first on every change of filter, the next on reaching its end. */
|
|
278
296
|
function usePages(load, key) {
|
|
@@ -337,8 +355,9 @@ function useEnd(on) {
|
|
|
337
355
|
return (node) => setEl(node instanceof Element ? node : null);
|
|
338
356
|
}
|
|
339
357
|
export function Library() {
|
|
358
|
+
const { t } = useI18n();
|
|
340
359
|
const host = useHost();
|
|
341
|
-
const
|
|
360
|
+
const target = useTarget();
|
|
342
361
|
const signed = Boolean(host.person);
|
|
343
362
|
const wide = useMedia().md;
|
|
344
363
|
// The kind is the address's (-/artifacts/<kind>), so the sidebar, a link and Back all name it.
|
|
@@ -358,8 +377,8 @@ export function Library() {
|
|
|
358
377
|
const id = setTimeout(() => setQ(typed.trim()), 250);
|
|
359
378
|
return () => clearTimeout(id);
|
|
360
379
|
}, [typed]);
|
|
361
|
-
const load = useMemo(() => (signed ? (cursor, signal) => list(
|
|
362
|
-
const pages = usePages(load, `${kind}|${source}|${scope}|${q}|${
|
|
380
|
+
const load = useMemo(() => (signed ? (cursor, signal) => list(target, { kind, source, scope, q, limit: PAGE, cursor }, { signal }) : null), [signed, target, kind, source, scope, q]);
|
|
381
|
+
const pages = usePages(load, `${kind}|${source}|${scope}|${q}|${target.org}`);
|
|
363
382
|
const end = useEnd(pages.more);
|
|
364
383
|
const reload = pages.reload;
|
|
365
384
|
useEffect(() => {
|
|
@@ -378,8 +397,8 @@ export function Library() {
|
|
|
378
397
|
arrived.current = true;
|
|
379
398
|
const id = named();
|
|
380
399
|
if (id)
|
|
381
|
-
get(
|
|
382
|
-
}, [signed,
|
|
400
|
+
get(target, id).then(look, () => look({ id, name: t('Item') }));
|
|
401
|
+
}, [signed, target, look, t]);
|
|
383
402
|
const acts = useActs((x) => {
|
|
384
403
|
pages.patch(x);
|
|
385
404
|
if (side.tabs.some((tab) => tab.kind === `${TAB}${x.id}`))
|
|
@@ -391,11 +410,14 @@ export function Library() {
|
|
|
391
410
|
const kinds = side.tabs
|
|
392
411
|
.filter((tab) => tab.kind.startsWith(TAB))
|
|
393
412
|
.map((tab) => ({ id: tab.kind, label: tab.title, render: () => _jsx(Preview, { id: tab.kind.slice(TAB.length), rows: acts.rows }, tab.kind) }));
|
|
394
|
-
const panel = (sheet) => _jsx(Panel, { side: side, kinds: kinds, sheet: sheet, onHide: () => setShown(false), label:
|
|
413
|
+
const panel = (sheet) => _jsx(Panel, { side: side, kinds: kinds, sheet: sheet, onHide: () => setShown(false), label: t('Item preview') });
|
|
395
414
|
const open = shown && side.tabs.length > 0;
|
|
396
415
|
const groups = days(pages.rows);
|
|
397
416
|
const narrowed = Boolean(kind || source || q || scope !== 'all');
|
|
398
|
-
|
|
417
|
+
// The choices in the reader's words; Chat, Dev and Studio stay their names.
|
|
418
|
+
const from = FROM.map((f) => ({ ...f, label: t(f.label) }));
|
|
419
|
+
const whose = WHOSE.map((w) => ({ ...w, label: t(w.label) }));
|
|
420
|
+
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsx(YStack, { flex: 1, minH: 0, minW: 0, overflow: "scroll", px: "$6", py: "$6", "$max-md": { px: '$4', py: '$4' }, children: _jsxs(YStack, { width: "100%", maxW: 1120, mx: "auto", gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(SizableText, { render: "h1", size: "$7", fontWeight: "600", color: "$ink", flex: 1, children: KINDS.find((k) => k.id === kind)?.id ? t(KINDS.find((k) => k.id === kind).label) : t('Library') }), _jsxs(XStack, { role: "radiogroup", "aria-label": t('View'), gap: "$1", children: [_jsx(ViewMark, { label: t('Grid'), on: view === 'grid', onPress: () => setView('grid'), children: _jsx(LayoutGrid, { size: 16 }) }), _jsx(ViewMark, { label: t('List'), on: view === 'list', onPress: () => setView('list'), children: _jsx(List, { size: 16 }) })] })] }), _jsxs(XStack, { items: "center", gap: "$2", maxW: 480, children: [_jsx(Search, { size: 16, opacity: 0.6 }), _jsx(YStack, { flex: 1, minW: 0, children: _jsx(Input, { value: typed, onChangeText: setTyped, placeholder: t('Search your library'), "aria-label": t('Search your library'), disabled: !signed }) })] }), _jsxs(YStack, { gap: "$2", children: [host.sections ? null : (_jsx(XStack, { role: "radiogroup", "aria-label": t('Kind'), gap: "$2", overflowX: "auto", pb: "$1", "$platform-web": { scrollbarWidth: 'none' }, children: KINDS.map((k) => (_jsx(Chip, { label: t(k.label), on: kind === k.id, onPress: () => setKind(k.id) }, k.id || 'all'))) })), _jsxs(XStack, { gap: "$2", flexWrap: "wrap", rowGap: "$2", children: [_jsx(ChipSelect, { name: t('From'), label: from.find((f) => f.id === source).label, chosen: from.find((f) => f.id === source), items: from, onChange: (f) => setSource(f.id), placement: "bottom-start", width: 200 }), _jsx(ChipSelect, { name: t('Whose'), label: whose.find((w) => w.id === scope).label, chosen: whose.find((w) => w.id === scope), items: whose, onChange: (w) => setScope(w.id), placement: "bottom-start", width: 220 })] })] }), acts.note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: acts.note })) : null, !signed ? (_jsx(Said, { says: t('Sign in to see your library.'), children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: t('Sign in') }) })) : pages.error && !pages.rows.length ? (_jsx(Said, { says: told(pages.error, t), children: _jsx(Button, { size: "sm", variant: "outline", onPress: pages.reload, children: t('Try again') }) })) : pages.loading && !pages.rows.length ? (_jsx(Said, { says: t('Reading…') })) : !pages.rows.length ? (_jsx(Said, { says: narrowed ? t('Nothing matches.') : t('Nothing here yet. What you make in Chat, Dev and Studio lands here.') })) : (_jsxs(YStack, { gap: "$5", children: [groups.map((g) => (_jsxs(YStack, { gap: "$3", role: "group", "aria-label": t(g.label), children: [_jsx(SizableText, { size: "$2", color: "$soft", fontWeight: "500", children: t(g.label) }), view === 'grid' ? (_jsx(YStack, { role: "list", "aria-label": made(g.label, t), gap: 16, "$platform-web": { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(46%, 168px), 1fr))' }, children: g.rows.map((x) => (_jsx(Tile, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) })) : (_jsx(YStack, { role: "list", "aria-label": made(g.label, t), gap: "$0.5", children: g.rows.map((x) => (_jsx(Row, { x: x, onOpen: () => look(x), rows: acts.rows(x) }, x.id))) }))] }, g.label))), _jsx(YStack, { ref: end, height: 1 }), pages.loading ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('Reading more…') })) : pages.error ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: told(pages.error, t) })) : null] })), signed && !q && !kind && (source === '' || source === 'dev') && scope !== 'shared' ? (_jsx(YStack, { pt: "$6", borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Sites, {}) })) : null] }) }), open && !wide ? panel(true) : null, open && wide ? (_jsx(YStack, { "data-slot": "library-side", ...pane, ml: gap, shrink: 0, minH: 0, width: 440, maxW: "45%", children: panel(false) })) : null, acts.dialogs] }));
|
|
399
421
|
}
|
|
400
422
|
function ViewMark({ label, on, onPress, children }) {
|
|
401
423
|
return (_jsx(XStack, { render: "button", role: "radio", "aria-checked": on, "aria-label": label, onPress: onPress, width: 32, height: 32, items: "center", justify: "center", rounded: "$3", bg: on ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", opacity: on ? 1 : 0.6, children: children }));
|