mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
Fix/agentflow Fixing agentflow UI style and make it similar to v2 (#6280)
* Fix output node not selectable * Fix highlight color * Fix previous node displaying label and not ID * Fix tool arguements not being called * Fix dropdown values to not be bold * Add standard sizing for all input text * FIx syntax highlighting styling * Use predefined tokens for font weight and size * Fix dialog size of Source * Fix color of syntax highlight in dark mode * Fix the input fileds color in dark and regular mode * Fix gemini comments * FIx inconsistent spacing * Standardise gap between label and field * Remove export for unused function and standardize dialog size accrosss code, rich text, and source * Fix test error
This commit is contained in:
@@ -3,6 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import type { InputParam, NodeData } from '@/core/types'
|
||||
|
||||
import { ArrayInput } from './ArrayInput'
|
||||
import type { VariableItem } from './VariablePicker'
|
||||
|
||||
// --- Mocks ---
|
||||
const mockOnDataChange = jest.fn()
|
||||
@@ -10,13 +11,20 @@ const mockOnDataChange = jest.fn()
|
||||
jest.mock('./NodeInputHandler', () => ({
|
||||
NodeInputHandler: ({
|
||||
inputParam,
|
||||
onDataChange
|
||||
data,
|
||||
onDataChange,
|
||||
variableItems
|
||||
}: {
|
||||
inputParam: InputParam
|
||||
data: NodeData
|
||||
onDataChange: (args: { inputParam: InputParam; newValue: unknown }) => void
|
||||
variableItems?: VariableItem[]
|
||||
}) => (
|
||||
<div data-testid={`input-handler-${inputParam.name}`}>
|
||||
<div
|
||||
data-testid={`input-handler-${inputParam.name}`}
|
||||
data-inputs={JSON.stringify(data.inputs)}
|
||||
data-variable-items={variableItems ? JSON.stringify(variableItems.map((v) => v.value)) : undefined}
|
||||
>
|
||||
<label>{inputParam.label}</label>
|
||||
<input data-testid={`input-${inputParam.name}`} onChange={(e) => onDataChange({ inputParam, newValue: e.target.value })} />
|
||||
</div>
|
||||
@@ -485,4 +493,163 @@ describe('ArrayInput', () => {
|
||||
const deleteButton = screen.getByTitle('Delete')
|
||||
expect(deleteButton).toBeDisabled()
|
||||
})
|
||||
|
||||
// Parent inputs are merged into itemData so async load methods inside array items
|
||||
// (e.g. listToolInputArgs) can access parent-level fields like toolAgentflowSelectedTool.
|
||||
it('should merge parent node inputs into itemData so async options inside array items can access parent fields', () => {
|
||||
const dataWithParentField: NodeData = {
|
||||
...mockNodeData,
|
||||
inputs: {
|
||||
toolAgentflowSelectedTool: 'calculator',
|
||||
toolInputArgs: [{ inputArgName: '', inputArgValue: '' }]
|
||||
}
|
||||
} as NodeData
|
||||
|
||||
const toolInputArgsParam: InputParam = {
|
||||
id: 'toolInputArgs',
|
||||
name: 'toolInputArgs',
|
||||
label: 'Tool Input Arguments',
|
||||
type: 'array',
|
||||
array: [
|
||||
{ id: 'inputArgName', name: 'inputArgName', label: 'Input Argument Name', type: 'asyncOptions' } as InputParam,
|
||||
{ id: 'inputArgValue', name: 'inputArgValue', label: 'Input Argument Value', type: 'string' } as InputParam
|
||||
]
|
||||
}
|
||||
|
||||
render(<ArrayInput inputParam={toolInputArgsParam} data={dataWithParentField} onDataChange={mockOnDataChange} />)
|
||||
|
||||
const argNameHandler = screen.getByTestId('input-handler-inputArgName')
|
||||
const inputs = JSON.parse(argNameHandler.getAttribute('data-inputs') ?? '{}')
|
||||
|
||||
// The parent-level field must be visible to the array item's async input
|
||||
expect(inputs.toolAgentflowSelectedTool).toBe('calculator')
|
||||
// The item's own fields are also present
|
||||
expect(inputs.inputArgName).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
// ── acceptNodeOutputAsVariable in ArrayInput ────────────────────────────────
|
||||
|
||||
describe('ArrayInput – acceptNodeOutputAsVariable', () => {
|
||||
const baseVariableItems: VariableItem[] = [
|
||||
{ label: 'question', description: "User's question", category: 'Chat Context', value: '{{question}}' }
|
||||
]
|
||||
|
||||
it('prepends {{output}} to variableItems when nested param has acceptNodeOutputAsVariable', () => {
|
||||
const inputParam: InputParam = {
|
||||
id: 'updateState',
|
||||
name: 'agentUpdateState',
|
||||
label: 'Update State',
|
||||
type: 'array',
|
||||
acceptVariable: true,
|
||||
array: [
|
||||
{ id: 'key', name: 'key', label: 'Key', type: 'string' } as InputParam,
|
||||
{
|
||||
id: 'value',
|
||||
name: 'value',
|
||||
label: 'Value',
|
||||
type: 'string',
|
||||
acceptVariable: true,
|
||||
acceptNodeOutputAsVariable: true
|
||||
} as InputParam
|
||||
]
|
||||
}
|
||||
|
||||
const data: NodeData = {
|
||||
id: 'node-1',
|
||||
name: 'agentNode',
|
||||
label: 'Agent',
|
||||
inputs: { agentUpdateState: [{ key: 'myKey', value: '' }] }
|
||||
} as NodeData
|
||||
|
||||
render(<ArrayInput inputParam={inputParam} data={data} onDataChange={mockOnDataChange} variableItems={baseVariableItems} />)
|
||||
|
||||
const valueHandler = screen.getByTestId('input-handler-value')
|
||||
const passedItems: string[] = JSON.parse(valueHandler.getAttribute('data-variable-items') ?? '[]')
|
||||
expect(passedItems[0]).toBe('{{output}}')
|
||||
expect(passedItems).toContain('{{question}}')
|
||||
})
|
||||
|
||||
it('does NOT prepend {{output}} when nested param lacks acceptNodeOutputAsVariable', () => {
|
||||
const inputParam: InputParam = {
|
||||
id: 'updateState',
|
||||
name: 'agentUpdateState',
|
||||
label: 'Update State',
|
||||
type: 'array',
|
||||
acceptVariable: true,
|
||||
array: [{ id: 'value', name: 'value', label: 'Value', type: 'string', acceptVariable: true } as InputParam]
|
||||
}
|
||||
|
||||
const data: NodeData = {
|
||||
id: 'node-1',
|
||||
name: 'agentNode',
|
||||
label: 'Agent',
|
||||
inputs: { agentUpdateState: [{ value: '' }] }
|
||||
} as NodeData
|
||||
|
||||
render(<ArrayInput inputParam={inputParam} data={data} onDataChange={mockOnDataChange} variableItems={baseVariableItems} />)
|
||||
|
||||
const valueHandler = screen.getByTestId('input-handler-value')
|
||||
const passedItems: string[] = JSON.parse(valueHandler.getAttribute('data-variable-items') ?? '[]')
|
||||
expect(passedItems).not.toContain('{{output}}')
|
||||
expect(passedItems).toContain('{{question}}')
|
||||
})
|
||||
|
||||
it('includes structured output fields in prepended variables', () => {
|
||||
const inputParam: InputParam = {
|
||||
id: 'updateState',
|
||||
name: 'agentUpdateState',
|
||||
label: 'Update State',
|
||||
type: 'array',
|
||||
array: [
|
||||
{
|
||||
id: 'value',
|
||||
name: 'value',
|
||||
label: 'Value',
|
||||
type: 'string',
|
||||
acceptVariable: true,
|
||||
acceptNodeOutputAsVariable: true
|
||||
} as InputParam
|
||||
]
|
||||
}
|
||||
|
||||
const data: NodeData = {
|
||||
id: 'node-1',
|
||||
name: 'agentNode',
|
||||
label: 'Agent',
|
||||
inputs: {
|
||||
agentUpdateState: [{ value: '' }],
|
||||
agentStructuredOutput: [{ key: 'sentiment', description: 'Sentiment score' }]
|
||||
}
|
||||
} as NodeData
|
||||
|
||||
render(<ArrayInput inputParam={inputParam} data={data} onDataChange={mockOnDataChange} variableItems={baseVariableItems} />)
|
||||
|
||||
const valueHandler = screen.getByTestId('input-handler-value')
|
||||
const passedItems: string[] = JSON.parse(valueHandler.getAttribute('data-variable-items') ?? '[]')
|
||||
expect(passedItems).toContain('{{output}}')
|
||||
expect(passedItems).toContain('{{output.sentiment}}')
|
||||
})
|
||||
|
||||
it('does not pass variableItems when nested param has no acceptVariable', () => {
|
||||
const inputParam: InputParam = {
|
||||
id: 'updateState',
|
||||
name: 'agentUpdateState',
|
||||
label: 'Update State',
|
||||
type: 'array',
|
||||
array: [{ id: 'key', name: 'key', label: 'Key', type: 'string', acceptNodeOutputAsVariable: true } as InputParam]
|
||||
}
|
||||
|
||||
const data: NodeData = {
|
||||
id: 'node-1',
|
||||
name: 'agentNode',
|
||||
label: 'Agent',
|
||||
inputs: { agentUpdateState: [{ key: '' }] }
|
||||
} as NodeData
|
||||
|
||||
render(<ArrayInput inputParam={inputParam} data={data} onDataChange={mockOnDataChange} variableItems={baseVariableItems} />)
|
||||
|
||||
const keyHandler = screen.getByTestId('input-handler-key')
|
||||
expect(keyHandler.getAttribute('data-variable-items')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,6 +11,30 @@ import { type AsyncInputProps, type ConfigInputComponentProps, NodeInputHandler
|
||||
import { useStableKeys } from './useStableKeys'
|
||||
import type { VariableItem } from './VariablePicker'
|
||||
|
||||
/**
|
||||
* Builds VariableItems representing the current node's own output.
|
||||
* Added when a nested sub-field declares `acceptNodeOutputAsVariable: true`,
|
||||
* matching the v2 suggestionOption behaviour where `{{output}}` and
|
||||
* `{{output.<key>}}` appear for structured-output fields.
|
||||
*/
|
||||
function buildNodeOutputVariables(data: NodeData): VariableItem[] {
|
||||
const vars: VariableItem[] = [
|
||||
{ label: 'output', description: 'Output from the current node', category: 'Node Outputs', value: '{{output}}' }
|
||||
]
|
||||
const structured = (
|
||||
(data.inputs?.agentStructuredOutput ?? data.inputs?.llmStructuredOutput ?? []) as Array<{ key: string; description?: string }>
|
||||
).filter((item) => item?.key)
|
||||
for (const item of structured) {
|
||||
vars.push({
|
||||
label: `output.${item.key}`,
|
||||
description: item.description ?? `Structured output field: ${item.key}`,
|
||||
category: 'Node Outputs',
|
||||
value: `{{output.${item.key}}}`
|
||||
})
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
export interface ArrayInputProps {
|
||||
inputParam: InputParam
|
||||
data: NodeData
|
||||
@@ -118,10 +142,13 @@ export function ArrayInput({
|
||||
<>
|
||||
{/* Render each array item */}
|
||||
{arrayItems.map((itemValues, index) => {
|
||||
// Create item-specific data context for nested NodeInputHandler
|
||||
// Create item-specific data context for nested NodeInputHandler.
|
||||
// Merge parent inputs first so async load methods (e.g. listToolInputArgs)
|
||||
// can read parent-level fields like toolAgentflowSelectedTool, while
|
||||
// item-level fields take precedence via the spread order.
|
||||
const itemData: NodeData = {
|
||||
...data,
|
||||
inputs: itemValues
|
||||
inputs: { ...data.inputs, ...itemValues }
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -178,7 +205,13 @@ export function ArrayInput({
|
||||
onConfigChange={onConfigChange}
|
||||
arrayIndex={index}
|
||||
parentArrayParam={inputParam}
|
||||
variableItems={param.acceptVariable ? variableItems : undefined}
|
||||
variableItems={
|
||||
param.acceptVariable
|
||||
? param.acceptNodeOutputAsVariable
|
||||
? [...buildNodeOutputVariables(data), ...(variableItems ?? [])]
|
||||
: variableItems
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -191,7 +224,7 @@ export function ArrayInput({
|
||||
size='small'
|
||||
variant='outlined'
|
||||
disabled={disabled}
|
||||
sx={{ borderRadius: '16px', mt: 2 }}
|
||||
sx={{ borderRadius: '16px', mt: 1 }}
|
||||
startIcon={<IconPlus />}
|
||||
onClick={handleAddItem}
|
||||
>
|
||||
|
||||
@@ -171,13 +171,13 @@ export function ConditionBuilder({
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Box sx={{ px: 2, pb: 2 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
size='small'
|
||||
variant='outlined'
|
||||
disabled={disabled}
|
||||
sx={{ borderRadius: '16px', mt: 2 }}
|
||||
sx={{ borderRadius: '16px', mt: 1 }}
|
||||
startIcon={<IconPlus />}
|
||||
onClick={handleAddItem}
|
||||
>
|
||||
|
||||
@@ -125,13 +125,15 @@ export function ExpandTextDialog({
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={15}
|
||||
value={localValue}
|
||||
disabled={disabled}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
data-testid='source-input'
|
||||
inputProps={{ style: { fontFamily: 'monospace', fontSize: '0.85em' } }}
|
||||
sx={{
|
||||
'& .MuiInputBase-root': { height: 'calc(100vh - 220px)', alignItems: 'flex-start' },
|
||||
'& textarea': { height: '100% !important', overflowY: 'auto !important', boxSizing: 'border-box' }
|
||||
}}
|
||||
/>
|
||||
) : suggestionItems?.length ? (
|
||||
<VariableInput
|
||||
|
||||
@@ -220,13 +220,13 @@ export function MessagesInput({ inputParam, data, disabled = false, variableItem
|
||||
))}
|
||||
|
||||
{/* Add button */}
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Box sx={{ px: 2, pb: 2 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
size='small'
|
||||
variant='outlined'
|
||||
disabled={isAddDisabled}
|
||||
sx={{ borderRadius: '16px', mt: 2 }}
|
||||
sx={{ borderRadius: '16px', mt: 1 }}
|
||||
startIcon={<IconPlus />}
|
||||
onClick={handleAddMessage}
|
||||
>
|
||||
|
||||
@@ -210,13 +210,15 @@ export function NodeInputHandler({
|
||||
}
|
||||
if (isExpandable) {
|
||||
return (
|
||||
<RichTextEditor
|
||||
value={typeof value === 'string' ? value : ''}
|
||||
onChange={(html) => handleDataChange(html)}
|
||||
placeholder={inputParam.placeholder}
|
||||
disabled={disabled}
|
||||
rows={inputParam.rows || 4}
|
||||
/>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<RichTextEditor
|
||||
value={typeof value === 'string' ? value : ''}
|
||||
onChange={(html) => handleDataChange(html)}
|
||||
placeholder={inputParam.placeholder}
|
||||
disabled={disabled}
|
||||
rows={inputParam.rows || 4}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return (
|
||||
@@ -308,7 +310,7 @@ export function NodeInputHandler({
|
||||
// acceptVariable: show a button that opens a dialog with JsonInput + variable support
|
||||
return (
|
||||
<Button
|
||||
sx={{ borderRadius: 25, width: '100%', mb: 0, mt: 2 }}
|
||||
sx={{ borderRadius: 25, width: '100%', mb: 0, mt: 1 }}
|
||||
variant='outlined'
|
||||
disabled={disabled}
|
||||
onClick={() => setJsonDialogOpen(true)}
|
||||
|
||||
@@ -77,7 +77,8 @@ const StyledEditorContent = styled(EditorContent, {
|
||||
overflowY: rows ? 'auto' : 'hidden',
|
||||
overflowX: rows ? 'auto' : 'hidden',
|
||||
lineHeight: rows ? `${tokens.typography.rowHeightRem}em` : `${tokens.typography.singleLineLineHeightEm}em`,
|
||||
fontWeight: 500,
|
||||
fontSize: tokens.typography.fontSize.md,
|
||||
fontWeight: tokens.typography.fontWeight.medium,
|
||||
color: disabled ? theme.palette.action.disabled : theme.palette.text.primary,
|
||||
border: `1px solid ${tokens.colors.border.input[mode]}`,
|
||||
borderRadius: '10px',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Box, Button, IconButton, TextField, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-react'
|
||||
|
||||
import { tokens } from '@/core/theme/tokens'
|
||||
import type { StateUpdate } from '@/core/types'
|
||||
|
||||
import type { SuggestionItem } from './SuggestionDropdown'
|
||||
@@ -87,7 +88,7 @@ export function StateKeyValueInput({ value, onChange, disabled = false, suggesti
|
||||
<IconTrash size={18} />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant='body2' fontWeight={500} sx={{ mb: 0.5 }}>
|
||||
<Typography variant='body2' fontWeight={tokens.typography.fontWeight.medium} sx={{ mb: 0.5 }}>
|
||||
Key
|
||||
</Typography>
|
||||
<TextField
|
||||
@@ -99,7 +100,7 @@ export function StateKeyValueInput({ value, onChange, disabled = false, suggesti
|
||||
onChange={(e) => handleKeyChange(index, e.target.value)}
|
||||
/>
|
||||
|
||||
<Typography variant='body2' fontWeight={500} sx={{ mt: 1.5, mb: 0.5 }}>
|
||||
<Typography variant='body2' fontWeight={tokens.typography.fontWeight.medium} sx={{ mt: 1.5, mb: 0.5 }}>
|
||||
Value
|
||||
</Typography>
|
||||
<VariableInput
|
||||
|
||||
@@ -245,13 +245,13 @@ export function StructuredOutputBuilder({ inputParam, data, disabled = false, on
|
||||
))}
|
||||
|
||||
{/* Add button */}
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Box sx={{ px: 2, pb: 2 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
size='small'
|
||||
variant='outlined'
|
||||
disabled={isAddDisabled}
|
||||
sx={{ borderRadius: '16px', mt: 2 }}
|
||||
sx={{ borderRadius: '16px', mt: 1 }}
|
||||
startIcon={<IconPlus />}
|
||||
onClick={handleAddEntry}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState }
|
||||
import { Divider, List, ListItem, ListItemButton, Paper, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
import { tokens } from '@/core/theme/tokens'
|
||||
|
||||
/**
|
||||
* Shape of items returned by the TipTap suggestion `items()` callback.
|
||||
* Matches the UI package's SuggestionList item shape.
|
||||
@@ -131,7 +133,7 @@ export const SuggestionDropdown = forwardRef<SuggestionDropdownRef, SuggestionDr
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Typography variant='body1' sx={{ fontWeight: 500 }}>
|
||||
<Typography variant='body1' sx={{ fontWeight: tokens.typography.fontWeight.medium }}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
{item.description && (
|
||||
|
||||
@@ -61,7 +61,8 @@ const StyledEditorContent = styled(EditorContent, {
|
||||
overflowY: rows ? 'auto' : 'hidden',
|
||||
overflowX: rows ? 'auto' : 'hidden',
|
||||
lineHeight: rows ? `${tokens.typography.rowHeightRem}em` : `${tokens.typography.singleLineLineHeightEm}em`,
|
||||
fontWeight: 500,
|
||||
fontSize: tokens.typography.fontSize.md,
|
||||
fontWeight: tokens.typography.fontWeight.medium,
|
||||
color: disabled ? theme.palette.action.disabled : theme.palette.text.primary,
|
||||
border: `1px solid ${tokens.colors.border.input[mode]}`,
|
||||
borderRadius: '10px',
|
||||
@@ -88,14 +89,13 @@ const StyledEditorContent = styled(EditorContent, {
|
||||
height: 0
|
||||
},
|
||||
|
||||
// Mention (variable) chip styling — matches original UI green style
|
||||
'& .mention': {
|
||||
backgroundColor: mode === 'dark' ? 'rgba(76, 175, 80, 0.2)' : 'rgba(76, 175, 80, 0.08)',
|
||||
borderRadius: '4px',
|
||||
padding: '1px 4px',
|
||||
fontWeight: 600,
|
||||
color: mode === 'dark' ? '#81c784' : '#2e7d32',
|
||||
whiteSpace: 'nowrap'
|
||||
backgroundColor: '#b3f0b8',
|
||||
borderRadius: '0.4rem',
|
||||
boxDecorationBreak: 'clone',
|
||||
WebkitBoxDecorationBreak: 'clone',
|
||||
color: '#0d7115',
|
||||
padding: '0.1rem 0.3rem'
|
||||
},
|
||||
|
||||
// Block element spacing
|
||||
|
||||
@@ -135,9 +135,9 @@ describe('createAgentflowTheme', () => {
|
||||
expect(theme.typography.h4.fontWeight).toBe(600)
|
||||
})
|
||||
|
||||
it('should set h5 to 0.875rem / 600 weight', () => {
|
||||
it('should set h5 to 0.875rem / 500 weight', () => {
|
||||
expect(theme.typography.h5.fontSize).toBe('0.875rem')
|
||||
expect(theme.typography.h5.fontWeight).toBe(600)
|
||||
expect(theme.typography.h5.fontWeight).toBe(500)
|
||||
})
|
||||
|
||||
it('should set h6 to 0.75rem / 500 weight', () => {
|
||||
|
||||
@@ -41,12 +41,12 @@ export function createAgentflowTheme(isDarkMode: boolean): Theme {
|
||||
}
|
||||
},
|
||||
typography: {
|
||||
h4: { fontSize: '1rem', fontWeight: 600 },
|
||||
h5: { fontSize: '0.875rem', fontWeight: 600 },
|
||||
h6: { fontSize: '0.75rem', fontWeight: 500 },
|
||||
subtitle1: { fontSize: '0.875rem', fontWeight: 500 },
|
||||
body1: { fontSize: '0.875rem', fontWeight: 400 },
|
||||
body2: { fontSize: '0.75rem', fontWeight: 400 }
|
||||
h4: { fontSize: tokens.typography.fontSize.lg, fontWeight: tokens.typography.fontWeight.semibold },
|
||||
h5: { fontSize: tokens.typography.fontSize.md, fontWeight: tokens.typography.fontWeight.medium },
|
||||
h6: { fontSize: tokens.typography.fontSize.sm, fontWeight: tokens.typography.fontWeight.medium },
|
||||
subtitle1: { fontSize: tokens.typography.fontSize.md, fontWeight: tokens.typography.fontWeight.medium },
|
||||
body1: { fontSize: tokens.typography.fontSize.md, fontWeight: tokens.typography.fontWeight.regular },
|
||||
body2: { fontSize: tokens.typography.fontSize.sm, fontWeight: tokens.typography.fontWeight.regular }
|
||||
},
|
||||
components: {
|
||||
MuiPaper: {
|
||||
@@ -62,7 +62,39 @@ export function createAgentflowTheme(isDarkMode: boolean): Theme {
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: tokens.colors.background.input[mode]
|
||||
backgroundColor: tokens.colors.background.input[mode],
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: tokens.colors.border.input[mode]
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: tokens.colors.border.hover[mode]
|
||||
}
|
||||
},
|
||||
input: {
|
||||
fontWeight: tokens.typography.fontWeight.medium
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&.Mui-selected': {
|
||||
color: isDarkMode ? '#fff' : tokens.colors.palette.secondary.dark,
|
||||
backgroundColor: isDarkMode
|
||||
? tokens.colors.background.listItemSelected.dark
|
||||
: tokens.colors.palette.secondary.light,
|
||||
'&:hover': {
|
||||
backgroundColor: isDarkMode
|
||||
? tokens.colors.background.listItemSelected.dark
|
||||
: tokens.colors.palette.secondary.light
|
||||
}
|
||||
},
|
||||
'&:hover': {
|
||||
color: isDarkMode ? '#fff' : tokens.colors.palette.secondary.dark,
|
||||
backgroundColor: isDarkMode
|
||||
? tokens.colors.background.listItemSelected.dark
|
||||
: tokens.colors.palette.secondary.light
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ const baseColors = {
|
||||
gray800: '#333',
|
||||
|
||||
// Dark mode grays
|
||||
lightBorderDefault: '#21212125',
|
||||
darkBorderDefault: 'rgba(255, 255, 255, 0.145)',
|
||||
darkGray100: '#1a1a1a',
|
||||
darkGray200: '#1a1a2e',
|
||||
@@ -35,6 +36,7 @@ const baseColors = {
|
||||
darkGray350: '#23262c',
|
||||
darkGray400: '#2d2d2d',
|
||||
darkGray450: '#32353b',
|
||||
darkGray460: '#454c59',
|
||||
darkGray500: '#404040',
|
||||
darkBlueHover: '#233345',
|
||||
darkGray600: '#525252',
|
||||
@@ -125,14 +127,15 @@ export const tokens = {
|
||||
card: { light: baseColors.white, dark: baseColors.darkGray350 },
|
||||
cardHover: { light: baseColors.gray75, dark: baseColors.darkGray500 },
|
||||
header: { light: baseColors.white, dark: baseColors.darkGray400 },
|
||||
input: { light: baseColors.white, dark: baseColors.darkGray450 },
|
||||
optionHover: { light: '', dark: baseColors.darkBlueHover }
|
||||
input: { light: baseColors.gray50, dark: baseColors.darkGray450 },
|
||||
optionHover: { light: '', dark: baseColors.darkBlueHover },
|
||||
listItemSelected: { light: '', dark: baseColors.darkGray460 }
|
||||
},
|
||||
|
||||
border: {
|
||||
default: { light: baseColors.gray300, dark: baseColors.darkBorderDefault },
|
||||
hover: { light: baseColors.gray400, dark: baseColors.darkGray600 },
|
||||
input: { light: baseColors.gray400, dark: baseColors.darkGray750 },
|
||||
input: { light: baseColors.lightBorderDefault, dark: baseColors.darkGray750 },
|
||||
validation: baseColors.nodeCondition
|
||||
},
|
||||
|
||||
@@ -241,6 +244,21 @@ export const tokens = {
|
||||
|
||||
// Typography
|
||||
typography: {
|
||||
// Font size scale
|
||||
fontSize: {
|
||||
xs: '0.625rem', // 10px — badge, compact caption
|
||||
sm: '0.75rem', // 12px — secondary label, helper text
|
||||
md: '0.875rem', // 14px — body, input, primary label
|
||||
lg: '1rem' // 16px — section heading
|
||||
},
|
||||
|
||||
// Font weight scale
|
||||
fontWeight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600
|
||||
},
|
||||
|
||||
/** Matches MUI OutlinedInput's default line-height (1.4375em) so the
|
||||
* RichTextEditor aligns with standard TextField height at the same row count. */
|
||||
rowHeightRem: 1.4375,
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface InputParam {
|
||||
rows?: number
|
||||
description?: string
|
||||
acceptVariable?: boolean
|
||||
acceptNodeOutputAsVariable?: boolean
|
||||
additionalParams?: boolean
|
||||
show?: Record<string, unknown>
|
||||
hide?: Record<string, unknown>
|
||||
|
||||
@@ -175,7 +175,7 @@ function ValidationFeedbackComponent({ nodes, edges, availableNodes, setNodes }:
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant='h6' sx={{ mt: 1, mb: 2, fontWeight: 600 }}>
|
||||
<Typography variant='h6' sx={{ mt: 1, mb: 2, fontWeight: tokens.typography.fontWeight.semibold }}>
|
||||
Checklist ({results.reduce((sum, r) => sum + r.issues.length, 0)})
|
||||
</Typography>
|
||||
|
||||
@@ -195,7 +195,9 @@ function ValidationFeedbackComponent({ nodes, edges, availableNodes, setNodes }:
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
{getNodeIcon(item)}
|
||||
<Typography sx={{ fontWeight: 500 }}>{item.label || item.name}</Typography>
|
||||
<Typography sx={{ fontWeight: tokens.typography.fontWeight.medium }}>
|
||||
{item.label || item.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{item.issues.map((issue, issueIndex) => (
|
||||
@@ -235,7 +237,11 @@ function ValidationFeedbackComponent({ nodes, edges, availableNodes, setNodes }:
|
||||
alt='Illustration of a checklist with no items, indicating no issues found'
|
||||
/>
|
||||
{hasValidated ? (
|
||||
<Typography variant='body2' color='success.main' sx={{ mt: 2, fontWeight: 500 }}>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='success.main'
|
||||
sx={{ mt: 2, fontWeight: tokens.typography.fontWeight.medium }}
|
||||
>
|
||||
No issues found in your flow!
|
||||
</Typography>
|
||||
) : (
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconSparkles } from '@tabler/icons-react'
|
||||
|
||||
import { tokens } from '@/core/theme/tokens'
|
||||
import type { FlowData } from '@/core/types'
|
||||
import { useApiContext, useConfigContext } from '@/infrastructure/store'
|
||||
|
||||
@@ -161,7 +162,7 @@ function GenerateFlowDialogComponent({ open, onClose, onGenerated }: GenerateFlo
|
||||
onClose={loading ? undefined : onClose}
|
||||
aria-labelledby='generate-flow-dialog-title'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='generate-flow-dialog-title'>
|
||||
<DialogTitle sx={{ fontSize: tokens.typography.fontSize.lg }} id='generate-flow-dialog-title'>
|
||||
What would you like to build?
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { JsonInput } from '@/atoms/JsonInput'
|
||||
import { SwitchInput } from '@/atoms/SwitchInput'
|
||||
import { TooltipWithParser } from '@/atoms/TooltipWithParser'
|
||||
import { getDefaultValueForType } from '@/core/primitives'
|
||||
import { tokens } from '@/core/theme/tokens'
|
||||
import type { ComponentCredentialSchema, CredentialSchemaInput } from '@/core/types'
|
||||
import { useApiContext } from '@/infrastructure/store/ApiContext'
|
||||
|
||||
@@ -184,7 +185,7 @@ export function CreateCredentialDialog({ open, credentialNames, onClose, onCreat
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} fullWidth maxWidth={showSelection ? 'md' : 'sm'}>
|
||||
<DialogTitle sx={{ fontSize: '1rem', p: 3, pb: 0 }}>
|
||||
<DialogTitle sx={{ fontSize: tokens.typography.fontSize.lg, p: 3, pb: 0 }}>
|
||||
{selectedSchema ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<div style={{ marginRight: 10 }}>
|
||||
@@ -236,7 +237,7 @@ export function CreateCredentialDialog({ open, credentialNames, onClose, onCreat
|
||||
)}
|
||||
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant='overline'>
|
||||
<Typography>
|
||||
Credential Name
|
||||
<span style={{ color: theme.palette.error.main }}> *</span>
|
||||
</Typography>
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('useAvailableVariables', () => {
|
||||
const nodeOutputs = result.current.filter((i) => i.category === 'Node Outputs')
|
||||
expect(nodeOutputs).toHaveLength(1)
|
||||
expect(nodeOutputs[0].value).toBe('{{llm_0}}')
|
||||
expect(nodeOutputs[0].label).toBe('llmAgentflow')
|
||||
expect(nodeOutputs[0].label).toBe('llm_0')
|
||||
})
|
||||
|
||||
it('excludes startAgentflow from node outputs', () => {
|
||||
|
||||
@@ -78,7 +78,6 @@ export function useAvailableVariables(nodeId: string): VariableItem[] {
|
||||
(node.data.inputs?.chainName as string) ??
|
||||
(node.data.inputs?.functionName as string) ??
|
||||
(node.data.inputs?.variableName as string) ??
|
||||
node.data.label ??
|
||||
node.data.id
|
||||
|
||||
const agentflowIcon = getAgentflowIcon(node.data.name)
|
||||
|
||||
Reference in New Issue
Block a user