frontend_nyla/src/pages/views/graphicalEditor/GraphicalEditorPage.tsx
2026-04-07 14:22:52 +02:00

190 lines
7.4 KiB
TypeScript

/**
* GraphicalEditorPage
*
* Layout: [UDB sidebar (collapsible)] [FlowEditor (flex)] [Chat/Tracing (inside FlowEditor)]
* UDB provides access to Files & Sources while configuring nodes.
* AI Chat and Tracing panels are managed by the FlowEditor's CanvasHeader.
*
* File/Source attachment UX mirrors the Workspace:
* - Files: click in UDB FilesTab → added as pendingFile chip in chat input
* - Data Sources: 🔗 picker button in chat input (loaded from UDB API)
*/
import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { FaDatabase, FaChevronLeft } from 'react-icons/fa';
import { useInstanceId, useMandateId } from '../../../hooks/useCurrentInstance';
import { useLanguage } from '../../../providers/language/LanguageContext';
import { Automation2FlowEditor as FlowEditor } from '../../../components/FlowEditor';
import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from '../../../components/FlowEditor';
import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
import api from '../../../api';
import styles from '../../FeatureView.module.css';
interface GraphicalEditorPageProps {
persistentInstanceId?: string;
persistentMandateId?: string;
}
export const GraphicalEditorPage: React.FC<GraphicalEditorPageProps> = ({
persistentInstanceId,
persistentMandateId,
}) => {
const urlInstanceId = useInstanceId();
const urlMandateId = useMandateId();
const instanceId = persistentInstanceId || urlInstanceId;
const mandateId = persistentMandateId || urlMandateId;
const [searchParams] = useSearchParams();
const initialWorkflowIdRef = useRef(searchParams.get('workflowId'));
const { currentLanguage } = useLanguage();
const language = (currentLanguage?.slice(0, 2) || 'de') as string;
const [udbTab, setUdbTab] = useState<UdbTab>('files');
const [udbOpen, setUdbOpen] = useState(true);
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
const [dataSources, setDataSources] = useState<EditorDataSource[]>([]);
const [featureDataSources, setFeatureDataSources] = useState<EditorFeatureDataSource[]>([]);
const udbContext: UdbContext = useMemo(() => ({
instanceId: instanceId || '',
mandateId: mandateId || '',
featureInstanceId: instanceId || '',
}), [instanceId, mandateId]);
useEffect(() => {
if (!instanceId) return;
api.get(`/api/workspace/${instanceId}/datasources`)
.then(res => {
const list = (res.data.dataSources || res.data || []).map((d: any) => ({
id: d.id, label: d.label || d.path || d.id, path: d.path, sourceType: d.sourceType,
}));
setDataSources(list);
})
.catch(() => setDataSources([]));
}, [instanceId]);
useEffect(() => {
if (!instanceId) return;
api.get(`/api/workspace/${instanceId}/feature-datasources`)
.then(res => {
const list = (res.data.featureDataSources || res.data || []).map((d: any) => ({
id: d.id, featureInstanceId: d.featureInstanceId, featureCode: d.featureCode,
tableName: d.tableName, label: d.label || d.tableName,
}));
setFeatureDataSources(list);
})
.catch(() => setFeatureDataSources([]));
}, [instanceId]);
const _handleFileSelect = useCallback((fileId: string, fileName?: string) => {
setPendingFiles(prev => {
if (prev.some(f => f.fileId === fileId)) return prev;
return [...prev, { fileId, fileName: fileName || fileId.slice(0, 12) }];
});
}, []);
const _handleRemovePendingFile = useCallback((fileId: string) => {
setPendingFiles(prev => prev.filter(f => f.fileId !== fileId));
}, []);
const _handleSourcesChanged = useCallback(() => {
if (!instanceId) return;
api.get(`/api/workspace/${instanceId}/datasources`)
.then(res => {
setDataSources((res.data.dataSources || res.data || []).map((d: any) => ({
id: d.id, label: d.label || d.path || d.id, path: d.path, sourceType: d.sourceType,
})));
})
.catch(() => {});
api.get(`/api/workspace/${instanceId}/feature-datasources`)
.then(res => {
setFeatureDataSources((res.data.featureDataSources || res.data || []).map((d: any) => ({
id: d.id, featureInstanceId: d.featureInstanceId, featureCode: d.featureCode,
tableName: d.tableName, label: d.label || d.tableName,
})));
})
.catch(() => {});
}, [instanceId]);
if (!instanceId) {
return (
<div className={styles.placeholder}>
<h2>Graphical Editor</h2>
<p>Keine Feature-Instanz gefunden.</p>
</div>
);
}
return (
<div style={{ flex: 1, minHeight: 0, display: 'flex', position: 'relative' }}>
{/* UDB Sidebar */}
{udbOpen ? (
<div style={{
width: 280, minWidth: 280,
borderRight: '1px solid var(--border-color, #e0e0e0)',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
background: 'var(--bg-primary, #fff)',
}}>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '6px 12px',
borderBottom: '1px solid var(--border-color, #e0e0e0)',
background: 'var(--bg-secondary, #f8f9fa)',
}}>
<span style={{ fontWeight: 600, fontSize: '13px', display: 'flex', alignItems: 'center', gap: 6 }}>
<FaDatabase style={{ fontSize: 12, opacity: 0.6 }} /> Daten
</span>
<button
onClick={() => setUdbOpen(false)}
title="Sidebar schliessen"
style={{
border: 'none', background: 'transparent', cursor: 'pointer',
fontSize: '14px', padding: '2px 4px', borderRadius: 4,
display: 'flex', alignItems: 'center',
}}
>
<FaChevronLeft style={{ fontSize: 12 }} />
</button>
</div>
<UnifiedDataBar
context={udbContext}
activeTab={udbTab}
onTabChange={setUdbTab}
hideTabs={['chats']}
onFileSelect={_handleFileSelect}
onSourcesChanged={_handleSourcesChanged}
/>
</div>
) : (
<button
onClick={() => setUdbOpen(true)}
title="Daten-Sidebar öffnen"
style={{
position: 'absolute', left: 0, top: '50%', transform: 'translateY(-50%)',
zIndex: 20, width: 28, height: 80,
border: '1px solid var(--border-color, #ddd)', borderLeft: 'none',
borderRadius: '0 8px 8px 0',
background: 'var(--bg-primary, #fff)', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '2px 0 6px rgba(0,0,0,0.06)',
}}
>
<FaDatabase style={{ fontSize: 13, opacity: 0.5 }} />
</button>
)}
{/* FlowEditor */}
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
<FlowEditor
instanceId={instanceId}
language={language}
initialWorkflowId={initialWorkflowIdRef.current}
pendingFiles={pendingFiles}
onRemovePendingFile={_handleRemovePendingFile}
dataSources={dataSources}
featureDataSources={featureDataSources}
/>
</div>
</div>
);
};