Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 165 additions & 15 deletions inc/Core/Admin/Pages/Agent/assets/react/components/SystemTasksTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@
* SystemTasksTab Component
*
* Card-based display of registered system tasks with trigger info,
* run history, enable/disable toggles, and Run Now functionality.
* run history, enable/disable toggles, Run Now, and editable AI prompts.
*
* @since 0.42.0
*/

import { useState } from '@wordpress/element';
import { useState, useMemo } from '@wordpress/element';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { client } from '@shared/utils/api';
import { useUpdateSettings } from '@shared/queries/settings';
import PromptField from '@shared/components/PromptField';
import {
useSystemTaskPrompts,
useSavePrompt,
useResetPrompt,
} from '../queries/systemTaskPrompts';

const SYSTEM_TASKS_KEY = [ 'system-tasks' ];

Expand Down Expand Up @@ -94,16 +100,96 @@ const TRIGGER_ICONS = {
tool: '\uD83E\uDD16',
};

const TaskCard = ( { task, onToggle, onRun, isToggling, isRunning } ) => {
/**
* PromptEditor — expandable section for a single prompt definition.
*/
const PromptEditor = ( { prompt, onSave, onReset } ) => {
const [ isResetting, setIsResetting ] = useState( false );
const effectiveValue = prompt.has_override
? prompt.override
: prompt.default;

const handleSave = async ( newValue ) => {
return await onSave( prompt.task_type, prompt.prompt_key, newValue );
};

const handleReset = async () => {
setIsResetting( true );
try {
await onReset( prompt.task_type, prompt.prompt_key );
} finally {
setIsResetting( false );
}
};

const variableKeys = prompt.variables
? Object.keys( prompt.variables )
: [];

return (
<div className="datamachine-prompt-editor">
<PromptField
value={ effectiveValue }
onSave={ handleSave }
label={ prompt.label }
placeholder={ prompt.default }
rows={ 8 }
help={ prompt.description }
/>

{ variableKeys.length > 0 && (
<div className="datamachine-prompt-variables">
<span className="datamachine-prompt-variables-label">
Variables:
</span>
{ variableKeys.map( ( key ) => (
<code
key={ key }
className="datamachine-prompt-variable"
title={ prompt.variables[ key ] }
>
{ `{{${ key }}}` }
</code>
) ) }
</div>
) }

{ prompt.has_override && (
<button
className="button button-link datamachine-prompt-reset"
onClick={ handleReset }
disabled={ isResetting }
>
{ isResetting ? 'Resetting...' : 'Reset to default' }
</button>
) }
</div>
);
};

const TaskCard = ( {
task,
prompts,
onToggle,
onRun,
onSavePrompt,
onResetPrompt,
isToggling,
isRunning,
} ) => {
const [ showPrompts, setShowPrompts ] = useState( false );
const status = formatStatus( task.last_status );
const hasToggle = Boolean( task.setting_key );
const lastRunDate = formatDate( task.last_run_at );
const triggerIcon = TRIGGER_ICONS[ task.trigger_type ] || '';
const hasPrompts = prompts.length > 0;

return (
<div className={ `datamachine-task-card ${
hasToggle && ! task.enabled ? 'is-disabled' : ''
}` }>
<div
className={ `datamachine-task-card ${
hasToggle && ! task.enabled ? 'is-disabled' : ''
}` }
>
<div className="datamachine-task-card-header">
<div className="datamachine-task-card-title">
<strong>{ task.label }</strong>
Expand Down Expand Up @@ -145,8 +231,7 @@ const TaskCard = ( { task, onToggle, onRun, isToggling, isRunning } ) => {
<span className="datamachine-task-card-meta-value">
{ lastRunDate ? (
<>
{ lastRunDate }
{ ' ' }
{ lastRunDate }{ ' ' }
<span className={ status.className }>
{ status.label }
</span>
Expand All @@ -169,18 +254,39 @@ const TaskCard = ( { task, onToggle, onRun, isToggling, isRunning } ) => {
</div>
</div>

{ task.supports_run && (
<div className="datamachine-task-card-actions">
<div className="datamachine-task-card-actions">
{ task.supports_run && (
<button
className="button button-secondary button-small"
onClick={ () => onRun( task.task_type ) }
disabled={
isRunning ||
( hasToggle && ! task.enabled )
isRunning || ( hasToggle && ! task.enabled )
}
>
{ isRunning ? 'Scheduling...' : 'Run Now' }
</button>
) }

{ hasPrompts && (
<button
className="button button-secondary button-small"
onClick={ () => setShowPrompts( ! showPrompts ) }
>
{ showPrompts ? 'Hide Prompts' : 'Edit Prompts' }
</button>
) }
</div>

{ showPrompts && hasPrompts && (
<div className="datamachine-task-prompts">
{ prompts.map( ( prompt ) => (
<PromptEditor
key={ `${ prompt.task_type }-${ prompt.prompt_key }` }
prompt={ prompt }
onSave={ onSavePrompt }
onReset={ onResetPrompt }
/>
) ) }
</div>
) }
</div>
Expand All @@ -189,18 +295,40 @@ const TaskCard = ( { task, onToggle, onRun, isToggling, isRunning } ) => {

const SystemTasksTab = () => {
const { data: tasks, isLoading, error } = useSystemTasks();
const {
data: allPrompts,
isLoading: promptsLoading,
} = useSystemTaskPrompts();
const updateMutation = useUpdateSettings();
const runMutation = useRunTask();
const saveMutation = useSavePrompt();
const resetMutation = useResetPrompt();
const queryClient = useQueryClient();
const [ runningTask, setRunningTask ] = useState( null );

// Group prompts by task_type for efficient lookup.
const promptsByTask = useMemo( () => {
if ( ! allPrompts ) {
return {};
}
const grouped = {};
for ( const prompt of allPrompts ) {
if ( ! grouped[ prompt.task_type ] ) {
grouped[ prompt.task_type ] = [];
}
grouped[ prompt.task_type ].push( prompt );
}
return grouped;
}, [ allPrompts ] );

const handleToggle = async ( settingKey, currentValue ) => {
try {
await updateMutation.mutateAsync( {
[ settingKey ]: ! currentValue,
} );
queryClient.invalidateQueries( { queryKey: SYSTEM_TASKS_KEY } );
} catch ( err ) {
// eslint-disable-next-line no-console
console.error( 'Failed to toggle system task:', err );
}
};
Expand All @@ -210,13 +338,31 @@ const SystemTasksTab = () => {
try {
await runMutation.mutateAsync( taskType );
} catch ( err ) {
// eslint-disable-next-line no-console
console.error( 'Failed to run task:', err );
} finally {
setRunningTask( null );
}
};

if ( isLoading ) {
const handleSavePrompt = async ( taskType, promptKey, prompt ) => {
try {
await saveMutation.mutateAsync( {
taskType,
promptKey,
prompt,
} );
return { success: true };
} catch ( err ) {
return { success: false, message: err.message };
}
};

const handleResetPrompt = async ( taskType, promptKey ) => {
await resetMutation.mutateAsync( { taskType, promptKey } );
};

if ( isLoading || promptsLoading ) {
return (
<div className="datamachine-system-tasks-loading">
<span className="spinner is-active"></span>
Expand Down Expand Up @@ -247,16 +393,20 @@ const SystemTasksTab = () => {
className="description"
style={ { marginTop: '16px', marginBottom: '16px' } }
>
Registered task definitions. Each task can run standalone via
Action Scheduler or as a step in a pipeline flow.
Registered task definitions. Each task can run via Action
Scheduler or as a step in a pipeline flow. Tasks with AI
prompts can be customized below.
</p>
<div className="datamachine-task-cards">
{ tasks.map( ( task ) => (
<TaskCard
key={ task.task_type }
task={ task }
prompts={ promptsByTask[ task.task_type ] || [] }
onToggle={ handleToggle }
onRun={ handleRun }
onSavePrompt={ handleSavePrompt }
onResetPrompt={ handleResetPrompt }
isToggling={ updateMutation.isPending }
isRunning={ runningTask === task.task_type }
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* System Task Prompts Queries
*
* TanStack Query hooks for system task prompt CRUD via REST API.
*
* @since 0.43.0
*/

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { client } from '@shared/utils/api';

const PROMPTS_KEY = [ 'system-task-prompts' ];

/**
* Fetch all system task prompt definitions with overrides.
*/
export const useSystemTaskPrompts = () =>
useQuery( {
queryKey: PROMPTS_KEY,
queryFn: async () => {
const result = await client.get( '/system/tasks/prompts' );
if ( ! result.success ) {
throw new Error(
result.message || 'Failed to fetch task prompts'
);
}
return result.data;
},
staleTime: 60 * 1000,
} );

/**
* Save a prompt override.
*/
export const useSavePrompt = () => {
const queryClient = useQueryClient();
return useMutation( {
mutationFn: async ( { taskType, promptKey, prompt } ) => {
const result = await client.put(
`/system/tasks/prompts/${ taskType }/${ promptKey }`,
{ prompt }
);
if ( ! result.success ) {
throw new Error(
result.message || 'Failed to save prompt'
);
}
return result.data;
},
onSuccess: () => {
queryClient.invalidateQueries( { queryKey: PROMPTS_KEY } );
},
} );
};

/**
* Reset a prompt to its default.
*/
export const useResetPrompt = () => {
const queryClient = useQueryClient();
return useMutation( {
mutationFn: async ( { taskType, promptKey } ) => {
const result = await client.delete(
`/system/tasks/prompts/${ taskType }/${ promptKey }`
);
if ( ! result.success ) {
throw new Error(
result.message || 'Failed to reset prompt'
);
}
return result.data;
},
onSuccess: () => {
queryClient.invalidateQueries( { queryKey: PROMPTS_KEY } );
},
} );
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import PromptField from '../shared/PromptField';
import PromptField from '@shared/components/PromptField';
import { updateSystemPrompt } from '../../utils/api';
import { useStepTypes, useTools } from '../../queries/config';

Expand Down
Loading
Loading