fixed nodes handovers
This commit is contained in:
parent
ad96c6d861
commit
c7e94aea79
3 changed files with 168 additions and 41 deletions
|
|
@ -1026,12 +1026,16 @@ export interface WorkspaceRunDetail {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
nodeType: string;
|
nodeType: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
inputSnapshot?: Record<string, unknown>;
|
||||||
output?: Record<string, unknown>;
|
output?: Record<string, unknown>;
|
||||||
|
inputFiles?: Array<{ id: string; fileName?: string }>;
|
||||||
|
outputFiles?: Array<{ id: string; fileName?: string }>;
|
||||||
error?: string;
|
error?: string;
|
||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
completedAt?: number;
|
completedAt?: number;
|
||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
tokensUsed?: number;
|
tokensUsed?: number;
|
||||||
|
retryCount?: number;
|
||||||
}>;
|
}>;
|
||||||
files: Array<{
|
files: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -1039,10 +1043,14 @@ export interface WorkspaceRunDetail {
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
sizeBytes?: number;
|
sizeBytes?: number;
|
||||||
}>;
|
}>;
|
||||||
|
unassignedFiles?: Array<{
|
||||||
|
id: string;
|
||||||
|
fileName?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchWorkspaceRuns(
|
export async function fetchWorkspaceRuns(
|
||||||
request: ApiRequestOptions['request'],
|
request: ApiRequestFunction,
|
||||||
params: {
|
params: {
|
||||||
scope?: 'mine' | 'mandate';
|
scope?: 'mine' | 'mandate';
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
@ -1061,14 +1069,14 @@ export async function fetchWorkspaceRuns(
|
||||||
if (params.offset) query.set('offset', String(params.offset));
|
if (params.offset) query.set('offset', String(params.offset));
|
||||||
const qs = query.toString();
|
const qs = query.toString();
|
||||||
const url = `/api/automations/runs${qs ? `?${qs}` : ''}`;
|
const url = `/api/automations/runs${qs ? `?${qs}` : ''}`;
|
||||||
const resp = await request({ url, method: 'GET' });
|
const resp = await request({ url, method: 'get' });
|
||||||
return resp as { runs: WorkspaceRun[]; total: number };
|
return resp as { runs: WorkspaceRun[]; total: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchWorkspaceRunDetail(
|
export async function fetchWorkspaceRunDetail(
|
||||||
request: ApiRequestOptions['request'],
|
request: ApiRequestFunction,
|
||||||
runId: string,
|
runId: string,
|
||||||
): Promise<WorkspaceRunDetail> {
|
): Promise<WorkspaceRunDetail> {
|
||||||
const resp = await request({ url: `/api/automations/runs/${runId}/detail`, method: 'GET' });
|
const resp = await request({ url: `/api/automations/runs/${runId}/detail`, method: 'get' });
|
||||||
return resp as WorkspaceRunDetail;
|
return resp as WorkspaceRunDetail;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import type { CanvasNode } from '../../editor/FlowCanvas';
|
||||||
import { DataRefRenderer } from './DataRefRenderer';
|
import { DataRefRenderer } from './DataRefRenderer';
|
||||||
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
||||||
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
||||||
|
import { getApiBaseUrl } from '../../../../../config/config';
|
||||||
|
|
||||||
const TextInput: React.FC<FieldRendererProps> = ({ param, value, onChange }) => (
|
const TextInput: React.FC<FieldRendererProps> = ({ param, value, onChange }) => (
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
|
@ -747,7 +748,7 @@ const ModelMultiSelect: React.FC<FieldRendererProps> = ({ param, value, onChange
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetch('/api/system/ai-models', { credentials: 'include' })
|
fetch(`${getApiBaseUrl()}/api/system/ai-models`, { credentials: 'include' })
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
|
||||||
|
|
@ -1083,6 +1083,100 @@ const _WorkflowsTab: React.FC<_WorkflowsTabProps> = ({ onWorkflowClick }) => {
|
||||||
// Workspace Tab (run detail only — no table)
|
// Workspace Tab (run detail only — no table)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
|
const _FILE_REF_KEYS = new Set(['fileId', 'documentId', 'fileIds', 'documents']);
|
||||||
|
|
||||||
|
function _isPlainObject(v: unknown): v is Record<string, unknown> {
|
||||||
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _stripFileRefKeys(value: unknown): unknown {
|
||||||
|
if (_isPlainObject(value)) {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(value)) {
|
||||||
|
if (_FILE_REF_KEYS.has(k)) continue;
|
||||||
|
const stripped = _stripFileRefKeys(v);
|
||||||
|
if (stripped !== undefined) out[k] = stripped;
|
||||||
|
}
|
||||||
|
return Object.keys(out).length > 0 ? out : undefined;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const out = value.map((v) => _stripFileRefKeys(v)).filter((v) => v !== undefined);
|
||||||
|
return out.length > 0 ? out : undefined;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatScalar(v: unknown): string {
|
||||||
|
if (v === null || v === undefined) return '—';
|
||||||
|
if (typeof v === 'string') return v;
|
||||||
|
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
||||||
|
return JSON.stringify(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _DataBlock: React.FC<{ data: unknown; emptyHint?: string }> = ({ data, emptyHint }) => {
|
||||||
|
if (data === undefined || data === null) {
|
||||||
|
return emptyHint ? <p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', margin: 0 }}>{emptyHint}</p> : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isPlainObject(data)) {
|
||||||
|
const entries = Object.entries(data);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return emptyHint ? <p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', margin: 0 }}>{emptyHint}</p> : null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
|
||||||
|
{entries.map(([k, v]) => {
|
||||||
|
const isComplex = _isPlainObject(v) || Array.isArray(v);
|
||||||
|
if (isComplex) {
|
||||||
|
return (
|
||||||
|
<details key={k} style={{ fontSize: '0.8rem' }}>
|
||||||
|
<summary style={{ cursor: 'pointer', color: 'var(--text-secondary)' }}>
|
||||||
|
<code style={{ fontWeight: 500 }}>{k}</code>
|
||||||
|
</summary>
|
||||||
|
<pre style={{ fontSize: '0.75rem', maxHeight: 240, overflow: 'auto', margin: '0.25rem 0 0 1rem', background: 'var(--bg-secondary)', padding: '0.5rem', borderRadius: 4 }}>
|
||||||
|
{JSON.stringify(v, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={k} style={{ display: 'flex', gap: '0.5rem', fontSize: '0.8rem', alignItems: 'baseline' }}>
|
||||||
|
<code style={{ color: 'var(--text-secondary)', minWidth: 140, flexShrink: 0 }}>{k}</code>
|
||||||
|
<span style={{ wordBreak: 'break-word' }}>{_formatScalar(v)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre style={{ fontSize: '0.75rem', maxHeight: 240, overflow: 'auto', margin: 0, background: 'var(--bg-secondary)', padding: '0.5rem', borderRadius: 4 }}>
|
||||||
|
{JSON.stringify(data, null, 2)}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _FileLinkList: React.FC<{ files: Array<{ id: string; fileName?: string }> }> = ({ files }) => {
|
||||||
|
if (!files.length) return null;
|
||||||
|
const baseUrl = api.defaults.baseURL || '';
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap', marginTop: '0.25rem' }}>
|
||||||
|
{files.map((f) => (
|
||||||
|
<a
|
||||||
|
key={f.id}
|
||||||
|
href={`${baseUrl}/api/files/${f.id}/download`}
|
||||||
|
download
|
||||||
|
style={{ padding: '0.3rem 0.6rem', border: '1px solid var(--border-color)', borderRadius: 4, textDecoration: 'none', color: 'var(--primary-color)', fontSize: '0.78rem' }}
|
||||||
|
>
|
||||||
|
<FaDownload style={{ marginRight: 4 }} />
|
||||||
|
{f.fileName || f.id}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface _WorkspaceTabProps {
|
interface _WorkspaceTabProps {
|
||||||
runId: string | null;
|
runId: string | null;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
|
@ -1113,20 +1207,20 @@ const _WorkspaceTab: React.FC<_WorkspaceTabProps> = ({ runId, onBack }) => {
|
||||||
|
|
||||||
if (!runId) {
|
if (!runId) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '1rem', color: 'var(--text-secondary)' }}>
|
<div style={{ padding: '1rem', flex: 1, color: 'var(--text-secondary)' }}>
|
||||||
<p>{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}</p>
|
<p>{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detailLoading || !runDetail) {
|
if (detailLoading || !runDetail) {
|
||||||
return <div style={{ padding: '1rem' }}><p>{t('Laden…')}</p></div>;
|
return <div style={{ padding: '1rem', flex: 1 }}><p>{t('Laden…')}</p></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { run, steps, files, workflow } = runDetail;
|
const { run, steps, workflow, unassignedFiles } = runDetail;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '1rem' }}>
|
<div style={{ padding: '1rem', flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
||||||
<button type="button" className={styles.secondaryButton} onClick={onBack} style={{ marginBottom: '1rem' }}>
|
<button type="button" className={styles.secondaryButton} onClick={onBack} style={{ marginBottom: '1rem' }}>
|
||||||
← {t('Zurück zum Dashboard')}
|
← {t('Zurück zum Dashboard')}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1148,41 +1242,65 @@ const _WorkspaceTab: React.FC<_WorkspaceTabProps> = ({ runId, onBack }) => {
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Schritte protokolliert.')}</p>
|
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Schritte protokolliert.')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
{steps.map((step) => (
|
{steps.map((step) => {
|
||||||
<details key={step.id} style={{ border: '1px solid var(--border-color)', borderRadius: 6, padding: '0.5rem' }}>
|
const inputData = _stripFileRefKeys(step.inputSnapshot ?? {});
|
||||||
<summary style={{ cursor: 'pointer', fontWeight: 500 }}>
|
const outputData = _stripFileRefKeys(step.output ?? {});
|
||||||
<span style={{ marginRight: '0.5rem', fontSize: '0.75rem', padding: '2px 6px', borderRadius: 4, background: step.status === 'completed' ? 'rgba(40,167,69,0.15)' : step.status === 'failed' ? 'rgba(220,53,69,0.15)' : 'rgba(0,123,255,0.15)', color: step.status === 'completed' ? 'var(--success-color)' : step.status === 'failed' ? 'var(--danger-color)' : 'var(--primary-color)' }}>
|
const inputFiles = step.inputFiles ?? [];
|
||||||
{step.status}
|
const outputFiles = step.outputFiles ?? [];
|
||||||
</span>
|
const hasInput = inputData !== undefined || inputFiles.length > 0;
|
||||||
{step.nodeType} ({step.nodeId})
|
const hasOutput = outputData !== undefined || outputFiles.length > 0;
|
||||||
{step.durationMs != null && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{step.durationMs}ms</span>}
|
return (
|
||||||
</summary>
|
<details key={step.id} style={{ border: '1px solid var(--border-color)', borderRadius: 6, padding: '0.5rem' }}>
|
||||||
{step.output && Object.keys(step.output).length > 0 && (
|
<summary style={{ cursor: 'pointer', fontWeight: 500 }}>
|
||||||
<pre style={{ fontSize: '0.75rem', maxHeight: 300, overflow: 'auto', marginTop: '0.5rem', background: 'var(--bg-secondary)', padding: '0.5rem', borderRadius: 4 }}>
|
<span style={{ marginRight: '0.5rem', fontSize: '0.75rem', padding: '2px 6px', borderRadius: 4, background: step.status === 'completed' ? 'rgba(40,167,69,0.15)' : step.status === 'failed' ? 'rgba(220,53,69,0.15)' : 'rgba(0,123,255,0.15)', color: step.status === 'completed' ? 'var(--success-color)' : step.status === 'failed' ? 'var(--danger-color)' : 'var(--primary-color)' }}>
|
||||||
{JSON.stringify(step.output, null, 2)}
|
{step.status}
|
||||||
</pre>
|
</span>
|
||||||
)}
|
{step.nodeType} ({step.nodeId})
|
||||||
{step.error && <p style={{ color: 'var(--danger-color)', marginTop: '0.25rem' }}>{step.error}</p>}
|
{step.durationMs != null && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{step.durationMs}ms</span>}
|
||||||
</details>
|
{(step.tokensUsed ?? 0) > 0 && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{step.tokensUsed} tokens</span>}
|
||||||
))}
|
</summary>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginTop: '0.5rem' }}>
|
||||||
|
{hasInput && (
|
||||||
|
<section>
|
||||||
|
<div style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: '0.25rem' }}>
|
||||||
|
{t('Input')}
|
||||||
|
</div>
|
||||||
|
<_DataBlock data={inputData} />
|
||||||
|
<_FileLinkList files={inputFiles} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{hasOutput && (
|
||||||
|
<section>
|
||||||
|
<div style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: '0.25rem' }}>
|
||||||
|
{t('Output')}
|
||||||
|
</div>
|
||||||
|
<_DataBlock data={outputData} />
|
||||||
|
<_FileLinkList files={outputFiles} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{step.error && (
|
||||||
|
<section>
|
||||||
|
<div style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--danger-color)', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: '0.25rem' }}>
|
||||||
|
{t('Fehler')}
|
||||||
|
</div>
|
||||||
|
<p style={{ color: 'var(--danger-color)', margin: 0, fontSize: '0.85rem' }}>{step.error}</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', fontSize: '0.75rem', color: 'var(--text-secondary)', borderTop: '1px solid var(--border-color)', paddingTop: '0.4rem' }}>
|
||||||
|
{step.startedAt && <span>{t('Start')}: {_formatTs(step.startedAt)}</span>}
|
||||||
|
{step.completedAt && <span>{t('Ende')}: {_formatTs(step.completedAt)}</span>}
|
||||||
|
{(step.retryCount ?? 0) > 0 && <span>{t('Wiederholungen')}: {step.retryCount}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{files.length > 0 && (
|
{unassignedFiles && unassignedFiles.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Dokumente')}</h4>
|
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Sonstige Dokumente')}</h4>
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
<_FileLinkList files={unassignedFiles} />
|
||||||
{files.map((f) => (
|
|
||||||
<a
|
|
||||||
key={f.id}
|
|
||||||
href={`/api/files/${f.id}/download`}
|
|
||||||
download
|
|
||||||
style={{ padding: '0.5rem 1rem', border: '1px solid var(--border-color)', borderRadius: 6, textDecoration: 'none', color: 'var(--primary-color)', fontSize: '0.85rem' }}
|
|
||||||
>
|
|
||||||
<FaDownload style={{ marginRight: 4 }} />
|
|
||||||
{f.fileName || f.id}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue