Spaces:
Running
Running
| """ | |
| app.py ―――――――――――――――――――――――――――――――――――――――――――――――― | |
| ✓ Preview · Save · Load 기능 포함 | |
| ✓ Load 시 노드/좌표/엣지 자동 복원 | |
| ✓ gr.update()를 사용한 강제 업데이트 | |
| """ | |
| import os, json, typing, tempfile | |
| import gradio as gr | |
| from gradio_workflowbuilder import WorkflowBuilder | |
| # ------------------------------------------------------------------- | |
| # 🛠️ 헬퍼 | |
| # ------------------------------------------------------------------- | |
| def export_pretty(data: typing.Dict[str, typing.Any]) -> str: | |
| print(f"[Export] Current workflow data: {json.dumps(data, indent=2)}") | |
| return json.dumps(data, indent=2, ensure_ascii=False) if data else "No workflow to export" | |
| def export_file(data: typing.Dict[str, typing.Any]) -> typing.Optional[str]: | |
| if not data: | |
| return None | |
| fd, path = tempfile.mkstemp(suffix=".json") | |
| with os.fdopen(fd, "w", encoding="utf-8") as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| return path | |
| def import_workflow(file_obj): | |
| """파일에서 JSON을 읽고 WorkflowBuilder를 업데이트""" | |
| if file_obj is None: | |
| return None, "No file selected", "Status: No file", gr.update(visible=False) | |
| try: | |
| with open(file_obj.name, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| print(f"[Import] Loaded data: {json.dumps(data, indent=2)}") | |
| # 데이터 검증 | |
| if not isinstance(data, dict): | |
| return None, "Invalid format: not a dictionary", "Status: Invalid format", gr.update(visible=False) | |
| # 필수 필드 확인 | |
| if 'nodes' not in data: | |
| data['nodes'] = [] | |
| if 'edges' not in data: | |
| data['edges'] = [] | |
| nodes_count = len(data.get('nodes', [])) | |
| edges_count = len(data.get('edges', [])) | |
| # 직접 데이터 반환 | |
| return ( | |
| data, # 직접 데이터 반환 | |
| json.dumps(data, indent=2, ensure_ascii=False), | |
| f"✅ Loaded: {nodes_count} nodes, {edges_count} edges", | |
| gr.update(visible=True) # JavaScript 실행 버튼 표시 | |
| ) | |
| except Exception as e: | |
| print(f"[Import] Error: {str(e)}") | |
| return None, f"Error: {str(e)}", f"❌ Error: {str(e)}", gr.update(visible=False) | |
| def reset_workflow(): | |
| """워크플로우 초기화""" | |
| empty_workflow = {"nodes": [], "edges": []} | |
| return empty_workflow, "Reset complete" | |
| def set_sample_workflow(): | |
| """샘플 워크플로우 설정""" | |
| sample = { | |
| "nodes": [ | |
| { | |
| "id": "1", | |
| "type": "default", | |
| "position": {"x": 100, "y": 100}, | |
| "data": {"label": "Start Node"} | |
| }, | |
| { | |
| "id": "2", | |
| "type": "default", | |
| "position": {"x": 300, "y": 100}, | |
| "data": {"label": "Process"} | |
| }, | |
| { | |
| "id": "3", | |
| "type": "default", | |
| "position": {"x": 500, "y": 100}, | |
| "data": {"label": "End Node"} | |
| } | |
| ], | |
| "edges": [ | |
| { | |
| "id": "e1-2", | |
| "source": "1", | |
| "target": "2" | |
| }, | |
| { | |
| "id": "e2-3", | |
| "source": "2", | |
| "target": "3" | |
| } | |
| ] | |
| } | |
| print(f"[Sample] Setting workflow: {json.dumps(sample, indent=2)}") | |
| return sample, json.dumps(sample, indent=2, ensure_ascii=False), "✅ Sample loaded" | |
| def debug_current_state(data): | |
| """현재 상태 디버깅""" | |
| if not data: | |
| return "Empty workflow", "{}" | |
| nodes = data.get('nodes', []) | |
| edges = data.get('edges', []) | |
| debug_info = f"Nodes: {len(nodes)}, Edges: {len(edges)}" | |
| if nodes: | |
| debug_info += f"\nFirst node: {nodes[0].get('id', 'unknown')}" | |
| return debug_info, json.dumps(data, indent=2) | |
| # ------------------------------------------------------------------- | |
| # 🎨 CSS | |
| # ------------------------------------------------------------------- | |
| CSS = """ | |
| .main-container{max-width:1600px;margin:0 auto;} | |
| .workflow-section{margin-bottom:2rem;min-height:500px;} | |
| .button-row{display:flex;gap:1rem;justify-content:center;margin:1rem 0;} | |
| .status-box{ | |
| padding:10px;border-radius:5px;margin-top:10px; | |
| background:#f0f9ff;border:1px solid #3b82f6;color:#1e40af; | |
| } | |
| .component-description{ | |
| padding:24px;background:linear-gradient(135deg,#f8fafc 0%,#e2e8f0 100%); | |
| border-left:4px solid #3b82f6;border-radius:12px; | |
| box-shadow:0 2px 8px rgba(0,0,0,.05);margin:16px 0; | |
| } | |
| """ | |
| # ------------------------------------------------------------------- | |
| # 🖥️ Gradio 앱 | |
| # ------------------------------------------------------------------- | |
| with gr.Blocks(title="🎨 Visual Workflow Builder", theme=gr.themes.Soft(), css=CSS) as demo: | |
| gr.Markdown("# 🎨 Visual Workflow Builder\n**Debug Version - Testing JSON Load**") | |
| # 상태 표시 | |
| status_text = gr.Textbox( | |
| label="Status", | |
| value="Ready", | |
| elem_classes=["status-box"], | |
| interactive=False | |
| ) | |
| # ─── Workflow Builder ─── | |
| with gr.Column(elem_classes=["workflow-section"]): | |
| workflow_builder = WorkflowBuilder( | |
| label="🎨 Visual Workflow Designer", | |
| value={"nodes": [], "edges": []}, | |
| elem_id="workflow_builder" | |
| ) | |
| # ─── Control Panel ─── | |
| gr.Markdown("## 🎮 Control Panel") | |
| # JavaScript force update button (hidden by default) | |
| btn_force_update = gr.Button("⚡ Force Update (JavaScript)", variant="warning", visible=False) | |
| loaded_data_store = gr.State(None) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 📥 Import") | |
| file_upload = gr.File( | |
| label="Select JSON file", | |
| file_types=[".json"], | |
| type="filepath" | |
| ) | |
| btn_load = gr.Button("🚀 Load File", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 🧪 Test") | |
| btn_sample = gr.Button("📊 Load Sample", variant="primary", size="lg") | |
| btn_reset = gr.Button("🔄 Reset", variant="stop", size="lg") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 💾 Export") | |
| btn_preview = gr.Button("👁️ Preview JSON", size="lg") | |
| btn_download = gr.DownloadButton("💾 Download JSON", size="lg") | |
| # ─── Alternative Render Method ─── | |
| with gr.Accordion("🔬 Alternative Load Method", open=False): | |
| gr.Markdown("If normal loading fails, try this dynamic rendering approach:") | |
| btn_render = gr.Button("🎯 Render with Data", variant="primary") | |
| render_container = gr.Column() | |
| def render_workflow_dynamic(data): | |
| if data: | |
| with gr.Column(): | |
| gr.Markdown("### Dynamically Rendered Workflow") | |
| return WorkflowBuilder( | |
| label="Dynamic Workflow", | |
| value=data | |
| ) | |
| else: | |
| return gr.Markdown("No data loaded yet") | |
| # ─── Debug Section ─── | |
| with gr.Accordion("🔍 Debug Info", open=True): | |
| with gr.Row(): | |
| debug_text = gr.Textbox(label="State Info", lines=2) | |
| debug_json = gr.Code(language="json", label="Current JSON", lines=10) | |
| btn_debug = gr.Button("🔍 Update Debug Info", variant="secondary") | |
| # ─── Code View ─── | |
| code_view = gr.Code(language="json", label="JSON Preview", lines=15) | |
| # HTML Fallback Viewer 함수 정의 | |
| def create_html_view(data): | |
| """JSON을 HTML로 시각화""" | |
| if not data: | |
| return "<p>No workflow data</p>" | |
| html = """ | |
| <div style='font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; border-radius: 8px;'> | |
| <h3>Workflow Visualization</h3> | |
| <div style='display: flex; gap: 20px; flex-wrap: wrap;'> | |
| """ | |
| # 노드 표시 | |
| nodes = data.get('nodes', []) | |
| for node in nodes: | |
| node_id = node.get('id', 'unknown') | |
| label = node.get('data', {}).get('label', 'Node') | |
| x = node.get('position', {}).get('x', 0) | |
| y = node.get('position', {}).get('y', 0) | |
| html += f""" | |
| <div style='background: white; border: 2px solid #3b82f6; border-radius: 8px; | |
| padding: 15px; margin: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| min-width: 150px;'> | |
| <strong>ID:</strong> {node_id}<br> | |
| <strong>Label:</strong> {label}<br> | |
| <strong>Position:</strong> ({x}, {y}) | |
| </div> | |
| """ | |
| html += "</div>" | |
| # 엣지 표시 | |
| edges = data.get('edges', []) | |
| if edges: | |
| html += "<h4>Connections:</h4><ul>" | |
| for edge in edges: | |
| source = edge.get('source', '?') | |
| target = edge.get('target', '?') | |
| html += f"<li>{source} → {target}</li>" | |
| html += "</ul>" | |
| html += "</div>" | |
| return html | |
| # HTML Fallback Viewer | |
| gr.Markdown("### 📊 HTML Workflow Viewer (Fallback)") | |
| html_viewer = gr.HTML(label="Workflow HTML View") | |
| # ─── Event Handlers ─── | |
| # Load file - 데이터를 State에 저장 | |
| file_upload.change( | |
| fn=import_workflow, | |
| inputs=file_upload, | |
| outputs=[loaded_data_store, code_view, status_text, btn_force_update] | |
| ) | |
| # Load button - State에서 WorkflowBuilder로 전송 | |
| btn_load.click( | |
| fn=lambda data: data if data else {"nodes": [], "edges": []}, | |
| inputs=loaded_data_store, | |
| outputs=workflow_builder | |
| ) | |
| # Force update with JavaScript | |
| btn_force_update.click( | |
| fn=None, | |
| inputs=loaded_data_store, | |
| outputs=None, | |
| js=""" | |
| (data) => { | |
| console.log('[Force Update] Attempting to update WorkflowBuilder with:', data); | |
| // WorkflowBuilder 엘리먼트 찾기 | |
| const workflowElement = document.querySelector('#workflow_builder'); | |
| if (workflowElement) { | |
| // 여러 방법 시도 | |
| // 방법 1: 직접 value 설정 | |
| if (workflowElement.__vue__) { | |
| workflowElement.__vue__.value = data; | |
| workflowElement.__vue__.$forceUpdate(); | |
| } | |
| // 방법 2: 이벤트 발생 | |
| const event = new CustomEvent('input', { | |
| detail: data, | |
| bubbles: true | |
| }); | |
| workflowElement.dispatchEvent(event); | |
| // 방법 3: Gradio 이벤트 | |
| if (window.gradio_config && window.gradio_config.components) { | |
| const component = Object.values(window.gradio_config.components).find( | |
| c => c.props && c.props.elem_id === 'workflow_builder' | |
| ); | |
| if (component) { | |
| component.props.value = data; | |
| } | |
| } | |
| } | |
| return data; | |
| } | |
| """ | |
| ) | |
| # Sample workflow | |
| btn_sample.click( | |
| fn=set_sample_workflow, | |
| outputs=[workflow_builder, code_view, status_text] | |
| ) | |
| # Reset | |
| btn_reset.click( | |
| fn=reset_workflow, | |
| outputs=[workflow_builder, status_text] | |
| ) | |
| # Preview | |
| btn_preview.click( | |
| fn=export_pretty, | |
| inputs=workflow_builder, | |
| outputs=code_view | |
| ) | |
| # Download | |
| btn_download.click( | |
| fn=export_file, | |
| inputs=workflow_builder | |
| ) | |
| # Debug | |
| btn_debug.click( | |
| fn=debug_current_state, | |
| inputs=workflow_builder, | |
| outputs=[debug_text, debug_json] | |
| ) | |
| # Auto-debug on change | |
| workflow_builder.change( | |
| fn=lambda x: f"Changed - Nodes: {len(x.get('nodes', []))}, Edges: {len(x.get('edges', []))}", | |
| inputs=workflow_builder, | |
| outputs=status_text | |
| ) | |
| # HTML viewer 업데이트 | |
| workflow_builder.change( | |
| fn=create_html_view, | |
| inputs=workflow_builder, | |
| outputs=html_viewer | |
| ) | |
| # Also update HTML view when loading | |
| loaded_data_store.change( | |
| fn=create_html_view, | |
| inputs=loaded_data_store, | |
| outputs=html_viewer | |
| ) | |
| # ─── Instructions ─── | |
| with gr.Accordion("📖 Troubleshooting", open=True): | |
| gr.Markdown( | |
| """ | |
| ### 🔧 If JSON doesn't load visually: | |
| 1. **Try the Sample first** - Click "Load Sample" to test if the component works | |
| 2. **Check Console** - Open browser DevTools (F12) for errors | |
| 3. **File Format** - Ensure your JSON has this structure: | |
| ```json | |
| { | |
| "nodes": [{ | |
| "id": "1", | |
| "type": "default", | |
| "position": {"x": 100, "y": 100}, | |
| "data": {"label": "Node"} | |
| }], | |
| "edges": [{ | |
| "id": "e1", | |
| "source": "1", | |
| "target": "2" | |
| }] | |
| } | |
| ``` | |
| 4. **Alternative Methods**: | |
| - Click file → Click "Load File" button | |
| - Try "Reset" then load again | |
| - Refresh page (F5) after loading | |
| - Use the "Force Update" button if it appears | |
| 5. **Component Limitations**: | |
| - Some custom Gradio components may have bugs with dynamic updates | |
| - The WorkflowBuilder might require specific node types or data formats | |
| ### 🚨 Known Issues with gradio_workflowbuilder: | |
| This appears to be a bug in the gradio_workflowbuilder component where it doesn't properly respond to value updates. Possible workarounds: | |
| 1. **Manual Recreation**: Copy the JSON and manually recreate nodes | |
| 2. **Component Update**: Check if there's a newer version: `pip install --upgrade gradio_workflowbuilder` | |
| 3. **Alternative Components**: Consider using other workflow builders or diagram libraries | |
| 4. **Report Bug**: Report this issue to the component maintainer | |
| ### 💡 Alternative Solution: | |
| If loading continues to fail, you might need to: | |
| ```python | |
| # Option 1: Recreate the component on each load | |
| @gr.render(inputs=[loaded_data_store]) | |
| def render_workflow(data): | |
| return WorkflowBuilder(value=data) | |
| # Option 2: Use a different workflow library | |
| # Consider react-flow, vis.js, or cytoscape.js wrapped in Gradio HTML | |
| ``` | |
| ### 🎨 HTML Fallback Viewer: | |
| If the WorkflowBuilder component is broken, use the HTML viewer below to at least see your workflow structure: | |
| """ | |
| ) | |
| # HTML Fallback Viewer | |
| gr.Markdown("### 📊 HTML Workflow Viewer (Fallback)") | |
| html_viewer = gr.HTML(label="Workflow HTML View") | |
| def create_html_view(data): | |
| """JSON을 HTML로 시각화""" | |
| if not data: | |
| return "<p>No workflow data</p>" | |
| html = """ | |
| <div style='font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; border-radius: 8px;'> | |
| <h3>Workflow Visualization</h3> | |
| <div style='display: flex; gap: 20px; flex-wrap: wrap;'> | |
| """ | |
| # 노드 표시 | |
| nodes = data.get('nodes', []) | |
| for node in nodes: | |
| node_id = node.get('id', 'unknown') | |
| label = node.get('data', {}).get('label', 'Node') | |
| x = node.get('position', {}).get('x', 0) | |
| y = node.get('position', {}).get('y', 0) | |
| html += f""" | |
| <div style='background: white; border: 2px solid #3b82f6; border-radius: 8px; | |
| padding: 15px; margin: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| min-width: 150px;'> | |
| <strong>ID:</strong> {node_id}<br> | |
| <strong>Label:</strong> {label}<br> | |
| <strong>Position:</strong> ({x}, {y}) | |
| </div> | |
| """ | |
| html += "</div>" | |
| # 엣지 표시 | |
| edges = data.get('edges', []) | |
| if edges: | |
| html += "<h4>Connections:</h4><ul>" | |
| for edge in edges: | |
| source = edge.get('source', '?') | |
| target = edge.get('target', '?') | |
| html += f"<li>{source} → {target}</li>" | |
| html += "</ul>" | |
| html += "</div>" | |
| return html | |
| # HTML Fallback Viewer | |
| gr.Markdown("### 📊 HTML Workflow Viewer (Fallback)") | |
| html_viewer = gr.HTML(label="Workflow HTML View") | |
| def create_html_view(data): | |
| """JSON을 HTML로 시각화""" | |
| if not data: | |
| return "<p>No workflow data</p>" | |
| html = """ | |
| <div style='font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; border-radius: 8px;'> | |
| <h3>Workflow Visualization</h3> | |
| <div style='display: flex; gap: 20px; flex-wrap: wrap;'> | |
| """ | |
| # 노드 표시 | |
| nodes = data.get('nodes', []) | |
| for node in nodes: | |
| node_id = node.get('id', 'unknown') | |
| label = node.get('data', {}).get('label', 'Node') | |
| x = node.get('position', {}).get('x', 0) | |
| y = node.get('position', {}).get('y', 0) | |
| html += f""" | |
| <div style='background: white; border: 2px solid #3b82f6; border-radius: 8px; | |
| padding: 15px; margin: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| min-width: 150px;'> | |
| <strong>ID:</strong> {node_id}<br> | |
| <strong>Label:</strong> {label}<br> | |
| <strong>Position:</strong> ({x}, {y}) | |
| </div> | |
| """ | |
| html += "</div>" | |
| # 엣지 표시 | |
| edges = data.get('edges', []) | |
| if edges: | |
| html += "<h4>Connections:</h4><ul>" | |
| for edge in edges: | |
| source = edge.get('source', '?') | |
| target = edge.get('target', '?') | |
| html += f"<li>{source} → {target}</li>" | |
| html += "</ul>" | |
| html += "</div>" | |
| return html | |
| # HTML viewer 업데이트 | |
| workflow_builder.change( | |
| fn=create_html_view, | |
| inputs=workflow_builder, | |
| outputs=html_viewer | |
| ) | |
| # ------------------------------------------------------------------- | |
| # 🚀 실행 | |
| # ------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| show_error=True, | |
| debug=True # 디버그 모드 활성화 | |
| ) |