Merge remote-tracking branch 'origin/dev' into feat/send-program
# Conflicts: # src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx
This commit is contained in:
@@ -4,8 +4,15 @@ import type {LogFilterPredicate} from "./useLogs.ts";
|
||||
|
||||
import styles from "./Filters.module.css";
|
||||
|
||||
/**
|
||||
* A generic setter type compatible with React's state setters.
|
||||
*/
|
||||
type Setter<T> = (value: T | ((prev: T) => T)) => void;
|
||||
|
||||
/**
|
||||
* Mapping of log level names to their corresponding numeric severity.
|
||||
* Used for comparison in log filtering predicates.
|
||||
*/
|
||||
const optionMapping = new Map([
|
||||
["ALL", 0],
|
||||
["DEBUG", 10],
|
||||
@@ -16,6 +23,17 @@ const optionMapping = new Map([
|
||||
["NONE", 999_999_999_999], // It is technically possible to have a higher level, but this is fine
|
||||
]);
|
||||
|
||||
/**
|
||||
* Renders a single log-level selector (dropdown) for a specific filter target.
|
||||
*
|
||||
* Used by both the global filter and agent-specific filters.
|
||||
*
|
||||
* @param name - The display name or identifier for the filter target.
|
||||
* @param level - The currently selected log level.
|
||||
* @param setLevel - Function to update the selected log level.
|
||||
* @param onDelete - Optional callback for deleting this filter element.
|
||||
* @returns A JSX element that renders a labeled dropdown for selecting log levels.
|
||||
*/
|
||||
function LevelPredicateElement({
|
||||
name,
|
||||
level,
|
||||
@@ -54,8 +72,19 @@ function LevelPredicateElement({
|
||||
</div>
|
||||
}
|
||||
|
||||
/** Key used for the global log-level predicate in the filter map. */
|
||||
const GLOBAL_LOG_LEVEL_PREDICATE_KEY = "global_log_level";
|
||||
|
||||
/**
|
||||
* Renders and manages the **global log-level filter**.
|
||||
*
|
||||
* This component defines a baseline log level that all logs must meet or exceed
|
||||
* to be displayed, unless overridden by per-agent filters.
|
||||
*
|
||||
* @param filterPredicates - Map of current log filter predicates.
|
||||
* @param setFilterPredicates - Setter function to update the filter predicates map.
|
||||
* @returns A JSX element rendering the global log-level selector.
|
||||
*/
|
||||
function GlobalLevelFilter({
|
||||
filterPredicates,
|
||||
setFilterPredicates,
|
||||
@@ -78,6 +107,7 @@ function GlobalLevelFilter({
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize default global level on mount.
|
||||
useEffect(() => {
|
||||
if (filterPredicates.has(GLOBAL_LOG_LEVEL_PREDICATE_KEY)) return;
|
||||
setSelected("INFO");
|
||||
@@ -91,8 +121,21 @@ function GlobalLevelFilter({
|
||||
/>;
|
||||
}
|
||||
|
||||
/** Prefix for agent-specific log-level predicate keys in the filter map. */
|
||||
const AGENT_LOG_LEVEL_PREDICATE_KEY_PREFIX = "agent_log_level_";
|
||||
|
||||
/**
|
||||
* Renders and manages **per-agent log-level filters**.
|
||||
*
|
||||
* Allows the user to set specific log levels for individual agents, overriding
|
||||
* the global filter for those agents. Includes functionality to add, edit,
|
||||
* or remove agent-level filters.
|
||||
*
|
||||
* @param filterPredicates - Map of current log filter predicates.
|
||||
* @param setFilterPredicates - Setter function to update the filter predicates map.
|
||||
* @param agentNames - Set of agent names available for filtering.
|
||||
* @returns A JSX element rendering agent-level filters and a dropdown to add new ones.
|
||||
*/
|
||||
function AgentLevelFilters({
|
||||
filterPredicates,
|
||||
setFilterPredicates,
|
||||
@@ -105,7 +148,7 @@ function AgentLevelFilters({
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Click outside to close
|
||||
// Close dropdown or panels when clicking outside or pressing Escape.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDocClick = (e: MouseEvent) => {
|
||||
@@ -124,13 +167,16 @@ function AgentLevelFilters({
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
// Identify which predicates correspond to agents.
|
||||
const agentPredicates = [...filterPredicates.keys()].filter((key) =>
|
||||
key.startsWith(AGENT_LOG_LEVEL_PREDICATE_KEY_PREFIX));
|
||||
|
||||
/**
|
||||
* Create or change the predicate for an agent. If the level is not given, the global level is used.
|
||||
* @param agentName The name of the agent.
|
||||
* @param level The level to filter by. If not given, the global level is used.
|
||||
/**
|
||||
* Creates or updates the log filter predicate for a specific agent.
|
||||
* Falls back to the global log level if no level is specified.
|
||||
*
|
||||
* @param agentName - The name of the agent to filter.
|
||||
* @param level - Optional log level to apply; defaults to the global level.
|
||||
*/
|
||||
const setAgentPredicate = (agentName: string, level?: string ) => {
|
||||
level = level ?? filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value ?? "ALL";
|
||||
@@ -147,6 +193,11 @@ function AgentLevelFilters({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the log filter predicate for a specific agent.
|
||||
*
|
||||
* @param agentName - The name of the agent whose filter should be removed.
|
||||
*/
|
||||
const deleteAgentPredicate = (agentName: string) => {
|
||||
setFilterPredicates((curr) => {
|
||||
const fullName = AGENT_LOG_LEVEL_PREDICATE_KEY_PREFIX + agentName;
|
||||
@@ -184,6 +235,17 @@ function AgentLevelFilters({
|
||||
</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Filters component that aggregates global and per-agent log filters.
|
||||
*
|
||||
* Combines the global log-level filter and agent-specific filters into a unified UI.
|
||||
* Updates a shared `Map<string, LogFilterPredicate>` to determine which logs are shown.
|
||||
*
|
||||
* @param filterPredicates - The map of all active log filter predicates.
|
||||
* @param setFilterPredicates - Setter to update the map of predicates.
|
||||
* @param agentNames - Set of available agent names to display filters for.
|
||||
* @returns A React component that renders all log filter controls.
|
||||
*/
|
||||
export default function Filters({
|
||||
filterPredicates,
|
||||
setFilterPredicates,
|
||||
|
||||
@@ -8,13 +8,26 @@ import {type Cell, useCell} from "../../utils/cellStore.ts";
|
||||
|
||||
import styles from "./Logging.module.css";
|
||||
|
||||
|
||||
/**
|
||||
* Zustand store definition for managing user preferences related to logging.
|
||||
*
|
||||
* Includes flags for toggling relative timestamps and automatic scroll behavior.
|
||||
*/
|
||||
type LoggingSettings = {
|
||||
/** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */
|
||||
showRelativeTime: boolean;
|
||||
/** Updates the `showRelativeTime` setting. */
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => void;
|
||||
/** Whether the log view should automatically scroll to the newest entry. */
|
||||
scrollToBottom: boolean;
|
||||
/** Updates the `scrollToBottom` setting. */
|
||||
setScrollToBottom: (scrollToBottom: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global Zustand store for logging UI preferences.
|
||||
*/
|
||||
const useLoggingSettings = create<LoggingSettings>((set) => ({
|
||||
showRelativeTime: false,
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }),
|
||||
@@ -22,6 +35,16 @@ const useLoggingSettings = create<LoggingSettings>((set) => ({
|
||||
setScrollToBottom: (scrollToBottom: boolean) => set({ scrollToBottom }),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Renders a single log message entry with colored level indicators and timestamp formatting.
|
||||
*
|
||||
* This component automatically re-renders when the underlying log record (`recordCell`)
|
||||
* changes. It also triggers the `onUpdate` callback whenever the record updates (e.g., for auto-scrolling).
|
||||
*
|
||||
* @param recordCell - A reactive `Cell` containing a single `LogRecord`.
|
||||
* @param onUpdate - Optional callback triggered when the log entry updates.
|
||||
* @returns A JSX element displaying a formatted log message.
|
||||
*/
|
||||
function LogMessage({
|
||||
recordCell,
|
||||
onUpdate,
|
||||
@@ -33,7 +56,8 @@ function LogMessage({
|
||||
const record = useCell(recordCell);
|
||||
|
||||
/**
|
||||
* Normalizes the log level number to a multiple of 10, for which there are CSS styles.
|
||||
* Normalizes the log level number to a multiple of 10,
|
||||
* for which there are CSS styles. (e.g., INFO = 20, ERROR = 40).
|
||||
*/
|
||||
const normalizedLevelNo = (() => {
|
||||
// By default, the highest level is 50 (CRITICAL). Custom levels can be higher, but we don't have more critical color.
|
||||
@@ -42,8 +66,10 @@ function LogMessage({
|
||||
return Math.round(record.levelno / 10) * 10;
|
||||
})();
|
||||
|
||||
/** Simplifies the logger name by showing only the last path segment. */
|
||||
const normalizedName = record.name.split(".").pop() || record.name;
|
||||
|
||||
// Notify parent component (e.g. for scroll updates) when this record changes.
|
||||
useEffect(() => {
|
||||
if (onUpdate) onUpdate();
|
||||
}, [record, onUpdate]);
|
||||
@@ -65,11 +91,23 @@ function LogMessage({
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a scrollable list of log messages.
|
||||
*
|
||||
* Handles:
|
||||
* - Auto-scrolling when new messages arrive.
|
||||
* - Allowing users to scroll manually and disable auto-scroll.
|
||||
* - A floating "Scroll to bottom" button when not at the bottom.
|
||||
*
|
||||
* @param recordCells - Array of reactive log records to display.
|
||||
* @returns A scrollable log list component.
|
||||
*/
|
||||
function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const lastElementRef = useRef<HTMLLIElement>(null)
|
||||
const { scrollToBottom, setScrollToBottom } = useLoggingSettings();
|
||||
|
||||
// Disable auto-scroll if the user manually scrolls.
|
||||
useEffect(() => {
|
||||
if (!scrollableRef.current) return;
|
||||
const currentScrollableRef = scrollableRef.current;
|
||||
@@ -85,6 +123,12 @@ function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
}
|
||||
}, [scrollableRef, setScrollToBottom]);
|
||||
|
||||
/**
|
||||
* Scrolls the last log message into view if auto-scroll is enabled,
|
||||
* or if forced (e.g., user clicks "Scroll to bottom").
|
||||
*
|
||||
* @param force - If true, forces scrolling even if `scrollToBottom` is false.
|
||||
*/
|
||||
function scrollLastElementIntoView(force = false) {
|
||||
if ((!scrollToBottom && !force) || !lastElementRef.current) return;
|
||||
lastElementRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
@@ -111,6 +155,19 @@ function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level logging panel component.
|
||||
*
|
||||
* Combines:
|
||||
* - The `Filters` component for adjusting log visibility.
|
||||
* - The `LogMessages` component for displaying filtered logs.
|
||||
* - Zustand-managed UI settings (auto-scroll, timestamp display).
|
||||
*
|
||||
* This component uses the `useLogs` hook to fetch and filter logs based on
|
||||
* active predicates, and re-renders automatically as new logs arrive.
|
||||
*
|
||||
* @returns The complete logging UI as a React element.
|
||||
*/
|
||||
export default function Logging() {
|
||||
const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>());
|
||||
const { filteredLogs, distinctNames } = useLogs(filterPredicates)
|
||||
|
||||
@@ -3,6 +3,19 @@ import {useCallback, useEffect, useRef, useState} from "react";
|
||||
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
|
||||
import {cell, type Cell} from "../../utils/cellStore.ts";
|
||||
|
||||
/**
|
||||
* Represents a single log record emitted by the backend logging system.
|
||||
*
|
||||
* @property name - The name of the logger or source (e.g., `"agent.core"`).
|
||||
* @property message - The message content of the log record.
|
||||
* @property levelname - The human-readable severity level (e.g., `"INFO"`, `"ERROR"`).
|
||||
* @property levelno - The numeric severity value corresponding to `levelname`.
|
||||
* @property created - The UNIX timestamp (in seconds) when this record was created.
|
||||
* @property relativeCreated - The time (in milliseconds) since the logging system started.
|
||||
* @property reference - (Optional) A reference identifier linking related log messages.
|
||||
* @property firstCreated - Timestamp of the first log in this reference group.
|
||||
* @property firstRelativeCreated - Relative timestamp of the first log in this reference group.
|
||||
*/
|
||||
export type LogRecord = {
|
||||
name: string;
|
||||
message: string;
|
||||
@@ -15,29 +28,68 @@ export type LogRecord = {
|
||||
firstRelativeCreated: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & { value: any };
|
||||
/**
|
||||
* A log filter predicate with priority support, used to determine whether
|
||||
* a log record should be displayed.
|
||||
*
|
||||
* This extends a general `PriorityFilterPredicate` and includes an optional
|
||||
* `value` field for UI metadata (e.g., selected log level or agent).
|
||||
*
|
||||
* @template T - The type of record being filtered (here, `LogRecord`).
|
||||
*/
|
||||
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any };
|
||||
|
||||
/**
|
||||
* React hook that manages the lifecycle of log records, including:
|
||||
* - Receiving live log messages via Server-Sent Events (SSE),
|
||||
* - Applying priority-based filtering rules,
|
||||
* - Managing distinct logger names and reference-linked messages.
|
||||
*
|
||||
* Returns both the filtered logs (as reactive `Cell<LogRecord>` objects)
|
||||
* and a set of distinct logger names for use in UI components (e.g., Filters).
|
||||
*
|
||||
* @param filterPredicates - A `Map` of log filter predicates, keyed by ID or type.
|
||||
* @returns An object containing:
|
||||
* - `filteredLogs`: The currently visible (filtered) log messages.
|
||||
* - `distinctNames`: A set of all distinct logger names encountered.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { filteredLogs, distinctNames } = useLogs(activeFilters);
|
||||
* ```
|
||||
*/
|
||||
export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
|
||||
/** Distinct logger names encountered across all logs. */
|
||||
const [distinctNames, setDistinctNames] = useState<Set<string>>(new Set());
|
||||
/** Filtered logs that pass all active predicates, stored as reactive cells. */
|
||||
const [filtered, setFiltered] = useState<Cell<LogRecord>[]>([]);
|
||||
|
||||
/** Persistent reference to the active EventSource connection. */
|
||||
const sseRef = useRef<EventSource | null>(null);
|
||||
/** Keeps a stable reference to the current filter map (avoids re-renders). */
|
||||
const filtersRef = useRef(filterPredicates);
|
||||
/** Stores all received logs (the unfiltered full history). */
|
||||
const logsRef = useRef<LogRecord[]>([]);
|
||||
|
||||
/** Map to store the first message for each reference, instance can be updated to change contents. */
|
||||
const firstByRefRef = useRef<Map<string, Cell<LogRecord>>>(new Map());
|
||||
|
||||
/**
|
||||
* Apply the filter predicates to a log record.
|
||||
* Apply all active filter predicates to a log record.
|
||||
* @param log The log record to apply the filters to.
|
||||
* @returns `true` if the record passes.
|
||||
* @returns `true` if the record passes all filters; otherwise `false`.
|
||||
*/
|
||||
const applyFilters = useCallback((log: LogRecord) =>
|
||||
applyPriorityPredicates(log, [...filtersRef.current.values()]), []);
|
||||
|
||||
/** Recomputes the entire filtered list. Use when filter predicates change. */
|
||||
/**
|
||||
* Fully recomputes the filtered log list based on the current
|
||||
* filter predicates and historical logs.
|
||||
*
|
||||
* Should be invoked whenever the filter map changes.
|
||||
*/
|
||||
const recomputeFiltered = useCallback(() => {
|
||||
const newFiltered: Cell<LogRecord>[] = [];
|
||||
firstByRefRef.current = new Map();
|
||||
@@ -49,6 +101,7 @@ export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
|
||||
firstRelativeCreated: message.relativeCreated,
|
||||
});
|
||||
|
||||
// Handle reference grouping: update the first message in the group.
|
||||
if (message.reference) {
|
||||
const first = firstByRefRef.current.get(message.reference);
|
||||
if (first) {
|
||||
@@ -59,14 +112,14 @@ export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
|
||||
firstRelativeCreated: prev.firstRelativeCreated ?? prev.relativeCreated,
|
||||
}));
|
||||
|
||||
// Don't add it to the list again
|
||||
continue;
|
||||
continue; // Don't add it to the list again (it's a duplicate).
|
||||
} else {
|
||||
// Add the first message with this reference to the registry
|
||||
firstByRefRef.current.set(message.reference, messageCell);
|
||||
}
|
||||
}
|
||||
|
||||
// Include only if it passes current filters.
|
||||
if (applyFilters(message)) {
|
||||
newFiltered.push(messageCell);
|
||||
}
|
||||
@@ -75,20 +128,23 @@ export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
|
||||
setFiltered(newFiltered);
|
||||
}, [applyFilters, setFiltered]);
|
||||
|
||||
// Reapply filters to all logs, only when filters change
|
||||
// Re-filter all logs whenever filter predicates change.
|
||||
useEffect(() => {
|
||||
filtersRef.current = filterPredicates;
|
||||
recomputeFiltered();
|
||||
}, [filterPredicates, recomputeFiltered]);
|
||||
|
||||
/**
|
||||
* Handle a new log message. Updates the filtered list and to the full history.
|
||||
* @param message The new log message.
|
||||
* Handles a newly received log record.
|
||||
* Updates the full log history, distinct names set, and filtered log list.
|
||||
*
|
||||
* @param message - The new log record to process.
|
||||
*/
|
||||
const handleNewMessage = useCallback((message: LogRecord) => {
|
||||
// Add to the full history for re-filtering on filter changes
|
||||
// Store in complete history for future refiltering.
|
||||
logsRef.current.push(message);
|
||||
|
||||
// Track distinct logger names.
|
||||
setDistinctNames((prev) => {
|
||||
if (prev.has(message.name)) return prev;
|
||||
const newSet = new Set(prev);
|
||||
@@ -96,12 +152,14 @@ export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// Wrap in a reactive cell for UI binding.
|
||||
const messageCell = cell<LogRecord>({
|
||||
...message,
|
||||
firstCreated: message.created,
|
||||
firstRelativeCreated: message.relativeCreated,
|
||||
});
|
||||
|
||||
// Handle reference-linked updates.
|
||||
if (message.reference) {
|
||||
const first = firstByRefRef.current.get(message.reference);
|
||||
if (first) {
|
||||
@@ -112,20 +170,28 @@ export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
|
||||
firstRelativeCreated: prev.firstRelativeCreated ?? prev.relativeCreated,
|
||||
}));
|
||||
|
||||
// Don't add it to the list again
|
||||
return;
|
||||
return; // Do not duplicate reference group entries.
|
||||
} else {
|
||||
// Add the first message with this reference to the registry
|
||||
firstByRefRef.current.set(message.reference, messageCell);
|
||||
}
|
||||
}
|
||||
|
||||
// Only append if message passes filters.
|
||||
if (applyFilters(message)) {
|
||||
setFiltered((curr) => [...curr, messageCell]);
|
||||
}
|
||||
}, [applyFilters, setFiltered]);
|
||||
|
||||
/**
|
||||
* Initializes the SSE (Server-Sent Events) stream for real-time logs.
|
||||
*
|
||||
* Subscribes to messages from the backend logging endpoint and
|
||||
* dispatches each message to `handleNewMessage`.
|
||||
*
|
||||
* Cleans up the EventSource connection when the component unmounts.
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Only create one SSE connection for the lifetime of the hook.
|
||||
if (sseRef.current) return;
|
||||
|
||||
const es = new EventSource("http://localhost:8000/logs/stream");
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import {useEffect, useRef} from "react";
|
||||
|
||||
/**
|
||||
* An element that always scrolls into view when it is rendered. When added to a list, the entire list will scroll to show this element.
|
||||
* A React component that automatically scrolls itself into view whenever rendered.
|
||||
*
|
||||
* This component is especially useful in scrollable containers to keep the most
|
||||
* recent content visible (e.g., chat applications, live logs, or notifications).
|
||||
*
|
||||
* It uses the browser's `Element.scrollIntoView()` API with smooth scrolling behavior.
|
||||
*
|
||||
* @returns A `<div>` element that scrolls into view when mounted or updated.
|
||||
*/
|
||||
export default function ScrollIntoView() {
|
||||
/** Ref to the DOM element that will be scrolled into view. */
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,15 +2,22 @@ import {useState} from "react";
|
||||
import styles from "./TextField.module.css";
|
||||
|
||||
/**
|
||||
* A text input element in our own style that calls `setValue` at every keystroke.
|
||||
* A styled text input that updates its value **in real time** at every keystroke.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.value - The value of the text input.
|
||||
* @param {(value: string) => void} props.setValue - A function that sets the value of the text input.
|
||||
* @param {string} [props.placeholder] - The placeholder text for the text input.
|
||||
* @param {string} [props.className] - Additional CSS classes for the text input.
|
||||
* @param {string} [props.id] - The ID of the text input.
|
||||
* @param {string} [props.ariaLabel] - The ARIA label for the text input.
|
||||
* Automatically toggles between read-only and editable modes to integrate with
|
||||
* drag-based UIs (like React Flow). Calls `onCommit` when editing is completed.
|
||||
*
|
||||
* @param props - Component properties.
|
||||
* @param props.value - The current text input value.
|
||||
* @param props.setValue - Callback invoked on every keystroke to update the value.
|
||||
* @param props.onCommit - Callback invoked when editing is finalized (on blur or Enter).
|
||||
* @param props.placeholder - Optional placeholder text displayed when the input is empty.
|
||||
* @param props.className - Optional additional CSS class names.
|
||||
* @param props.id - Optional unique HTML `id` for the input element.
|
||||
* @param props.ariaLabel - Optional ARIA label for accessibility.
|
||||
* @param props.invalid - If true, applies error styling to indicate invalid input.
|
||||
*
|
||||
* @returns A styled `<input>` element that updates its value in real time.
|
||||
*/
|
||||
export function RealtimeTextField({
|
||||
value = "",
|
||||
@@ -31,14 +38,19 @@ export function RealtimeTextField({
|
||||
ariaLabel?: string,
|
||||
invalid?: boolean,
|
||||
}) {
|
||||
/** Tracks whether the input is currently read-only (for drag compatibility). */
|
||||
const [readOnly, setReadOnly] = useState(true);
|
||||
|
||||
/** Finalizes editing and calls `onCommit` when the user exits the field. */
|
||||
const updateData = () => {
|
||||
setReadOnly(true);
|
||||
onCommit();
|
||||
};
|
||||
|
||||
const updateOnEnter = (event: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") (event.target as HTMLInputElement).blur(); };
|
||||
/** Handles the Enter key — commits the input by triggering a blur event. */
|
||||
const updateOnEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter")
|
||||
(event.target as HTMLInputElement).blur(); };
|
||||
|
||||
return <input
|
||||
type={"text"}
|
||||
@@ -57,15 +69,22 @@ export function RealtimeTextField({
|
||||
}
|
||||
|
||||
/**
|
||||
* A text input element in our own style that calls `setValue` once the user presses the enter key or clicks outside the input.
|
||||
* A styled text input that updates its value **only on commit** (when the user
|
||||
* presses Enter or clicks outside the input).
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.value - The value of the text input.
|
||||
* @param {(value: string) => void} props.setValue - A function that sets the value of the text input.
|
||||
* @param {string} [props.placeholder] - The placeholder text for the text input.
|
||||
* @param {string} [props.className] - Additional CSS classes for the text input.
|
||||
* @param {string} [props.id] - The ID of the text input.
|
||||
* @param {string} [props.ariaLabel] - The ARIA label for the text input.
|
||||
* Internally wraps `RealtimeTextField` and buffers input changes locally,
|
||||
* calling `setValue` only once editing is complete.
|
||||
*
|
||||
* @param props - Component properties.
|
||||
* @param props.value - The current text input value.
|
||||
* @param props.setValue - Callback invoked when the user commits the change.
|
||||
* @param props.placeholder - Optional placeholder text displayed when the input is empty.
|
||||
* @param props.className - Optional additional CSS class names.
|
||||
* @param props.id - Optional unique HTML `id` for the input element.
|
||||
* @param props.ariaLabel - Optional ARIA label for accessibility.
|
||||
* @param props.invalid - If true, applies error styling to indicate invalid input.
|
||||
*
|
||||
* @returns A styled `<input>` element that updates its parent state only on commit.
|
||||
*/
|
||||
export function TextField({
|
||||
value = "",
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* A minimal counter component that demonstrates basic React state handling.
|
||||
*
|
||||
* Maintains an internal count value and provides buttons to increment and reset it.
|
||||
*
|
||||
* @returns A JSX element rendering the counter UI.
|
||||
*/
|
||||
function Counter() {
|
||||
/** The current counter value. */
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Displays the current connection status of a robot in real time.
|
||||
*
|
||||
* Opens an SSE connection to the backend (`/robot/ping_stream`) that emits
|
||||
* simple boolean JSON messages (`true` or `false`). Updates automatically when
|
||||
* the robot connects or disconnects.
|
||||
*
|
||||
* @returns A React element showing the current robot connection status.
|
||||
*/
|
||||
export default function ConnectedRobots() {
|
||||
|
||||
/**
|
||||
* The current connection state:
|
||||
* - `true`: Robot is connected.
|
||||
* - `false`: Robot is not connected.
|
||||
* - `null`: Connection status is unknown (initial check in progress).
|
||||
*/
|
||||
const [connected, setConnected] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// We're excepting a stream of data like that looks like this: `data = False` or `data = True`
|
||||
// Open a Server-Sent Events (SSE) connection to receive live ping updates.
|
||||
// We're expecting a stream of data like that looks like this: `data = False` or `data = True`
|
||||
const eventSource = new EventSource("http://localhost:8000/robot/ping_stream");
|
||||
eventSource.onmessage = (event) => {
|
||||
|
||||
// Receive message and parse
|
||||
// Expecting messages in JSON format: `true` or `false`
|
||||
console.log("received message:", event.data);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Set connected to value.
|
||||
try {
|
||||
setConnected(data)
|
||||
}
|
||||
@@ -26,6 +41,8 @@ export default function ConnectedRobots() {
|
||||
console.log("Ping message not in correct format:", event.data);
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up the SSE connection when the component unmounts.
|
||||
return () => eventSource.close();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@ import { Link } from 'react-router'
|
||||
import pepperLogo from '../../assets/pepper_transp2_small.svg'
|
||||
import styles from './Home.module.css'
|
||||
|
||||
/**
|
||||
* The home page component providing navigation and project branding.
|
||||
*
|
||||
* Renders the Pepper logo and a set of navigational links
|
||||
* implemented via React Router.
|
||||
*
|
||||
* @returns A JSX element representing the app’s home page.
|
||||
*/
|
||||
function Home() {
|
||||
return (
|
||||
<div className={`flex-col ${styles.gapXl}`}>
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Displays a live robot interaction panel with user input, conversation history,
|
||||
* and real-time updates from the robot backend via Server-Sent Events (SSE).
|
||||
*
|
||||
* @returns A React element rendering the interactive robot UI.
|
||||
*/
|
||||
export default function Robot() {
|
||||
/** The text message currently entered by the user. */
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
/** Whether the robot’s microphone or listening mode is currently active. */
|
||||
const [listening, setListening] = useState(false);
|
||||
const [conversation, setConversation] = useState<{"role": "user" | "assistant", "content": string}[]>([])
|
||||
/** The ongoing conversation history as a sequence of user/assistant messages. */
|
||||
const [conversation, setConversation] = useState<
|
||||
{"role": "user" | "assistant", "content": string}[]>([])
|
||||
/** Reference to the scrollable conversation container for auto-scrolling. */
|
||||
const conversationRef = useRef<HTMLDivElement | null>(null);
|
||||
/**
|
||||
* Index used to force refresh the SSE connection or clear conversation.
|
||||
* Incrementing this value triggers a reset of the live data stream.
|
||||
*/
|
||||
const [conversationIndex, setConversationIndex] = useState(0);
|
||||
|
||||
/**
|
||||
* Sends a message to the robot backend.
|
||||
*
|
||||
* Makes a POST request to `/message` with the user’s text.
|
||||
* The backend may respond with confirmation or error information.
|
||||
*/
|
||||
const sendMessage = async () => {
|
||||
try {
|
||||
const response = await fetch("http://localhost:8000/message", {
|
||||
@@ -24,6 +45,17 @@ export default function Robot() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Establishes a persistent Server-Sent Events (SSE) connection
|
||||
* to receive real-time updates from the robot backend.
|
||||
*
|
||||
* Handles three event types:
|
||||
* - `voice_active`: whether the robot is currently listening.
|
||||
* - `speech`: recognized user speech input.
|
||||
* - `llm_response`: the robot’s language model-generated reply.
|
||||
*
|
||||
* The connection resets whenever `conversationIndex` changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const eventSource = new EventSource("http://localhost:8000/sse");
|
||||
|
||||
@@ -43,6 +75,10 @@ export default function Robot() {
|
||||
};
|
||||
}, [conversationIndex]);
|
||||
|
||||
/**
|
||||
* Automatically scrolls the conversation view to the bottom
|
||||
* whenever a new message is added.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!conversationRef || !conversationRef.current) return;
|
||||
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
|
||||
|
||||
@@ -12,7 +12,10 @@ import TriggerNode, { TriggerConnects, TriggerReduce } from "./nodes/TriggerNode
|
||||
import { TriggerNodeDefaults } from "./nodes/TriggerNode.default";
|
||||
|
||||
/**
|
||||
* The types of the nodes we have registered.
|
||||
* Registered node types in the visual programming system.
|
||||
*
|
||||
* Key: the node type string used in the flow graph.
|
||||
* Value: the corresponding React component for rendering the node.
|
||||
*/
|
||||
export const NodeTypes = {
|
||||
start: StartNode,
|
||||
@@ -24,8 +27,9 @@ export const NodeTypes = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The default functions of the nodes we have registered.
|
||||
* Default data and settings for each node type.
|
||||
* These are defined in the <node>.default.ts files.
|
||||
* These defaults are used when a new node is created to initialize its properties.
|
||||
*/
|
||||
export const NodeDefaults = {
|
||||
start: StartNodeDefaults,
|
||||
@@ -38,7 +42,10 @@ export const NodeDefaults = {
|
||||
|
||||
|
||||
/**
|
||||
* The reduce functions of the nodes we have registered.
|
||||
* Reduce functions for each node type.
|
||||
*
|
||||
* A reduce function extracts the relevant data from a node and its children.
|
||||
* Used during graph evaluation or export.
|
||||
*/
|
||||
export const NodeReduces = {
|
||||
start: StartReduce,
|
||||
@@ -51,7 +58,9 @@ export const NodeReduces = {
|
||||
|
||||
|
||||
/**
|
||||
* The connection functionality of the nodes we have registered.
|
||||
* Connection functions for each node type.
|
||||
*
|
||||
* These functions define how nodes of a particular type can connect to other nodes.
|
||||
*/
|
||||
export const NodeConnects = {
|
||||
start: StartConnects,
|
||||
@@ -63,8 +72,9 @@ export const NodeConnects = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions that define whether a node should be deleted, currently constant only for start and end.
|
||||
* Any node types that aren't mentioned are 'true', and can be deleted by default.
|
||||
* Defines whether a node type can be deleted.
|
||||
*
|
||||
* Returns a function per node type. Nodes not explicitly listed are deletable by default.
|
||||
*/
|
||||
export const NodeDeletes = {
|
||||
start: () => false,
|
||||
@@ -72,8 +82,10 @@ export const NodeDeletes = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines which types are variables in the phase node-
|
||||
* any node that is NOT mentioned here, is automatically seen as a variable of a phase.
|
||||
* Defines which node types are considered variables in a phase node.
|
||||
*
|
||||
* Any node type not listed here is automatically treated as part of a phase.
|
||||
* This allows the system to dynamically group nodes under a phase node.
|
||||
*/
|
||||
export const NodesInPhase = {
|
||||
start: () => false,
|
||||
|
||||
@@ -13,13 +13,14 @@ import { NodeDefaults, NodeConnects, NodeDeletes } from './NodeRegistry';
|
||||
|
||||
|
||||
/**
|
||||
* Create a node given the correct data
|
||||
* @param type the type of the node to create
|
||||
* @param id the id of the node to create
|
||||
* @param position the position of the node to create
|
||||
* @param data the data in the node to create
|
||||
* @param deletable if this node should be able to be deleted IN ANY WAY POSSIBLE
|
||||
* @constructor
|
||||
* A Function to create a new node with the correct default data and properties.
|
||||
*
|
||||
* @param id - The unique ID of the node.
|
||||
* @param type - The type of node to create (must exist in NodeDefaults).
|
||||
* @param position - The XY position of the node in the flow canvas.
|
||||
* @param data - The data object to initialize the node with.
|
||||
* @param deletable - Optional flag to indicate if the node can be deleted (can be deleted by default).
|
||||
* @returns A fully initialized Node object ready to be added to the flow.
|
||||
*/
|
||||
function createNode(id: string, type: string, position: XYPosition, data: Record<string, unknown>, deletable? : boolean) {
|
||||
const defaultData = NodeDefaults[type as keyof typeof NodeDefaults]
|
||||
@@ -33,7 +34,7 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
|
||||
return {...defaultData, ...newData}
|
||||
}
|
||||
|
||||
//* Initial nodes, created by using createNode. */
|
||||
//* Initial nodes to populate the flow at startup.
|
||||
const initialNodes : Node[] = [
|
||||
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
|
||||
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
|
||||
@@ -41,7 +42,7 @@ const initialNodes : Node[] = [
|
||||
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"]}),
|
||||
];
|
||||
|
||||
// * Initial edges * /
|
||||
//* Initial edges to connect the startup nodes.
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'start-phase-1', source: 'start', target: 'phase-1' },
|
||||
{ id: 'phase-1-end', source: 'phase-1', target: 'end' },
|
||||
@@ -52,16 +53,33 @@ const initialEdges: Edge[] = [
|
||||
* How we have defined the functions for our FlowState.
|
||||
* We have the normal functionality of a default FlowState with some exceptions to account for extra functionality.
|
||||
* The biggest changes are in onConnect and onDelete, which we have given extra functionality based on the nodes defined functions.
|
||||
*
|
||||
* * Provides:
|
||||
* - Node and edge state management
|
||||
* - Node creation, deletion, and updates
|
||||
* - Custom connection handling via NodeConnects
|
||||
* - Edge reconnection handling
|
||||
*/
|
||||
const useFlowStore = create<FlowState>((set, get) => ({
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
edgeReconnectSuccessful: true,
|
||||
|
||||
/**
|
||||
* Handles changes to nodes triggered by ReactFlow.
|
||||
*/
|
||||
onNodesChange: (changes) =>
|
||||
set({nodes: applyNodeChanges(changes, get().nodes)}),
|
||||
|
||||
/**
|
||||
* Handles changes to edges triggered by ReactFlow.
|
||||
*/
|
||||
onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }),
|
||||
|
||||
/**
|
||||
* Handles creating a new connection between nodes.
|
||||
* Updates edges and calls the node-specific connection functions.
|
||||
*/
|
||||
onConnect: (connection) => {
|
||||
const edges = addEdge(connection, get().edges);
|
||||
const nodes = get().nodes;
|
||||
@@ -86,6 +104,9 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
set({ nodes, edges });
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles reconnecting an edge between nodes.
|
||||
*/
|
||||
onReconnect: (oldEdge, newConnection) => {
|
||||
get().edgeReconnectSuccessful = true;
|
||||
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
|
||||
@@ -98,7 +119,11 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
}
|
||||
set({ edgeReconnectSuccessful: true });
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a node by ID, respecting NodeDeletes rules.
|
||||
* Also removes all edges connected to that node.
|
||||
*/
|
||||
deleteNode: (nodeId) => {
|
||||
// Let's find our node to check if they have a special deletion function
|
||||
const ourNode = get().nodes.find((n)=>n.id==nodeId);
|
||||
@@ -112,10 +137,19 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
})}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Replaces the entire nodes array in the store.
|
||||
*/
|
||||
setNodes: (nodes) => set({ nodes }),
|
||||
|
||||
/**
|
||||
* Replaces the entire edges array in the store.
|
||||
*/
|
||||
setEdges: (edges) => set({ edges }),
|
||||
|
||||
/**
|
||||
* Updates the data of a node by merging new data with existing data.
|
||||
*/
|
||||
updateNodeData: (nodeId, data) => {
|
||||
set({
|
||||
nodes: get().nodes.map((node) => {
|
||||
@@ -127,6 +161,9 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new node to the flow store.
|
||||
*/
|
||||
addNode: (node: Node) => {
|
||||
set({ nodes: [...get().nodes, node] });
|
||||
},
|
||||
|
||||
@@ -2,23 +2,76 @@
|
||||
import type { Edge, OnNodesChange, OnEdgesChange, OnConnect, OnReconnect, Node } from '@xyflow/react';
|
||||
import type { NodeTypes } from './NodeRegistry';
|
||||
|
||||
export type AppNode = typeof NodeTypes
|
||||
/**
|
||||
* Type representing all registered node types.
|
||||
* This corresponds to the keys of NodeTypes in NodeRegistry.
|
||||
*/
|
||||
export type AppNode = typeof NodeTypes;
|
||||
|
||||
/**
|
||||
* The FlowState type defines the shape of the Zustand store used for managing the visual programming flow.
|
||||
*
|
||||
* Includes:
|
||||
* - Nodes and edges currently in the flow
|
||||
* - Callbacks for node and edge changes
|
||||
* - Node deletion and updates
|
||||
* - Edge reconnection handling
|
||||
*/
|
||||
export type FlowState = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
edgeReconnectSuccessful: boolean;
|
||||
|
||||
/** Handler for changes to nodes triggered by ReactFlow */
|
||||
onNodesChange: OnNodesChange;
|
||||
|
||||
/** Handler for changes to edges triggered by ReactFlow */
|
||||
onEdgesChange: OnEdgesChange;
|
||||
|
||||
/** Handler for creating a new connection between nodes */
|
||||
onConnect: OnConnect;
|
||||
|
||||
/** Handler for reconnecting an existing edge */
|
||||
onReconnect: OnReconnect;
|
||||
|
||||
/** Called when an edge reconnect process starts */
|
||||
onReconnectStart: () => void;
|
||||
|
||||
/**
|
||||
* Called when an edge reconnect process ends.
|
||||
* @param _ - event or unused parameter
|
||||
* @param edge - the edge that finished reconnecting
|
||||
*/
|
||||
onReconnectEnd: (_: unknown, edge: { id: string }) => void;
|
||||
|
||||
/**
|
||||
* Deletes a node and any connected edges.
|
||||
* @param nodeId - the ID of the node to delete
|
||||
*/
|
||||
deleteNode: (nodeId: string) => void;
|
||||
|
||||
/**
|
||||
* Replaces the current nodes array in the store.
|
||||
* @param nodes - new array of nodes
|
||||
*/
|
||||
setNodes: (nodes: Node[]) => void;
|
||||
|
||||
/**
|
||||
* Replaces the current edges array in the store.
|
||||
* @param edges - new array of edges
|
||||
*/
|
||||
setEdges: (edges: Edge[]) => void;
|
||||
|
||||
/**
|
||||
* Updates the data of a node by merging new data with existing node data.
|
||||
* @param nodeId - the ID of the node to update
|
||||
* @param data - object containing new data fields to merge
|
||||
*/
|
||||
updateNodeData: (nodeId: string, data: object) => void;
|
||||
|
||||
/**
|
||||
* Adds a new node to the flow.
|
||||
* @param node - the Node object to add
|
||||
*/
|
||||
addNode: (node: Node) => void;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,7 +6,12 @@ import styles from '../../VisProg.module.css';
|
||||
import { NodeDefaults, type NodeTypes } from '../NodeRegistry'
|
||||
|
||||
/**
|
||||
* DraggableNodeProps dictates the type properties of a DraggableNode
|
||||
* Props for a draggable node within the drag-and-drop toolbar.
|
||||
*
|
||||
* @property className - Optional custom CSS classes for styling.
|
||||
* @property children - The visual content or label rendered inside the draggable node.
|
||||
* @property nodeType - The type of node represented (key from `NodeTypes`).
|
||||
* @property onDrop - Function called when the node is dropped on the flow pane.
|
||||
*/
|
||||
interface DraggableNodeProps {
|
||||
className?: string;
|
||||
@@ -16,15 +21,20 @@ interface DraggableNodeProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition of a node inside the drag and drop toolbar.
|
||||
* These nodes require an onDrop function that dictates
|
||||
* how the node is created in the graph.
|
||||
* A draggable node element used in the drag-and-drop toolbar.
|
||||
*
|
||||
* Integrates with the NeoDrag library to handle drag events.
|
||||
* On drop, it calls the provided `onDrop` function with the node type and drop position.
|
||||
*
|
||||
* @param props - The draggable node configuration.
|
||||
* @returns A React element representing a draggable node.
|
||||
*/
|
||||
function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) {
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const [position, setPosition] = useState<XYPosition>({ x: 0, y: 0 });
|
||||
|
||||
// @ts-expect-error from the neodrag package — safe to ignore
|
||||
// The NeoDrag hook enables smooth drag functionality for this element.
|
||||
// @ts-expect-error: NeoDrag typing incompatibility — safe to ignore.
|
||||
useDraggable(draggableRef, {
|
||||
position,
|
||||
onDrag: ({ offsetX, offsetY }) => {
|
||||
@@ -44,16 +54,23 @@ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeP
|
||||
}
|
||||
|
||||
/**
|
||||
* addNode — adds a new node to the flow using the unified class-based system.
|
||||
* Keeps numbering logic for phase/norm nodes.
|
||||
* Adds a new node to the flow graph.
|
||||
*
|
||||
* Handles:
|
||||
* - Automatic node ID generation based on existing nodes of the same type.
|
||||
* - Loading of default data from the `NodeDefaults` registry.
|
||||
* - Integration with the flow store to update global node state.
|
||||
*
|
||||
* @param nodeType - The type of node to create (from `NodeTypes`).
|
||||
* @param position - The XY position in the flow canvas where the node will appear.
|
||||
*/
|
||||
function addNode(nodeType: keyof typeof NodeTypes, position: XYPosition) {
|
||||
const { nodes, setNodes } = useFlowStore.getState();
|
||||
|
||||
// Find out if there's any default data about our ndoe
|
||||
// Load any predefined data for this node type.
|
||||
const defaultData = NodeDefaults[nodeType] ?? {}
|
||||
|
||||
// Currently, we find out what the Id is by checking the last node and adding one
|
||||
// Currently, we find out what the Id is by checking the last node and adding one.
|
||||
const sameTypeNodes = nodes.filter((node) => node.type === nodeType);
|
||||
const nextNumber =
|
||||
sameTypeNodes.length > 0
|
||||
@@ -77,16 +94,28 @@ function addNode(nodeType: keyof typeof NodeTypes, position: XYPosition) {
|
||||
}
|
||||
|
||||
/**
|
||||
* DndToolbar defines how the drag and drop toolbar component works
|
||||
* and includes the default onDrop behavior.
|
||||
* The drag-and-drop toolbar component for the visual programming interface.
|
||||
*
|
||||
* Displays draggable node templates based on entries in `NodeDefaults`.
|
||||
* Each droppable node can be dragged into the flow pane to instantiate it.
|
||||
*
|
||||
* Automatically filters nodes whose `droppable` flag is set to `true`.
|
||||
*
|
||||
* @returns A React element representing the drag-and-drop toolbar.
|
||||
*/
|
||||
export function DndToolbar() {
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
|
||||
/**
|
||||
* Handles dropping a node onto the flow pane.
|
||||
* Translates screen coordinates into flow coordinates using React Flow utilities.
|
||||
*/
|
||||
const handleNodeDrop = useCallback(
|
||||
(nodeType: keyof typeof NodeTypes, screenPosition: XYPosition) => {
|
||||
const flow = document.querySelector('.react-flow');
|
||||
const flowRect = flow?.getBoundingClientRect();
|
||||
|
||||
// Only add the node if it is inside the flow canvas area.
|
||||
const isInFlow =
|
||||
flowRect &&
|
||||
screenPosition.x >= flowRect.left &&
|
||||
@@ -103,7 +132,7 @@ export function DndToolbar() {
|
||||
);
|
||||
|
||||
|
||||
// Map over our default settings to see which of them have their droppable data set to true
|
||||
// Map over the default nodes to get all nodes that can be dropped from the toolbar.
|
||||
const droppableNodes = Object.entries(NodeDefaults)
|
||||
.filter(([, data]) => data.droppable)
|
||||
.map(([type, data]) => ({
|
||||
|
||||
@@ -2,7 +2,12 @@ import { NodeToolbar } from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
|
||||
//Toolbar definitions
|
||||
/**
|
||||
* Props for the Toolbar component.
|
||||
*
|
||||
* @property nodeId - The ID of the node this toolbar is attached to.
|
||||
* @property allowDelete - If `true`, the delete button is enabled; otherwise disabled.
|
||||
*/
|
||||
type ToolbarProps = {
|
||||
nodeId: string;
|
||||
allowDelete: boolean;
|
||||
@@ -10,12 +15,12 @@ type ToolbarProps = {
|
||||
|
||||
/**
|
||||
* Node Toolbar definition:
|
||||
* handles: node deleting functionality
|
||||
* can be added to any custom node component as a React component
|
||||
* Handles: node deleting functionality
|
||||
* Can be integrated to any custom node component as a React component
|
||||
*
|
||||
* @param {string} nodeId
|
||||
* @param {boolean} allowDelete
|
||||
* @returns {React.JSX.Element}
|
||||
* @param {string} nodeId - The ID of the node for which the toolbar is rendered.
|
||||
* @param {boolean} allowDelete - Enables or disables the delete functionality.
|
||||
* @returns {React.JSX.Element} A JSX element representing the toolbar.
|
||||
* @constructor
|
||||
*/
|
||||
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
|
||||
|
||||
@@ -93,6 +93,12 @@ export function GoalReduce(node: Node, nodes: Node[]) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is made with this node type (Goal)
|
||||
* @param thisNode the node of this node type which function is called
|
||||
* @param otherNode the other node which was part of the connection
|
||||
* @param isThisSource whether this instance of the node was the source in the connection, true = yes.
|
||||
*/
|
||||
export function GoalConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) {
|
||||
// Replace this for connection logic
|
||||
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
|
||||
|
||||
@@ -76,6 +76,12 @@ export function NormReduce(node: Node, nodes: Node[]) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is made with this node type (Norm)
|
||||
* @param thisNode the node of this node type which function is called
|
||||
* @param otherNode the other node which was part of the connection
|
||||
* @param isThisSource whether this instance of the node was the source in the connection, true = yes.
|
||||
*/
|
||||
export function NormConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) {
|
||||
// Replace this for connection logic
|
||||
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
|
||||
|
||||
@@ -14,10 +14,16 @@ import { RealtimeTextField, TextField } from '../../../../components/TextField';
|
||||
import duplicateIndices from '../../../../utils/duplicateIndices';
|
||||
|
||||
/**
|
||||
* The default data dot a Trigger node
|
||||
* @param label: the label of this Trigger
|
||||
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
|
||||
* @param children: ID's of children of this node
|
||||
* The default data structure for a Trigger node
|
||||
*
|
||||
* Represents configuration for a node that activates when a specific condition is met,
|
||||
* such as keywords being spoken or emotions detected.
|
||||
*
|
||||
* @property label: the display label of this Trigger node.
|
||||
* @property droppable: Whether this node can be dropped from the toolbar (default: true).
|
||||
* @property triggerType - The type of trigger ("keywords" or a custom string).
|
||||
* @property triggers - The list of keyword triggers (if applicable).
|
||||
* @property hasReduce - Whether this node supports reduction logic.
|
||||
*/
|
||||
export type TriggerNodeData = {
|
||||
label: string;
|
||||
@@ -31,14 +37,20 @@ export type TriggerNodeData = {
|
||||
export type TriggerNode = Node<TriggerNodeData>
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether a Trigger node can connect to another node or edge.
|
||||
*
|
||||
* @param connection - The connection or edge being attempted to connect towards.
|
||||
* @returns `true` if the connection is defined; otherwise, `false`.
|
||||
*/
|
||||
export function TriggerNodeCanConnect(connection: Connection | Edge): boolean {
|
||||
return (connection != undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how a Trigger node should be rendered
|
||||
* @param props NodeProps, like id, label, children
|
||||
* @returns React.JSX.Element
|
||||
* @param props - Node properties provided by React Flow, including `id` and `data`.
|
||||
* @returns The rendered TriggerNode React element (React.JSX.Element).
|
||||
*/
|
||||
export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||
const data = props.data;
|
||||
@@ -66,9 +78,10 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces each Trigger, including its children down into its relevant data.
|
||||
* @param node The Node Properties of this node.
|
||||
* @param nodes all the nodes in the graph.
|
||||
* Reduces each Trigger, including its children down into its core data.
|
||||
* @param node - The Trigger node to reduce.
|
||||
* @param nodes - The list of all nodes in the current flow graph.
|
||||
* @returns A simplified object containing the node label and its list of triggers.
|
||||
*/
|
||||
export function TriggerReduce(node: Node, nodes: Node[]) {
|
||||
// Replace this for nodes functionality
|
||||
@@ -93,10 +106,11 @@ export function TriggerReduce(node: Node, nodes: Node[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is made with this node type (trigger)
|
||||
* @param thisNode the node of this node type which function is called
|
||||
* @param otherNode the other node which was part of the connection
|
||||
* @param isThisSource whether this instance of the node was the source in the connection, true = yes.
|
||||
* Handles logic that occurs when a connection is made involving a Trigger node.
|
||||
*
|
||||
* @param thisNode - The current Trigger node being connected.
|
||||
* @param otherNode - The other node involved in the connection.
|
||||
* @param isThisSource - Whether this node was the source of the connection.
|
||||
*/
|
||||
export function TriggerConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) {
|
||||
// Replace this for connection logic
|
||||
@@ -106,23 +120,32 @@ export function TriggerConnects(thisNode: Node, otherNode: Node, isThisSource: b
|
||||
}
|
||||
|
||||
// Definitions for the possible triggers, being keywords and emotions
|
||||
|
||||
/** Represents a single keyword trigger entry. */
|
||||
type Keyword = { id: string, keyword: string };
|
||||
|
||||
/** Properties for an emotion-type trigger node. */
|
||||
export type EmotionTriggerNodeProps = {
|
||||
type: "emotion";
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** Props for a keyword-type trigger node. */
|
||||
export type KeywordTriggerNodeProps = {
|
||||
type: "keywords";
|
||||
value: Keyword[];
|
||||
}
|
||||
|
||||
/** Union type for all possible Trigger node configurations. */
|
||||
export type TriggerNodeProps = EmotionTriggerNodeProps | KeywordTriggerNodeProps;
|
||||
|
||||
/**
|
||||
* The JSX element that is responsible for updating the field and showing the text
|
||||
* @param param0 the function that updates the field
|
||||
* @returns React.JSX.Element that handles adding keywords
|
||||
* Renders an input element that allows users to add new keyword triggers.
|
||||
*
|
||||
* When the input is committed, the `addKeyword` callback is called with the new keyword.
|
||||
*
|
||||
* @param param0 - An object containing the `addKeyword` function.
|
||||
* @returns A React element(React.JSX.Element) providing an input for adding keywords.
|
||||
*/
|
||||
function KeywordAdder({ addKeyword }: { addKeyword: (keyword: string) => void }) {
|
||||
const [input, setInput] = useState("");
|
||||
@@ -146,6 +169,14 @@ function KeywordAdder({ addKeyword }: { addKeyword: (keyword: string) => void })
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays and manages a list of keyword triggers for a Trigger node.
|
||||
* Handles adding, editing, and removing keywords, as well as detecting duplicate entries.
|
||||
*
|
||||
* @param keywords - The current list of keyword triggers.
|
||||
* @param setKeywords - A callback to update the keyword list in the parent node.
|
||||
* @returns A React element(React.JSX.Element) for editing keyword triggers.
|
||||
*/
|
||||
function Keywords({
|
||||
keywords,
|
||||
setKeywords,
|
||||
|
||||
@@ -2,12 +2,56 @@ import {useSyncExternalStore} from "react";
|
||||
|
||||
type Unsub = () => void;
|
||||
|
||||
|
||||
/**
|
||||
* A simple reactive state container that holds a value of type `T` that provides methods to get, set, and subscribe.
|
||||
*/
|
||||
export type Cell<T> = {
|
||||
/**
|
||||
* Returns the current value stored in the cell.
|
||||
*/
|
||||
get: () => T;
|
||||
/**
|
||||
* Updates the cell's value, pass either a direct value or an updater function.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* count.set(5);
|
||||
* count.set(prev => prev + 1);
|
||||
* ```
|
||||
*/
|
||||
set: (next: T | ((prev: T) => T)) => void;
|
||||
|
||||
/**
|
||||
* Subscribe to changes in the cell's value, meaning the provided callback is called whenever the value changes.
|
||||
* Returns an unsubscribe function.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const unsubscribe = count.subscribe(() => console.log(count.get()));
|
||||
* // later:
|
||||
* unsubscribe();
|
||||
* ```
|
||||
*/
|
||||
subscribe: (callback: () => void) => Unsub;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new reactive state container (`Cell`) with an initial value.
|
||||
*
|
||||
* This function allows you to store and mutate state outside of React,
|
||||
* while still supporting subscriptions for reactivity.
|
||||
*
|
||||
* @param initial - The initial value for the cell.
|
||||
* @returns A Cell object with `get`, `set`, and `subscribe` methods.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const count = cell(0);
|
||||
* count.set(10);
|
||||
* console.log(count.get()); // 10
|
||||
* ```
|
||||
*/
|
||||
export function cell<T>(initial: T): Cell<T> {
|
||||
let value = initial;
|
||||
const listeners = new Set<() => void>();
|
||||
@@ -24,6 +68,29 @@ export function cell<T>(initial: T): Cell<T> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that subscribes a component to a Cell.
|
||||
*
|
||||
* Automatically re-renders the component whenever the Cell's value changes.
|
||||
* Uses React’s built-in `useSyncExternalStore` for correct subscription behavior.
|
||||
*
|
||||
* @param c - The cell to subscribe to.
|
||||
* @returns The current value of the cell.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const count = cell(0);
|
||||
*
|
||||
* function Counter() {
|
||||
* const value = useCell(count);
|
||||
* return (
|
||||
* <button onClick={() => count.set(v => v + 1)}>
|
||||
* Count: {value}
|
||||
* </button>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useCell<T>(c: Cell<T>) {
|
||||
return useSyncExternalStore(c.subscribe, c.get, c.get);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user