""" app.py ―――――――――――――――――――――――――――――――――――――――――――――――― ✓ Preview · Save · Load 기능 포함 ✓ Load 시 노드/좌표/엣지 자동 복원 ✓ 동적 렌더링으로 기존 WorkflowBuilder 업데이트 """ import os, json, typing, tempfile import gradio as gr from gradio_workflowbuilder import WorkflowBuilder # ------------------------------------------------------------------- # 🛠️ 헬퍼 # ------------------------------------------------------------------- def export_pretty(data: typing.Dict[str, typing.Any]) -> str: 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 load_json_file(file_obj): """JSON 파일 읽기""" if file_obj is None: return None, "No file selected" try: with open(file_obj.name, "r", encoding="utf-8") as f: data = json.load(f) # 데이터 검증 if not isinstance(data, dict): return None, "Invalid format: not a dictionary" # 필수 필드 확인 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, f"✅ Loaded: {nodes_count} nodes, {edges_count} edges" except Exception as e: return None, f"❌ Error: {str(e)}" def create_sample_workflow(): """샘플 워크플로우 생성""" return { "nodes": [ { "id": "node_1", "type": "default", "position": {"x": 100, "y": 100}, "data": {"label": "Start Node"} }, { "id": "node_2", "type": "default", "position": {"x": 300, "y": 100}, "data": {"label": "Process"} }, { "id": "node_3", "type": "default", "position": {"x": 500, "y": 100}, "data": {"label": "End Node"} } ], "edges": [ { "id": "edge_1", "source": "node_1", "target": "node_2" }, { "id": "edge_2", "source": "node_2", "target": "node_3" } ] } # ------------------------------------------------------------------- # 🎨 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; } .workflow-container{position:relative;} """ # ------------------------------------------------------------------- # 🖥️ Gradio 앱 # ------------------------------------------------------------------- with gr.Blocks(title="🎨 Visual Workflow Builder", theme=gr.themes.Soft(), css=CSS) as demo: with gr.Column(elem_classes=["main-container"]): gr.Markdown("# 🎨 Visual Workflow Builder\n**Create sophisticated workflows with drag-and-drop simplicity.**") gr.HTML( """

Powered by svelteflow • Create custom workflows

""" ) # State for storing loaded data loaded_data = gr.State(None) trigger_update = gr.State(False) # ─── Dynamic Workflow Container ─── with gr.Column(elem_classes=["workflow-container"]): @gr.render(inputs=[loaded_data, trigger_update]) def render_workflow(data, trigger): """동적으로 WorkflowBuilder 렌더링""" workflow_value = data if data else {"nodes": [], "edges": []} return WorkflowBuilder( label="🎨 Visual Workflow Designer", info="Drag from output ➜ input • Click nodes to edit properties", value=workflow_value, elem_id="main_workflow" ) # ─── Control Panel ─── gr.Markdown("## 💾 Export · 📂 Import") with gr.Row(elem_classes=["button-row"]): with gr.Column(scale=2): file_upload = gr.File( label="📂 Select JSON file", file_types=[".json"], type="filepath" ) with gr.Column(scale=1): btn_load = gr.Button("📥 Load Workflow", variant="primary", size="lg") btn_sample = gr.Button("🎯 Load Sample", variant="secondary") with gr.Row(elem_classes=["button-row"]): btn_preview = gr.Button("👁️ Preview JSON") btn_download = gr.DownloadButton("💾 Download JSON") btn_reset = gr.Button("🔄 Reset", variant="stop") # Status status_text = gr.Textbox( label="Status", value="Ready", elem_classes=["status-box"], interactive=False ) # Code View code_view = gr.Code( language="json", label="JSON Preview", lines=10, visible=False ) btn_toggle_code = gr.Button("📋 Toggle Code View", size="sm") # ─── Event Handlers ─── # File upload → Store data file_upload.change( fn=load_json_file, inputs=file_upload, outputs=[loaded_data, status_text] ) # Load button → Trigger render update btn_load.click( fn=lambda current_trigger: not current_trigger, inputs=trigger_update, outputs=trigger_update ) # Sample workflow btn_sample.click( fn=lambda: (create_sample_workflow(), "✅ Sample loaded"), outputs=[loaded_data, status_text] ).then( fn=lambda current_trigger: not current_trigger, inputs=trigger_update, outputs=trigger_update ) # Reset btn_reset.click( fn=lambda: ({"nodes": [], "edges": []}, "✅ Reset complete"), outputs=[loaded_data, status_text] ).then( fn=lambda current_trigger: not current_trigger, inputs=trigger_update, outputs=trigger_update ) # Toggle code view btn_toggle_code.click( fn=lambda visible: gr.update(visible=not visible), inputs=code_view, outputs=code_view ) # Preview - 현재 워크플로우를 가져와서 표시 btn_preview.click( fn=lambda data: (gr.update(visible=True), export_pretty(data)), inputs=loaded_data, outputs=[code_view, code_view] ) # Download - loaded_data를 파일로 내보내기 btn_download.click( fn=export_file, inputs=loaded_data ) # ─── Instructions ─── with gr.Accordion("📖 How to Use", open=False): gr.Markdown( """ ### 🚀 Quick Start 1. **Create** — Add nodes and connect them in the workflow designer 2. **Save** — Click "Download JSON" to save your workflow 3. **Load** — Select a JSON file and click "Load Workflow" ### 📝 Workflow Format ```json { "nodes": [ { "id": "unique_id", "type": "default", "position": {"x": 100, "y": 100}, "data": {"label": "Node Name"} } ], "edges": [ { "id": "edge_id", "source": "source_node_id", "target": "target_node_id" } ] } ``` ### 💡 Tips - The workflow will automatically update when you load a file - Use "Load Sample" to see an example workflow - Toggle code view to see the JSON structure """ ) # ------------------------------------------------------------------- # 🚀 실행 # ------------------------------------------------------------------- if __name__ == "__main__": demo.launch(server_name="0.0.0.0", show_error=True)