Spaces:
Running
Running
Zach Wentz commited on
Commit ·
d321359
1
Parent(s): 7843e97
🤖 Deploy chat_env environment - 2025-10-19 23:03:44
Browse files- src/core/env_server/web_interface.py +279 -21
src/core/env_server/web_interface.py
CHANGED
|
@@ -244,7 +244,15 @@ def create_web_interface_app(
|
|
| 244 |
@app.post("/web/step")
|
| 245 |
async def web_step(request: Dict[str, Any]):
|
| 246 |
"""Step endpoint for web interface."""
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
return await web_manager.step_environment(action_data)
|
| 249 |
|
| 250 |
@app.get("/web/state")
|
|
@@ -258,6 +266,14 @@ def create_web_interface_app(
|
|
| 258 |
def get_web_interface_html(action_cls: Type[Action]) -> str:
|
| 259 |
"""Generate the HTML for the web interface."""
|
| 260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
# Get action fields for dynamic form generation
|
| 262 |
action_fields = []
|
| 263 |
if hasattr(action_cls, '__dataclass_fields__'):
|
|
@@ -500,6 +516,103 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
|
|
| 500 |
max-height: 200px;
|
| 501 |
overflow-y: auto;
|
| 502 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
</style>
|
| 504 |
</head>
|
| 505 |
<body>
|
|
@@ -511,14 +624,8 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
|
|
| 511 |
HumanAgent Interface
|
| 512 |
</div>
|
| 513 |
<div class="pane-content">
|
| 514 |
-
<!-- Action Form -->
|
| 515 |
-
|
| 516 |
-
<h3>Take Action</h3>
|
| 517 |
-
<form id="action-form">
|
| 518 |
-
{_generate_action_form_fields(action_fields)}
|
| 519 |
-
<button type="submit" class="btn" id="step-btn">Step</button>
|
| 520 |
-
</form>
|
| 521 |
-
</div>
|
| 522 |
|
| 523 |
<!-- Control Buttons -->
|
| 524 |
<div style="margin-bottom: 20px;">
|
|
@@ -618,11 +725,32 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
|
|
| 618 |
}}
|
| 619 |
|
| 620 |
setupEventListeners() {{
|
| 621 |
-
//
|
| 622 |
-
document.getElementById('
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
|
| 627 |
// Reset button
|
| 628 |
document.getElementById('reset-btn').addEventListener('click', () => {{
|
|
@@ -635,6 +763,61 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
|
|
| 635 |
}});
|
| 636 |
}}
|
| 637 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
async submitAction() {{
|
| 639 |
const formData = new FormData(document.getElementById('action-form'));
|
| 640 |
const action = {{}};
|
|
@@ -716,6 +899,9 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
|
|
| 716 |
}}
|
| 717 |
|
| 718 |
updateUI(episodeState) {{
|
|
|
|
|
|
|
|
|
|
| 719 |
// Update current state
|
| 720 |
document.getElementById('env-status').textContent =
|
| 721 |
episodeState.is_reset ? 'Reset' : 'Running';
|
|
@@ -724,14 +910,19 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
|
|
| 724 |
document.getElementById('step-count').textContent =
|
| 725 |
episodeState.step_count.toString();
|
| 726 |
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
observationDiv.textContent = JSON.stringify(
|
| 731 |
-
episodeState.current_observation, null, 2
|
| 732 |
-
);
|
| 733 |
}} else {{
|
| 734 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
}}
|
| 736 |
|
| 737 |
// Update action logs
|
|
@@ -752,6 +943,25 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
|
|
| 752 |
`).join('');
|
| 753 |
}}
|
| 754 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
}}
|
| 756 |
|
| 757 |
// Initialize the web interface when the page loads
|
|
@@ -764,6 +974,54 @@ def get_web_interface_html(action_cls: Type[Action]) -> str:
|
|
| 764 |
""".replace('{_generate_action_form_fields(action_fields)}', _generate_action_form_fields(action_fields))
|
| 765 |
|
| 766 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
def _generate_action_form_fields(action_fields: List[Dict[str, Any]]) -> str:
|
| 768 |
"""Generate HTML form fields for action input."""
|
| 769 |
if not action_fields:
|
|
|
|
| 244 |
@app.post("/web/step")
|
| 245 |
async def web_step(request: Dict[str, Any]):
|
| 246 |
"""Step endpoint for web interface."""
|
| 247 |
+
# Check if this is a message-based request (chat environment)
|
| 248 |
+
if "message" in request:
|
| 249 |
+
message = request["message"]
|
| 250 |
+
# Convert message to action using the environment's message_to_action method
|
| 251 |
+
action = web_manager.env.message_to_action(message)
|
| 252 |
+
action_data = {"tokens": action.tokens.tolist()}
|
| 253 |
+
else:
|
| 254 |
+
action_data = request.get("action", {})
|
| 255 |
+
|
| 256 |
return await web_manager.step_environment(action_data)
|
| 257 |
|
| 258 |
@app.get("/web/state")
|
|
|
|
| 266 |
def get_web_interface_html(action_cls: Type[Action]) -> str:
|
| 267 |
"""Generate the HTML for the web interface."""
|
| 268 |
|
| 269 |
+
# Check if this is a chat environment by looking for tokens field
|
| 270 |
+
is_chat_env = False
|
| 271 |
+
if hasattr(action_cls, '__dataclass_fields__'):
|
| 272 |
+
for field_name, field_info in action_cls.__dataclass_fields__.items():
|
| 273 |
+
if field_name == 'tokens' and hasattr(field_info.type, '__name__') and 'Tensor' in field_info.type.__name__:
|
| 274 |
+
is_chat_env = True
|
| 275 |
+
break
|
| 276 |
+
|
| 277 |
# Get action fields for dynamic form generation
|
| 278 |
action_fields = []
|
| 279 |
if hasattr(action_cls, '__dataclass_fields__'):
|
|
|
|
| 516 |
max-height: 200px;
|
| 517 |
overflow-y: auto;
|
| 518 |
}}
|
| 519 |
+
|
| 520 |
+
/* Chat Interface Styles */
|
| 521 |
+
.chat-interface {{
|
| 522 |
+
background: white;
|
| 523 |
+
border: 1px solid #e0e0e0;
|
| 524 |
+
border-radius: 8px;
|
| 525 |
+
padding: 20px;
|
| 526 |
+
margin-bottom: 20px;
|
| 527 |
+
}}
|
| 528 |
+
|
| 529 |
+
.chat-messages {{
|
| 530 |
+
background: #f8f9fa;
|
| 531 |
+
border: 1px solid #e0e0e0;
|
| 532 |
+
border-radius: 8px;
|
| 533 |
+
padding: 15px;
|
| 534 |
+
margin-bottom: 15px;
|
| 535 |
+
max-height: 400px;
|
| 536 |
+
overflow-y: auto;
|
| 537 |
+
}}
|
| 538 |
+
|
| 539 |
+
.chat-message {{
|
| 540 |
+
margin-bottom: 15px;
|
| 541 |
+
padding: 10px;
|
| 542 |
+
border-radius: 8px;
|
| 543 |
+
}}
|
| 544 |
+
|
| 545 |
+
.chat-message:last-child {{
|
| 546 |
+
margin-bottom: 0;
|
| 547 |
+
}}
|
| 548 |
+
|
| 549 |
+
.chat-message.user {{
|
| 550 |
+
background: #e3f2fd;
|
| 551 |
+
margin-left: 20px;
|
| 552 |
+
}}
|
| 553 |
+
|
| 554 |
+
.chat-message.assistant {{
|
| 555 |
+
background: #f3e5f5;
|
| 556 |
+
margin-right: 20px;
|
| 557 |
+
}}
|
| 558 |
+
|
| 559 |
+
.chat-message.system {{
|
| 560 |
+
background: #e8f5e8;
|
| 561 |
+
font-style: italic;
|
| 562 |
+
}}
|
| 563 |
+
|
| 564 |
+
.message-role {{
|
| 565 |
+
font-weight: 600;
|
| 566 |
+
font-size: 12px;
|
| 567 |
+
color: #666;
|
| 568 |
+
margin-bottom: 5px;
|
| 569 |
+
}}
|
| 570 |
+
|
| 571 |
+
.message-content {{
|
| 572 |
+
font-size: 14px;
|
| 573 |
+
line-height: 1.4;
|
| 574 |
+
}}
|
| 575 |
+
|
| 576 |
+
.chat-input-container {{
|
| 577 |
+
border-top: 1px solid #e0e0e0;
|
| 578 |
+
padding-top: 15px;
|
| 579 |
+
}}
|
| 580 |
+
|
| 581 |
+
.role-selector {{
|
| 582 |
+
margin-bottom: 10px;
|
| 583 |
+
}}
|
| 584 |
+
|
| 585 |
+
.role-selector label {{
|
| 586 |
+
font-weight: 500;
|
| 587 |
+
margin-right: 10px;
|
| 588 |
+
}}
|
| 589 |
+
|
| 590 |
+
.role-selector select {{
|
| 591 |
+
padding: 5px 10px;
|
| 592 |
+
border: 1px solid #ddd;
|
| 593 |
+
border-radius: 4px;
|
| 594 |
+
}}
|
| 595 |
+
|
| 596 |
+
.message-input {{
|
| 597 |
+
display: flex;
|
| 598 |
+
gap: 10px;
|
| 599 |
+
align-items: flex-end;
|
| 600 |
+
}}
|
| 601 |
+
|
| 602 |
+
.message-input textarea {{
|
| 603 |
+
flex: 1;
|
| 604 |
+
padding: 10px;
|
| 605 |
+
border: 1px solid #ddd;
|
| 606 |
+
border-radius: 4px;
|
| 607 |
+
resize: vertical;
|
| 608 |
+
font-family: inherit;
|
| 609 |
+
}}
|
| 610 |
+
|
| 611 |
+
.message-input textarea:focus {{
|
| 612 |
+
outline: none;
|
| 613 |
+
border-color: #007bff;
|
| 614 |
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
| 615 |
+
}}
|
| 616 |
</style>
|
| 617 |
</head>
|
| 618 |
<body>
|
|
|
|
| 624 |
HumanAgent Interface
|
| 625 |
</div>
|
| 626 |
<div class="pane-content">
|
| 627 |
+
<!-- Action Form or Chat Interface -->
|
| 628 |
+
{_generate_action_interface(action_fields, is_chat_env)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
|
| 630 |
<!-- Control Buttons -->
|
| 631 |
<div style="margin-bottom: 20px;">
|
|
|
|
| 725 |
}}
|
| 726 |
|
| 727 |
setupEventListeners() {{
|
| 728 |
+
// Check if this is a chat environment
|
| 729 |
+
const isChatEnv = document.getElementById('chat-messages') !== null;
|
| 730 |
+
|
| 731 |
+
if (isChatEnv) {{
|
| 732 |
+
// Chat environment event listeners
|
| 733 |
+
document.getElementById('send-message-btn').addEventListener('click', () => {{
|
| 734 |
+
this.sendMessage();
|
| 735 |
+
}});
|
| 736 |
+
|
| 737 |
+
// Send message on Enter (but allow Shift+Enter for new lines)
|
| 738 |
+
document.getElementById('message-input').addEventListener('keydown', (e) => {{
|
| 739 |
+
if (e.key === 'Enter' && !e.shiftKey) {{
|
| 740 |
+
e.preventDefault();
|
| 741 |
+
this.sendMessage();
|
| 742 |
+
}}
|
| 743 |
+
}});
|
| 744 |
+
}} else {{
|
| 745 |
+
// Traditional action form submission
|
| 746 |
+
const actionForm = document.getElementById('action-form');
|
| 747 |
+
if (actionForm) {{
|
| 748 |
+
actionForm.addEventListener('submit', (e) => {{
|
| 749 |
+
e.preventDefault();
|
| 750 |
+
this.submitAction();
|
| 751 |
+
}});
|
| 752 |
+
}}
|
| 753 |
+
}}
|
| 754 |
|
| 755 |
// Reset button
|
| 756 |
document.getElementById('reset-btn').addEventListener('click', () => {{
|
|
|
|
| 763 |
}});
|
| 764 |
}}
|
| 765 |
|
| 766 |
+
async sendMessage() {{
|
| 767 |
+
const messageInput = document.getElementById('message-input');
|
| 768 |
+
const roleSelect = document.getElementById('message-role');
|
| 769 |
+
const message = messageInput.value.trim();
|
| 770 |
+
const role = roleSelect.value;
|
| 771 |
+
|
| 772 |
+
if (!message) {{
|
| 773 |
+
return;
|
| 774 |
+
}}
|
| 775 |
+
|
| 776 |
+
// Add message to chat display immediately
|
| 777 |
+
this.addMessageToChat(role, message);
|
| 778 |
+
|
| 779 |
+
// Clear input
|
| 780 |
+
messageInput.value = '';
|
| 781 |
+
|
| 782 |
+
try {{
|
| 783 |
+
// Send message to server to convert to action and step
|
| 784 |
+
const response = await fetch('/web/step', {{
|
| 785 |
+
method: 'POST',
|
| 786 |
+
headers: {{ 'Content-Type': 'application/json' }},
|
| 787 |
+
body: JSON.stringify({{
|
| 788 |
+
message: {{
|
| 789 |
+
role: role,
|
| 790 |
+
content: message
|
| 791 |
+
}}
|
| 792 |
+
}})
|
| 793 |
+
}});
|
| 794 |
+
|
| 795 |
+
if (!response.ok) {{
|
| 796 |
+
throw new Error(`HTTP error! status: ${{response.status}}`);
|
| 797 |
+
}}
|
| 798 |
+
|
| 799 |
+
const result = await response.json();
|
| 800 |
+
console.log('Message sent:', result);
|
| 801 |
+
}} catch (error) {{
|
| 802 |
+
console.error('Error sending message:', error);
|
| 803 |
+
alert('Error sending message: ' + error.message);
|
| 804 |
+
}}
|
| 805 |
+
}}
|
| 806 |
+
|
| 807 |
+
addMessageToChat(role, content) {{
|
| 808 |
+
const chatMessages = document.getElementById('chat-messages');
|
| 809 |
+
const messageDiv = document.createElement('div');
|
| 810 |
+
messageDiv.className = `chat-message ${{role}}`;
|
| 811 |
+
|
| 812 |
+
messageDiv.innerHTML = `
|
| 813 |
+
<div class="message-role">${{role.charAt(0).toUpperCase() + role.slice(1)}}</div>
|
| 814 |
+
<div class="message-content">${{content}}</div>
|
| 815 |
+
`;
|
| 816 |
+
|
| 817 |
+
chatMessages.appendChild(messageDiv);
|
| 818 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 819 |
+
}}
|
| 820 |
+
|
| 821 |
async submitAction() {{
|
| 822 |
const formData = new FormData(document.getElementById('action-form'));
|
| 823 |
const action = {{}};
|
|
|
|
| 899 |
}}
|
| 900 |
|
| 901 |
updateUI(episodeState) {{
|
| 902 |
+
// Check if this is a chat environment
|
| 903 |
+
const isChatEnv = document.getElementById('chat-messages') !== null;
|
| 904 |
+
|
| 905 |
// Update current state
|
| 906 |
document.getElementById('env-status').textContent =
|
| 907 |
episodeState.is_reset ? 'Reset' : 'Running';
|
|
|
|
| 910 |
document.getElementById('step-count').textContent =
|
| 911 |
episodeState.step_count.toString();
|
| 912 |
|
| 913 |
+
if (isChatEnv) {{
|
| 914 |
+
// Update chat interface
|
| 915 |
+
this.updateChatInterface(episodeState);
|
|
|
|
|
|
|
|
|
|
| 916 |
}} else {{
|
| 917 |
+
// Update traditional observation display
|
| 918 |
+
const observationDiv = document.getElementById('current-observation');
|
| 919 |
+
if (episodeState.current_observation) {{
|
| 920 |
+
observationDiv.textContent = JSON.stringify(
|
| 921 |
+
episodeState.current_observation, null, 2
|
| 922 |
+
);
|
| 923 |
+
}} else {{
|
| 924 |
+
observationDiv.textContent = 'No observation yet';
|
| 925 |
+
}}
|
| 926 |
}}
|
| 927 |
|
| 928 |
// Update action logs
|
|
|
|
| 943 |
`).join('');
|
| 944 |
}}
|
| 945 |
}}
|
| 946 |
+
|
| 947 |
+
updateChatInterface(episodeState) {{
|
| 948 |
+
const chatMessages = document.getElementById('chat-messages');
|
| 949 |
+
if (!chatMessages) return;
|
| 950 |
+
|
| 951 |
+
// Clear existing messages (except system message)
|
| 952 |
+
const systemMessage = chatMessages.querySelector('.chat-message.system');
|
| 953 |
+
chatMessages.innerHTML = '';
|
| 954 |
+
if (systemMessage) {{
|
| 955 |
+
chatMessages.appendChild(systemMessage);
|
| 956 |
+
}}
|
| 957 |
+
|
| 958 |
+
// Add messages from current observation
|
| 959 |
+
if (episodeState.current_observation && episodeState.current_observation.messages) {{
|
| 960 |
+
episodeState.current_observation.messages.forEach(msg => {{
|
| 961 |
+
this.addMessageToChat(msg.role, msg.content);
|
| 962 |
+
}});
|
| 963 |
+
}}
|
| 964 |
+
}}
|
| 965 |
}}
|
| 966 |
|
| 967 |
// Initialize the web interface when the page loads
|
|
|
|
| 974 |
""".replace('{_generate_action_form_fields(action_fields)}', _generate_action_form_fields(action_fields))
|
| 975 |
|
| 976 |
|
| 977 |
+
def _generate_action_interface(action_fields: List[Dict[str, Any]], is_chat_env: bool) -> str:
|
| 978 |
+
"""Generate either a chat interface or action form based on environment type."""
|
| 979 |
+
if is_chat_env:
|
| 980 |
+
return _generate_chat_interface()
|
| 981 |
+
else:
|
| 982 |
+
return _generate_action_form(action_fields)
|
| 983 |
+
|
| 984 |
+
def _generate_chat_interface() -> str:
|
| 985 |
+
"""Generate a chat-style interface for chat environments."""
|
| 986 |
+
return '''
|
| 987 |
+
<!-- Chat Interface -->
|
| 988 |
+
<div class="chat-interface">
|
| 989 |
+
<h3>Chat Interface</h3>
|
| 990 |
+
<div class="chat-messages" id="chat-messages">
|
| 991 |
+
<div class="chat-message system">
|
| 992 |
+
<div class="message-role">System</div>
|
| 993 |
+
<div class="message-content">Chat environment ready. Send a message to start the conversation.</div>
|
| 994 |
+
</div>
|
| 995 |
+
</div>
|
| 996 |
+
<div class="chat-input-container">
|
| 997 |
+
<div class="role-selector">
|
| 998 |
+
<label for="message-role">Role:</label>
|
| 999 |
+
<select id="message-role">
|
| 1000 |
+
<option value="user">User</option>
|
| 1001 |
+
<option value="assistant">Assistant</option>
|
| 1002 |
+
</select>
|
| 1003 |
+
</div>
|
| 1004 |
+
<div class="message-input">
|
| 1005 |
+
<textarea id="message-input" placeholder="Type your message here..." rows="3"></textarea>
|
| 1006 |
+
<button class="btn" id="send-message-btn">Send Message</button>
|
| 1007 |
+
</div>
|
| 1008 |
+
</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
'''
|
| 1011 |
+
|
| 1012 |
+
def _generate_action_form(action_fields: List[Dict[str, Any]]) -> str:
|
| 1013 |
+
"""Generate a traditional action form for non-chat environments."""
|
| 1014 |
+
return f'''
|
| 1015 |
+
<!-- Action Form -->
|
| 1016 |
+
<div class="action-form">
|
| 1017 |
+
<h3>Take Action</h3>
|
| 1018 |
+
<form id="action-form">
|
| 1019 |
+
{_generate_action_form_fields(action_fields)}
|
| 1020 |
+
<button type="submit" class="btn" id="step-btn">Step</button>
|
| 1021 |
+
</form>
|
| 1022 |
+
</div>
|
| 1023 |
+
'''
|
| 1024 |
+
|
| 1025 |
def _generate_action_form_fields(action_fields: List[Dict[str, Any]]) -> str:
|
| 1026 |
"""Generate HTML form fields for action input."""
|
| 1027 |
if not action_fields:
|