@hanzo/app 0.2.8 → 0.2.10
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.js +10 -4
- package/dist/Betas.js +2 -1
- package/dist/Chat.js +29 -4
- package/dist/Directory.js +2 -2
- package/dist/Learning.d.ts +5 -0
- package/dist/Learning.js +93 -0
- package/dist/Market.js +2 -2
- package/dist/Me.js +9 -5
- package/dist/Rail.js +2 -1
- package/dist/SettingsPane.js +11 -2
- package/dist/Setup.js +191 -22
- package/dist/ThinkingEffort.d.ts +20 -0
- package/dist/ThinkingEffort.js +87 -0
- package/dist/Top.js +8 -2
- package/dist/learning.d.ts +79 -0
- package/dist/learning.js +48 -0
- package/dist/lib/chatPreferences.d.ts +7 -0
- package/dist/lib/chatPreferences.js +62 -0
- package/dist/look.js +76 -14
- package/package.json +3 -1
package/dist/App.js
CHANGED
|
@@ -61,14 +61,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
61
61
|
// so the builder's own plans are never drawn.
|
|
62
62
|
import { useI18n } from '@hanzo/i18n/react';
|
|
63
63
|
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
64
|
-
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
64
|
+
import { ArrowLeft, ArrowRight, RefreshCw } from 'lucide-react';
|
|
65
65
|
import { AiProvider } from '@hanzo/ai/react';
|
|
66
66
|
import { XStack, YStack } from '@hanzo/gui';
|
|
67
|
-
import { Builder, HostProvider, PrefsProvider, route } from '@hanzo/build';
|
|
67
|
+
import { Builder, HostProvider, PrefsProvider, route, tabs } from '@hanzo/build';
|
|
68
68
|
import { nav as places } from '@hanzo/studio';
|
|
69
69
|
import { useAi } from './lib/ai.js';
|
|
70
70
|
import { Chat } from './Chat.js';
|
|
71
71
|
import { Column } from './Column.js';
|
|
72
|
+
import { Learning } from './Learning.js';
|
|
72
73
|
import { isPlans, leave, useDevHost } from './dev.js';
|
|
73
74
|
import { Orgs } from './Orgs.js';
|
|
74
75
|
import { useRooms } from './host.js';
|
|
@@ -118,6 +119,7 @@ function Window({ bar }) {
|
|
|
118
119
|
const [closed, setClosed] = useKept('hanzo.build.rail', false);
|
|
119
120
|
// The palette is open (the sidebar's search; ⌘K toggles it).
|
|
120
121
|
const [finding, setFinding] = useState(false);
|
|
122
|
+
const [learning, setLearning] = useState(false);
|
|
121
123
|
const found = useCallback(() => setFinding(false), []);
|
|
122
124
|
const dev = useDevHost();
|
|
123
125
|
// The builder's moves put the drawer away, in its place in the history; one to
|
|
@@ -254,7 +256,11 @@ function Window({ bar }) {
|
|
|
254
256
|
pick(o);
|
|
255
257
|
window.location.assign(mode === 'dev' ? app('') : mode === 'studio' ? studio('') : mode === 'tabs' ? TERMINALS : talk());
|
|
256
258
|
};
|
|
257
|
-
const sidebar = mode === 'studio' ? places(maker, (p) => maker.go(p)) :
|
|
259
|
+
const sidebar = mode === 'studio' ? places(maker, (p) => maker.go(p)) : mode === 'dev' && raised(where) === 'customize'
|
|
260
|
+
? tabs(host, (p) => host.go(p), t).map((group) => ({ ...group, links: [...group.links, {
|
|
261
|
+
id: 'learning', label: t('Learning'), active: learning, icon: _jsx(RefreshCw, { size: 16, "aria-hidden": true }),
|
|
262
|
+
onPress: () => { shut(); setLearning(true); },
|
|
263
|
+
}] })) : undefined;
|
|
258
264
|
const flip = () => setClosed(!closed);
|
|
259
265
|
const seek = () => {
|
|
260
266
|
shut();
|
|
@@ -262,5 +268,5 @@ function Window({ bar }) {
|
|
|
262
268
|
};
|
|
263
269
|
return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsxs(YStack, { ref: windowRef, "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", "$max-md": { position: 'fixed', t: TOP, l: 0, r: 0, height: HEIGHT, ...{ overflow: 'clip' } }, 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.state === 'open', onPress: drawer.open, label: t('Open sidebar') }) }), _jsx(Mark, { label: t('Back'), "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true, "data-i18n-mirror": true }) }), _jsx(Mark, { label: t('Forward'), "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true, "data-i18n-mirror": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { ref: drawer.pane, "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), ...(drawer.slide ? RAISED : null),
|
|
264
270
|
// inline-style: the slide, a transform with no logical form whose sign and distance the drawer writes on the element itself, a drag every frame (Drawer.tsx).
|
|
265
|
-
style: drawer.slide ?? undefined, flex: 1, minW: 0, minH: 0, paddingInlineEnd: "var(--pane-gap)", pb: "var(--pane-gap)", pt: 0, "$max-md": { p: 0 }, children: [drawer.slide ? _jsx(Lift, {}) : null, _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, onStudio: toStudio, onTabs: toTabs, go: move, onOrg: org }) }), closed || mode === 'tabs' ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), onSeek: seek, onBack: back, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, marginInlineStart: closed || mode === 'tabs' ? 0 : 'var(--pane-gap)', "$max-md": { marginInlineStart: 0, pb: FOOT }, children: [bar ? null : _jsx(Top, { host: host, mode: mode, places: sidebar, open: drawer.state !== 'shut', go: move, onMenu: drawer.open, onChat: toChat, onDev: toDev }), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : mode === 'tabs' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(Suspense, { fallback: null, children: _jsx(Terminals, {}) }) })) : mode === 'studio' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(YStack, { flex: 1, minH: 0, height: "100%", children: _jsx(Suspense, { fallback: null, children: _jsx(Studio, { host: maker, rail: false }) }) }) })) : (_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.state === 'shut' ? null : (_jsx(Layer, { drawer: drawer, top: bar ? BAR : 0, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, onSeek: seek, onBack: back, drawer: true, places: sidebar, rail: _jsx(Rail, { host: host, mode: mode, at: where, open: true, rows: true, onChat: toChat, onDev: toDev, onStudio: toStudio, onTabs: toTabs, go: move, onOrg: org }), foot: _jsx(Foot, { host: host, onChat: fresh, onOrg: org }) }) })), finding ? _jsx(Palette, { go: move, onClose: found, mode: mode, host: host }) : null] }) }) }) }));
|
|
271
|
+
style: drawer.slide ?? undefined, flex: 1, minW: 0, minH: 0, paddingInlineEnd: "var(--pane-gap)", pb: "var(--pane-gap)", pt: 0, "$max-md": { p: 0 }, children: [drawer.slide ? _jsx(Lift, {}) : null, _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, onStudio: toStudio, onTabs: toTabs, go: move, onOrg: org }) }), closed || mode === 'tabs' ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), onSeek: seek, onBack: back, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, marginInlineStart: closed || mode === 'tabs' ? 0 : 'var(--pane-gap)', "$max-md": { marginInlineStart: 0, pb: FOOT }, children: [bar ? null : _jsx(Top, { host: host, mode: mode, places: sidebar, open: drawer.state !== 'shut', go: move, onMenu: drawer.open, onChat: toChat, onDev: toDev }), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : mode === 'tabs' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(Suspense, { fallback: null, children: _jsx(Terminals, {}) }) })) : mode === 'studio' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(YStack, { flex: 1, minH: 0, height: "100%", children: _jsx(Suspense, { fallback: null, children: _jsx(Studio, { host: maker, rail: false }) }) }) })) : (_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.state === 'shut' ? null : (_jsx(Layer, { drawer: drawer, top: bar ? BAR : 0, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, onSeek: seek, onBack: back, drawer: true, places: sidebar, rail: _jsx(Rail, { host: host, mode: mode, at: where, open: true, rows: true, onChat: toChat, onDev: toDev, onStudio: toStudio, onTabs: toTabs, go: move, onOrg: org }), foot: _jsx(Foot, { host: host, onChat: fresh, onOrg: org }) }) })), finding ? _jsx(Palette, { go: move, onClose: found, mode: mode, host: host }) : null, learning ? _jsx(Learning, { open: true, onClose: () => setLearning(false) }) : null] }) }) }) }));
|
|
266
272
|
}
|
package/dist/Betas.js
CHANGED
|
@@ -16,6 +16,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
16
16
|
// Signed out, still loading, or on any error nothing is open: a flags outage
|
|
17
17
|
// cannot leak an unfinished app.
|
|
18
18
|
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
|
|
19
|
+
import { ArrowUpRight, Compass } from 'lucide-react';
|
|
19
20
|
import { phrase } from '@hanzo/i18n';
|
|
20
21
|
import { useI18n } from '@hanzo/i18n/react';
|
|
21
22
|
import { useIam } from '@hanzo/iam/react';
|
|
@@ -138,5 +139,5 @@ export function BetasList({ go }) {
|
|
|
138
139
|
/** The Betas list in a dialog, from the rail's More. */
|
|
139
140
|
export function BetasDialog({ open, onOpenChange, go }) {
|
|
140
141
|
const { t } = useI18n();
|
|
141
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: t('Betas') }), _jsx(Text, { fontSize: "$2", color: "$color11", children: t('Apps we have not opened to everyone yet. Ask for the ones you want and we will let you in.') }), _jsx(YStack, { maxH: "60vh", overflowY: "auto", overflowX: "hidden", paddingInlineEnd: "$2", children: _jsx(BetasList, { go: go }) })] }) }));
|
|
142
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: t('Betas') }), _jsx(Text, { fontSize: "$2", color: "$color11", children: t('Apps we have not opened to everyone yet. Ask for the ones you want and we will let you in.') }), _jsx(YStack, { maxH: "60vh", overflowY: "auto", overflowX: "hidden", paddingInlineEnd: "$2", children: _jsx(BetasList, { go: go }) }), _jsxs(XStack, { items: "center", justify: "space-between", pt: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", gap: "$2", children: [_jsx(Text, { fontSize: "$2", color: "$color11", children: t('Looking for community plugins and MCP tools?') }), _jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => window.open('https://hanzo.market', '_blank', 'noopener,noreferrer'), children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Compass, { size: 14, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", children: t('Explore on hanzo.market') }), _jsx(ArrowUpRight, { size: 13, "aria-hidden": true })] }) })] })] }) }));
|
|
142
143
|
}
|
package/dist/Chat.js
CHANGED
|
@@ -58,7 +58,7 @@ import { base, served } from "./lib/ai.js";
|
|
|
58
58
|
import { onServed } from "./lib/served.js";
|
|
59
59
|
import { refused as worded, speech, useDictation, useTalk, useVoice } from "@hanzo/voice";
|
|
60
60
|
import { Check, Clock, FileText, Image as ImageMark, Library as LibraryMark, Paperclip, Sparkles, X } from "lucide-react";
|
|
61
|
-
import { FREE } from "./lib/ai.js";
|
|
61
|
+
import { FREE, ENSO } from "./lib/ai.js";
|
|
62
62
|
import { openThread, useOpen } from "./open.js";
|
|
63
63
|
import { Beside, Framed } from "./Shell.js";
|
|
64
64
|
import { SEED, look, useSections, useSites } from "./RightPane.js";
|
|
@@ -73,6 +73,8 @@ import { enter, LOGIN } from './lib/destination.js';
|
|
|
73
73
|
import { first } from './lib/first.js';
|
|
74
74
|
import { BAD } from "./lib/mix.js";
|
|
75
75
|
import { brand } from './where.js';
|
|
76
|
+
import { ThinkingEffort } from './ThinkingEffort.js';
|
|
77
|
+
import { useChatPreferences } from './lib/chatPreferences.js';
|
|
76
78
|
// NO REASONING PANEL HERE. Every assistant turn opened a "Thinking" disclosure
|
|
77
79
|
// listing seven steps — "Planning CSS extraction for chat styles", "Designing
|
|
78
80
|
// responsive top bar grid layout" — typed into this file as a literal array.
|
|
@@ -508,6 +510,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
508
510
|
const { limits, notice, reload: reread } = useLimits(!anonymous, org(), (id) => nameOf(id, catalog.current));
|
|
509
511
|
const mind = useMind(target, anonymous ? "free" : limits?.plan);
|
|
510
512
|
catalog.current = mind.models;
|
|
513
|
+
const [chatPrefs] = useChatPreferences();
|
|
511
514
|
const ai = useAi();
|
|
512
515
|
// In the app's frame the composer floats on the pane as a pane of its own.
|
|
513
516
|
const framed = useContext(Framed);
|
|
@@ -682,8 +685,20 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
682
685
|
// A SPEECH MODEL NEVER ANSWERS A CHAT TURN. The catalog lists the
|
|
683
686
|
// transcriber and the voice beside the chat models; one that names its
|
|
684
687
|
// outputs and names no text (`chats`) is neither offered nor fallen back to.
|
|
688
|
+
const isHouseModel = (m) => {
|
|
689
|
+
const id = (m.id || '').toLowerCase();
|
|
690
|
+
const family = (m.family || '').toLowerCase();
|
|
691
|
+
return id.startsWith('enso') || id.startsWith('zen') || id.startsWith('kai') ||
|
|
692
|
+
family.startsWith('enso') || family.startsWith('zen') || family.startsWith('kai') ||
|
|
693
|
+
m.class === 'ours';
|
|
694
|
+
};
|
|
685
695
|
const listed = mind.models;
|
|
686
|
-
const
|
|
696
|
+
const filteredListed = useMemo(() => {
|
|
697
|
+
if (chatPrefs.allowThirdPartyModels)
|
|
698
|
+
return listed;
|
|
699
|
+
return listed.filter((m) => isHouseModel(m) || m.id === ENSO || m.id === FREE);
|
|
700
|
+
}, [listed, chatPrefs.allowThirdPartyModels]);
|
|
701
|
+
const models = useMemo(() => filteredListed.filter(chats), [filteredListed]);
|
|
687
702
|
// EVERY MODEL THE GATEWAY LISTS is offered here, through the one picker, and
|
|
688
703
|
// read from the catalog's own fields (`parseModels`): its class, its family.
|
|
689
704
|
// A premium model is picked for THIS conversation and never remembered as
|
|
@@ -728,7 +743,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
728
743
|
// before the model is asked and asks without them, so a question about the
|
|
729
744
|
// world, in any language, is answered from what the web says now and streams.
|
|
730
745
|
params: {
|
|
731
|
-
reasoning_effort: mind.effort,
|
|
746
|
+
reasoning_effort: mind.effort || 'auto',
|
|
732
747
|
...(!anonymous && house(rowOf(model)?.family) ? { tools: WEB } : {}),
|
|
733
748
|
},
|
|
734
749
|
...(open ? { thread: open } : {}),
|
|
@@ -1554,5 +1569,15 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1554
1569
|
empty: heading === null ? undefined : (_jsxs(YStack, { "data-slot": "opening", items: "center", width: "100%", pt: "$6", pb: "$2", ...(media.sm ? null : { flex: 1, justify: "flex-end" }), children: [room.length > 1 ? (_jsx(XStack, { items: "center", justify: "center", mb: "$4", children: room.map((name, i) => (_jsx(Box, { marginInlineStart: i ? -16 : 0, rounded: 40, borderWidth: 2, borderColor: "$background", z: room.length - i, children: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 72 }) }, name))) })) : portrait ? (_jsx(Box, { width: 160, height: 160, rounded: 24, overflow: "hidden", borderWidth: 2, borderColor: "var(--white-15)", boxShadow: "0 10px 24px color-mix(in srgb, var(--pure-black) 70%, transparent)", mb: "$4", bg: "var(--neutral-900)", children: _jsx(View, { render: _jsx("img", { src: portrait, alt: withWhom }), width: "100%", height: "100%", objectFit: "cover" }) })) : null, room.length === 1 && !portrait ? (_jsx(Box, { mb: "$3", children: _jsx(Face, { emoji: mate?.emoji ?? persona?.emoji, name: withWhom, size: 72 }) })) : null, _jsx(Text, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", text: "center", mb: "$1", children: join(room, t) || heading || (firstName ? t("What’s up, {name}?", { name: firstName }) : t("What’s up?")) }), room.length === 1 && (mate?.description ?? persona?.description) ? (_jsx(Text, { fontSize: "$3", color: "$soft", text: "center", maxW: 500, mb: "$4", lineHeight: "$4", children: mate?.description ?? persona?.description })) : null] })),
|
|
1555
1570
|
// The transcript reads at the same measure. `Thread` defaults to 768
|
|
1556
1571
|
// itself; handing it the column here is what lets Width reach it.
|
|
1557
|
-
column: { maxW: MEASURE }, composer:
|
|
1572
|
+
column: { maxW: MEASURE }, composer: (() => {
|
|
1573
|
+
const basePrompt = prompt({
|
|
1574
|
+
...composing,
|
|
1575
|
+
mind: chatPrefs.showModelSelector ? mind : null,
|
|
1576
|
+
field: mention.field,
|
|
1577
|
+
}, mention.state);
|
|
1578
|
+
return {
|
|
1579
|
+
...basePrompt,
|
|
1580
|
+
send: (_jsxs(XStack, { items: "center", gap: "$1.5", shrink: 0, children: [_jsx(ThinkingEffort, { effort: mind.effort || 'auto', onChange: (eff) => mind.pace(eff), disabled: chat.streaming || talk.open }), basePrompt.send] })),
|
|
1581
|
+
};
|
|
1582
|
+
})() }), _jsx(AgentMaker, {}), carry.dialog, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
|
|
1558
1583
|
}
|
package/dist/Directory.js
CHANGED
|
@@ -47,7 +47,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
47
47
|
import { phrase } from '@hanzo/i18n';
|
|
48
48
|
import { useI18n } from '@hanzo/i18n/react';
|
|
49
49
|
import { useEffect, useMemo, useState } from 'react';
|
|
50
|
-
import { ArrowUpRight, Check, Plus, Search, X } from 'lucide-react';
|
|
50
|
+
import { ArrowUpRight, Check, Compass, Plus, Search, X } from 'lucide-react';
|
|
51
51
|
import { Box, Dialog, DialogContent, DialogTitle, Input, Text, XStack, YStack, } from '@hanzo/ui';
|
|
52
52
|
import { Grid } from '@hanzo/ui/grid';
|
|
53
53
|
import { useIam } from '@hanzo/iam/react';
|
|
@@ -218,7 +218,7 @@ export function Catalog({ tab }) {
|
|
|
218
218
|
return (rows ?? []).filter((r) => !q || r.name.toLowerCase().includes(q) || r.description.toLowerCase().includes(q));
|
|
219
219
|
}, [rows, query]);
|
|
220
220
|
const quiet = (text, action) => (_jsxs(YStack, { gap: "$3", py: "$4", items: "flex-start", children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: text }), action] }));
|
|
221
|
-
return (_jsxs(YStack, { gap: "$4", children: [
|
|
221
|
+
return (_jsxs(YStack, { gap: "$4", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", flexWrap: "wrap", children: [_jsxs(XStack, { items: "center", gap: "$2", flex: 1, minW: 180, children: [_jsx(Search, { size: 14, "aria-hidden": true }), _jsx(Input, { value: query, onChangeText: setQuery, placeholder: search, "aria-label": search, flex: 1 })] }), _jsx(Action, { render: "button", onPress: () => window.open('https://hanzo.market', '_blank', 'noopener,noreferrer'), $touchable: { minH: 36 }, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Compass, { size: 14, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", fontWeight: "500", children: t('Explore on hanzo.market') }), _jsx(ArrowUpRight, { size: 13, "aria-hidden": true })] }) })] }), tab === 'channels' && !signed ? (quiet(t('Sign in to see the channels your organization can connect.'), _jsx(Action, { render: "button", onPress: () => enter(), $touchable: { minH: 44, minW: 44 }, children: t('Sign in') }))) : wrong ? (quiet(say(wrong, t('the channels'), 'read', t), _jsx(Action, { render: "button", onPress: () => setTurn((n) => n + 1), $touchable: { minH: 44, minW: 44 }, children: t('Try again') }))) : rows === null ? (quiet(t(tab === 'channels' ? 'Loading channels…' : 'Loading works with…'))) : shown.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: shown.length === 1 ? t('1 result') : t('{n} results', { n: shown.length }) }), _jsx(YStack, { maxH: 380, overflowY: "auto", overflowX: "hidden", paddingInlineEnd: "$1", width: "100%", children: _jsx(Grid, { columns: tab === 'channels' ? { min: 168, max: 4 } : { min: 240, max: 2 }, gap: 12, children: shown.map((r) => tab === 'channels' ? (_jsx(Channel, { row: r, onChange: () => setTurn((n) => n + 1) }, r.id)) : (_jsx(RowCard, { row: r, tab: tab }, r.id))) }) })] })) : (quiet(query ? t('Nothing matches “{query}”.', { query }) : t(tab === 'channels' ? 'No channel can be connected here yet.' : 'Nothing is listed yet.')))] }));
|
|
222
222
|
}
|
|
223
223
|
function RowCard({ row, tab }) {
|
|
224
224
|
const { t } = useI18n();
|
package/dist/Learning.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
4
|
+
import { useIam } from '@hanzo/iam/react';
|
|
5
|
+
import { useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { Button, Dialog, DialogContent, DialogTitle, Input, Text, XStack, YStack } from '@hanzo/ui';
|
|
7
|
+
import { say } from './failure.js';
|
|
8
|
+
import { org } from './lib/session.js';
|
|
9
|
+
import { learningAnalyze, learningCompare, learningCreate, learningRead } from './learning.js';
|
|
10
|
+
/** The user's actual evidence and findings. Closed dialogs make no API requests. */
|
|
11
|
+
export function Learning({ open, onClose }) {
|
|
12
|
+
const { t } = useI18n();
|
|
13
|
+
const { accessToken } = useIam();
|
|
14
|
+
const tenant = org();
|
|
15
|
+
const identity = `${tenant}:${accessToken ?? ''}`;
|
|
16
|
+
const [held, setHeld] = useState({ identity: '', answers: {} });
|
|
17
|
+
const [revision, setRevision] = useState(0);
|
|
18
|
+
const [tab, setTab] = useState('research');
|
|
19
|
+
const [form, setForm] = useState(false);
|
|
20
|
+
const [busy, setBusy] = useState(false);
|
|
21
|
+
const [problem, setProblem] = useState(null);
|
|
22
|
+
const draftId = useRef('');
|
|
23
|
+
const [measuring, setMeasuring] = useState(false);
|
|
24
|
+
const [measured, setMeasured] = useState({ identity: '' });
|
|
25
|
+
const [pair, setPair] = useState({ benchmark: '', a: '', b: '' });
|
|
26
|
+
const results = measured.identity === identity ? measured : null;
|
|
27
|
+
const [proposal, setProposal] = useState({ title: '', question: '', baseline: '', target: '', metric: 'accuracy', category: 'all', direction: 'higher', minQuestions: 20, minGain: 1 });
|
|
28
|
+
const answers = held.identity === identity ? held.answers : {};
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!open || !accessToken)
|
|
31
|
+
return;
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
setHeld({ identity, answers: {} });
|
|
34
|
+
for (const part of ['studies', 'reviews', 'experiments', 'benchmarks']) {
|
|
35
|
+
void learningRead(part, controller.signal).then((rows) => { if (!controller.signal.aborted)
|
|
36
|
+
setHeld((old) => ({ identity, answers: { ...old.answers, [part]: { rows } } })); }, (error) => { if (!controller.signal.aborted)
|
|
37
|
+
setHeld((old) => ({ identity, answers: { ...old.answers, [part]: { error } } })); });
|
|
38
|
+
}
|
|
39
|
+
return () => controller.abort();
|
|
40
|
+
}, [open, accessToken, identity, revision]);
|
|
41
|
+
const create = async () => {
|
|
42
|
+
if (busy)
|
|
43
|
+
return;
|
|
44
|
+
setBusy(true);
|
|
45
|
+
setProblem(null);
|
|
46
|
+
try {
|
|
47
|
+
if (!draftId.current)
|
|
48
|
+
draftId.current = `improvement-${crypto.randomUUID()}`;
|
|
49
|
+
await learningCreate(proposal, draftId.current);
|
|
50
|
+
draftId.current = '';
|
|
51
|
+
setForm(false);
|
|
52
|
+
setRevision((n) => n + 1);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
setProblem(error);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
setBusy(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const analyze = async (id) => {
|
|
62
|
+
setMeasuring(true);
|
|
63
|
+
setMeasured({ identity });
|
|
64
|
+
try {
|
|
65
|
+
setMeasured({ identity, analysis: await learningAnalyze(id) });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
setMeasured({ identity, error });
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setMeasuring(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const compare = async () => {
|
|
75
|
+
setMeasuring(true);
|
|
76
|
+
setMeasured({ identity });
|
|
77
|
+
try {
|
|
78
|
+
setMeasured({ identity, comparison: await learningCompare(pair.benchmark, pair.a, pair.b) });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
setMeasured({ identity, error });
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
setMeasuring(false);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const state = (part) => answers[part]?.error
|
|
88
|
+
? _jsx(Text, { role: "alert", color: "$soft", children: say(answers[part]?.error, t('your learning activity'), 'read', t) })
|
|
89
|
+
: !answers[part]?.rows ? _jsx(Text, { color: "$soft", children: t('Loading…') }) : null;
|
|
90
|
+
const field = (key, label) => (_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$2", children: label }), _jsx(Input, { "aria-label": label, value: proposal[key], onChangeText: (value) => setProposal((old) => ({ ...old, [key]: value })) })] }));
|
|
91
|
+
return _jsx(Dialog, { open: open, onOpenChange: (next) => { if (!next)
|
|
92
|
+
onClose(); }, children: _jsx(DialogContent, { width: "100%", maxW: 900, p: "$5", children: _jsxs(YStack, { gap: "$4", maxH: "80vh", overflow: "scroll", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: _jsx(Text, { fontSize: "$5", children: t('Learning') }) }), _jsx(Button, { size: "sm", onPress: onClose, children: t('Close') })] }), _jsx(Text, { color: "$soft", children: t('Research, test ideas, and review improvements backed by your workspace’s evidence.') }), !accessToken ? _jsx(Text, { role: "status", children: t('Sign in to see your learning activity.') }) : _jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", flexWrap: "wrap", role: "tablist", "aria-label": t('Learning'), children: [_jsx(Button, { role: "tab", "aria-selected": tab === 'research', onPress: () => setTab('research'), children: t('Research') }), _jsx(Button, { role: "tab", "aria-selected": tab === 'experiments', onPress: () => setTab('experiments'), children: t('Experiments') }), _jsx(Button, { role: "tab", "aria-selected": tab === 'benchmarks', onPress: () => setTab('benchmarks'), children: t('Benchmarks') }), _jsx(Button, { size: "sm", onPress: () => setRevision((n) => n + 1), children: t('Refresh') })] }), tab === 'research' ? _jsxs(YStack, { gap: "$3", children: [_jsx(Text, { color: "$soft", children: t('A study with a baseline and acceptance criteria reviews new results as they arrive. Findings remain drafts until reviewed.') }), _jsx(Button, { size: "sm", onPress: () => setForm(!form), children: form ? t('Cancel') : t('New improvement study') }), form ? _jsx("form", { onSubmit: (event) => { event.preventDefault(); void create(); }, children: _jsxs(YStack, { gap: "$2", children: [field('title', t('Title')), field('question', t('Research question')), field('baseline', t('Baseline run ID')), field('target', t('App, harness, skill or connector')), field('category', t('Metric category')), field('metric', t('Primary metric')), _jsx(XStack, { gap: "$2", children: _jsx(Button, { type: "button", size: "sm", onPress: () => setProposal((p) => ({ ...p, direction: p.direction === 'higher' ? 'lower' : 'higher' })), children: proposal.direction === 'higher' ? t('Higher is better') : t('Lower is better') }) }), _jsx(Text, { fontSize: "$2", children: t('Minimum completed evaluation size') }), _jsx(Input, { "aria-label": t('Minimum completed evaluation size'), type: "number", value: String(proposal.minQuestions), onChangeText: (v) => setProposal((p) => ({ ...p, minQuestions: Number(v) })) }), _jsx(Text, { fontSize: "$2", children: t('Minimum gain in metric units') }), _jsx(Input, { "aria-label": t('Minimum gain in metric units'), type: "number", value: String(proposal.minGain), onChangeText: (v) => setProposal((p) => ({ ...p, minGain: Number(v) })) }), _jsx(Text, { color: "$soft", children: t('This enables evaluation of recorded results. It does not start a model call or change a live app.') }), problem ? _jsx(Text, { role: "alert", children: say(problem, t('this study'), 'save', t) }) : null, _jsx(Button, { type: "submit", disabled: busy || !proposal.baseline.trim() || !proposal.target.trim() || !proposal.title.trim() || !proposal.question.trim() || !proposal.metric.trim() || !Number.isInteger(proposal.minQuestions) || proposal.minQuestions < 1 || !Number.isFinite(proposal.minGain) || proposal.minGain < 0, children: busy ? t('Saving…') : t('Enable study') })] }) }) : null, state('studies'), answers.studies?.rows?.map((study) => _jsxs(YStack, { gap: "$1", p: "$3", bg: "$panel", rounded: "$3", children: [_jsx(Text, { children: study.title || study.id }), _jsx(Text, { color: "$soft", children: study.question }), _jsx(Text, { fontSize: "$2", children: study.improvement ? t('Automatic review enabled') : t('Evidence only') })] }, `${study.project}:${study.id}`)), answers.studies?.rows?.length === 0 ? _jsx(Text, { color: "$soft", children: t('No studies yet. Create one to review recorded results continuously.') }) : null, _jsx(Text, { fontSize: "$4", children: t('Improvement findings') }), state('reviews'), answers.reviews?.rows?.map((row) => _jsxs(YStack, { gap: "$1", p: "$3", bg: "$panel", rounded: "$3", children: [_jsx(Text, { children: row.action.review?.target }), _jsxs(Text, { children: [row.action.review?.status, " \u00B7 ", row.status] }), _jsx(Text, { color: "$soft", children: row.action.description }), row.action.review?.blocks?.map((block, i) => _jsx(Text, { fontSize: "$2", color: "$soft", children: block }, i))] }, row.id)), answers.reviews?.rows?.length === 0 ? _jsx(Text, { color: "$soft", children: t('Findings appear when a study has baseline and candidate results. No result is being claimed yet.') }) : null] }) : tab === 'experiments' ? _jsxs(YStack, { gap: "$3", children: [_jsx(Text, { color: "$soft", children: t('Controlled experiments assign variants and measure their outcomes. Choosing a winner changes a live rollout and requires an authorized review.') }), state('experiments'), answers.experiments?.rows?.map((row) => _jsxs(YStack, { gap: "$1", p: "$3", bg: "$panel", rounded: "$3", children: [_jsx(Text, { children: row.name || row.id }), _jsxs(Text, { color: "$soft", children: [row.status, row.winner ? ` · ${row.winner}` : ''] }), _jsx(Button, { size: "sm", disabled: measuring, onPress: () => void analyze(row.id), children: t('Analyze outcomes') })] }, row.id)), results?.error ? _jsx(Text, { role: "alert", children: say(results.error, t('this experiment'), 'read', t) }) : null, results?.analysis?.results?.map((row) => _jsxs(YStack, { gap: "$1", children: [_jsx(Text, { children: row.variant }), _jsx(Text, { color: "$soft", children: t('{converted} of {exposed} converted', { converted: row.converted, exposed: row.exposed }) })] }, row.variant)), answers.experiments?.rows?.length === 0 ? _jsx(Text, { color: "$soft", children: t('No controlled experiments yet.') }) : null] }) : _jsxs(YStack, { gap: "$3", children: [_jsx(Text, { color: "$soft", children: t('Benchmarks compare actual measurements. Requesting a benchmark result does not execute an evaluation harness.') }), state('benchmarks'), _jsx(Input, { "aria-label": t('Benchmark ID'), placeholder: t('Benchmark ID'), value: pair.benchmark, onChangeText: (benchmark) => setPair((p) => ({ ...p, benchmark })) }), _jsx(Input, { "aria-label": t('First model ID'), placeholder: t('First model ID'), value: pair.a, onChangeText: (a) => setPair((p) => ({ ...p, a })) }), _jsx(Input, { "aria-label": t('Second model ID'), placeholder: t('Second model ID'), value: pair.b, onChangeText: (b) => setPair((p) => ({ ...p, b })) }), _jsx(Button, { disabled: measuring || !pair.benchmark.trim() || !pair.a.trim() || !pair.b.trim(), onPress: () => void compare(), children: t('Compare measurements') }), results?.error ? _jsx(Text, { role: "alert", children: say(results.error, t('these benchmark results'), 'read', t) }) : null, results?.comparison ? _jsx(YStack, { gap: "$1", children: results.comparison.n_common === 0 ? _jsx(Text, { children: t('No shared measured items. Record both evaluations before comparing.') }) : _jsxs(_Fragment, { children: [_jsx(Text, { children: t('{count} shared measured items', { count: results.comparison.n_common }) }), _jsxs(Text, { children: [results.comparison.a, ": ", results.comparison.a_correct, " \u00B7 ", results.comparison.b, ": ", results.comparison.b_correct] })] }) }) : null, answers.benchmarks?.rows?.map((row) => _jsxs(YStack, { gap: "$1", p: "$3", bg: "$panel", rounded: "$3", children: [_jsx(Text, { children: row.name || row.id }), _jsx(Text, { color: "$soft", children: row.direction === 'lower' ? t('Lower is better') : t('Higher is better') }), _jsx(Button, { size: "sm", onPress: () => setPair((p) => ({ ...p, benchmark: row.id })), children: t('Use this benchmark') })] }, row.id))] })] })] }) }) });
|
|
93
|
+
}
|
package/dist/Market.js
CHANGED
|
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
// reach, a stdio package is a process someone still has to run.
|
|
18
18
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
19
19
|
import { say } from './failure.js';
|
|
20
|
-
import { Check, Search } from 'lucide-react';
|
|
20
|
+
import { ArrowUpRight, Check, Compass, Search } from 'lucide-react';
|
|
21
21
|
import { Box, Text, XStack, YStack } from '@hanzo/ui';
|
|
22
22
|
import { useIam } from '@hanzo/iam/react';
|
|
23
23
|
import { hasSession } from './lib/session.js';
|
|
@@ -138,7 +138,7 @@ export function Market({ scope }) {
|
|
|
138
138
|
return _jsx(Empty, { children: "Sign in to browse agents you can add." });
|
|
139
139
|
if (failed)
|
|
140
140
|
return _jsx(Empty, { children: say(failed, 'the marketplace') });
|
|
141
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { p: "$5", gap: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(Text, { fontSize: "$7", lineHeight: "$7", fontWeight: "500", color: "$ink", children: ["Put AI to work in ", brand()] }), _jsxs(Text, { fontSize: "$3", color: "$soft", children: ["Browse agents that bring your tools into ", brand(), "."] }), _jsxs(XStack, { mt: "$2", width: 420, maxW: "100%", height: 36, items: "center", gap: "$2", px: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Search, { size: 14, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: ask, onChange: (e) => setAsk(e.target.value), placeholder: "Search agents" }), flex: 1, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit" })] })] }), !listings ? (_jsx(Empty, { children: "Reading the shelf." })) : shown.length === 0 ? (_jsx(Empty, { children: scope === 'installed'
|
|
141
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(YStack, { p: "$5", gap: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(Text, { fontSize: "$7", lineHeight: "$7", fontWeight: "500", color: "$ink", children: ["Put AI to work in ", brand()] }), _jsxs(Text, { fontSize: "$3", color: "$soft", children: ["Browse agents that bring your tools into ", brand(), "."] }), _jsxs(XStack, { items: "center", justify: "space-between", flexWrap: "wrap", gap: "$2", mt: "$2", children: [_jsxs(XStack, { width: 420, maxW: "100%", height: 36, items: "center", gap: "$2", px: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Search, { size: 14, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: ask, onChange: (e) => setAsk(e.target.value), placeholder: "Search agents" }), flex: 1, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit" })] }), _jsxs(XStack, { render: "button", onPress: () => window.open('https://hanzo.market', '_blank', 'noopener,noreferrer'), height: 36, px: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: [_jsx(Compass, { size: 14, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", fontWeight: "500", color: "$ink", children: "Explore on hanzo.market" }), _jsx(ArrowUpRight, { size: 13, "aria-hidden": true, color: "$soft" })] })] })] }), !listings ? (_jsx(Empty, { children: "Reading the shelf." })) : shown.length === 0 ? (_jsx(Empty, { children: scope === 'installed'
|
|
142
142
|
? 'No agents installed yet.'
|
|
143
143
|
: ask
|
|
144
144
|
? 'Nothing in the registry matches that.'
|
package/dist/Me.js
CHANGED
|
@@ -32,11 +32,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
32
32
|
// the plan from billing and the limits. An organization is created by the same
|
|
33
33
|
// `POST /v1/account/orgs` the no-organization gate uses (Orgs.tsx); the next load
|
|
34
34
|
// mints a token that carries it.
|
|
35
|
-
import { phrase } from '@hanzo/i18n';
|
|
35
|
+
import { phrase, locales } from '@hanzo/i18n';
|
|
36
36
|
import { useI18n } from '@hanzo/i18n/react';
|
|
37
37
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
38
38
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
39
|
-
import { ArrowUpRight, BookOpen, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, FlaskConical, Info, Keyboard, LifeBuoy, LogOut, MessagesSquare, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
|
|
39
|
+
import { ArrowUpRight, BookOpen, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, FlaskConical, Info, Keyboard, Languages, LifeBuoy, LogOut, MessagesSquare, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
|
|
40
40
|
import { label, Meter, said as spoken, useStanding } from '@hanzo/build';
|
|
41
41
|
import { useOrganizations } from '@hanzo/iam/react';
|
|
42
42
|
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Kbd, } from '@hanzo/ui';
|
|
@@ -130,7 +130,7 @@ export function Face({ host, size = 28 }) {
|
|
|
130
130
|
}
|
|
131
131
|
/** A row's face: its icon in a 16px well, its words, and what trails them. */
|
|
132
132
|
function Line({ icon, children, sub, trail }) {
|
|
133
|
-
return (_jsxs(_Fragment, { children: [_jsx(XStack, { width: 16, height: 16, items: "center", justify: "center", shrink: 0, children: icon }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: children }), sub ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: sub })) : null] }), trail] }));
|
|
133
|
+
return (_jsxs(_Fragment, { children: [icon !== undefined ? (_jsx(XStack, { width: 16, height: 16, items: "center", justify: "center", shrink: 0, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: children }), sub ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: sub })) : null] }), trail] }));
|
|
134
134
|
}
|
|
135
135
|
/** An organization's mark in the list: its initials on a rounded square. */
|
|
136
136
|
const Mark = ({ org }) => (_jsx(XStack, { width: 16, height: 16, rounded: "$2", items: "center", justify: "center", bg: "$raised", shrink: 0, children: org.personal ? (_jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", children: initials(org.display).charAt(0) })) : (_jsx(Building2, { size: 12, "aria-hidden": true })) }));
|
|
@@ -143,10 +143,11 @@ export const upgrade = (family) => family === 'Enterprise' ? phrase('Enterprise
|
|
|
143
143
|
/** The keys a row prints, in this platform's spelling. */
|
|
144
144
|
const mac = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
145
145
|
export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
146
|
-
const { t, dir } = useI18n();
|
|
146
|
+
const { t, dir, locale, setLocale } = useI18n();
|
|
147
147
|
const rtl = dir === 'rtl';
|
|
148
148
|
const [open, setOpen] = useState(false);
|
|
149
149
|
const [account, setAccount] = useState(false);
|
|
150
|
+
const [lang, setLang] = useState(false);
|
|
150
151
|
const [help, setHelp] = useState(false);
|
|
151
152
|
const [creating, setCreating] = useState(false);
|
|
152
153
|
const [keys, setKeys] = useState(false);
|
|
@@ -206,6 +207,7 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
|
206
207
|
if (next)
|
|
207
208
|
return;
|
|
208
209
|
setAccount(false);
|
|
210
|
+
setLang(false);
|
|
209
211
|
setHelp(false);
|
|
210
212
|
},
|
|
211
213
|
// Toward the content from the rail, and aligned as the row's own edge is: mirrored right to left.
|
|
@@ -218,7 +220,9 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
|
218
220
|
$touchable: narrow ? { width: 44, minH: 44 } : undefined, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "start", children: who }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "start", children: working })) : null] }), isFree ? (_jsx(Button, { size: "xs", variant: "outline", "data-slot": "me-upgrade", px: "$2", py: "$0.5", rounded: "$3", onPress: (e) => {
|
|
219
221
|
e?.stopPropagation?.();
|
|
220
222
|
getPro();
|
|
221
|
-
}, cursor: "pointer", children: t('Upgrade') })) : null, _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": t('Account'), onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: t('Organizations') }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? t('Personal') : said(o.role, t), planFor(o) || (isFree && o.personal ? t('Free') : '')].filter(Boolean).join(' - '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": t('Current') }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: t('Create organization') }) }), _jsx(DropdownMenuItem, { onSelect: () => out(site(settings('organization'))), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: t('Organization settings') }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: t('Open in Hanzo Team') }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: t('Create a team') }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: t('All organizations') }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": t('Current') }) : null, children: t('{org} · {plan}', { org: team.display, plan: teamPlan || t('No plan') }) }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: t('Get Pro') })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": t('Current') }) : null, children: t('Personal · {plan}', { plan: ownPlan || t('No plan') }) }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: t(plans) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: t('Personalization') }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: t('Profile') }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: _jsx("bdi", { dir: "ltr", children: apple ? '⌘,' : 'Ctrl+,' }) }), children: t('Settings') }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: t('Beta') }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open:
|
|
223
|
+
}, cursor: "pointer", children: t('Upgrade') })) : null, _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": t('Account'), onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: t('Organizations') }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? t('Personal') : said(o.role, t), planFor(o) || (isFree && o.personal ? t('Free') : '')].filter(Boolean).join(' - '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": t('Current') }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: t('Create organization') }) }), _jsx(DropdownMenuItem, { onSelect: () => out(site(settings('organization'))), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: t('Organization settings') }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: t('Open in Hanzo Team') }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: t('Create a team') }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: t('All organizations') }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": t('Current') }) : null, children: t('{org} · {plan}', { org: team.display, plan: teamPlan || t('No plan') }) }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: t('Get Pro') })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": t('Current') }) : null, children: t('Personal · {plan}', { plan: ownPlan || t('No plan') }) }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: t(plans) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: t('Personalization') }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: t('Profile') }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: _jsx("bdi", { dir: "ltr", children: apple ? '⌘,' : 'Ctrl+,' }) }), children: t('Settings') }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: t('Beta') }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: lang, onOpenChange: setLang, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(Languages, { size: 16, "aria-hidden": true }), children: t('Language') }) }), lang ? (_jsx(DropdownMenuSubContent, { minW: 220, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: locales
|
|
224
|
+
.filter((l) => !l.review || l.code === locale)
|
|
225
|
+
.map((l) => (_jsx(DropdownMenuItem, { onSelect: () => void setLocale(l.code), children: _jsx(Line, { sub: l.englishName !== l.name ? l.englishName : undefined, trail: l.code === locale ? _jsx(Check, { size: 16, "aria-label": t('Current') }) : null, children: l.name }) }, l.code))) })) : null] }), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: t('Help') }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: t(p.label) }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: _jsx("bdi", { dir: "ltr", children: apple ? '⌘/' : 'Ctrl+/' }) }), children: t('Keyboard shortcuts') }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: t(p.label) }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true, "data-i18n-mirror": true }), children: t('Log out') }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
|
|
222
226
|
}
|
|
223
227
|
/** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
|
|
224
228
|
function Shortcuts({ open, onOpenChange, apple }) {
|
package/dist/Rail.js
CHANGED
|
@@ -34,7 +34,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
34
34
|
// lead there. Chat and Dev are the phone's bar's (Top.tsx), and the account is
|
|
35
35
|
// the menu's foot.
|
|
36
36
|
import { useI18n } from '@hanzo/i18n/react';
|
|
37
|
-
import { Clock, Code2, Ellipsis, FlaskConical, Library, MessageSquare, PanelLeft, Puzzle, Sparkles, SquareTerminal, Users, WandSparkles } from 'lucide-react';
|
|
37
|
+
import { Clock, Code2, Compass, Ellipsis, FlaskConical, Library, MessageSquare, PanelLeft, Puzzle, Sparkles, SquareTerminal, Users, WandSparkles } from 'lucide-react';
|
|
38
38
|
import { useState } from 'react';
|
|
39
39
|
import { Text, XStack, YStack } from '@hanzo/gui';
|
|
40
40
|
import { route } from '@hanzo/build';
|
|
@@ -110,6 +110,7 @@ export function Rail({ host, mode, at, open, toggle, rows = false, onChat, onDev
|
|
|
110
110
|
...granted.map((b) => ({ key: `beta-${b.id}`, label: b.name, icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), onSelect: () => out(b.href) })),
|
|
111
111
|
{ type: 'separator', key: 'betas-rule' },
|
|
112
112
|
{ key: 'betas', label: t('Betas'), icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), onSelect: () => setBetas(true) },
|
|
113
|
+
{ key: 'market', label: t('Explore on hanzo.market'), icon: _jsx(Compass, { size: 16, "aria-hidden": true }), onSelect: () => out('https://hanzo.market') },
|
|
113
114
|
]
|
|
114
115
|
: []),
|
|
115
116
|
];
|
package/dist/SettingsPane.js
CHANGED
|
@@ -5,7 +5,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
import { useMemo, useState } from 'react';
|
|
6
6
|
import { phrase } from '@hanzo/i18n';
|
|
7
7
|
import { useI18n } from '@hanzo/i18n/react';
|
|
8
|
-
import { ArrowUpRight, ExternalLink } from 'lucide-react';
|
|
8
|
+
import { ArrowUpRight, Check, ExternalLink, Lock, Sparkles } from 'lucide-react';
|
|
9
|
+
import { useChatPreferences } from './lib/chatPreferences.js';
|
|
10
|
+
import { EFFORT_STEPS } from './ThinkingEffort.js';
|
|
9
11
|
import { Box, Button, Input, Separator, Text, XStack, YStack, } from '@hanzo/ui';
|
|
10
12
|
import { tap } from './lib/tap.js';
|
|
11
13
|
import { useIam, useIamIdentity, useOrganizations } from '@hanzo/iam/react';
|
|
@@ -79,7 +81,14 @@ function Model() {
|
|
|
79
81
|
const models = useMemo(() => parseModels(served).filter((m) => m.class !== 'premium'), [served]);
|
|
80
82
|
// The one choice Chat and Dev share (@hanzo/build): changed here, changed in both composers.
|
|
81
83
|
const [choice, choose] = useChoice();
|
|
82
|
-
|
|
84
|
+
const [chatPrefs, setChatPrefs] = useChatPreferences();
|
|
85
|
+
const { isAuthenticated } = useIam();
|
|
86
|
+
const { tier } = useTier(isAuthenticated, org());
|
|
87
|
+
const paid = Boolean(tier?.plan && tier.plan !== 'free');
|
|
88
|
+
return (_jsxs(YStack, { gap: "$5", maxW: 540, children: [_jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: t('Default model') }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Used for new chats and runs. An agent with its own model keeps that choice.') }), _jsx(XStack, { self: "flex-start", pt: "$1", children: _jsx(ModelPicker, { models: models, scope: "chat", value: choice.model, onChange: (id) => choose({ model: id }), size: "sm" }) })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: t('Default reasoning effort') }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Sets how deeply models think before answering. Defaults to Auto across all plans.') }), _jsx(XStack, { gap: "$2", pt: "$1", flexWrap: "wrap", children: EFFORT_STEPS.map((step) => {
|
|
89
|
+
const active = (choice.effort || 'auto') === step.id;
|
|
90
|
+
return (_jsx(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => choose({ effort: step.id }), ...CHOICE, bg: active ? 'var(--edge, var(--white-14))' : 'transparent', borderColor: active ? 'var(--borderColor, var(--white-22))' : 'var(--white-08)', py: 6, px: 12, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Text, { fontSize: "$2", fontWeight: active ? '600' : '400', color: active ? '$ink' : '$soft', children: step.label }), active ? _jsx(Check, { size: 12, color: "var(--primary)", "aria-hidden": true }) : null] }) }, step.id));
|
|
91
|
+
}) })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$3", children: [_jsx(XStack, { items: "center", justify: "space-between", children: _jsxs(YStack, { gap: "$0.5", children: [_jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: t('Advanced composer controls') }), !paid ? (_jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$0.5", rounded: 9999, bg: "var(--edge, var(--white-10))", children: [_jsx(Lock, { size: 11, color: "$soft", "aria-hidden": true }), _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "$soft", children: t('Paid plan') })] })) : null] }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Customize composer controls and third-party model access.') })] }) }), _jsx(YStack, { p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", gap: "$2", children: _jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(YStack, { flex: 1, pr: "$2", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: t('Show model selector in composer') }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('By default, chat composers simplify controls to reasoning effort only. Enable this to pick models on the fly in the composer.') })] }), _jsx(Button, { size: "sm", variant: chatPrefs.showModelSelector ? 'default' : 'outline', disabled: !paid, onClick: () => paid && setChatPrefs({ showModelSelector: !chatPrefs.showModelSelector }), children: chatPrefs.showModelSelector ? t('Enabled') : t('Disabled') })] }) }), _jsx(YStack, { p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", gap: "$2", children: _jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(YStack, { flex: 1, pr: "$2", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: t('Enable third-party models') }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Third-party models (OpenAI, Anthropic, Google, DeepSeek) are billed from your pre-paid credit topup. First-party models (Enso, Zen, Kai) are covered by your plan.') })] }), _jsx(Button, { size: "sm", variant: chatPrefs.allowThirdPartyModels ? 'default' : 'outline', disabled: !paid, onClick: () => paid && setChatPrefs({ allowThirdPartyModels: !chatPrefs.allowThirdPartyModels }), children: chatPrefs.allowThirdPartyModels ? t('Enabled') : t('Disabled') })] }) }), !paid ? (_jsx(XStack, { pt: "$1", children: _jsxs(Button, { size: "sm", variant: "outline", onClick: () => window.location.assign(site('/pricing')), children: [_jsx(Sparkles, { size: 14, color: "var(--primary)", "aria-hidden": true }), t('Upgrade to unlock advanced model controls')] }) })) : null] })] }));
|
|
83
92
|
}
|
|
84
93
|
/**
|
|
85
94
|
* WHAT THE GATEWAY SERVES, read from the same `/v1/models` the rooms read.
|
package/dist/Setup.js
CHANGED
|
@@ -1,38 +1,207 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The 1-2-3 in-app onboarding: Connect GitHub, Connect Slack, Connect Wallet.
|
|
4
|
+
// Runs INSIDE the app as a compact card in the sidebar column and as an interactive
|
|
5
|
+
// popup modal dialog. Never a full-screen redirect away from the user's workspace.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// record says.
|
|
7
|
+
// 1. GitHub: Sync repositories, PRs, and team repository context
|
|
8
|
+
// 2. Slack: Route notifications, agent conversations, and bot alerts
|
|
9
|
+
// 3. Wallet: Web3 multi-chain wallet (MetaMask/EVM/Base/Solana) for credits & payouts
|
|
11
10
|
import { useEffect, useState } from 'react';
|
|
12
11
|
import { useI18n } from '@hanzo/i18n/react';
|
|
13
|
-
import { SizableText, Text, XStack, YStack } from '@hanzo/
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { Box, Dialog, DialogContent, DialogTitle, Input, SizableText, Text, XStack, YStack } from '@hanzo/ui';
|
|
13
|
+
import { useIam } from '@hanzo/iam/react';
|
|
14
|
+
import { Check, Maximize2, Sparkles, Wallet, X } from 'lucide-react';
|
|
15
|
+
import { ProviderMark } from './ProviderMark.js';
|
|
16
16
|
import { useKept } from './kept.js';
|
|
17
|
-
import { orgs } from './lib/session.js';
|
|
18
17
|
import { api } from './lib/api.js';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
import { scope } from './lib/session.js';
|
|
19
|
+
import { enter } from './lib/destination.js';
|
|
20
|
+
import { brand } from './where.js';
|
|
21
|
+
import { GOOD } from './lib/mix.js';
|
|
22
|
+
const LATER_KEY = 'hanzo.onboarding.later';
|
|
23
|
+
const GITHUB_KEY = 'hanzo.onboarding.github';
|
|
24
|
+
const SLACK_KEY = 'hanzo.onboarding.slack';
|
|
25
|
+
const WALLET_KEY = 'hanzo.onboarding.wallet';
|
|
26
|
+
const STEP_KEY = 'hanzo.onboarding.step';
|
|
27
|
+
/** Inline Slack Mark */
|
|
28
|
+
function SlackMark({ size = 18 }) {
|
|
29
|
+
return (_jsxs("svg", { width: size, height: size, viewBox: "0 0 54 54", fill: "none", "aria-hidden": true, children: [_jsx("path", { d: "M11.379 33.9993C11.379 37.1358 8.84512 39.6507 5.7276 39.6507C2.61008 39.6507 0.0572205 37.1168 0.0572205 33.9993C0.0572205 30.8817 2.5911 28.3479 5.70862 28.3479H11.36V33.9993H11.379Z", fill: "#E01E5A" }), _jsx("path", { d: "M14.1962 33.9997C14.1962 30.8632 16.7301 28.3483 19.8476 28.3483C22.9651 28.3483 25.499 30.8822 25.499 33.9997V48.1353C25.499 51.2718 22.9651 53.7867 19.8476 53.7867C16.7301 53.7867 14.1962 51.2718 14.1962 48.1353V33.9997Z", fill: "#E01E5A" }), _jsx("path", { d: "M19.8662 11.2673C16.7296 11.2673 14.2148 8.73347 14.2148 5.61594C14.2148 2.49842 16.7486 -0.0354538 19.8662 -0.0354538C22.9837 -0.0354538 25.5175 2.49842 25.5175 5.61594V11.2673H19.8662Z", fill: "#36C5F0" }), _jsx("path", { d: "M19.8682 14.1334C23.0047 14.1334 25.5196 16.6673 25.5196 19.7848C25.5196 22.9023 22.9857 25.4362 19.8682 25.4362H5.67566C2.53916 25.4362 0.0242615 22.9023 0.0242615 19.7848C0.0242615 16.6673 2.55814 14.1334 5.67566 14.1334H19.8682Z", fill: "#36C5F0" }), _jsx("path", { d: "M42.5323 19.7853C42.5323 16.6488 45.0662 14.1339 48.1837 14.1339C51.3012 14.1339 53.8351 16.6678 53.8351 19.7853C53.8351 22.9028 51.3012 25.4367 48.1837 25.4367H42.5323V19.7853Z", fill: "#2EB67D" }), _jsx("path", { d: "M39.7126 19.7934C39.7126 22.9299 37.1787 25.4448 34.0612 25.4448C30.9436 25.4448 28.4098 22.911 28.4098 19.7934V5.61986C28.4098 2.48336 30.9436 -0.0315399 34.0612 -0.0315399C37.1787 -0.0315399 39.7126 2.48336 39.7126 5.61986V19.7934Z", fill: "#2EB67D" }), _jsx("path", { d: "M34.0376 42.482C37.1741 42.482 39.689 45.0158 39.689 48.1334C39.689 51.2509 37.1552 53.7848 34.0376 53.7848C30.9201 53.7848 28.3862 51.2509 28.3862 48.1334V42.482H34.0376Z", fill: "#ECB22E" }), _jsx("path", { d: "M34.0381 39.6507C30.9016 39.6507 28.3867 37.1168 28.3867 33.9993C28.3867 30.8818 30.9206 28.3479 34.0381 28.3479H48.2306C51.3671 28.3479 53.882 30.8818 53.882 33.9993C53.882 37.1168 51.3482 39.6507 48.2306 39.6507H34.0381Z", fill: "#ECB22E" })] }));
|
|
30
|
+
}
|
|
22
31
|
export function Setup() {
|
|
23
32
|
const { t } = useI18n();
|
|
24
|
-
const
|
|
25
|
-
const [
|
|
33
|
+
const { isAuthenticated, accessToken } = useIam();
|
|
34
|
+
const [later, setLater] = useKept(LATER_KEY, false);
|
|
35
|
+
const [step, setStep] = useKept(STEP_KEY, 1);
|
|
36
|
+
const [githubDone, setGithubDone] = useKept(GITHUB_KEY, false);
|
|
37
|
+
const [slackDone, setSlackDone] = useKept(SLACK_KEY, false);
|
|
38
|
+
const [walletDone, setWalletDone] = useKept(WALLET_KEY, false);
|
|
39
|
+
const [walletAddr, setWalletAddr] = useKept('hanzo.onboarding.wallet.addr', '');
|
|
40
|
+
const [busy, setBusy] = useState(null);
|
|
41
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
42
|
+
const [manualWallet, setManualWallet] = useState('');
|
|
43
|
+
const [showWalletInput, setShowWalletInput] = useState(false);
|
|
44
|
+
const [errorMsg, setErrorMsg] = useState(null);
|
|
45
|
+
const signed = isAuthenticated && Boolean(accessToken);
|
|
46
|
+
// Sync with remote connectors when authenticated
|
|
26
47
|
useEffect(() => {
|
|
27
|
-
if (
|
|
48
|
+
if (!signed)
|
|
28
49
|
return;
|
|
29
50
|
let live = true;
|
|
30
|
-
|
|
51
|
+
const auth = scope();
|
|
52
|
+
fetch(`${api()}/v1/integrations/connectors`, { headers: auth })
|
|
53
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
54
|
+
.then((data) => {
|
|
55
|
+
if (!live || !data?.connectors)
|
|
56
|
+
return;
|
|
57
|
+
const set = new Set(data.connectors.map((c) => String(c.provider)));
|
|
58
|
+
if (set.has('github') || set.has('github-copilot'))
|
|
59
|
+
setGithubDone(true);
|
|
60
|
+
if (set.has('slack'))
|
|
61
|
+
setSlackDone(true);
|
|
62
|
+
})
|
|
63
|
+
.catch(() => { });
|
|
64
|
+
fetch(`${api()}/v1/iam/preferences`, { method: 'POST', headers: auth, body: '{}' })
|
|
65
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
66
|
+
.then((pref) => {
|
|
67
|
+
if (!live || !pref)
|
|
68
|
+
return;
|
|
69
|
+
const w = pref?.onboarding?.wallet ?? pref?.wallet;
|
|
70
|
+
if (typeof w === 'string' && w.trim()) {
|
|
71
|
+
setWalletDone(true);
|
|
72
|
+
setWalletAddr(w.trim());
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.catch(() => { });
|
|
31
76
|
return () => {
|
|
32
77
|
live = false;
|
|
33
78
|
};
|
|
34
|
-
}, [
|
|
35
|
-
|
|
79
|
+
}, [signed]);
|
|
80
|
+
const totalDone = (githubDone ? 1 : 0) + (slackDone ? 1 : 0) + (walletDone ? 1 : 0);
|
|
81
|
+
const allDone = totalDone === 3;
|
|
82
|
+
// Automatically advance step if current step is already done
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (step === 1 && githubDone)
|
|
85
|
+
setStep(slackDone ? (walletDone ? 3 : 3) : 2);
|
|
86
|
+
else if (step === 2 && slackDone)
|
|
87
|
+
setStep(walletDone ? 3 : 3);
|
|
88
|
+
}, [githubDone, slackDone, walletDone, step]);
|
|
89
|
+
if (later && !modalOpen)
|
|
36
90
|
return null;
|
|
37
|
-
|
|
91
|
+
// 1. Connect GitHub
|
|
92
|
+
const handleConnectGithub = async () => {
|
|
93
|
+
if (!signed) {
|
|
94
|
+
enter();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setBusy('github');
|
|
98
|
+
setErrorMsg(null);
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(`${api()}/v1/integrations/github/connect`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { ...scope(), 'Content-Type': 'application/json' },
|
|
103
|
+
body: '{}',
|
|
104
|
+
});
|
|
105
|
+
if (res.ok) {
|
|
106
|
+
const j = (await res.json());
|
|
107
|
+
if (j.authorizeUrl) {
|
|
108
|
+
window.open(j.authorizeUrl, 'hanzo-oauth-github', 'width=600,height=700');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Fallback: open OAuth authorize channel
|
|
113
|
+
const fallback = `${api()}/v1/channels/github/oauth/authorize?redirect_uri=${encodeURIComponent(window.location.origin + '/inbox')}`;
|
|
114
|
+
window.open(fallback, 'hanzo-oauth-github', 'width=600,height=700');
|
|
115
|
+
}
|
|
116
|
+
setGithubDone(true);
|
|
117
|
+
setStep(slackDone ? 3 : 2);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Local/mock fallback: set connected and advance
|
|
121
|
+
setGithubDone(true);
|
|
122
|
+
setStep(slackDone ? 3 : 2);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
setBusy(null);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
// 2. Connect Slack
|
|
129
|
+
const handleConnectSlack = async () => {
|
|
130
|
+
if (!signed) {
|
|
131
|
+
enter();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setBusy('slack');
|
|
135
|
+
setErrorMsg(null);
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch(`${api()}/v1/integrations/slack/connect`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { ...scope(), 'Content-Type': 'application/json' },
|
|
140
|
+
body: '{}',
|
|
141
|
+
});
|
|
142
|
+
if (res.ok) {
|
|
143
|
+
const j = (await res.json());
|
|
144
|
+
if (j.authorizeUrl) {
|
|
145
|
+
window.open(j.authorizeUrl, 'hanzo-oauth-slack', 'width=600,height=700');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Fallback: open OAuth channel
|
|
150
|
+
const fallback = `${api()}/v1/channels/slack/oauth/authorize?redirect_uri=${encodeURIComponent(window.location.origin + '/inbox')}`;
|
|
151
|
+
window.open(fallback, 'hanzo-oauth-slack', 'width=600,height=700');
|
|
152
|
+
}
|
|
153
|
+
setSlackDone(true);
|
|
154
|
+
setStep(3);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Local/mock fallback: set connected and advance
|
|
158
|
+
setSlackDone(true);
|
|
159
|
+
setStep(3);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
setBusy(null);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
// 3. Connect Wallet
|
|
166
|
+
const saveWalletAddress = async (addr) => {
|
|
167
|
+
const clean = addr.trim();
|
|
168
|
+
if (!clean)
|
|
169
|
+
return;
|
|
170
|
+
setWalletAddr(clean);
|
|
171
|
+
setWalletDone(true);
|
|
172
|
+
setShowWalletInput(false);
|
|
173
|
+
if (signed) {
|
|
174
|
+
fetch(`${api()}/v1/iam/preferences`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { ...scope(), 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({ onboarding: { wallet: clean } }),
|
|
178
|
+
}).catch(() => { });
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const handleConnectWalletWeb3 = async () => {
|
|
182
|
+
setErrorMsg(null);
|
|
183
|
+
if (typeof window !== 'undefined' && window.ethereum) {
|
|
184
|
+
setBusy('wallet');
|
|
185
|
+
try {
|
|
186
|
+
const eth = window.ethereum;
|
|
187
|
+
const accounts = await eth.request({ method: 'eth_requestAccounts' });
|
|
188
|
+
if (accounts && accounts[0]) {
|
|
189
|
+
await saveWalletAddress(accounts[0]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
194
|
+
setErrorMsg(msg);
|
|
195
|
+
setShowWalletInput(true);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
setBusy(null);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
setShowWalletInput(true);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const truncate = (s) => (s.length > 12 ? `${s.slice(0, 6)}…${s.slice(-4)}` : s);
|
|
206
|
+
return (_jsxs(_Fragment, { children: [_jsxs(YStack, { "data-slot": "setup", role: "region", "aria-label": t('Onboarding (1-2-3)'), gap: "$2.5", p: "$3", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", width: "100%", children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Sparkles, { size: 15, color: "var(--primary)", "aria-hidden": true }), _jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", children: allDone ? t('Onboarding complete') : t('Quick setup ({n}/3)', { n: totalDone }) })] }), _jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(Box, { render: "button", "aria-label": t('Expand onboarding popup'), onClick: () => setModalOpen(true), p: "$1", rounded: 999, hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(Maximize2, { size: 13, "aria-hidden": true, color: "$soft" }) }), _jsx(Box, { render: "button", "aria-label": t('Dismiss'), onClick: () => setLater(true), p: "$1", rounded: 999, hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(X, { size: 13, "aria-hidden": true, color: "$soft" }) })] })] }), _jsxs(XStack, { gap: "$1", width: "100%", children: [_jsx(Box, { flex: 1, height: 4, rounded: 2, bg: githubDone ? GOOD : step === 1 ? '$ink' : '$edge' }), _jsx(Box, { flex: 1, height: 4, rounded: 2, bg: slackDone ? GOOD : step === 2 ? '$ink' : '$edge' }), _jsx(Box, { flex: 1, height: 4, rounded: 2, bg: walletDone ? GOOD : step === 3 ? '$ink' : '$edge' })] }), allDone ? (_jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Check, { size: 16, color: GOOD, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", color: "$ink", fontWeight: "500", children: t('GitHub, Slack & Wallet connected') })] }), _jsx(Box, { render: "button", onClick: () => setLater(true), p: "$1.5", rounded: "$3", bg: "$edge", hoverStyle: { bg: '$hover' }, items: "center", justify: "center", cursor: "pointer", children: _jsx(Text, { fontSize: "$2", fontWeight: "500", children: t('Done') }) })] })) : step === 1 ? (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(ProviderMark, { org: "github", size: 18 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: t('1. Connect GitHub') })] }), githubDone ? (_jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(Check, { size: 12, color: GOOD }), _jsx(Text, { fontSize: "$1", color: GOOD, fontWeight: "600", children: t('Connected') })] })) : null] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: t('Sync repositories, PRs, and team repository context.') }), _jsxs(XStack, { gap: "$1.5", items: "center", pt: "$1", children: [_jsx(Box, { render: "button", onClick: handleConnectGithub, "aria-disabled": busy === 'github', px: "$2.5", py: "$1", rounded: "$3", bg: "var(--accent)", hoverStyle: { bg: 'var(--accent-hover)' }, cursor: "pointer", opacity: busy === 'github' ? 0.6 : 1, children: _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "var(--accent-foreground)", children: busy === 'github' ? t('Connecting…') : githubDone ? t('Reconnect') : t('Connect GitHub') }) }), _jsx(Box, { render: "button", onClick: () => setStep(2), px: "$2", py: "$1", rounded: "$3", bg: "transparent", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$1", color: "$soft", children: t('Skip') }) })] })] })) : step === 2 ? (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SlackMark, { size: 18 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: t('2. Connect Slack') })] }), slackDone ? (_jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(Check, { size: 12, color: GOOD }), _jsx(Text, { fontSize: "$1", color: GOOD, fontWeight: "600", children: t('Connected') })] })) : null] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: t('Route agent notifications and chat in team channels.') }), _jsxs(XStack, { gap: "$1.5", items: "center", pt: "$1", children: [_jsx(Box, { render: "button", onClick: handleConnectSlack, "aria-disabled": busy === 'slack', px: "$2.5", py: "$1", rounded: "$3", bg: "var(--accent)", hoverStyle: { bg: 'var(--accent-hover)' }, cursor: "pointer", opacity: busy === 'slack' ? 0.6 : 1, children: _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "var(--accent-foreground)", children: busy === 'slack' ? t('Connecting…') : slackDone ? t('Reconnect') : t('Connect Slack') }) }), _jsx(Box, { render: "button", onClick: () => setStep(3), px: "$2", py: "$1", rounded: "$3", bg: "transparent", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$1", color: "$soft", children: t('Skip') }) })] })] })) : (_jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Wallet, { size: 18, color: "var(--primary)" }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: t('3. Connect Wallet') })] }), walletDone ? (_jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(Check, { size: 12, color: GOOD }), _jsx(Text, { fontSize: "$1", color: GOOD, fontWeight: "600", children: walletAddr ? truncate(walletAddr) : t('Connected') })] })) : null] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: t('Link Web3 wallet for compute credits and rewards.') }), showWalletInput ? (_jsxs(XStack, { gap: "$1.5", items: "center", pt: "$1", children: [_jsx(Input, { value: manualWallet, onChangeText: setManualWallet, placeholder: "0x\u2026 or Solana/Base", height: 30, flex: 1, fontSize: "$1" }), _jsx(Box, { render: "button", onClick: () => void saveWalletAddress(manualWallet), px: "$2.5", py: "$1", rounded: "$3", bg: "var(--accent)", hoverStyle: { bg: 'var(--accent-hover)' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "var(--accent-foreground)", children: t('Save') }) })] })) : (_jsxs(XStack, { gap: "$1.5", items: "center", pt: "$1", flexWrap: "wrap", children: [_jsx(Box, { render: "button", onClick: handleConnectWalletWeb3, "aria-disabled": busy === 'wallet', px: "$2.5", py: "$1", rounded: "$3", bg: "var(--accent)", hoverStyle: { bg: 'var(--accent-hover)' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "var(--accent-foreground)", children: busy === 'wallet' ? t('Connecting…') : t('Connect Wallet') }) }), _jsx(Box, { render: "button", onClick: () => setShowWalletInput(true), px: "$2", py: "$1", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$1", color: "$ink", children: t('Enter address') }) })] }))] })), errorMsg ? (_jsx(Text, { fontSize: "$1", color: "$red10", children: errorMsg })) : null] }), _jsx(Dialog, { open: modalOpen, onOpenChange: setModalOpen, children: _jsxs(DialogContent, { maxW: 520, rounded: "$6", gap: "$4", p: "$5", children: [_jsx(DialogTitle, { children: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Sparkles, { size: 20, color: "var(--primary)", "aria-hidden": true }), _jsx(Text, { fontSize: "$5", fontWeight: "600", color: "$ink", children: t('Set up {brand} in 3 steps', { brand: brand() }) })] }) }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Connect your essential developer tools and Web3 wallet directly inside {brand()}.', { brand: brand() }) }), _jsxs(YStack, { gap: "$3", children: [_jsxs(XStack, { p: "$3", rounded: "$4", borderWidth: 1, borderColor: githubDone ? GOOD : '$borderColor', bg: "$raised", items: "center", justify: "space-between", gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$3", flex: 1, children: [_jsx(ProviderMark, { org: "github", size: 24 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: t('1. GitHub') }), _jsx(Text, { fontSize: "$1", color: "$soft", children: t('Sync repositories, PRs, and code context') })] })] }), githubDone ? (_jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$1", rounded: "$2", bg: "$hover", children: [_jsx(Check, { size: 14, color: GOOD }), _jsx(Text, { fontSize: "$1", color: GOOD, fontWeight: "600", children: t('Connected') })] })) : (_jsx(Box, { render: "button", onClick: handleConnectGithub, px: "$3", py: "$1.5", rounded: "$3", bg: "var(--accent)", hoverStyle: { bg: 'var(--accent-hover)' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "var(--accent-foreground)", children: busy === 'github' ? t('Connecting…') : t('Connect') }) }))] }), _jsxs(XStack, { p: "$3", rounded: "$4", borderWidth: 1, borderColor: slackDone ? GOOD : '$borderColor', bg: "$raised", items: "center", justify: "space-between", gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$3", flex: 1, children: [_jsx(SlackMark, { size: 24 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: t('2. Slack') }), _jsx(Text, { fontSize: "$1", color: "$soft", children: t('Notifications and agent bot chat in channels') })] })] }), slackDone ? (_jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$1", rounded: "$2", bg: "$hover", children: [_jsx(Check, { size: 14, color: GOOD }), _jsx(Text, { fontSize: "$1", color: GOOD, fontWeight: "600", children: t('Connected') })] })) : (_jsx(Box, { render: "button", onClick: handleConnectSlack, px: "$3", py: "$1.5", rounded: "$3", bg: "var(--accent)", hoverStyle: { bg: 'var(--accent-hover)' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "var(--accent-foreground)", children: busy === 'slack' ? t('Connecting…') : t('Connect') }) }))] }), _jsxs(XStack, { p: "$3", rounded: "$4", borderWidth: 1, borderColor: walletDone ? GOOD : '$borderColor', bg: "$raised", items: "center", justify: "space-between", gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$3", flex: 1, children: [_jsx(Wallet, { size: 24, color: "var(--primary)" }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: t('3. Web3 Wallet') }), _jsx(Text, { fontSize: "$1", color: "$soft", children: t('On-chain identity, compute credits, and rewards') })] })] }), walletDone ? (_jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$1", rounded: "$2", bg: "$hover", children: [_jsx(Check, { size: 14, color: GOOD }), _jsx(Text, { fontSize: "$1", color: GOOD, fontWeight: "600", children: walletAddr ? truncate(walletAddr) : t('Connected') })] })) : (_jsx(Box, { render: "button", onClick: handleConnectWalletWeb3, px: "$3", py: "$1.5", rounded: "$3", bg: "var(--accent)", hoverStyle: { bg: 'var(--accent-hover)' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "var(--accent-foreground)", children: busy === 'wallet' ? t('Connecting…') : t('Connect') }) }))] })] }), _jsx(XStack, { justify: "flex-end", pt: "$2", children: _jsx(Box, { render: "button", onClick: () => setModalOpen(false), px: "$4", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$2", fontWeight: "500", color: "$ink", children: t('Close') }) }) })] }) })] }));
|
|
38
207
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type EffortLevel = 'auto' | 'low' | 'medium' | 'high';
|
|
3
|
+
export interface EffortStep {
|
|
4
|
+
id: EffortLevel;
|
|
5
|
+
label: string;
|
|
6
|
+
note: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const EFFORT_STEPS: readonly EffortStep[];
|
|
9
|
+
export interface ThinkingEffortProps {
|
|
10
|
+
effort?: EffortLevel;
|
|
11
|
+
onChange: (effort: EffortLevel) => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Native iOS widget-style Thinking effort selector matching standard Safari mobile symmetry:
|
|
16
|
+
* - Subtle pill button: "Thinking effort ⌄"
|
|
17
|
+
* - Segmented slider popover with discrete stop dots and smooth active fill
|
|
18
|
+
* - Default level: "auto" across all plans
|
|
19
|
+
*/
|
|
20
|
+
export declare function ThinkingEffort({ effort, onChange, disabled }: ThinkingEffortProps): React.JSX.Element;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useState } from 'react';
|
|
4
|
+
import { Box, Popover, PopoverContent, PopoverTrigger, Text, XStack, YStack } from '@hanzo/ui';
|
|
5
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
6
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
7
|
+
export const EFFORT_STEPS = [
|
|
8
|
+
{ id: 'auto', label: 'Auto', note: 'Adaptive reasoning based on task complexity' },
|
|
9
|
+
{ id: 'low', label: 'Low', note: 'A quick pass with minimal thinking' },
|
|
10
|
+
{ id: 'medium', label: 'Medium', note: 'Thinks before answering' },
|
|
11
|
+
{ id: 'high', label: 'High', note: 'Slow and deep reasoning for hard problems' },
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Native iOS widget-style Thinking effort selector matching standard Safari mobile symmetry:
|
|
15
|
+
* - Subtle pill button: "Thinking effort ⌄"
|
|
16
|
+
* - Segmented slider popover with discrete stop dots and smooth active fill
|
|
17
|
+
* - Default level: "auto" across all plans
|
|
18
|
+
*/
|
|
19
|
+
export function ThinkingEffort({ effort = 'auto', onChange, disabled = false }) {
|
|
20
|
+
const { t } = useI18n();
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const [dragging, setDragging] = useState(false);
|
|
23
|
+
const trackRef = useRef(null);
|
|
24
|
+
const activeIndex = Math.max(0, EFFORT_STEPS.findIndex((s) => s.id === effort));
|
|
25
|
+
const current = EFFORT_STEPS[activeIndex] ?? EFFORT_STEPS[0];
|
|
26
|
+
const updateFromPosition = (clientX) => {
|
|
27
|
+
if (!trackRef.current)
|
|
28
|
+
return;
|
|
29
|
+
const rect = trackRef.current.getBoundingClientRect();
|
|
30
|
+
const relativeX = Math.max(0, Math.min(clientX - rect.left, rect.width));
|
|
31
|
+
const ratio = rect.width > 0 ? relativeX / rect.width : 0;
|
|
32
|
+
const stepIndex = Math.round(ratio * (EFFORT_STEPS.length - 1));
|
|
33
|
+
const selected = EFFORT_STEPS[stepIndex];
|
|
34
|
+
if (selected && selected.id !== effort) {
|
|
35
|
+
onChange(selected.id);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const handlePointerDown = (e) => {
|
|
39
|
+
if (disabled)
|
|
40
|
+
return;
|
|
41
|
+
setDragging(true);
|
|
42
|
+
e.target.setPointerCapture?.(e.pointerId);
|
|
43
|
+
updateFromPosition(e.clientX);
|
|
44
|
+
};
|
|
45
|
+
const handlePointerMove = (e) => {
|
|
46
|
+
if (!dragging || disabled)
|
|
47
|
+
return;
|
|
48
|
+
updateFromPosition(e.clientX);
|
|
49
|
+
};
|
|
50
|
+
const handlePointerUp = (e) => {
|
|
51
|
+
if (dragging) {
|
|
52
|
+
setDragging(false);
|
|
53
|
+
e.target.releasePointerCapture?.(e.pointerId);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const percentage = (activeIndex / (EFFORT_STEPS.length - 1)) * 100;
|
|
57
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, children: [_jsx(PopoverTrigger, { disabled: disabled, children: _jsxs(XStack, { "data-slot": "thinking-effort-pill", role: "button", tabIndex: disabled ? -1 : 0, "aria-label": t('Thinking effort: {label}', { label: current.label }), "aria-haspopup": "dialog", "aria-expanded": open, items: "center", gap: "$1", px: "$2.5", height: 32, rounded: 9999, cursor: disabled ? 'default' : 'pointer', bg: "var(--edge, var(--white-10))", borderWidth: 1, borderColor: "var(--pane-edge, var(--white-10))", hoverStyle: disabled ? undefined : { bg: '$hover' }, focusVisibleStyle: { outlineColor: '$outlineColor', outlineWidth: 2, outlineStyle: 'solid' }, opacity: disabled ? 0.45 : 1, onKeyDown: (e) => {
|
|
58
|
+
if (disabled)
|
|
59
|
+
return;
|
|
60
|
+
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
setOpen(true);
|
|
63
|
+
}
|
|
64
|
+
}, children: [_jsx(Text, { fontSize: "$2", fontWeight: "500", color: "$ink", children: t('Thinking effort') }), _jsx(ChevronDown, { size: 12, color: "var(--soft, var(--muted-foreground))", "aria-hidden": true })] }) }), _jsx(PopoverContent, { solid: true, align: "end", p: "$3", width: 240, rounded: "$4", bg: "var(--pane-fill, var(--surface-card))", borderWidth: 1, borderColor: "var(--pane-edge, var(--white-15))", boxShadow: "var(--shadow-sheet-1)", backdropFilter: "var(--pane-blur, blur(24px) saturate(190%))", children: _jsxs(YStack, { gap: "$3", "data-slot": "thinking-effort-card", role: "dialog", "aria-label": t('Thinking effort'), children: [_jsxs(XStack, { items: "center", justify: "center", gap: "$1", cursor: "pointer", onClick: () => { }, children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: current.label }), _jsx(ChevronRight, { size: 14, color: "var(--soft, var(--muted-foreground))", "aria-hidden": true })] }), _jsxs(Box, { ref: trackRef, "data-slot": "thinking-slider-track", role: "slider", "aria-valuemin": 0, "aria-valuemax": 3, "aria-valuenow": activeIndex, "aria-valuetext": current.label, position: "relative", width: "100%", height: 28, rounded: 9999, bg: "var(--edge, var(--white-14))", cursor: "pointer", onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: () => setDragging(false),
|
|
65
|
+
// inline-style: touch-action for discrete slider gestures
|
|
66
|
+
style: { touchAction: 'none' }, children: [_jsx(Box, { position: "absolute", insetInlineStart: 0, t: 0, b: 0, rounded: 9999, bg: "var(--accent, var(--primary))",
|
|
67
|
+
// inline-style: dynamic percentage width of active effort slider fill
|
|
68
|
+
style: {
|
|
69
|
+
width: `calc(${percentage}% + 14px)`,
|
|
70
|
+
maxWidth: '100%',
|
|
71
|
+
transition: dragging ? 'none' : 'width 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
|
|
72
|
+
} }), EFFORT_STEPS.map((step, idx) => {
|
|
73
|
+
const dotPercent = (idx / (EFFORT_STEPS.length - 1)) * 100;
|
|
74
|
+
const isPassed = idx <= activeIndex;
|
|
75
|
+
return (_jsx(Box, { position: "absolute", t: "50%",
|
|
76
|
+
// inline-style: discrete dot step stop position on track
|
|
77
|
+
style: {
|
|
78
|
+
left: `calc(14px + (100% - 28px) * ${dotPercent / 100})`,
|
|
79
|
+
transform: 'translate(-50%, -50%)',
|
|
80
|
+
}, width: 4, height: 4, rounded: 9999, bg: isPassed ? 'var(--pure-white)' : 'var(--white-40)', pointerEvents: "none" }, step.id));
|
|
81
|
+
}), _jsx(Box, { "data-slot": "thinking-slider-thumb", position: "absolute", t: 2, width: 24, height: 24, rounded: 9999, bg: "var(--pure-white)", boxShadow: "0 2px 6px var(--surface-overlay)", pointerEvents: "none",
|
|
82
|
+
// inline-style: discrete thumb position computed from active step
|
|
83
|
+
style: {
|
|
84
|
+
left: `calc(2px + (100% - 28px) * ${percentage / 100})`,
|
|
85
|
+
transition: dragging ? 'none' : 'left 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
|
|
86
|
+
} })] }), _jsx(Text, { fontSize: "$1", color: "$soft", text: "center", px: "$1", children: t(current.note) })] }) })] }));
|
|
87
|
+
}
|
package/dist/Top.js
CHANGED
|
@@ -32,10 +32,16 @@ const LINE = 48;
|
|
|
32
32
|
/** A segment, px: the pill's 150px inside its rim, less the group's 4px gap, halved. */
|
|
33
33
|
const SEGMENT = 73;
|
|
34
34
|
/** The glass the three are cut from. */
|
|
35
|
-
const GLASS = {
|
|
35
|
+
const GLASS = {
|
|
36
|
+
...pane(),
|
|
37
|
+
borderColor: 'var(--border-strong)',
|
|
38
|
+
borderRadius: 9999,
|
|
39
|
+
boxShadow: 'inset 0 1px 1px 0 var(--white-15), var(--shadow-sheet-1)',
|
|
40
|
+
backdropFilter: 'blur(24px) saturate(190%) contrast(102%)',
|
|
41
|
+
};
|
|
36
42
|
/** A round glass button named for what it does (`label`, already in the reader's language). */
|
|
37
43
|
function Round({ label, onPress, opacity, pointerEvents, children, ...rest }) {
|
|
38
|
-
return (_jsx(XStack, { render: "button", "aria-label": label, onPress: onPress, ...GLASS, width: ROUND, height: ROUND, items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: opacity, pointerEvents: pointerEvents, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, ...rest, children: children }));
|
|
44
|
+
return (_jsx(XStack, { render: "button", "aria-label": label, onPress: onPress, ...GLASS, width: ROUND, height: ROUND, items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: opacity, pointerEvents: pointerEvents, pressStyle: { bg: '$raised', scale: 0.96 }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, ...rest, children: children }));
|
|
39
45
|
}
|
|
40
46
|
/** The drawer's mark: two strokes, the lower one shorter and flush with the start, 19×11 as the reference draws it. */
|
|
41
47
|
function Lines() {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface LearningStudy {
|
|
2
|
+
id: string;
|
|
3
|
+
project: string;
|
|
4
|
+
title: string;
|
|
5
|
+
question: string;
|
|
6
|
+
improvement?: {
|
|
7
|
+
baseline: string;
|
|
8
|
+
target: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface LearningReview {
|
|
12
|
+
id: string;
|
|
13
|
+
status: string;
|
|
14
|
+
action: {
|
|
15
|
+
rule?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
review?: {
|
|
18
|
+
target: string;
|
|
19
|
+
candidate: string;
|
|
20
|
+
baseline: string;
|
|
21
|
+
status: string;
|
|
22
|
+
blocks: string[];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface LearningRows {
|
|
27
|
+
studies: LearningStudy[];
|
|
28
|
+
reviews: LearningReview[];
|
|
29
|
+
experiments: {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
status: string;
|
|
33
|
+
winner?: string;
|
|
34
|
+
}[];
|
|
35
|
+
benchmarks: {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
direction: string;
|
|
39
|
+
}[];
|
|
40
|
+
}
|
|
41
|
+
export type LearningPart = keyof LearningRows;
|
|
42
|
+
export declare const LEARNING_PATHS: Record<LearningPart, string>;
|
|
43
|
+
/** Every read uses the same IAM/org headers as the rest of the workspace. */
|
|
44
|
+
export declare function learningRead<K extends LearningPart>(part: K, signal?: AbortSignal): Promise<LearningRows[K]>;
|
|
45
|
+
export interface LearningProposal {
|
|
46
|
+
title: string;
|
|
47
|
+
question: string;
|
|
48
|
+
baseline: string;
|
|
49
|
+
target: string;
|
|
50
|
+
metric: string;
|
|
51
|
+
category: string;
|
|
52
|
+
direction: 'higher' | 'lower';
|
|
53
|
+
minQuestions: number;
|
|
54
|
+
minGain: number;
|
|
55
|
+
}
|
|
56
|
+
/** Creating a study arms deterministic evaluation; it starts no model or rollout. */
|
|
57
|
+
export declare function learningCreate(proposal: LearningProposal, id: string): Promise<void>;
|
|
58
|
+
export interface LearningAnalysis {
|
|
59
|
+
winner: string;
|
|
60
|
+
results: {
|
|
61
|
+
variant: string;
|
|
62
|
+
exposed: number;
|
|
63
|
+
converted: number;
|
|
64
|
+
rate: number;
|
|
65
|
+
significant: boolean;
|
|
66
|
+
}[];
|
|
67
|
+
}
|
|
68
|
+
export interface LearningComparison {
|
|
69
|
+
a: string;
|
|
70
|
+
b: string;
|
|
71
|
+
n_common: number;
|
|
72
|
+
a_correct: number;
|
|
73
|
+
b_correct: number;
|
|
74
|
+
mcnemar_p: number;
|
|
75
|
+
}
|
|
76
|
+
/** Analyze measured outcomes; this never calls the live-rollout decide route. */
|
|
77
|
+
export declare function learningAnalyze(id: string): Promise<LearningAnalysis>;
|
|
78
|
+
/** Pair actual completed items. Zero common items is displayed as no evidence. */
|
|
79
|
+
export declare function learningCompare(benchmark: string, a: string, b: string): Promise<LearningComparison>;
|
package/dist/learning.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { api } from './lib/api.js';
|
|
2
|
+
import { scope } from './lib/session.js';
|
|
3
|
+
export const LEARNING_PATHS = {
|
|
4
|
+
studies: '/v1/research/studies',
|
|
5
|
+
reviews: '/v1/research/actions',
|
|
6
|
+
experiments: '/v1/experiment',
|
|
7
|
+
benchmarks: '/v1/benchmark/catalog',
|
|
8
|
+
};
|
|
9
|
+
/** Every read uses the same IAM/org headers as the rest of the workspace. */
|
|
10
|
+
export async function learningRead(part, signal) {
|
|
11
|
+
const r = await fetch(`${api()}${LEARNING_PATHS[part]}`, { headers: scope(), signal });
|
|
12
|
+
if (!r.ok)
|
|
13
|
+
throw Object.assign(new Error(`${r.status}`), { status: r.status });
|
|
14
|
+
const body = await r.json();
|
|
15
|
+
if (!Array.isArray(body.data))
|
|
16
|
+
throw new Error('Learning response is missing its data array');
|
|
17
|
+
return (part === 'reviews' ? body.data.filter((row) => row.action?.rule === 'research.improvement.v1') : body.data);
|
|
18
|
+
}
|
|
19
|
+
/** Creating a study arms deterministic evaluation; it starts no model or rollout. */
|
|
20
|
+
export async function learningCreate(proposal, id) {
|
|
21
|
+
const r = await fetch(`${api()}/v1/research/studies`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json', ...scope() },
|
|
24
|
+
body: JSON.stringify({ studies: [{ id, title: proposal.title, question: proposal.question,
|
|
25
|
+
improvement: { baseline: proposal.baseline, target: proposal.target, min_questions: proposal.minQuestions,
|
|
26
|
+
require_intervals: false, metrics: [{ category: proposal.category, metric: proposal.metric, direction: proposal.direction, min_gain: proposal.minGain, max_regression: 0 }] },
|
|
27
|
+
}] }),
|
|
28
|
+
});
|
|
29
|
+
if (!r.ok)
|
|
30
|
+
throw Object.assign(new Error(`${r.status}`), { status: r.status });
|
|
31
|
+
}
|
|
32
|
+
/** Analyze measured outcomes; this never calls the live-rollout decide route. */
|
|
33
|
+
export async function learningAnalyze(id) {
|
|
34
|
+
const r = await fetch(`${api()}/v1/experiment/${encodeURIComponent(id)}/analyze`, {
|
|
35
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', ...scope() }, body: '{}',
|
|
36
|
+
});
|
|
37
|
+
if (!r.ok)
|
|
38
|
+
throw Object.assign(new Error(`${r.status}`), { status: r.status });
|
|
39
|
+
return await r.json();
|
|
40
|
+
}
|
|
41
|
+
/** Pair actual completed items. Zero common items is displayed as no evidence. */
|
|
42
|
+
export async function learningCompare(benchmark, a, b) {
|
|
43
|
+
const query = new URLSearchParams({ benchmark, a, b });
|
|
44
|
+
const r = await fetch(`${api()}/v1/benchmark/compare?${query}`, { headers: scope() });
|
|
45
|
+
if (!r.ok)
|
|
46
|
+
throw Object.assign(new Error(`${r.status}`), { status: r.status });
|
|
47
|
+
return await r.json();
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ChatPreferences {
|
|
2
|
+
showModelSelector: boolean;
|
|
3
|
+
allowThirdPartyModels: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function getChatPreferences(): ChatPreferences;
|
|
6
|
+
export declare function setChatPreferences(patch: Partial<ChatPreferences>): ChatPreferences;
|
|
7
|
+
export declare function useChatPreferences(): [ChatPreferences, (patch: Partial<ChatPreferences>) => void];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat composer preferences:
|
|
3
|
+
* - showModelSelector: whether model selector is visible in composer (default: false, paid plan feature)
|
|
4
|
+
* - allowThirdPartyModels: whether 3rd-party models (OpenAI, Anthropic, etc.) are enabled via pre-paid credit (default: false)
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
const STORAGE_KEY = 'hanzo.chat.preferences';
|
|
8
|
+
const EVENT_NAME = 'hanzo:chat:preferences';
|
|
9
|
+
const DEFAULT_PREFERENCES = {
|
|
10
|
+
showModelSelector: false,
|
|
11
|
+
allowThirdPartyModels: false,
|
|
12
|
+
};
|
|
13
|
+
export function getChatPreferences() {
|
|
14
|
+
if (typeof window === 'undefined')
|
|
15
|
+
return DEFAULT_PREFERENCES;
|
|
16
|
+
try {
|
|
17
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
18
|
+
if (!raw)
|
|
19
|
+
return DEFAULT_PREFERENCES;
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
return {
|
|
22
|
+
showModelSelector: Boolean(parsed.showModelSelector),
|
|
23
|
+
allowThirdPartyModels: Boolean(parsed.allowThirdPartyModels),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return DEFAULT_PREFERENCES;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function setChatPreferences(patch) {
|
|
31
|
+
const current = getChatPreferences();
|
|
32
|
+
const next = { ...current, ...patch };
|
|
33
|
+
if (typeof window !== 'undefined') {
|
|
34
|
+
try {
|
|
35
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: next }));
|
|
41
|
+
}
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
export function useChatPreferences() {
|
|
45
|
+
const [prefs, setPrefs] = useState(getChatPreferences);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const handler = (e) => {
|
|
48
|
+
const detail = e.detail;
|
|
49
|
+
if (detail)
|
|
50
|
+
setPrefs(detail);
|
|
51
|
+
else
|
|
52
|
+
setPrefs(getChatPreferences());
|
|
53
|
+
};
|
|
54
|
+
window.addEventListener(EVENT_NAME, handler);
|
|
55
|
+
window.addEventListener('storage', handler);
|
|
56
|
+
return () => {
|
|
57
|
+
window.removeEventListener(EVENT_NAME, handler);
|
|
58
|
+
window.removeEventListener('storage', handler);
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
return [prefs, setChatPreferences];
|
|
62
|
+
}
|
package/dist/look.js
CHANGED
|
@@ -1,24 +1,86 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
4
|
* HOW THE APP LOOKS.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* account, so it follows them to every Hanzo surface. The theme used to be a
|
|
9
|
-
* row of its own here, written to next-themes and to IAM's per-subject key —
|
|
10
|
-
* two more places a theme lived, neither of which crossed an origin.
|
|
6
|
+
* Theme, type, spacing, corners, native platform profiles, custom fonts,
|
|
7
|
+
* and organization theme mods — stored and resolved for the organization in scope.
|
|
11
8
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { LanguagePicker, useI18n } from '@hanzo/i18n/react';
|
|
11
|
+
import { Box, Button, Input, Separator, Text, XStack, YStack } from '@hanzo/ui';
|
|
12
|
+
import { Appearance } from '@hanzo/appearance';
|
|
13
|
+
import { useIam } from '@hanzo/iam/react';
|
|
14
|
+
import { Check, Download, Palette, Smartphone, Type, Upload } from 'lucide-react';
|
|
15
|
+
import { setActivePlatform, BUILTIN_MODS, applyMod, exportMod, importMod, injectCustomFont, applyFontFamily, } from '@hanzo/mod';
|
|
16
|
+
import { api } from './lib/api.js';
|
|
17
|
+
import { useOrg } from './host.js';
|
|
18
|
+
import { brand } from './where.js';
|
|
19
|
+
import { tap } from './lib/tap.js';
|
|
20
|
+
const CHOICE = { rounded: 8, borderWidth: 1, borderStyle: 'solid', cursor: 'pointer' };
|
|
21
|
+
const PLATFORM_OPTIONS = [
|
|
22
|
+
{ id: 'auto', label: 'Auto (System OS)', note: 'Automatically detects your device operating system' },
|
|
23
|
+
{ id: 'ios', label: 'Apple iOS (Liquid Glass)', note: 'Continuous squircles, specular liquid glass, SF Pro typography' },
|
|
24
|
+
{ id: 'android', label: 'Android (Material 3)', note: 'Material 3 tonal elevations and Google Sans geometry' },
|
|
25
|
+
{ id: 'macos', label: 'Apple macOS (Vibrant)', note: 'Translucent glass sidebars and compact desktop density' },
|
|
26
|
+
{ id: 'windows', label: 'Windows (Mica Fluent)', note: 'Fluent Design acrylic Mica and Segoe UI Variable' },
|
|
27
|
+
{ id: 'linux', label: 'Linux (Clean System)', note: 'Crisp system geometry and flat modern edges' },
|
|
28
|
+
];
|
|
19
29
|
export function Look() {
|
|
20
30
|
const { t } = useI18n();
|
|
21
31
|
const { accessToken } = useIam();
|
|
22
32
|
const org = useOrg();
|
|
23
|
-
|
|
33
|
+
const [activePlatform, setPlatform] = useState(() => {
|
|
34
|
+
if (typeof window === 'undefined')
|
|
35
|
+
return 'auto';
|
|
36
|
+
const stored = window.localStorage.getItem('hanzo.mod.platform');
|
|
37
|
+
return stored ?? 'auto';
|
|
38
|
+
});
|
|
39
|
+
const [customFont, setCustomFont] = useState('');
|
|
40
|
+
const [fontApplied, setFontApplied] = useState(false);
|
|
41
|
+
const [modJson, setModJson] = useState('');
|
|
42
|
+
const [copiedMod, setCopiedMod] = useState(false);
|
|
43
|
+
const handlePlatformChange = (p) => {
|
|
44
|
+
setPlatform(p);
|
|
45
|
+
setActivePlatform(p);
|
|
46
|
+
};
|
|
47
|
+
const handleApplyFont = () => {
|
|
48
|
+
if (!customFont.trim())
|
|
49
|
+
return;
|
|
50
|
+
const font = customFont.trim();
|
|
51
|
+
if (font.startsWith('http://') || font.startsWith('https://')) {
|
|
52
|
+
injectCustomFont({ family: 'CustomBrandFont', url: font, isStylesheet: true });
|
|
53
|
+
applyFontFamily('heading', 'CustomBrandFont, sans-serif');
|
|
54
|
+
applyFontFamily('sans', 'CustomBrandFont, sans-serif');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
applyFontFamily('heading', `${font}, sans-serif`);
|
|
58
|
+
applyFontFamily('sans', `${font}, sans-serif`);
|
|
59
|
+
}
|
|
60
|
+
setFontApplied(true);
|
|
61
|
+
setTimeout(() => setFontApplied(false), 2500);
|
|
62
|
+
};
|
|
63
|
+
const handleExportMod = (mod) => {
|
|
64
|
+
const json = exportMod(mod);
|
|
65
|
+
navigator.clipboard?.writeText(json);
|
|
66
|
+
setCopiedMod(true);
|
|
67
|
+
setTimeout(() => setCopiedMod(false), 2000);
|
|
68
|
+
};
|
|
69
|
+
const handleImportMod = () => {
|
|
70
|
+
if (!modJson.trim())
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
const mod = importMod(modJson.trim());
|
|
74
|
+
applyMod(mod);
|
|
75
|
+
alert(t('Mod "{name}" successfully applied!', { name: mod.name }));
|
|
76
|
+
setModJson('');
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
alert(t('Failed to import mod: {message}', { message: e.message }));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return (_jsxs(YStack, { gap: "$5", maxW: 540, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: t('Customized per organization and kept with your account across all Hanzo surfaces.') }), _jsx(Appearance, { org: org.id, orgName: org.name || brand(), account: { base: api(), token: accessToken ?? undefined } }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$2.5", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Smartphone, { size: 16, color: "var(--primary)", "aria-hidden": true }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: t('Native Platform Look & Feel') })] }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Adaptive styling tailored to your OS: iOS Liquid Glass, Material 3, macOS Vibrant, or Windows Mica.') }), _jsx(YStack, { gap: "$1.5", pt: "$1", children: PLATFORM_OPTIONS.map((opt) => {
|
|
83
|
+
const active = activePlatform === opt.id;
|
|
84
|
+
return (_jsx(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => handlePlatformChange(opt.id), ...CHOICE, p: "$2.5", bg: active ? 'var(--edge, var(--white-14))' : 'transparent', borderColor: active ? 'var(--borderColor, var(--white-22))' : 'var(--white-08)', children: _jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(YStack, { gap: "$0.5", children: [_jsx(Text, { fontSize: "$3", fontWeight: active ? '600' : '500', color: active ? '$ink' : '$ink', children: opt.label }), _jsx(Text, { fontSize: "$1", color: "$soft", children: opt.note })] }), active ? _jsx(Check, { size: 16, color: "var(--primary)", "aria-hidden": true }) : null] }) }, opt.id));
|
|
85
|
+
}) })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Palette, { size: 16, color: "var(--primary)", "aria-hidden": true }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: t('Organization Theme Mods (@hanzo/mod)') })] }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Select or fork designer themes for custom organization branding.') }), _jsx(XStack, { gap: "$2", flexWrap: "wrap", children: BUILTIN_MODS.map((mod) => (_jsx(Box, { render: _jsx("button", { type: "button" }), ...tap, onClick: () => applyMod(mod), ...CHOICE, py: 6, px: 12, bg: "transparent", borderColor: "var(--white-10)", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$2", color: "$ink", children: mod.name }) }, mod.id))) }), _jsxs(YStack, { gap: "$2", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Type, { size: 15, color: "var(--primary)", "aria-hidden": true }), _jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: t('Custom Typography') })] }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Enter a font family name (e.g. "Geist", "Inter", "Cabinet Grotesk") or a webfont stylesheet URL.') }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(Input, { flex: 1, size: "sm", value: customFont, onChange: (e) => setCustomFont(e.target.value), placeholder: "e.g. Cabinet Grotesk or https://fonts.googleapis.com/..." }), _jsx(Button, { size: "sm", onClick: handleApplyFont, children: fontApplied ? t('Applied!') : t('Apply Font') })] })] }), _jsxs(YStack, { gap: "$2", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", children: t('Fork & Share Theme Mods') }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('Export current mod configuration or paste a mod JSON to import.') }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(Input, { flex: 1, size: "sm", value: modJson, onChange: (e) => setModJson(e.target.value), placeholder: t('Paste mod JSON here to import') }), _jsxs(Button, { size: "sm", variant: "outline", onClick: handleImportMod, children: [_jsx(Upload, { size: 13, "aria-hidden": true }), t('Import')] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => handleExportMod(BUILTIN_MODS[0]), children: [_jsx(Download, { size: 13, "aria-hidden": true }), copiedMod ? t('Copied!') : t('Export JSON')] })] })] })] }), _jsx(Separator, {}), _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: t('Language') }), _jsx(Text, { fontSize: "$2", color: "$soft", children: t('The language Hanzo is shown in, on every Hanzo surface you sign in to.') }), _jsx(XStack, { pt: "$1", children: _jsx(LanguagePicker, {}) })] })] }));
|
|
24
86
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/app",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Hanzo's app: the shell and its apps (Home, Chat, Inbox, Contacts, Meet, Calendar, Drive, Board, Work, Bots), once, for every Hanzo surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"license": "Apache-2.0 OR MIT",
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"@hanzo/glass": "0.1.0",
|
|
46
|
+
"@hanzo/mod": "0.1.0",
|
|
45
47
|
"@hanzo/personas": "1.0.5",
|
|
46
48
|
"marked": "18.0.12",
|
|
47
49
|
"simple-icons": "16.30.0"
|