128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
import React from 'react';
|
|
|
|
type WorkflowRuntime = {
|
|
mode?: string;
|
|
};
|
|
|
|
const LOCAL_WORKFLOW_URL = '/workflow-ui/';
|
|
const DOCKER_WORKFLOW_URL = '/api/comfyui/proxy/';
|
|
|
|
export default function Workflows() {
|
|
const [ready, setReady] = React.useState(false);
|
|
const [loading, setLoading] = React.useState(true);
|
|
const [message, setMessage] = React.useState('正在加载工作流前端...');
|
|
const [workflowUrl, setWorkflowUrl] = React.useState(LOCAL_WORKFLOW_URL);
|
|
const [backendLabel, setBackendLabel] = React.useState('');
|
|
const [reloadKey, setReloadKey] = React.useState(0);
|
|
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
|
|
|
|
const openFullscreen = () => {
|
|
window.open(workflowUrl, '_blank', 'noopener,noreferrer');
|
|
};
|
|
|
|
const reloadFrame = () => {
|
|
if (iframeRef.current) {
|
|
setLoading(true);
|
|
iframeRef.current.src = workflowUrl;
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
setMessage('正在重新检查工作流入口...');
|
|
setReloadKey((value) => value + 1);
|
|
};
|
|
|
|
React.useEffect(() => {
|
|
let cancelled = false;
|
|
const controller = new AbortController();
|
|
const loadRuntime = async () => {
|
|
const runtimeResponse = await fetch('/api/workflow-ui/runtime', { signal: controller.signal });
|
|
const runtime = runtimeResponse.ok ? ((await runtimeResponse.json()) as WorkflowRuntime) : {};
|
|
const dockerMode = runtime.mode === 'docker';
|
|
const targetUrl = dockerMode ? DOCKER_WORKFLOW_URL : LOCAL_WORKFLOW_URL;
|
|
if (cancelled) return;
|
|
setBackendLabel(resolveBackendLabel(runtime.mode));
|
|
setWorkflowUrl(targetUrl);
|
|
const response = await fetch(targetUrl, { signal: controller.signal });
|
|
if (cancelled) return;
|
|
setReady(response.ok);
|
|
setLoading(false);
|
|
setMessage(response.ok ? '' : 'Workflow Studio 前端未构建,请先执行 workflow-ui:install');
|
|
};
|
|
void loadRuntime().catch((error) => {
|
|
if (cancelled || error?.name === 'AbortError') return;
|
|
setReady(false);
|
|
setLoading(false);
|
|
setMessage('工作流入口不可用,请检查服务配置');
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
controller.abort();
|
|
};
|
|
}, [reloadKey]);
|
|
|
|
return (
|
|
<div className="workflow-shell">
|
|
<div className="workflow-toolbar">
|
|
<div className="workflow-toolbar-main">
|
|
<div className="workflow-title">工作流</div>
|
|
{backendLabel && <div className="workflow-backend-badge">{backendLabel}</div>}
|
|
</div>
|
|
<div className="workflow-actions">
|
|
<button type="button" onClick={reloadFrame} disabled={!ready} className="btn btn-link workflow-action-btn">
|
|
刷新
|
|
</button>
|
|
<button type="button" onClick={openFullscreen} className="btn btn-primary workflow-primary-btn">
|
|
新窗口打开
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{ready ? (
|
|
<div className="workflow-frame-wrap">
|
|
{loading && (
|
|
<div className="workflow-frame-loading" aria-live="polite">
|
|
<div className="workflow-loading-bar" />
|
|
<span>正在连接工作流前端...</span>
|
|
</div>
|
|
)}
|
|
<iframe
|
|
ref={iframeRef}
|
|
src={workflowUrl}
|
|
title="Workflow Studio"
|
|
allow="clipboard-read; clipboard-write"
|
|
referrerPolicy="no-referrer"
|
|
onLoad={() => setLoading(false)}
|
|
className="workflow-frame"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="workflow-empty">
|
|
<div className="workflow-empty-panel">
|
|
<div className="workflow-empty-title">Workflow Studio</div>
|
|
<div className="workflow-empty-desc">{message}</div>
|
|
<button type="button" onClick={reloadFrame} className="btn btn-soft-primary workflow-empty-btn">
|
|
重新检查
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function resolveBackendLabel(mode?: string) {
|
|
switch (mode) {
|
|
case 'cloud':
|
|
return 'Comfy Cloud API 后端';
|
|
case 'runninghub':
|
|
return 'RunningHub API 后端';
|
|
case 'external':
|
|
return '外部 ComfyUI API 后端';
|
|
case 'docker':
|
|
return 'Docker 完整版 ComfyUI';
|
|
case 'boosapi':
|
|
return 'BoosAPI 工作流执行器';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|