mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-23 02:31:31 +00:00
Internal renames so source matches the published @hanzogui/[email protected] dist (Gui.ts, createGui.ts, views/GuiProvider.mjs, views/GuiRoot.mjs). Renames applied: - Files: Hanzogui.ts -> Gui.ts, createHanzogui.ts -> createGui.ts, HanzoguiProvider.tsx -> GuiProvider.tsx, HanzoguiRoot[.native].tsx -> GuiRoot[.native].tsx, HanzoguiPlugin.ts -> GuiPlugin.ts, withHanzogui.ts -> withGui.ts, loadHanzogui.ts -> loadGui.ts, v4-hanzogui.ts -> v4-gui.ts, *.d.ts mirrors, hanzogui.config.ts -> gui.config.ts, hanzogui.build.ts -> gui.build.ts, hanzogui.css -> gui.css, hanzogui-build.js -> gui-build.js, etc. - Directories: pkgs/core/create-hanzogui -> pkgs/core/create-gui, apps/sandbox/config/hanzogui -> apps/sandbox/config/gui. - Identifiers: Hanzogui* -> Gui*, createHanzogui -> createGui, withHanzogui -> withGui, loadHanzogui -> loadGui, HANZOGUI_* env -> GUI_*, __hanzogui* globals -> __gui*, hanzoguiOptions/hanzoguiConfig/ hanzoguiPlugin etc. -> guiOptions/guiConfig/guiPlugin, CSS vars --hanzogui-* -> --gui-*, data-hanzogui-* -> data-gui-*, [hanzogui] log prefix -> [gui]. - Removed duplicate pkgs/packages/hanzogui-dev-config; the canonical pkgs/packages/gui-dev-config (@hanzogui/dev-config) already existed with the correct shape. apps/demos now consumes @hanzogui/dev-config. Preserved (public npm names): - "hanzogui" umbrella package (pkgs/ui/hanzogui/package.json) - "@hanzogui/*" namespace - "hanzogui-loader" package name (pkgs/compiler/loader) - "hanzogui-build-test-*" test fixture names - "hanzogui-publish-smoke" workflow id - pkgs/core/helpers-hanzogui dir (not on rename list) - apps/gui.hanzo.ai/ marketing site directory - AGENTS.md symlink restored after perl -i clobbered it Smoke build (turbo run build): 169/169 workspaces successful. Sample tests: pkgs/core/core-test getSplitStyles.android* + createGui.web all green.
216 lines
6.4 KiB
TypeScript
216 lines
6.4 KiB
TypeScript
// PayloadInputWithEncoding — reusable form widget for any dialog that
|
|
// needs to send a Payload to the backend (StartWorkflow, SignalWorkflow,
|
|
// ActivityStart, …). Today the existing dialogs use a plain TextArea
|
|
// + encodeJsonPayload; this widget extends that with:
|
|
//
|
|
// * an explicit encoding selector (json/plain | json/protobuf | binary | none)
|
|
// * a "Format" button that runs JSON.parse → JSON.stringify(_, null, 2)
|
|
// * an inline parse-error chip so the user sees JSON mistakes
|
|
// without having to submit the form
|
|
// * a stable controlled API: the parent owns the textarea text and
|
|
// the encoding choice; the widget reports back an encoded Payload
|
|
// (or null) via onPayloadChange.
|
|
//
|
|
// Tier 1A and 1B can swap their existing TextArea blocks for
|
|
// <PayloadInputWithEncoding/> by lifting two pieces of state and
|
|
// reading payload from onPayloadChange. The existing
|
|
// `encodeJsonPayload` helper in ActionDialog stays for back-compat.
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import { Button, Text, TextArea, XStack, YStack } from 'hanzogui'
|
|
import {
|
|
PAYLOAD_ENCODINGS,
|
|
type PayloadEncoding,
|
|
} from '../../lib/types'
|
|
|
|
export interface EncodedPayload {
|
|
metadata: { encoding: string }
|
|
data: string
|
|
}
|
|
|
|
export interface PayloadInputWithEncodingProps {
|
|
// The textarea text. Controlled by the parent so existing form-state
|
|
// shapes don't have to change.
|
|
value: string
|
|
onChange: (next: string) => void
|
|
// The encoding tag. Controlled by the parent for the same reason.
|
|
encoding: PayloadEncoding
|
|
onEncodingChange: (next: PayloadEncoding) => void
|
|
// Called whenever the encoded payload changes — null when the
|
|
// textarea is empty / encoding is `none` / JSON parse fails.
|
|
onPayloadChange?: (payload: EncodedPayload | null) => void
|
|
label?: string
|
|
placeholder?: string
|
|
disabled?: boolean
|
|
minH?: number
|
|
}
|
|
|
|
export function PayloadInputWithEncoding({
|
|
value,
|
|
onChange,
|
|
encoding,
|
|
onEncodingChange,
|
|
onPayloadChange,
|
|
label = 'Payload',
|
|
placeholder = '{"key": "value"}',
|
|
disabled,
|
|
minH = 120,
|
|
}: PayloadInputWithEncodingProps) {
|
|
const [parseError, setParseError] = useState<string | null>(null)
|
|
|
|
// Re-encode whenever inputs change. Keeps the parent's payload
|
|
// state in sync without forcing them to call an encoder manually.
|
|
const encoded = useMemo(() => {
|
|
if (encoding === 'none') {
|
|
setParseError(null)
|
|
return null
|
|
}
|
|
if (!value.trim()) {
|
|
setParseError(null)
|
|
return null
|
|
}
|
|
if (encoding === 'json/plain') {
|
|
try {
|
|
// Validate the JSON, then re-encode the original string. We
|
|
// intentionally encode the user's text (not a re-stringify)
|
|
// so trailing whitespace + key order are preserved.
|
|
JSON.parse(value)
|
|
setParseError(null)
|
|
} catch (e) {
|
|
setParseError(e instanceof Error ? e.message : String(e))
|
|
return null
|
|
}
|
|
} else {
|
|
setParseError(null)
|
|
}
|
|
return {
|
|
metadata: { encoding },
|
|
data: utf8ToBase64(value),
|
|
}
|
|
}, [value, encoding])
|
|
|
|
useEffect(() => {
|
|
onPayloadChange?.(encoded)
|
|
}, [encoded, onPayloadChange])
|
|
|
|
function format() {
|
|
try {
|
|
const parsed = JSON.parse(value)
|
|
onChange(JSON.stringify(parsed, null, 2))
|
|
setParseError(null)
|
|
} catch (e) {
|
|
setParseError(e instanceof Error ? e.message : String(e))
|
|
}
|
|
}
|
|
|
|
return (
|
|
<YStack gap="$2">
|
|
<XStack items="center" justify="space-between" gap="$2" flexWrap="wrap">
|
|
<Text fontSize="$1" color="$placeholderColor">
|
|
{label}
|
|
</Text>
|
|
<XStack items="center" gap="$2" flexWrap="wrap">
|
|
<EncodingSelect
|
|
value={encoding}
|
|
onChange={onEncodingChange}
|
|
disabled={disabled}
|
|
/>
|
|
{encoding === 'json/plain' ? (
|
|
<Button
|
|
size="$2"
|
|
chromeless
|
|
onPress={format}
|
|
disabled={disabled || !value.trim()}
|
|
>
|
|
<Text fontSize="$2" color={'#86efac' as never}>
|
|
Format
|
|
</Text>
|
|
</Button>
|
|
) : null}
|
|
</XStack>
|
|
</XStack>
|
|
<TextArea
|
|
size="$3"
|
|
minH={minH}
|
|
value={value}
|
|
onChangeText={onChange}
|
|
placeholder={encoding === 'none' ? '(no payload)' : placeholder}
|
|
fontFamily={'ui-monospace, SFMono-Regular, monospace' as never}
|
|
disabled={disabled || encoding === 'none'}
|
|
/>
|
|
{parseError ? (
|
|
<XStack
|
|
items="center"
|
|
gap="$2"
|
|
px="$2"
|
|
py="$1.5"
|
|
bg={'#1f1213' as never}
|
|
borderColor="#7f1d1d"
|
|
borderWidth={1}
|
|
rounded="$2"
|
|
>
|
|
<Text fontSize="$1" color={'#fca5a5' as never}>
|
|
JSON: {parseError}
|
|
</Text>
|
|
</XStack>
|
|
) : null}
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
function EncodingSelect({
|
|
value,
|
|
onChange,
|
|
disabled,
|
|
}: {
|
|
value: PayloadEncoding
|
|
onChange: (v: PayloadEncoding) => void
|
|
disabled?: boolean
|
|
}) {
|
|
// gui has no Select primitive yet (see WorkflowSearchBar etc.);
|
|
// a native <select> nested inside a gui XStack styles cleanly
|
|
// enough and stays a11y-friendly without extra deps.
|
|
return (
|
|
<select
|
|
value={value}
|
|
disabled={disabled}
|
|
onChange={(e) => onChange(e.target.value as PayloadEncoding)}
|
|
aria-label="Payload encoding"
|
|
style={{
|
|
background: 'transparent',
|
|
color: '#cbd5e1',
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
borderRadius: 6,
|
|
padding: '4px 8px',
|
|
fontSize: 12,
|
|
fontFamily: 'inherit',
|
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
}}
|
|
>
|
|
{PAYLOAD_ENCODINGS.map((e) => (
|
|
<option key={e} value={e}>
|
|
{e}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)
|
|
}
|
|
|
|
// utf8ToBase64 — round-trips utf-8 text through bytes so multi-byte
|
|
// characters survive btoa. Equivalent to encodeJsonPayload's encoder
|
|
// but exposed for callers that want the raw transform.
|
|
export function utf8ToBase64(text: string): string {
|
|
const bytes = new TextEncoder().encode(text)
|
|
let bin = ''
|
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i])
|
|
return btoa(bin)
|
|
}
|
|
|
|
// base64ToUtf8 — inverse of utf8ToBase64, used by PayloadDecoder.
|
|
export function base64ToUtf8(b64: string): string {
|
|
const bin = atob(b64)
|
|
const bytes = new Uint8Array(bin.length)
|
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
|
return new TextDecoder().decode(bytes)
|
|
}
|