""" API 문서 생성 모듈 API 엔드포인트 정보와 문서 생성 함수를 포함합니다. """ import gradio as gr import json # API 엔드포인트별 상세 정보 정의 API_ENDPOINTS = { "tide_level": { "path": "/api/tide_level", "title": "특정 시간 조위 조회", "description": "지정한 관측소(`station_id`)의 특정 시간(`target_time`)에 대한 예측 조위 정보를 반환합니다. `target_time`을 지정하지 않으면 현재 시간에 가장 가까운 데이터를 반환합니다.", "parameters": [ {"name": "station_id", "type": "string", "required": True, "description": "조회할 관측소의 고유 ID입니다. (예: 'DT_0001')"}, {"name": "target_time", "type": "string", "required": False, "description": "조회할 시간입니다. ISO 8601 형식(YYYY-MM-DDTHH:MM:SS)을 권장하며, 생략 시 현재 시간으로 자동 설정됩니다."} ], "example_params": {"station_id": "DT_0001", "target_time": "2025-08-10T09:00:00"}, "example_params_current": {"station_id": "DT_0001"}, "response_example": { "success": True, "timestamp": "2025-08-10T11:50:13.854658+09:00", "meta": { "obs_post_id": "DT_0001", "obs_post_name": "인천", "obs_lat": "37.452", "obs_lon": "126.592", "data_type": "prediction" }, "data": { "record_time": "2025-08-10T11:50:00+09:00", "record_time_kst": "2025-08-10 11:50:00 KST", "tide_level": 151.9, "residual_value": None, "harmonic_value": 151.9, "data_source": "harmonic_only", "confidence": "medium", "note": "잔차 예측이 없어 조화 예측만 제공됩니다", "query_time": "2025-08-10 11:50:12 KST", "matched_time_diff_seconds": 12.357893 } } }, "tide_series": { "path": "/api/tide_series", "title": "시계열 조위 데이터 조회", "description": "지정된 기간 동안의 시계열 조위 데이터를 조회합니다. 공공 API와 유사한 형식으로 반환되며, 간격(interval)을 지정할 수 있습니다.", "parameters": [ {"name": "station_id", "type": "string", "required": True, "description": "조회할 관측소의 고유 ID입니다."}, {"name": "start_time", "type": "string", "required": False, "description": "조회 시작 시간입니다. 생략 시 현재 시간부터 시작합니다."}, {"name": "end_time", "type": "string", "required": False, "description": "조회 종료 시간입니다. 생략 시 시작 시간으로부터 24시간 후까지 조회합니다."}, {"name": "interval", "type": "integer", "required": False, "description": "데이터 간격(분 단위). 기본값: 60분, 최소값: 5분"} ], "example_params": {"station_id": "DT_0001", "start_time": "2025-08-10T00:00:00", "end_time": "2025-08-11T00:00:00", "interval": 60}, "response_example": { "success": True, "timestamp": "2025-08-10T00:30:00.123456+09:00", "meta": { "obs_post_id": "DT_0001", "obs_post_name": "인천", "start_time": "2025-08-10T00:00:00+09:00", "end_time": "2025-08-11T00:00:00+09:00", "interval_minutes": 60, "total_records": 25 }, "data": { "tidal_obs": [ { "record_time": "2025-08-10 00:00", "pred_tide": 125.3, "harmonic_tide": 125.3, "residual_tide": None }, { "record_time": "2025-08-10 01:00", "pred_tide": 142.7, "harmonic_tide": 142.7, "residual_tide": None } ] } } }, "extremes": { "path": "/api/extremes", "title": "만조/간조 정보 조회", "description": "특정 날짜의 만조(high tide)와 간조(low tide) 정보를 조회합니다. 주요 만조/간조와 부차 만조/간조를 구분하여 제공합니다.", "parameters": [ {"name": "station_id", "type": "string", "required": True, "description": "조회할 관측소의 고유 ID입니다."}, {"name": "date", "type": "string", "required": False, "description": "조회할 날짜 (YYYY-MM-DD 형식). 생략 시 오늘 날짜로 설정됩니다."}, {"name": "include_secondary", "type": "boolean", "required": False, "description": "부차 만조/간조 포함 여부. 기본값: false"} ], "example_params": {"station_id": "DT_0001", "date": "2025-08-10", "include_secondary": True}, "response_example": { "success": True, "timestamp": "2025-08-10T00:35:00.123456+09:00", "meta": { "obs_post_id": "DT_0001", "obs_post_name": "인천", "date": "2025-08-10", "include_secondary": True }, "data": { "high_tides": [ { "time": "2025-08-10T06:15:00+09:00", "time_kst": "2025-08-10 06:15:00 KST", "level": 812.5, "type": "primary" }, { "time": "2025-08-10T18:45:00+09:00", "time_kst": "2025-08-10 18:45:00 KST", "level": 798.3, "type": "primary" } ], "low_tides": [ { "time": "2025-08-10T00:30:00+09:00", "time_kst": "2025-08-10 00:30:00 KST", "level": 98.2, "type": "primary" }, { "time": "2025-08-10T12:45:00+09:00", "time_kst": "2025-08-10 12:45:00 KST", "level": 112.7, "type": "primary" } ] } } }, "alert": { "path": "/api/alert", "title": "위험 수위 체크", "description": "향후 지정된 시간 동안 주의 수위 또는 경고 수위에 도달하는지 확인합니다. 위험 시점과 예상 수위를 반환합니다.", "parameters": [ {"name": "station_id", "type": "string", "required": True, "description": "체크할 관측소의 고유 ID입니다."}, {"name": "hours_ahead", "type": "integer", "required": False, "description": "확인할 시간 범위(시간 단위). 기본값: 24시간, 최대: 72시간"}, {"name": "warning_level", "type": "number", "required": False, "description": "주의 수위(cm). 기본값: 700cm"}, {"name": "danger_level", "type": "number", "required": False, "description": "경고 수위(cm). 기본값: 750cm"} ], "example_params": {"station_id": "DT_0001", "hours_ahead": 24, "warning_level": 700, "danger_level": 750}, "response_example": { "success": True, "timestamp": "2025-08-10T00:40:00.123456+09:00", "meta": { "obs_post_id": "DT_0001", "obs_post_name": "인천", "check_period": { "start": "2025-08-10T00:40:00+09:00", "end": "2025-08-11T00:40:00+09:00" }, "warning_level": 700, "danger_level": 750 }, "data": { "alert_status": "DANGER", "max_level": 812.5, "max_level_time": "2025-08-10T06:15:00+09:00", "warning_events": [ { "time": "2025-08-10T05:30:00+09:00", "level": 702.3, "type": "warning_exceeded" } ], "danger_events": [ { "time": "2025-08-10T06:00:00+09:00", "level": 755.8, "type": "danger_exceeded" } ], "recommendation": "경고 수위 초과 예상. 해안가 접근 주의 필요" } } }, "compare": { "path": "/api/compare", "title": "다중 관측소 비교", "description": "여러 관측소의 조위를 동시에 비교합니다. 지정한 시간의 각 관측소별 조위 정보를 한 번에 조회할 수 있습니다.", "parameters": [ {"name": "station_ids", "type": "array", "required": True, "description": "비교할 관측소 ID 목록 (배열 형태). 예: ['DT_0001', 'DT_0002']"}, {"name": "target_time", "type": "string", "required": False, "description": "비교할 시간. 생략 시 현재 시간으로 설정됩니다."} ], "example_params": {"station_ids": ["DT_0001", "DT_0002", "DT_0003"], "target_time": "2025-08-10T09:00:00"}, "response_example": { "success": True, "timestamp": "2025-08-10T00:45:00.123456+09:00", "meta": { "target_time": "2025-08-10T09:00:00+09:00", "station_count": 3 }, "data": { "comparisons": [ { "station_id": "DT_0001", "station_name": "인천", "tide_level": 425.3, "data_time": "2025-08-10T09:00:00+09:00" }, { "station_id": "DT_0002", "station_name": "안흥", "tide_level": 312.8, "data_time": "2025-08-10T09:00:00+09:00" }, { "station_id": "DT_0003", "station_name": "보령", "tide_level": 298.5, "data_time": "2025-08-10T09:00:00+09:00" } ], "statistics": { "max_level": 425.3, "max_station": "인천", "min_level": 298.5, "min_station": "보령", "avg_level": 345.5 } } } }, "health": { "path": "/api/health", "title": "시스템 상태 확인", "description": "API 서버 및 연결된 시스템의 상태를 확인합니다. 데이터베이스 연결, API 키 설정 등을 점검합니다.", "parameters": [], "example_params": {}, "response_example": { "success": True, "timestamp": "2025-08-10T00:50:00.123456+09:00", "status": "healthy", "services": { "api_server": "running", "supabase": "connected", "gemini_api": "configured", "predictions": "available" }, "uptime": "2 hours 15 minutes", "version": "1.0.0" } } } def copy_to_clipboard(text): """클립보드에 텍스트 복사 (JavaScript 실행)""" return f""" """ def generate_api_docs(endpoint_key: str): """엔드포인트별 상세 API 문서를 생성합니다.""" base_url = "https://alwaysgood-my-tide-env.hf.space" endpoint_info = API_ENDPOINTS[endpoint_key] # 섹션 제목 gr.Markdown("---") gr.Markdown(f"## 📚 API 사용 안내서") gr.Markdown(f"### `{endpoint_info['path']}` : {endpoint_info['title']}") gr.Markdown(endpoint_info['description']) # 기본 정보 gr.Markdown("- **Method**: `GET`") gr.Markdown(f"- **URL**: `{base_url}{endpoint_info['path']}`") gr.Markdown("") # 요청 파라미터 테이블 if endpoint_info['parameters']: gr.Markdown("### 요청 파라미터 (Query Parameters)") # 테이블 헤더 table_md = "|파라미터 (Parameter)|타입 (Type)|필수 (Required)|설명 (Description)|\n" table_md += "|---|---|---|---|\n" # 파라미터 정보 for param in endpoint_info['parameters']: required_text = "**Yes**" if param['required'] else "No" table_md += f"|`{param['name']}`|`{param['type']}`|{required_text}|{param['description']}|\n" gr.Markdown(table_md) gr.Markdown("") # 사용 예시 gr.Markdown("### 사용 예시 (Usage Examples)") # Python 예시 python_code = f'''import requests import json BASE_URL = "{base_url}" ''' # 엔드포인트별 특별 처리 if endpoint_key == "tide_level": python_code += f''' # 1. 현재 조위 조회 (인천 관측소) params_now = {{ "station_id": "DT_0001" }} response_now = requests.get(f"{{BASE_URL}}{endpoint_info['path']}", params=params_now) print("--- 현재 조위 조회 결과 ---") print(response_now.json()) # 2. 특정 시간 조위 조회 (2025년 8월 10일 오전 9시) params_specific_time = {{ "station_id": "DT_0001", "target_time": "2025-08-10T09:00:00" }} response_specific = requests.get(f"{{BASE_URL}}{endpoint_info['path']}", params=params_specific_time) print("\\n--- 특정 시간 조위 조회 결과 ---") print(response_specific.json())''' elif endpoint_key == "compare": python_code += f''' # 여러 관측소 동시 비교 params = {{ "station_ids": ["DT_0001", "DT_0002", "DT_0003"], "target_time": "2025-08-10T09:00:00" }} response = requests.get(f"{{BASE_URL}}{endpoint_info['path']}", params=params) if response.status_code == 200: data = response.json() print("--- 관측소별 조위 비교 ---") for station in data['data']['comparisons']: print(f"{{station['station_name']}}: {{station['tide_level']}}cm") else: print(f"Error: {{response.status_code}}")''' elif endpoint_key == "health": python_code += f''' # 시스템 상태 확인 response = requests.get(f"{{BASE_URL}}{endpoint_info['path']}") if response.status_code == 200: data = response.json() print(f"시스템 상태: {{data['status']}}") print(f"서비스 상태: {{data['services']}}") else: print(f"Error: {{response.status_code}}")''' else: # 일반적인 경우 python_code += f''' # 요청 파라미터 설정 params = {json.dumps(endpoint_info['example_params'], indent=4, ensure_ascii=False)} response = requests.get(f"{{BASE_URL}}{endpoint_info['path']}", params=params) if response.status_code == 200: data = response.json() print(json.dumps(data, indent=2, ensure_ascii=False)) else: print(f"Error: {{response.status_code}}")''' gr.Markdown("#### **Python (`requests` 사용)**") gr.Markdown(f"```python\n{python_code.strip()}\n```") # curl 예시 # URL 파라미터 생성 if endpoint_key == "compare": # 배열 파라미터는 특별 처리 curl_params = "&".join([f"station_ids={sid}" for sid in endpoint_info['example_params']['station_ids']]) if 'target_time' in endpoint_info['example_params']: curl_params += f"&target_time={endpoint_info['example_params']['target_time']}" elif endpoint_key == "health": curl_params = "" else: curl_params = "&".join([f"{k}={v}" for k, v in endpoint_info['example_params'].items()]) curl_url = f"{base_url}{endpoint_info['path']}" if curl_params: curl_url += f"?{curl_params}" curl_code = f'# 요청 예시\ncurl -X GET "{curl_url}"' # 특별 케이스 추가 if endpoint_key == "tide_level": curl_code = f'''# 현재 조위 조회 curl -X GET "{base_url}{endpoint_info['path']}?station_id=DT_0001" # 특정 시간 조위 조회 curl -X GET "{base_url}{endpoint_info['path']}?station_id=DT_0001&target_time=2025-08-10T09:00:00"''' gr.Markdown("#### **curl (Command Line)**") gr.Markdown(f"```bash\n{curl_code}\n```") # JavaScript 예시 if endpoint_key == "compare": js_code = f'''const stationIds = ['DT_0001', 'DT_0002', 'DT_0003']; const params = new URLSearchParams(); stationIds.forEach(id => params.append('station_ids', id)); params.append('target_time', '2025-08-10T09:00:00'); const url = `{base_url}{endpoint_info['path']}?${{params}}`; fetch(url) .then(response => response.json()) .then(data => {{ console.log('비교 결과:', data); data.data.comparisons.forEach(station => {{ console.log(`${{station.station_name}}: ${{station.tide_level}}cm`); }}); }}) .catch(error => {{ console.error('Error:', error); }});''' elif endpoint_key == "health": js_code = f'''const url = '{base_url}{endpoint_info['path']}'; fetch(url) .then(response => response.json()) .then(data => {{ console.log('시스템 상태:', data.status); console.log('서비스:', data.services); }}) .catch(error => {{ console.error('Error:', error); }});''' else: # 일반 케이스 params_str = "&".join([f"{k}={v}" for k, v in endpoint_info.get('example_params_current', endpoint_info['example_params']).items()]) js_code = f'''const stationId = 'DT_0001'; const url = `{base_url}{endpoint_info['path']}?{params_str}`; fetch(url) .then(response => response.json()) .then(data => {{ console.log(data); }}) .catch(error => {{ console.error('Error:', error); }});''' gr.Markdown("#### **JavaScript (`fetch` API)**") gr.Markdown(f"```javascript\n{js_code}\n```") # 브라우저 직접 접속 - 복사 버튼 추가 gr.Markdown("#### **웹 브라우저**") gr.Markdown("아래 주소를 복사하여 웹 브라우저 주소창에 붙여넣기만 해도 결과를 확인할 수 있습니다.") browser_params = "" if endpoint_key != "health": if endpoint_key == "compare": browser_params = "?station_ids=DT_0001&station_ids=DT_0002" else: browser_params = "?" + "&".join([f"{k}={v}" for k, v in endpoint_info.get('example_params_current', {'station_id': 'DT_0001'}).items()]) browser_url = f"{base_url}{endpoint_info['path']}{browser_params}" with gr.Row(): url_textbox = gr.Textbox( value=browser_url, interactive=False, show_label=False, scale=4 ) copy_btn = gr.Button("📋 복사", scale=1, size="sm") # 복사 버튼 클릭 이벤트 copy_btn.click( fn=lambda x: x, # 단순히 URL을 반환 inputs=[url_textbox], outputs=[], js=f"""(x) => {{ navigator.clipboard.writeText('{browser_url}'); alert('URL이 클립보드에 복사되었습니다!'); return x; }}""" ) # 응답 형식 gr.Markdown("") gr.Markdown("### 응답 형식 (Response Format)") # 성공 응답 테이블 gr.Markdown("#### **성공 (200 OK)**") # 응답 구조를 테이블로 표시 if endpoint_key == "tide_level": # 메타 정보 테이블 gr.Markdown("##### **Meta 정보**") meta_table = "|필드 (Field)|타입 (Type)|설명 (Description)|\n" meta_table += "|---|---|---|\n" meta_table += "|`success`|`boolean`|요청 성공 여부|\n" meta_table += "|`timestamp`|`string`|응답 생성 시간 (ISO 8601 형식)|\n" meta_table += "|`meta.obs_post_id`|`string`|관측소 ID (요청한 station_id와 동일)|\n" meta_table += "|`meta.obs_post_name`|`string`|관측소 이름|\n" meta_table += "|`meta.obs_lat`|`string`|관측소 위도|\n" meta_table += "|`meta.obs_lon`|`string`|관측소 경도|\n" meta_table += "|`meta.data_type`|`string`|데이터 타입 (prediction/observation)|\n" gr.Markdown(meta_table) gr.Markdown("##### **Data 정보**") data_table = "|필드 (Field)|타입 (Type)|설명 (Description)|\n" data_table += "|---|---|---|\n" data_table += "|`data.record_time`|`string`|데이터 기록 시간 (ISO 8601)|\n" data_table += "|`data.record_time_kst`|`string`|데이터 기록 시간 (KST 형식)|\n" data_table += "|`data.tide_level`|`number`|**예측 조위 높이 (cm)**|\n" data_table += "|`data.residual_value`|`number/null`|잔차 예측값|\n" data_table += "|`data.harmonic_value`|`number`|조화 예측값|\n" data_table += "|`data.data_source`|`string`|데이터 소스 (harmonic_only/combined)|\n" data_table += "|`data.confidence`|`string`|예측 신뢰도 (high/medium/low)|\n" data_table += "|`data.note`|`string`|추가 설명|\n" data_table += "|`data.query_time`|`string`|쿼리 실행 시간|\n" data_table += "|`data.matched_time_diff_seconds`|`number`|요청 시간과 데이터 시간 차이(초)|\n" gr.Markdown(data_table) elif endpoint_key == "tide_series": gr.Markdown("##### **응답 구조**") series_table = "|필드 (Field)|타입 (Type)|설명 (Description)|\n" series_table += "|---|---|---|\n" series_table += "|`success`|`boolean`|요청 성공 여부|\n" series_table += "|`meta.obs_post_id`|`string`|관측소 ID|\n" series_table += "|`meta.obs_post_name`|`string`|관측소 이름|\n" series_table += "|`meta.start_time`|`string`|조회 시작 시간|\n" series_table += "|`meta.end_time`|`string`|조회 종료 시간|\n" series_table += "|`meta.interval_minutes`|`integer`|데이터 간격(분)|\n" series_table += "|`meta.total_records`|`integer`|전체 레코드 수|\n" series_table += "|`data.tidal_obs`|`array`|시계열 조위 데이터 배열|\n" series_table += "|`data.tidal_obs[].record_time`|`string`|기록 시간|\n" series_table += "|`data.tidal_obs[].pred_tide`|`number`|예측 조위(cm)|\n" series_table += "|`data.tidal_obs[].harmonic_tide`|`number`|조화 예측값|\n" series_table += "|`data.tidal_obs[].residual_tide`|`number/null`|잔차 예측값|\n" gr.Markdown(series_table) elif endpoint_key == "extremes": gr.Markdown("##### **응답 구조**") extremes_table = "|필드 (Field)|타입 (Type)|설명 (Description)|\n" extremes_table += "|---|---|---|\n" extremes_table += "|`success`|`boolean`|요청 성공 여부|\n" extremes_table += "|`meta.obs_post_id`|`string`|관측소 ID|\n" extremes_table += "|`meta.obs_post_name`|`string`|관측소 이름|\n" extremes_table += "|`meta.date`|`string`|조회 날짜|\n" extremes_table += "|`meta.include_secondary`|`boolean`|부차 만조/간조 포함 여부|\n" extremes_table += "|`data.high_tides`|`array`|만조 정보 배열|\n" extremes_table += "|`data.high_tides[].time`|`string`|만조 시간|\n" extremes_table += "|`data.high_tides[].level`|`number`|만조 높이(cm)|\n" extremes_table += "|`data.high_tides[].type`|`string`|만조 타입 (primary/secondary)|\n" extremes_table += "|`data.low_tides`|`array`|간조 정보 배열|\n" extremes_table += "|`data.low_tides[].time`|`string`|간조 시간|\n" extremes_table += "|`data.low_tides[].level`|`number`|간조 높이(cm)|\n" extremes_table += "|`data.low_tides[].type`|`string`|간조 타입 (primary/secondary)|\n" gr.Markdown(extremes_table) elif endpoint_key == "alert": gr.Markdown("##### **응답 구조**") alert_table = "|필드 (Field)|타입 (Type)|설명 (Description)|\n" alert_table += "|---|---|---|\n" alert_table += "|`success`|`boolean`|요청 성공 여부|\n" alert_table += "|`meta.obs_post_id`|`string`|관측소 ID|\n" alert_table += "|`meta.obs_post_name`|`string`|관측소 이름|\n" alert_table += "|`meta.check_period.start`|`string`|확인 시작 시간|\n" alert_table += "|`meta.check_period.end`|`string`|확인 종료 시간|\n" alert_table += "|`meta.warning_level`|`number`|주의 수위(cm)|\n" alert_table += "|`meta.danger_level`|`number`|경고 수위(cm)|\n" alert_table += "|`data.alert_status`|`string`|경보 상태 (SAFE/WARNING/DANGER)|\n" alert_table += "|`data.max_level`|`number`|기간 중 최고 수위|\n" alert_table += "|`data.max_level_time`|`string`|최고 수위 시간|\n" alert_table += "|`data.warning_events`|`array`|주의 수위 초과 이벤트|\n" alert_table += "|`data.danger_events`|`array`|경고 수위 초과 이벤트|\n" alert_table += "|`data.recommendation`|`string`|권고 사항|\n" gr.Markdown(alert_table) elif endpoint_key == "compare": gr.Markdown("##### **응답 구조**") compare_table = "|필드 (Field)|타입 (Type)|설명 (Description)|\n" compare_table += "|---|---|---|\n" compare_table += "|`success`|`boolean`|요청 성공 여부|\n" compare_table += "|`meta.target_time`|`string`|비교 시간|\n" compare_table += "|`meta.station_count`|`integer`|비교 관측소 수|\n" compare_table += "|`data.comparisons`|`array`|관측소별 조위 정보 배열|\n" compare_table += "|`data.comparisons[].station_id`|`string`|관측소 ID|\n" compare_table += "|`data.comparisons[].station_name`|`string`|관측소 이름|\n" compare_table += "|`data.comparisons[].tide_level`|`number`|조위 높이(cm)|\n" compare_table += "|`data.comparisons[].data_time`|`string`|데이터 시간|\n" compare_table += "|`data.statistics.max_level`|`number`|최고 조위|\n" compare_table += "|`data.statistics.max_station`|`string`|최고 조위 관측소|\n" compare_table += "|`data.statistics.min_level`|`number`|최저 조위|\n" compare_table += "|`data.statistics.min_station`|`string`|최저 조위 관측소|\n" compare_table += "|`data.statistics.avg_level`|`number`|평균 조위|\n" gr.Markdown(compare_table) elif endpoint_key == "health": gr.Markdown("##### **응답 구조**") health_table = "|필드 (Field)|타입 (Type)|설명 (Description)|\n" health_table += "|---|---|---|\n" health_table += "|`success`|`boolean`|요청 성공 여부|\n" health_table += "|`timestamp`|`string`|응답 시간|\n" health_table += "|`status`|`string`|시스템 상태 (healthy/degraded/error)|\n" health_table += "|`services.api_server`|`string`|API 서버 상태|\n" health_table += "|`services.supabase`|`string`|Supabase 연결 상태|\n" health_table += "|`services.gemini_api`|`string`|Gemini API 상태|\n" health_table += "|`services.predictions`|`string`|예측 서비스 상태|\n" health_table += "|`uptime`|`string`|서비스 가동 시간|\n" health_table += "|`version`|`string`|API 버전|\n" gr.Markdown(health_table) # 응답 예시 (JSON) gr.Markdown("##### **응답 예시**") response_json = json.dumps(endpoint_info['response_example'], indent=2, ensure_ascii=False) gr.Markdown(f"```json\n{response_json}\n```") # 실패 응답 (파라미터가 필요한 경우에만) if any(p['required'] for p in endpoint_info['parameters']): gr.Markdown("#### **실패 (422 Unprocessable Entity - 파라미터 누락 시)**") error_response = '''{ "detail": [ { "type": "missing", "loc": [ "query", "station_id" ], "msg": "Field required", "input": null } ] }''' gr.Markdown(f"```json\n{error_response}\n```")