mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 06:32:24 +00:00
feat(flows): improve workflow builder UX and diagnostics (#4574)
This commit is contained in:
@@ -15,9 +15,20 @@
|
||||
* - `transform` → `set` (key → =-expression map)
|
||||
* - `trigger` → `trigger_kind` + kind-specific (`schedule`, `toolkit`/`trigger_slug`)
|
||||
*/
|
||||
import createDebug from 'debug';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { NodeKind } from '../../../../lib/flows/types';
|
||||
import { useT } from '../../../../lib/i18n/I18nContext';
|
||||
import type { FlowConnection } from '../../../../services/api/flowsApi';
|
||||
import {
|
||||
ComposioActionField,
|
||||
type ComposioActionSchema,
|
||||
ComposioToolkitField,
|
||||
ComposioTriggerField,
|
||||
fetchActionSchema,
|
||||
} from './composioFields';
|
||||
import { NATIVE_TOOL_PREFIX, NativeToolField } from './nativeToolFields';
|
||||
import {
|
||||
configString,
|
||||
configStringMap,
|
||||
@@ -25,16 +36,34 @@ import {
|
||||
ExpressionField,
|
||||
JsonField,
|
||||
KeyMapField,
|
||||
ModelHintField,
|
||||
SelectField,
|
||||
TextAreaField,
|
||||
TextField,
|
||||
UpstreamInsertSelect,
|
||||
} from './nodeConfigFields';
|
||||
import { ScheduleField } from './ScheduleField';
|
||||
import type { UpstreamExpressionOption } from './upstreamOptions';
|
||||
|
||||
const log = createDebug('app:flows:nodeConfig:forms');
|
||||
|
||||
/** Derive the toolkit slug from a `composio:<toolkit>:<conn>` connection ref. */
|
||||
function toolkitFromConnectionRef(ref: string): string {
|
||||
const parts = ref.split(':');
|
||||
return parts[0] === 'composio' && parts.length >= 2 ? parts[1] : '';
|
||||
}
|
||||
|
||||
export interface NodeConfigFormProps {
|
||||
config: Record<string, unknown>;
|
||||
/** Shallow-merge patch into the node's config (undefined values are still set). */
|
||||
onChange: (patch: Record<string, unknown>) => void;
|
||||
connections: FlowConnection[];
|
||||
/**
|
||||
* `=nodes.…` expressions referencing this node's upstream ancestors, for the
|
||||
* insert pickers on expression-bearing fields. Optional — absent (or empty)
|
||||
* simply hides the pickers.
|
||||
*/
|
||||
upstreamOptions?: UpstreamExpressionOption[];
|
||||
}
|
||||
|
||||
export type NodeConfigForm = (props: NodeConfigFormProps) => React.ReactElement;
|
||||
@@ -43,9 +72,10 @@ export type NodeConfigForm = (props: NodeConfigFormProps) => React.ReactElement;
|
||||
|
||||
const TRIGGER_KINDS = ['manual', 'schedule', 'webhook', 'app_event'] as const;
|
||||
|
||||
function TriggerForm({ config, onChange }: NodeConfigFormProps) {
|
||||
function TriggerForm({ config, onChange, connections }: NodeConfigFormProps) {
|
||||
const { t } = useT();
|
||||
const kind = configString(config, 'trigger_kind') || 'manual';
|
||||
const toolkit = configString(config, 'toolkit');
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<SelectField
|
||||
@@ -59,28 +89,28 @@ function TriggerForm({ config, onChange }: NodeConfigFormProps) {
|
||||
}))}
|
||||
/>
|
||||
{kind === 'schedule' && (
|
||||
<TextField
|
||||
label={t('flows.nodeConfig.trigger.scheduleLabel')}
|
||||
hint={t('flows.nodeConfig.trigger.scheduleHint')}
|
||||
<ScheduleField
|
||||
value={configString(config, 'schedule')}
|
||||
onChange={v => onChange({ schedule: v })}
|
||||
placeholder="0 9 * * 1"
|
||||
testId="node-config-trigger-schedule"
|
||||
/>
|
||||
)}
|
||||
{kind === 'app_event' && (
|
||||
<>
|
||||
<TextField
|
||||
<ComposioToolkitField
|
||||
label={t('flows.nodeConfig.trigger.toolkitLabel')}
|
||||
value={configString(config, 'toolkit')}
|
||||
onChange={v => onChange({ toolkit: v })}
|
||||
placeholder="github"
|
||||
value={toolkit}
|
||||
// Changing the app clears a now-mismatched trigger slug.
|
||||
onChange={v => onChange({ toolkit: v, trigger_slug: '' })}
|
||||
connections={connections}
|
||||
testId="node-config-trigger-toolkit"
|
||||
/>
|
||||
<TextField
|
||||
<ComposioTriggerField
|
||||
label={t('flows.nodeConfig.trigger.triggerSlugLabel')}
|
||||
value={configString(config, 'trigger_slug')}
|
||||
onChange={v => onChange({ trigger_slug: v })}
|
||||
placeholder="GITHUB_STAR_ADDED"
|
||||
toolkit={toolkit}
|
||||
testId="node-config-trigger-slug"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -97,7 +127,7 @@ function TriggerForm({ config, onChange }: NodeConfigFormProps) {
|
||||
|
||||
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
||||
|
||||
function HttpRequestForm({ config, onChange, connections }: NodeConfigFormProps) {
|
||||
function HttpRequestForm({ config, onChange, connections, upstreamOptions }: NodeConfigFormProps) {
|
||||
const { t } = useT();
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -113,6 +143,7 @@ function HttpRequestForm({ config, onChange, connections }: NodeConfigFormProps)
|
||||
value={configString(config, 'url')}
|
||||
onChange={v => onChange({ url: v })}
|
||||
placeholder="https://api.example.com/v1/resource"
|
||||
upstreamOptions={upstreamOptions}
|
||||
testId="node-config-http-url"
|
||||
/>
|
||||
<CredentialPickerField
|
||||
@@ -141,23 +172,36 @@ function HttpRequestForm({ config, onChange, connections }: NodeConfigFormProps)
|
||||
|
||||
// ── agent ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function AgentForm({ config, onChange, connections }: NodeConfigFormProps) {
|
||||
function AgentForm({ config, onChange, connections, upstreamOptions }: NodeConfigFormProps) {
|
||||
const { t } = useT();
|
||||
const prompt = configString(config, 'prompt');
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<TextAreaField
|
||||
label={t('flows.nodeConfig.agent.promptLabel')}
|
||||
value={configString(config, 'prompt')}
|
||||
value={prompt}
|
||||
onChange={v => onChange({ prompt: v })}
|
||||
placeholder={t('flows.nodeConfig.agent.promptPlaceholder')}
|
||||
rows={5}
|
||||
testId="node-config-agent-prompt"
|
||||
/>
|
||||
<TextField
|
||||
{upstreamOptions && upstreamOptions.length > 0 && (
|
||||
// Appends the picked `=nodes.…` expression to the prompt (a prompt is
|
||||
// prose, so inserting must not clobber what's already written).
|
||||
<div className="-mt-2 flex justify-end">
|
||||
<UpstreamInsertSelect
|
||||
options={upstreamOptions}
|
||||
onInsert={expr => onChange({ prompt: prompt ? `${prompt} ${expr}` : expr })}
|
||||
testId="node-config-agent-prompt-upstream"
|
||||
className="cursor-pointer rounded-md border border-line-strong bg-surface-muted px-1.5 py-1 text-[11px] text-content-muted focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ModelHintField
|
||||
label={t('flows.nodeConfig.agent.modelLabel')}
|
||||
hint={t('flows.nodeConfig.agent.modelHint')}
|
||||
value={configString(config, 'model')}
|
||||
onChange={v => onChange({ model: v })}
|
||||
placeholder="gpt-4o-mini"
|
||||
testId="node-config-agent-model"
|
||||
/>
|
||||
<CredentialPickerField
|
||||
@@ -172,25 +216,148 @@ function AgentForm({ config, onChange, connections }: NodeConfigFormProps) {
|
||||
|
||||
// ── tool_call ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function ToolCallForm({ config, onChange, connections }: NodeConfigFormProps) {
|
||||
/** Read `config.args` as a plain object (Composio args are a JSON object). */
|
||||
function configArgs(config: Record<string, unknown>): Record<string, unknown> {
|
||||
const args = config.args;
|
||||
return args && typeof args === 'object' && !Array.isArray(args)
|
||||
? (args as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function ToolCallForm({ config, onChange, connections, upstreamOptions }: NodeConfigFormProps) {
|
||||
const { t } = useT();
|
||||
const slug = configString(config, 'slug');
|
||||
// Two flavours of tool_call: a native OpenHuman "Tool" (provider=openhuman /
|
||||
// slug `oh:...`) vs a Composio "App action". The palette seeds `provider`.
|
||||
const isNative =
|
||||
configString(config, 'provider') === 'openhuman' || slug.startsWith(NATIVE_TOOL_PREFIX);
|
||||
|
||||
// The connected account is chosen first; its toolkit scopes the action list.
|
||||
const connectionRef = configString(config, 'connection_ref');
|
||||
const toolkit = toolkitFromConnectionRef(connectionRef);
|
||||
|
||||
// Required-arg preflight rows (Composio only): once an action is selected,
|
||||
// fetch its JSON-schema `parameters` so each required arg gets its own
|
||||
// labeled ExpressionField row. `null` (fetch failed / unknown action /
|
||||
// native tool) degrades gracefully to just the raw JsonField below. The
|
||||
// fetched schema is tagged with its toolkit/slug key so switching action
|
||||
// discards a stale schema without a synchronous setState in the effect.
|
||||
const schemaKey = `${toolkit} | ||||