Skip to content

Commit e54c6dd

Browse files
added TimerNode and other additional code and docs updates
1 parent d2af7ad commit e54c6dd

50 files changed

Lines changed: 606 additions & 329 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

client/src/components/BaseDialog/BaseDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function BaseDialog ({
4141
<DialogTitle style={{cursor: 'move'}} id="draggable-dialog-title">
4242
{title}
4343
</DialogTitle>
44-
<DialogContent sx={{p: 0, m: 2}}>
44+
<DialogContent sx={{pt: '16px !important', pb: 0, pl: 0, pr: 0, m: 2}}>
4545
{children}
4646
</DialogContent>
4747
<DialogActions>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/************************************************************************
2+
* Copyright (C) 2025 Code Forge Temple *
3+
* This file is part of agentic-signal project *
4+
* See the LICENSE file in the project root for license details. *
5+
************************************************************************/
6+
7+
import {TextField, TextFieldProps} from '@mui/material';
8+
import {useDebouncedState} from '../../hooks/useDebouncedState';
9+
import {useEffect} from 'react';
10+
11+
type DebouncedTextFieldProps = Omit<TextFieldProps, 'onChange'> & {
12+
value: string | number;
13+
onChange: (value: string) => void;
14+
delay?: number;
15+
};
16+
17+
export function DebouncedTextField ({
18+
value,
19+
onChange,
20+
delay = 300,
21+
type = 'text',
22+
...textFieldProps
23+
}: DebouncedTextFieldProps) {
24+
const [debouncedValue, setDebouncedValue] = useDebouncedState({
25+
callback: onChange,
26+
delay,
27+
initialValue: String(value)
28+
});
29+
30+
useEffect(() => {
31+
if (String(value) !== debouncedValue) {
32+
setDebouncedValue(String(value));
33+
}
34+
// eslint-disable-next-line react-hooks/exhaustive-deps
35+
}, [value]);
36+
37+
return (
38+
<TextField
39+
{...textFieldProps}
40+
type={type}
41+
value={debouncedValue}
42+
onChange={(e) => setDebouncedValue(e.target.value)}
43+
/>
44+
);
45+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './DebouncedTextField';

client/src/components/Dock/components/ActionsDock/components/Settings/Settings.tsx

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,15 @@
77
import {useState, useEffect} from 'react';
88
import './style.scss';
99
import {useSettings} from '../../../../../../hooks/useSettings';
10-
import {useDebouncedState} from '../../../../../../hooks/useDebouncedState';
11-
import {TextField, IconButton, InputAdornment, Box, LinearProgress, List, ListItem, ListItemText} from '@mui/material';
10+
import {IconButton, InputAdornment, Box, LinearProgress, List, ListItem, ListItemText} from '@mui/material';
1211
import {Plus, Trash} from 'iconoir-react';
1312
import {OllamaService} from '../../../../../../services/ollamaService';
13+
import {DebouncedTextField} from '../../../../../DebouncedTextField';
1414

1515
export const Settings = () => {
1616
const {settings, setSetting} = useSettings();
1717
const [hoveredOllamaListItemIdx, setHoveredOllamaListItemIdx] = useState<number | null>(null);
1818

19-
const [debouncedOllamaHost, setDebouncedOllamaHost] = useDebouncedState({
20-
callback: (value: string) => {
21-
setSetting("ollamaHost", value);
22-
},
23-
delay: 300,
24-
initialValue: settings.ollamaHost
25-
});
26-
2719
const [modelInput, setModelInput] = useState('');
2820
const [isPulling, setIsPulling] = useState(false);
2921
const [progress, setProgress] = useState<number | null>(null);
@@ -97,24 +89,24 @@ export const Settings = () => {
9789

9890
return (
9991
<>
100-
<TextField
92+
<DebouncedTextField
10193
label="Ollama Host"
10294
variant="outlined"
10395
fullWidth
104-
value={debouncedOllamaHost}
105-
onChange={(e) => setDebouncedOllamaHost(e.target.value)}
96+
value={settings.ollamaHost}
97+
onChange={(value) => setSetting("ollamaHost", value)}
10698
sx={{mt: 2}}
10799
/>
108100
<Box sx={{display: 'flex', alignItems: 'center', mt: 2}}>
109-
<TextField
101+
<DebouncedTextField
110102
label="Add Ollama Model"
111103
variant="outlined"
112104
value={
113105
isPulling && progress !== null
114106
? `Downloading ${modelInput} ${progress}%`
115107
: modelInput
116108
}
117-
onChange={e => setModelInput(e.target.value)}
109+
onChange={value => setModelInput(value)}
118110
disabled={isPulling || !isOllamaHostValid}
119111
fullWidth
120112
helperText={!isOllamaHostValid ? "Invalid Ollama host - cannot add models" : ""}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/************************************************************************
2+
* Copyright (C) 2025 Code Forge Temple *
3+
* This file is part of agentic-signal project *
4+
* See the LICENSE file in the project root for license details. *
5+
************************************************************************/
6+
7+
import {ReactNode} from 'react';
8+
import {FormControl, FormLabel} from "@mui/material";
9+
10+
11+
type FieldsetGroupProps = {
12+
children: ReactNode;
13+
title: string;
14+
};
15+
16+
export const FieldsetGroup = ({children, title}: FieldsetGroupProps) => {
17+
return (
18+
<FormControl component="fieldset"
19+
sx={{
20+
width: '100%',
21+
boxSizing: 'border-box',
22+
border: '1px solid',
23+
borderColor: 'divider',
24+
borderRadius: 2,
25+
pt: 2, pb: 0, pl: 2, pr: 2,
26+
mb: 2,
27+
}}
28+
>
29+
<FormLabel
30+
component="legend"
31+
sx={{
32+
color: 'text.secondary',
33+
fontSize: 'small',
34+
padding: '0 7px 0 7px',
35+
margin: '0 -7px 0 -7px !important',
36+
fontWeight: 400,
37+
lineHeight: 1.4375,
38+
mb: 1
39+
}}
40+
>
41+
{title}
42+
</FormLabel>
43+
{children}
44+
</FormControl>
45+
);
46+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './FieldsetGroup';

client/src/components/nodes/BaseNode/BaseNode.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const DEFAULT_PORT_COLOR = "#ffc107";
1515

1616
const PORT_IDS = {
1717
input: "left-target",
18-
output: "right-source"
18+
output: "right-source",
1919
};
2020

2121
type OnClick = (() => void) | {callback: () => void; highlight: boolean};
@@ -24,13 +24,14 @@ type BaseNodeProps = {
2424
id: string;
2525
nodeIcon: React.ReactElement<{className?: string}>;
2626
title: string;
27+
run?: OnClick;
2728
running?: boolean;
29+
stoppable?: boolean;
2830
ports: {
2931
input?: boolean | {isValidConnection?: IsValidConnection, color?: string};
3032
output?: boolean | {isValidConnection?: IsValidConnection, color?: string};
3133
};
3234
settings?: OnClick;
33-
run?: OnClick;
3435
logs?: OnClick;
3536
output?: OnClick;
3637
extraPorts?: React.ReactNode;
@@ -46,7 +47,7 @@ function buttonsPropsFactory (buttonProp: OnClick): React.SVGProps<SVGSVGElement
4647
}
4748
}
4849

49-
export const BaseNode = ({id, nodeIcon, title, running, ports, settings, run, logs, output, extraPorts}: BaseNodeProps) => {
50+
export const BaseNode = ({id, nodeIcon, title, running, ports, settings, run, stoppable, logs, output, extraPorts}: BaseNodeProps) => {
5051
const {setEdges} = useReactFlow();
5152

5253
useEffect(() => {
@@ -106,7 +107,8 @@ export const BaseNode = ({id, nodeIcon, title, running, ports, settings, run, lo
106107
<EyeSolid {...buttonsPropsFactory(output)} />
107108
</Tooltip>
108109
) : null}
109-
{run && running ? <PlaySolid width={20} height={20} className="highlight" /> : null}
110+
{run && running && !stoppable ? <PlaySolid width={20} height={20} className="highlight" /> : null}
111+
{run && running && stoppable ? <PlaySolid {...buttonsPropsFactory(run)} className="highlight" /> : null}
110112
{run && !running ? (
111113
<Tooltip title={"run"} placement="bottom" arrow enterDelay={1000}>
112114
<Play {...buttonsPropsFactory(run)} />

client/src/components/nodes/DataSourceNode.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {BaseDialog} from "../BaseDialog";
1515
import {LogsDialog} from "../LogsDialog";
1616
import {TaskNodeIcons} from "../../constants";
1717
import {useDebouncedState} from "../../hooks/useDebouncedState";
18+
import {useTimerTrigger} from "../../hooks/useTimerTrigger";
19+
import {TimerTriggerPort} from "./TimerNode/TimerTriggerPort";
1820

1921
const DATA_SOURCE_TYPES = {
2022
TEXT: "text",
@@ -31,7 +33,7 @@ export function DataSourceNode ({data, id, type}: NodeProps<AppNode>) {
3133
const [isRunning, setIsRunning] = useState(false);
3234
const [error, setError] = useState<string | null>(null);
3335
const [openLogs, setOpenLogs] = useState(false);
34-
const {title, dataSource, onResultUpdate, onConfigChange} = data;
36+
const {title, dataSource, input, onResultUpdate, onConfigChange} = data;
3537

3638
const handleRun = useCallback(() => {
3739
setError(null);
@@ -55,6 +57,8 @@ export function DataSourceNode ({data, id, type}: NodeProps<AppNode>) {
5557
}, setIsRunning);
5658
}, [dataSource.type, dataSource.value, id, onResultUpdate]);
5759

60+
useTimerTrigger(input?.timerTrigger, handleRun);
61+
5862
const [debouncedDataSource, setDebouncedDataSource] = useDebouncedState({
5963
callback: (value: string) => {
6064
onConfigChange(id, {dataSource: {...dataSource, value}});
@@ -71,6 +75,9 @@ export function DataSourceNode ({data, id, type}: NodeProps<AppNode>) {
7175
ports={{
7276
output: true
7377
}}
78+
extraPorts = {
79+
<TimerTriggerPort />
80+
}
7481
title={title}
7582
run={handleRun}
7683
running={isRunning}

client/src/components/nodes/GetDataNode.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import {type NodeProps} from "@xyflow/react";
88
import {useCallback, useState} from "react";
99
import {AppNode, assertIsEnhancedNodeData, assertIsGetDataNodeData} from "../../types/workflow";
1010
import {BaseNode} from "./BaseNode";
11-
import {FormControl, InputLabel, MenuItem, Select, TextField} from "@mui/material";
11+
import {FormControl, InputLabel, MenuItem, Select} from "@mui/material";
1212

1313
import {runTask} from "./BaseNode/utils";
1414
import {BaseDialog} from "../BaseDialog";
1515
import {LogsDialog} from "../LogsDialog";
1616
import {FetchDataType, TaskNodeIcons} from "../../constants";
1717
import {parseUrl} from "../../utils";
18-
import {useDebouncedState} from "../../hooks/useDebouncedState";
18+
import {DebouncedTextField} from "../DebouncedTextField";
19+
import {useTimerTrigger} from "../../hooks/useTimerTrigger";
20+
import {TimerTriggerPort} from "./TimerNode/TimerTriggerPort";
1921

2022
const DATA_TYPE_LABEL = "Data Type";
2123

@@ -27,24 +29,25 @@ export function GetDataNode ({data, id, type}: NodeProps<AppNode>) {
2729
const [error, setError] = useState<string | null>(null);
2830
const [openSettings, setOpenSettings] = useState(false);
2931
const [openLogs, setOpenLogs] = useState(false);
32+
const {input, url, title, dataType, onResultUpdate, onConfigChange} = data;
3033

3134
const handleRun = useCallback(() => {
3235
setError(null);
33-
data.onResultUpdate(id);
36+
onResultUpdate(id);
3437

3538
runTask(async () => {
3639
try {
37-
const url = parseUrl(data.url);
40+
const parsedUrl = parseUrl(url);
3841

39-
const response = await fetch(url);
42+
const response = await fetch(parsedUrl);
4043

4144
if (!response.ok) {
4245
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
4346
}
4447

4548
let getData;
4649

47-
switch (data.dataType) {
50+
switch (dataType) {
4851
case "json":
4952
getData = await response.json();
5053
break;
@@ -61,25 +64,19 @@ export function GetDataNode ({data, id, type}: NodeProps<AppNode>) {
6164
getData = await response.text();
6265
}
6366

64-
data.onResultUpdate(id, getData);
67+
onResultUpdate(id, getData);
6568
} catch (error) {
6669
setError(`Error fetching data: ${error instanceof Error ? error.message : 'Unknown error'}`);
6770

68-
data.onResultUpdate(id);
71+
onResultUpdate(id);
6972
}
7073

7174
}, setIsRunning);
7275
}, [data, id]);
7376

74-
const [url, setUrl] = useDebouncedState({
75-
callback: (value: string) => {
76-
data.onConfigChange(id, {url: value});
77-
},
78-
delay: 300,
79-
initialValue: data.url
80-
});
77+
useTimerTrigger(input?.timerTrigger, handleRun);
8178

82-
const hasMissingConfig = !data.url || !data.dataType;
79+
const hasMissingConfig = !url || !dataType;
8380

8481
return (
8582
<>
@@ -89,7 +86,10 @@ export function GetDataNode ({data, id, type}: NodeProps<AppNode>) {
8986
ports={{
9087
output: true
9188
}}
92-
title={data.title}
89+
extraPorts = {
90+
<TimerTriggerPort />
91+
}
92+
title={title}
9393
settings={{callback: () => setOpenSettings(true), highlight: hasMissingConfig}}
9494
run={handleRun}
9595
running={isRunning}
@@ -99,33 +99,33 @@ export function GetDataNode ({data, id, type}: NodeProps<AppNode>) {
9999
<LogsDialog
100100
open={openLogs}
101101
onClose={() => setOpenLogs(false)}
102-
title={data.title}
102+
title={title}
103103
error={error}
104104
/>
105105

106106
<BaseDialog
107107
open={openSettings}
108108
onClose={() => setOpenSettings(false)}
109-
title={data.title}
109+
title={title}
110110
>
111-
<TextField
111+
<DebouncedTextField
112112
label="URL"
113113
variant="outlined"
114114
fullWidth
115115
value={url}
116-
onChange={(e) => {
117-
setUrl(e.target.value);
116+
onChange={(value) => {
117+
onConfigChange(id, {url: value});
118118
}}
119-
sx={{mt: 2}}
119+
sx={{mb: 2}}
120120
/>
121121
<FormControl fullWidth size="small" sx={{mb: 2, mt: 1}}>
122122
<InputLabel id="data-type-label">{DATA_TYPE_LABEL}</InputLabel>
123123
<Select
124124
labelId="data-type-label"
125125
label={DATA_TYPE_LABEL}
126-
value={data.dataType || ""}
126+
value={dataType || ""}
127127
onChange={e => {
128-
data.onConfigChange(id, {dataType: e.target.value});
128+
onConfigChange(id, {dataType: e.target.value});
129129
}}
130130
>
131131
<MenuItem value="" disabled>

0 commit comments

Comments
 (0)