updated feature automation and feature chat playground routes

This commit is contained in:
Ida Dittrich 2026-03-09 13:49:03 +01:00
parent 0dbcad771b
commit df1344c228
5 changed files with 147 additions and 106 deletions

View file

@ -102,12 +102,15 @@ function App() {
<Route path="gdpr" element={<GDPRPage />} />
{/* ============================================== */}
{/* WORKFLOWS ROUTES (global) */}
{/* WORKFLOWS ROUTES (deprecated - redirect to /) */}
{/* Workflows are accessed via feature routes: */}
{/* /mandates/:mandateId/chatplayground/:id/workflows */}
{/* /mandates/:mandateId/automation/:id/definitions */}
{/* ============================================== */}
<Route path="workflows">
<Route path="playground" element={<PlaygroundPage />} />
<Route path="list" element={<WorkflowsPage />} />
<Route path="automations" element={<AutomationDefinitionsView />} />
<Route path="list" element={<Navigate to="/" replace />} />
<Route path="automations" element={<Navigate to="/" replace />} />
</Route>
{/* ============================================== */}

View file

@ -69,17 +69,24 @@ export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<an
// ============================================================================
/**
* Fetch all workflows for the current user
* Endpoint: GET /api/workflows/
* Fetch workflows - feature-scoped or global (legacy)
* When apiBaseUrl is provided: GET {apiBaseUrl}/workflows
* Otherwise (legacy): GET /api/workflows/
*/
export async function fetchWorkflows(request: ApiRequestFunction): Promise<Workflow[]> {
console.log('📤 fetchWorkflows: Making API request to /api/workflows/');
export async function fetchWorkflows(
request: ApiRequestFunction,
params?: { pagination?: string },
apiBaseUrl?: string
): Promise<Workflow[] | { items: Workflow[]; pagination?: any }> {
const url = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/';
console.log('📤 fetchWorkflows: Making API request to', url);
try {
const data = await request({
url: '/api/workflows/',
method: 'get'
});
const requestConfig: any = { url, method: 'get' as const };
if (params?.pagination) {
requestConfig.params = { pagination: params.pagination };
}
const data = await request(requestConfig);
console.log('📥 fetchWorkflows: API response:', data);
@ -129,30 +136,30 @@ export async function fetchWorkflows(request: ApiRequestFunction): Promise<Workf
/**
* Fetch a single workflow by ID
* Endpoint: GET /api/workflows/{workflowId}
* When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}
* Otherwise: GET /api/workflows/{workflowId}
*/
export async function fetchWorkflow(
request: ApiRequestFunction,
workflowId: string
workflowId: string,
apiBaseUrl?: string
): Promise<Workflow & { messages?: WorkflowMessage[]; logs?: WorkflowLog[] }> {
return await request({
url: `/api/workflows/${workflowId}`,
method: 'get'
});
const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
return await request({ url, method: 'get' });
}
/**
* Fetch workflow status (lightweight status check)
* Endpoint: GET /api/workflows/{workflowId}/status
* When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/status
* Otherwise: GET /api/workflows/{workflowId}/status
*/
export async function fetchWorkflowStatus(
request: ApiRequestFunction,
workflowId: string
workflowId: string,
apiBaseUrl?: string
): Promise<Workflow | { status: string } | null> {
const data = await request({
url: `/api/workflows/${workflowId}/status`,
method: 'get'
});
const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/status` : `/api/workflows/${workflowId}/status`;
const data = await request({ url, method: 'get' });
if (data && typeof data === 'object') {
if (data.status) {
@ -166,20 +173,18 @@ export async function fetchWorkflowStatus(
/**
* Fetch workflow messages
* Endpoint: GET /api/workflows/{workflowId}/messages
* Query params: messageId (optional) - fetch only newer messages
* When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/messages
* Otherwise: GET /api/workflows/{workflowId}/messages
*/
export async function fetchWorkflowMessages(
request: ApiRequestFunction,
workflowId: string,
messageId?: string
messageId?: string,
apiBaseUrl?: string
): Promise<WorkflowMessage[]> {
const params = messageId ? { messageId } : undefined;
const data = await request({
url: `/api/workflows/${workflowId}/messages`,
method: 'get',
params
});
const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages` : `/api/workflows/${workflowId}/messages`;
const data = await request({ url, method: 'get', params });
if (Array.isArray(data)) {
return data;
@ -199,20 +204,18 @@ export async function fetchWorkflowMessages(
/**
* Fetch workflow logs
* Endpoint: GET /api/workflows/{workflowId}/logs
* Query params: logId (optional) - fetch only newer logs
* When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/logs
* Otherwise: GET /api/workflows/{workflowId}/logs
*/
export async function fetchWorkflowLogs(
request: ApiRequestFunction,
workflowId: string,
logId?: string
logId?: string,
apiBaseUrl?: string
): Promise<WorkflowLog[]> {
const params = logId ? { logId } : undefined;
const data = await request({
url: `/api/workflows/${workflowId}/logs`,
method: 'get',
params
});
const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/logs` : `/api/workflows/${workflowId}/logs`;
const data = await request({ url, method: 'get', params });
if (Array.isArray(data)) {
return data;
@ -232,7 +235,7 @@ export async function fetchWorkflowLogs(
/**
* Fetch unified chat data (messages, logs, stats, documents)
* Endpoint: GET /api/chatplayground/{instanceId}/{workflowId}/chatData
* Endpoint: GET /api/chatplayground/{instanceId}/workflows/{workflowId}/chatData
* Query params: afterTimestamp (optional) - fetch only data created after this time
*/
export async function fetchChatData(
@ -243,7 +246,7 @@ export async function fetchChatData(
): Promise<ChatDataResponse> {
const params = afterTimestamp ? { afterTimestamp: afterTimestamp.toString() } : undefined;
const requestConfig = {
url: `/api/chatplayground/${instanceId}/${workflowId}/chatData`,
url: `/api/chatplayground/${instanceId}/workflows/${workflowId}/chatData`,
method: 'get' as const,
params
};
@ -372,7 +375,7 @@ export async function startWorkflowApi(
/**
* Stop a running workflow
* Endpoint: POST /api/chatplayground/{instanceId}/{workflowId}/stop
* Endpoint: POST /api/chatplayground/{instanceId}/workflows/{workflowId}/stop
*/
export async function stopWorkflowApi(
request: ApiRequestFunction,
@ -380,52 +383,53 @@ export async function stopWorkflowApi(
workflowId: string
): Promise<void> {
await request({
url: `/api/chatplayground/${instanceId}/${workflowId}/stop`,
url: `/api/chatplayground/${instanceId}/workflows/${workflowId}/stop`,
method: 'post'
});
}
/**
* Update workflow properties
* Endpoint: PUT /api/workflows/{workflowId}
* When apiBaseUrl provided: PUT {apiBaseUrl}/workflows/{workflowId}
* Otherwise: PUT /api/workflows/{workflowId}
*/
export async function updateWorkflowApi(
request: ApiRequestFunction,
workflowId: string,
updateData: Partial<{ name: string; description?: string; tags?: string[] }>
updateData: Partial<{ name: string; description?: string; tags?: string[] }>,
apiBaseUrl?: string
): Promise<Workflow> {
return await request({
url: `/api/workflows/${workflowId}`,
method: 'put',
data: updateData
});
const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
return await request({ url, method: 'put', data: updateData });
}
/**
* Delete a workflow and all associated data
* Endpoint: DELETE /api/workflows/{workflowId}
* When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId}
* Otherwise: DELETE /api/workflows/{workflowId}
*/
export async function deleteWorkflowApi(
request: ApiRequestFunction,
workflowId: string
workflowId: string,
apiBaseUrl?: string
): Promise<void> {
await request({
url: `/api/workflows/${workflowId}`,
method: 'delete'
});
const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`;
await request({ url, method: 'delete' });
}
/**
* Delete multiple workflows
* When apiBaseUrl provided, uses feature-scoped delete endpoint
*/
export async function deleteWorkflowsApi(
request: ApiRequestFunction,
workflowIds: string[]
workflowIds: string[],
apiBaseUrl?: string
): Promise<void> {
// Delete workflows one by one since there's no bulk delete endpoint
const base = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows';
const deletePromises = workflowIds.map(workflowId =>
request({
url: `/api/workflows/${workflowId}`,
url: `${base}/${workflowId}`,
method: 'delete'
}).catch(error => {
console.error(`Failed to delete workflow ${workflowId}:`, error);
@ -438,33 +442,33 @@ export async function deleteWorkflowsApi(
/**
* Delete a message from a workflow
* Endpoint: DELETE /api/workflows/{workflowId}/messages/{messageId}
* When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId}/messages/{messageId}
* Otherwise: DELETE /api/workflows/{workflowId}/messages/{messageId}
*/
export async function deleteMessageApi(
request: ApiRequestFunction,
workflowId: string,
messageId: string
messageId: string,
apiBaseUrl?: string
): Promise<void> {
await request({
url: `/api/workflows/${workflowId}/messages/${messageId}`,
method: 'delete'
});
const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages/${messageId}` : `/api/workflows/${workflowId}/messages/${messageId}`;
await request({ url, method: 'delete' });
}
/**
* Delete a file reference from a message
* Endpoint: DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId}
* When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId}/messages/{messageId}/files/{fileId}
* Otherwise: DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId}
*/
export async function deleteFileFromMessageApi(
request: ApiRequestFunction,
workflowId: string,
messageId: string,
fileId: string
fileId: string,
apiBaseUrl?: string
): Promise<void> {
await request({
url: `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`,
method: 'delete'
});
const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages/${messageId}/files/${fileId}` : `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`;
await request({ url, method: 'delete' });
}
/**

View file

@ -46,8 +46,17 @@ export interface PaginationParams {
search?: string;
}
// Workflows list hook
export function useUserWorkflows() {
/** Get apiBaseUrl from instanceId and featureCode for feature-scoped workflow APIs */
export function getWorkflowApiBaseUrl(instanceId: string | undefined, featureCode: string | undefined): string | undefined {
if (!instanceId || !featureCode) return undefined;
if (featureCode === 'chatplayground') return `/api/chatplayground/${instanceId}`;
if (featureCode === 'automation') return `/api/automations/${instanceId}`;
return undefined;
}
// Workflows list hook - pass instanceId and featureCode when in feature context for feature-scoped API
export function useUserWorkflows(options?: { instanceId?: string; featureCode?: string }) {
const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode);
const [workflows, setWorkflows] = useState<UserWorkflow[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
@ -113,16 +122,11 @@ export function useUserWorkflows() {
}
let data: any;
const url = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/';
if (Object.keys(requestParams).length > 0) {
// Use request for paginated queries
data = await request({
url: '/api/workflows/',
method: 'get',
params: requestParams
});
data = await request({ url, method: 'get', params: requestParams });
} else {
// Use API function for simple queries
data = await fetchWorkflowsApi(request);
data = await fetchWorkflowsApi(request, undefined, apiBaseUrl);
}
// Handle paginated response
@ -164,7 +168,7 @@ export function useUserWorkflows() {
setWorkflows([]);
setPagination(null);
}
}, [request]);
}, [request, apiBaseUrl]);
// Optimistically remove a workflow from the local state
const removeOptimistically = (workflowId: string) => {
@ -185,13 +189,13 @@ export function useUserWorkflows() {
// Fetch a single workflow by ID
const fetchWorkflowById = useCallback(async (workflowId: string): Promise<UserWorkflow | null> => {
try {
const workflow = await fetchWorkflowByIdApi(request, workflowId);
const workflow = await fetchWorkflowByIdApi(request, workflowId, apiBaseUrl);
return workflow as UserWorkflow | null;
} catch (error: any) {
console.error('Error fetching workflow by ID:', error);
return null;
}
}, [request]);
}, [request, apiBaseUrl]);
// Generate edit fields from attributes dynamically
const generateEditFieldsFromAttributes = useCallback((): Array<{
@ -375,8 +379,9 @@ export function useUserWorkflows() {
};
}
// Workflow operations hook
export function useWorkflowOperations() {
// Workflow operations hook - pass instanceId and featureCode when in feature context for feature-scoped API
export function useWorkflowOperations(options?: { instanceId?: string; featureCode?: string }) {
const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode);
const [startingWorkflow, setStartingWorkflow] = useState(false);
const [stoppingWorkflows, setStoppingWorkflows] = useState<Set<string>>(new Set());
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
@ -476,7 +481,7 @@ export function useWorkflowOperations() {
workflowId,
setDeletingWorkflows,
setDeleteError,
() => deleteWorkflowApi(request, workflowId),
() => deleteWorkflowApi(request, workflowId, apiBaseUrl),
{
default: 'Failed to delete workflow',
notFound: 'Workflow not found or has already been deleted.',
@ -512,7 +517,7 @@ export function useWorkflowOperations() {
try {
// Delete workflows one by one since there's no bulk delete endpoint
await deleteWorkflowsApi(request, workflowIds);
await deleteWorkflowsApi(request, workflowIds, apiBaseUrl);
// Add a small delay to ensure backend has time to process
await new Promise(resolve => setTimeout(resolve, 300));
@ -547,7 +552,7 @@ export function useWorkflowOperations() {
operationKey,
setDeletingMessages,
setDeleteMessageError,
() => deleteMessageApi(request, workflowId, messageId),
() => deleteMessageApi(request, workflowId, messageId, apiBaseUrl),
{
default: 'Failed to delete message',
notFound: 'Message not found or has already been deleted.',
@ -566,7 +571,7 @@ export function useWorkflowOperations() {
operationKey,
setDeletingFiles,
setDeleteFileError,
() => deleteFileFromMessageApi(request, workflowId, messageId, fileId),
() => deleteFileFromMessageApi(request, workflowId, messageId, fileId, apiBaseUrl),
{
default: 'Failed to delete file',
notFound: 'File not found or has already been deleted.',
@ -580,7 +585,7 @@ export function useWorkflowOperations() {
setEditingWorkflows(prev => new Set(prev).add(workflowId));
try {
const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData);
const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData, apiBaseUrl);
return { success: true, workflowData: updatedWorkflow };
} catch (error: any) {

View file

@ -13,6 +13,7 @@ import { FaSync, FaRobot, FaRocket, FaPlus, FaFileAlt, FaStop, FaList, FaTimes,
import { useToast } from '../../../contexts/ToastContext';
import { useApiRequest } from '../../../hooks/useApi';
import { useFeatureStore } from '../../../stores/featureStore';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import styles from '../../admin/Admin.module.css';
interface WorkflowLog {
@ -24,11 +25,19 @@ interface WorkflowLog {
}
export const AutomationDefinitionsView: React.FC = () => {
const { instanceId: routeInstanceId, featureCode: routeFeatureCode } = useCurrentInstance();
const { getAllInstances } = useFeatureStore();
const instances = getAllInstances();
const chatbotInstance = instances.find(i => i.featureCode === 'chatbot') || instances[0];
const mandateId = chatbotInstance?.mandateId;
const featureInstanceId = chatbotInstance?.id;
const automationInstance = instances.find(i => i.featureCode === 'automation');
// When under automation feature route, use route context; otherwise use featureStore
const mandateId = routeFeatureCode === 'automation' && routeInstanceId
? (automationInstance?.mandateId ?? chatbotInstance?.mandateId)
: chatbotInstance?.mandateId;
const featureInstanceId = routeFeatureCode === 'automation' && routeInstanceId
? routeInstanceId
: (chatbotInstance?.id ?? automationInstance?.id);
const automationWorkflowInstanceId = routeFeatureCode === 'automation' ? routeInstanceId : undefined;
const {
data: automations,
@ -66,6 +75,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: boolean;
automationId: string | null;
automationLabel: string;
featureInstanceId: string | null;
workflowId: string | null;
status: 'starting' | 'running' | 'completed' | 'stopped' | 'error';
logs: WorkflowLog[];
@ -73,6 +83,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: false,
automationId: null,
automationLabel: '',
featureInstanceId: null,
workflowId: null,
status: 'starting',
logs: [],
@ -259,14 +270,16 @@ export const AutomationDefinitionsView: React.FC = () => {
setShowEditor(true);
};
const pollWorkflowLogs = useCallback(async (workflowId: string) => {
const pollWorkflowLogs = useCallback(async (workflowId: string, instanceId: string) => {
try {
const contextHeaders: Record<string, string> = {};
if (mandateId) contextHeaders['X-Mandate-Id'] = mandateId;
const logsUrl = `/api/automations/${instanceId}/workflows/${workflowId}/logs`;
const workflowUrl = `/api/automations/${instanceId}/workflows/${workflowId}`;
const response = await request({
url: `/api/workflows/${workflowId}/logs`,
url: logsUrl,
method: 'get',
params: lastLogIdRef.current ? { afterId: lastLogIdRef.current } : {},
params: lastLogIdRef.current ? { logId: lastLogIdRef.current } : {},
additionalConfig: { headers: contextHeaders },
});
const logs: WorkflowLog[] = response?.items || response || [];
@ -279,7 +292,7 @@ export const AutomationDefinitionsView: React.FC = () => {
lastLogIdRef.current = logs[logs.length - 1].id;
}
const statusResponse = await request({
url: `/api/workflows/${workflowId}`,
url: workflowUrl,
method: 'get',
additionalConfig: { headers: contextHeaders },
});
@ -309,6 +322,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: true,
automationId: automation.id,
automationLabel: automation.label,
featureInstanceId: automation.featureInstanceId ?? automationWorkflowInstanceId ?? null,
workflowId: null,
status: 'starting',
logs: [{ id: 'init', timestamp: Date.now() / 1000, message: 'Automatisierung wird gestartet...' }],
@ -316,14 +330,17 @@ export const AutomationDefinitionsView: React.FC = () => {
try {
const result = await handleAutomationExecute(automation.id);
const workflowId = result?.id;
if (workflowId) {
const instanceId = automation.featureInstanceId ?? automationWorkflowInstanceId;
if (workflowId && instanceId) {
setExecutionModal(prev => ({
...prev,
workflowId,
status: 'running',
logs: [...prev.logs, { id: 'started', timestamp: Date.now() / 1000, message: `Workflow ${workflowId} gestartet`, status: 'running' }],
}));
pollIntervalRef.current = setInterval(() => pollWorkflowLogs(workflowId), 2000);
pollIntervalRef.current = setInterval(() => pollWorkflowLogs(workflowId, instanceId), 2000);
} else if (workflowId && !instanceId) {
setExecutionModal(prev => ({ ...prev, status: 'error', logs: [...prev.logs, { id: 'error', timestamp: Date.now() / 1000, message: 'Keine Feature-Instanz für Polling', status: 'error' }] }));
}
} catch (err: any) {
setExecutionModal(prev => ({
@ -337,11 +354,17 @@ export const AutomationDefinitionsView: React.FC = () => {
const handleStopWorkflow = async () => {
if (!executionModal.workflowId) return;
const instanceId = executionModal.featureInstanceId ?? automationWorkflowInstanceId;
if (!instanceId) {
showError('Keine Feature-Instanz für Stopp verfügbar');
return;
}
try {
const stopHeaders: Record<string, string> = {};
if (mandateId) stopHeaders['X-Mandate-Id'] = mandateId;
const stopUrl = `/api/automations/${instanceId}/workflows/${executionModal.workflowId}/stop`;
await request({
url: `/api/workflows/${executionModal.workflowId}/stop`,
url: stopUrl,
method: 'post',
additionalConfig: { headers: stopHeaders },
});
@ -363,6 +386,7 @@ export const AutomationDefinitionsView: React.FC = () => {
visible: false,
automationId: null,
automationLabel: '',
featureInstanceId: null,
workflowId: null,
status: 'starting',
logs: [],

View file

@ -6,11 +6,12 @@
*/
import React, { useState, useMemo, useEffect } from 'react';
import { useUserWorkflows, useWorkflowOperations } from '../../hooks/useWorkflows';
import { useUserWorkflows, useWorkflowOperations, getWorkflowApiBaseUrl } from '../../hooks/useWorkflows';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaList, FaPlay } from 'react-icons/fa';
import { useNavigate } from 'react-router-dom';
import { useCurrentInstance } from '../../hooks/useCurrentInstance';
import styles from '../admin/Admin.module.css';
interface Workflow {
@ -23,8 +24,12 @@ interface Workflow {
export const WorkflowsPage: React.FC = () => {
const navigate = useNavigate();
const { instanceId, featureCode } = useCurrentInstance();
const workflowOptions = instanceId && featureCode ? { instanceId, featureCode } : undefined;
const apiBaseUrl = getWorkflowApiBaseUrl(instanceId, featureCode);
const apiEndpoint = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/';
// Data hook
// Data hook - pass instance context when in feature route
const {
data: workflows,
attributes,
@ -35,16 +40,16 @@ export const WorkflowsPage: React.FC = () => {
refetch,
fetchWorkflowById,
updateOptimistically,
} = useUserWorkflows();
} = useUserWorkflows(workflowOptions);
// Operations hook
// Operations hook - pass instance context when in feature route
const {
handleWorkflowDelete,
handleWorkflowDeleteMultiple,
handleWorkflowUpdate,
handleInlineUpdate,
deletingWorkflows,
} = useWorkflowOperations();
} = useWorkflowOperations(workflowOptions);
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
@ -173,7 +178,7 @@ export const WorkflowsPage: React.FC = () => {
<FormGeneratorTable
data={workflows}
columns={columns}
apiEndpoint="/api/workflows/"
apiEndpoint={apiEndpoint}
loading={loading}
pagination={true}
pageSize={25}