feat: menu updates

This commit is contained in:
Kevin Bruton 2025-09-29 19:09:20 +02:00
parent e0848e960c
commit 9201f77c9f
6 changed files with 328 additions and 106 deletions

Binary file not shown.

View File

@ -52,11 +52,8 @@ def update_execution_state(state: Dict[str, Any]):
print(f"📡 Callback received state keys: {list(state.keys())}")
with app_state_lock:
# Initialize the complete execution tree structure if not exists
if not app_state["execution_tree"] or (
len(app_state["execution_tree"]) == 1 and
app_state["execution_tree"][0]["id"] == "initialization"
):
# Ensure execution tree is initialized
if not app_state["execution_tree"]:
app_state["execution_tree"] = initialize_complete_execution_tree()
# Map LangGraph node names to our tracking system
@ -149,77 +146,70 @@ def update_execution_state(state: Dict[str, Any]):
update_agent_status(agent_info, "completed", report_data, state)
# Update overall progress
root_node = app_state["execution_tree"][0]
execution_tree = app_state["execution_tree"]
total_agents = len(agent_state_mapping)
completed_agents = count_completed_agents(root_node)
completed_agents = count_completed_agents(execution_tree)
app_state["overall_progress"] = min(100, int((completed_agents / max(total_agents, 1)) * 100))
print(f"📊 Progress updated: {app_state['overall_progress']}% ({completed_agents}/{total_agents} agents)")
def initialize_complete_execution_tree():
"""Initialize the complete execution tree with all agents in pending state."""
return [{
"id": "root",
"name": f"📈 Trading Analysis for {app_state['company_symbol']}",
"status": "in_progress",
"content": f"Comprehensive trading analysis for {app_state['company_symbol']}",
"children": [
{
"id": "data_collection_phase",
"name": "📊 Data Collection Phase",
"status": "pending",
"content": "Collecting market data and analysis from various sources",
"children": [
create_agent_node("market_analyst", "📈 Market Analyst"),
create_agent_node("social_analyst", "📱 Social Media Analyst"),
create_agent_node("news_analyst", "📰 News Analyst"),
create_agent_node("fundamentals_analyst", "📊 Fundamentals Analyst")
]
},
{
"id": "research_phase",
"name": "🔍 Research Phase",
"status": "pending",
"content": "Research and debate investment perspectives",
"children": [
create_agent_node("bull_researcher", "🐂 Bull Researcher"),
create_agent_node("bear_researcher", "🐻 Bear Researcher"),
create_agent_node("research_manager", "🔍 Research Manager")
]
},
{
"id": "planning_phase",
"name": "📋 Planning Phase",
"status": "pending",
"content": "Develop trading strategy and execution plan",
"children": [
create_agent_node("trade_planner", "📋 Trade Planner")
]
},
{
"id": "execution_phase",
"name": "⚡ Execution Phase",
"status": "pending",
"content": "Execute trades based on analysis and planning",
"children": [
create_agent_node("trader", "⚡ Trader")
]
},
{
"id": "risk_analysis_phase",
"name": "⚠️ Risk Management Phase",
"status": "pending",
"content": "Assess and manage investment risks",
"children": [
create_agent_node("risky_analyst", "🚨 Aggressive Risk Analyst"),
create_agent_node("neutral_analyst", "⚖️ Neutral Risk Analyst"),
create_agent_node("safe_analyst", "🛡️ Conservative Risk Analyst"),
create_agent_node("risk_judge", "⚠️ Risk Judge")
]
}
],
"timestamp": time.time()
}]
return [
{
"id": "data_collection_phase",
"name": "📊 Data Collection Phase",
"status": "pending",
"content": "Collecting market data and analysis from various sources",
"children": [
create_agent_node("market_analyst", "📈 Market Analyst"),
create_agent_node("social_analyst", "📱 Social Media Analyst"),
create_agent_node("news_analyst", "📰 News Analyst"),
create_agent_node("fundamentals_analyst", "📊 Fundamentals Analyst")
]
},
{
"id": "research_phase",
"name": "🔍 Research Phase",
"status": "pending",
"content": "Research and debate investment perspectives",
"children": [
create_agent_node("bull_researcher", "🐂 Bull Researcher"),
create_agent_node("bear_researcher", "🐻 Bear Researcher"),
create_agent_node("research_manager", "🔍 Research Manager")
]
},
{
"id": "planning_phase",
"name": "📋 Planning Phase",
"status": "pending",
"content": "Develop trading strategy and execution plan",
"children": [
create_agent_node("trade_planner", "📋 Trade Planner")
]
},
{
"id": "execution_phase",
"name": "⚡ Execution Phase",
"status": "pending",
"content": "Execute trades based on analysis and planning",
"children": [
create_agent_node("trader", "⚡ Trader")
]
},
{
"id": "risk_analysis_phase",
"name": "⚠️ Risk Management Phase",
"status": "pending",
"content": "Assess and manage investment risks",
"children": [
create_agent_node("risky_analyst", "🚨 Aggressive Risk Analyst"),
create_agent_node("neutral_analyst", "⚖️ Neutral Risk Analyst"),
create_agent_node("safe_analyst", "🛡️ Conservative Risk Analyst"),
create_agent_node("risk_judge", "⚠️ Risk Judge")
]
}
]
def create_agent_node(agent_id: str, agent_name: str):
"""Create a standardized agent node with report and messages sub-items."""
@ -262,10 +252,10 @@ def get_nested_value(data: dict, key_path: str):
def update_agent_status(agent_info: dict, status: str, report_data: any, full_state: dict):
"""Update an agent's status and content in the execution tree."""
root_node = app_state["execution_tree"][0]
execution_tree = app_state["execution_tree"]
# Find the agent in the tree
agent_node = find_agent_in_tree(agent_info["agent_id"], root_node)
agent_node = find_agent_in_tree(agent_info["agent_id"], execution_tree)
if not agent_node:
return
@ -287,14 +277,15 @@ def update_agent_status(agent_info: dict, status: str, report_data: any, full_st
messages_node["content"] = extract_agent_messages(full_state, agent_info["agent_id"])
# Update phase status if all agents in phase are completed
update_phase_status_if_complete(agent_info["phase"], root_node)
update_phase_status_if_complete(agent_info["phase"], execution_tree)
def find_agent_in_tree(agent_id: str, root_node: dict):
def find_agent_in_tree(agent_id: str, tree: list):
"""Find an agent node in the execution tree."""
for phase in root_node["children"]:
for agent in phase["children"]:
if agent["id"] == agent_id:
return agent
for phase in tree:
if phase.get("children"):
for agent in phase["children"]:
if agent["id"] == agent_id:
return agent
return None
def find_item_by_id(item_id: str, items: list):
@ -326,9 +317,9 @@ def extract_agent_messages(state: dict, agent_id: str) -> str:
else:
return "💬 Agent Messages\n\nExecution completed without specific message logs"
def update_phase_status_if_complete(phase_id: str, root_node: dict):
def update_phase_status_if_complete(phase_id: str, execution_tree: list):
"""Update phase status to completed if all its agents are completed."""
phase_node = find_item_by_id(f"{phase_id}_phase", root_node["children"])
phase_node = find_item_by_id(f"{phase_id}_phase", execution_tree)
if not phase_node:
return
@ -338,13 +329,14 @@ def update_phase_status_if_complete(phase_id: str, root_node: dict):
phase_node["status"] = "completed"
phase_node["content"] = f"{phase_node['name']} - All agents completed successfully"
def count_completed_agents(root_node: dict) -> int:
def count_completed_agents(execution_tree: list) -> int:
"""Count the number of completed agents across all phases."""
count = 0
for phase in root_node["children"]:
for agent in phase["children"]:
if agent["status"] == "completed":
count += 1
for phase in execution_tree:
if phase.get("children"):
for agent in phase["children"]:
if agent["status"] == "completed":
count += 1
return count
def run_trading_process(company_symbol: str, config: Dict[str, Any]):
@ -475,15 +467,8 @@ async def start_process(
"analysis_date": analysis_date
}
# Initialize execution tree with startup message
app_state["execution_tree"] = [{
"id": "initialization",
"name": f"🚀 Initializing Trading Analysis for {company_symbol}",
"status": "in_progress",
"content": f"Starting comprehensive trading analysis for {company_symbol}...\n\nConfiguration:\n• LLM Provider: {llm_provider}\n• Quick Think Model: {quick_think_llm}\n• Deep Think Model: {deep_think_llm}\n• Max Debate Rounds: {max_debate_rounds}\n• Cost Per Trade: ${cost_per_trade}\n• Analysis Date: {analysis_date}\n\nInitializing trading agents and preparing analysis pipeline...",
"children": [],
"timestamp": time.time()
}]
# Initialize execution tree with complete structure
app_state["execution_tree"] = initialize_complete_execution_tree()
background_tasks.add_task(run_trading_process, company_symbol, app_state["config"])
@ -496,6 +481,58 @@ async def get_status():
template = jinja_env.get_template("_partials/left_panel.html")
return template.render(tree=app_state["execution_tree"], app_state=app_state)
@app.get("/status-content", response_class=HTMLResponse)
async def get_status_content():
with app_state_lock:
if not app_state["execution_tree"]:
return HTMLResponse(content="<p>No process running. Start a new one from the configuration.</p>")
template = jinja_env.get_template("_partials/tree_content.html")
tree_content = template.render(tree=app_state["execution_tree"])
# Add out-of-band updates for progress bar
progress_updates = f'''
<div id="overall-progress-bar" hx-swap-oob="true" style="width:{app_state["overall_progress"]}%;"></div>
<span id="overall-progress-text" hx-swap-oob="true">{app_state["overall_progress"]}% ({app_state["overall_status"]})</span>
'''
return HTMLResponse(content=tree_content + progress_updates)
@app.get("/status-updates")
async def get_status_updates():
"""Return only the status updates as JSON for targeted updates."""
with app_state_lock:
status_updates = {}
def extract_status_info(items, prefix=""):
for item in items:
item_id = item["id"]
status_updates[item_id] = {
"status": item["status"],
"status_icon": get_status_icon(item["status"])
}
if item.get("children"):
extract_status_info(item["children"])
extract_status_info(app_state["execution_tree"])
return {
"status_updates": status_updates,
"overall_progress": app_state["overall_progress"],
"overall_status": app_state["overall_status"]
}
def get_status_icon(status: str) -> str:
"""Get the status icon for a given status."""
if status == 'completed':
return ''
elif status == 'in_progress':
return ''
elif status == 'error':
return ''
else:
return '⏸️'
def find_item_in_tree(item_id: str, tree: list) -> Dict[str, Any] | None:
"""Recursively searches the execution tree for an item by its ID."""
for item in tree:

View File

@ -341,6 +341,8 @@ body {
.status-icon {
margin-right: 8px;
font-size: 0.9em;
transition: all 0.3s ease;
display: inline-block;
}
/* Specific styling for different item types */

View File

@ -1,5 +1,5 @@
{% macro render_item(item) %}
<li class="process-item status-{{ item.status }}">
<li class="process-item status-{{ item.status }}" id="item-{{ item.id }}">
<div class="item-header">
{% if item.children %}
<button class="toggle-btn" onclick="toggleNode(this)" aria-label="Toggle children">
@ -9,7 +9,7 @@
<span class="toggle-spacer"></span>
{% endif %}
<span hx-get="/content/{{ item.id }}" hx-target="#right-panel" hx-swap="innerHTML" class="item-name clickable">
<span class="status-icon">
<span class="status-icon" id="status-icon-{{ item.id }}">
{% if item.status == 'completed' %}✅
{% elif item.status == 'in_progress' %}⏳
{% elif item.status == 'error' %}❌
@ -20,7 +20,7 @@
</span>
</div>
{% if item.children %}
<ul class="item-children collapsed">
<ul class="item-children collapsed" id="children-{{ item.id }}">
{% for child in item.children %}
{{ render_item(child) }}
{% endfor %}
@ -32,15 +32,20 @@
<div id="overall-progress-bar" hx-swap-oob="true" style="width:{{ app_state.overall_progress }}%;"></div>
<span id="overall-progress-text" hx-swap-oob="true">{{ app_state.overall_progress }}% ({{ app_state.overall_status }})</span>
<div id="left-panel-content" hx-get="/status" hx-trigger="every 5s" hx-swap="innerHTML">
<h2>Execution Status</h2>
{% if tree %}
<ul class="execution-tree">
{% for item in tree %}
{{ render_item(item) }}
{% endfor %}
</ul>
<h2>
{% if app_state.company_symbol %}
Trading Analysis for {{ app_state.company_symbol }}
{% else %}
<p>No process running. Start a new one from the configuration.</p>
Execution Status
{% endif %}
</div>
</h2>
{% if tree %}
<ul class="execution-tree" id="execution-tree" hx-get="/status-content" hx-trigger="every 5s" hx-swap="innerHTML">
{% for item in tree %}
{{ render_item(item) }}
{% endfor %}
</ul>
{% else %}
<p id="execution-tree">No process running. Start a new one from the configuration.</p>
{% endif %}

View File

@ -0,0 +1,34 @@
{% macro render_item(item) %}
<li class="process-item status-{{ item.status }}" id="item-{{ item.id }}">
<div class="item-header">
{% if item.children %}
<button class="toggle-btn" onclick="toggleNode(this)" aria-label="Toggle children">
<span class="toggle-icon"></span>
</button>
{% else %}
<span class="toggle-spacer"></span>
{% endif %}
<span hx-get="/content/{{ item.id }}" hx-target="#right-panel" hx-swap="innerHTML" class="item-name clickable">
<span class="status-icon" id="status-icon-{{ item.id }}">
{% if item.status == 'completed' %}✅
{% elif item.status == 'in_progress' %}⏳
{% elif item.status == 'error' %}❌
{% else %}⏸️
{% endif %}
</span>
{{ item.name }}
</span>
</div>
{% if item.children %}
<ul class="item-children collapsed" id="children-{{ item.id }}">
{% for child in item.children %}
{{ render_item(child) }}
{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}
{% for item in tree %}
{{ render_item(item) }}
{% endfor %}

View File

@ -203,10 +203,154 @@
}
}
// Save the current expansion state of all toggleable items
function saveExpansionState() {
const state = {};
const executionTree = document.querySelector('.execution-tree');
if (executionTree) {
const items = executionTree.querySelectorAll('.process-item');
items.forEach(item => {
const children = item.querySelector('.item-children');
const button = item.querySelector('.toggle-btn');
if (children && button) {
const itemId = getItemId(item);
if (itemId) {
state[itemId] = children.classList.contains('expanded');
}
}
});
}
return state;
}
// Restore the expansion state after content update
function restoreExpansionState(savedState) {
if (!savedState) return;
const executionTree = document.querySelector('.execution-tree');
if (executionTree) {
const items = executionTree.querySelectorAll('.process-item');
items.forEach(item => {
const children = item.querySelector('.item-children');
const button = item.querySelector('.toggle-btn');
if (children && button) {
const itemId = getItemId(item);
if (itemId && savedState.hasOwnProperty(itemId)) {
if (savedState[itemId]) {
// Expand
children.classList.remove('collapsed');
children.classList.add('expanded');
button.classList.add('expanded');
} else {
// Collapse
children.classList.remove('expanded');
children.classList.add('collapsed');
button.classList.remove('expanded');
}
}
}
});
}
}
// Get item ID from the clickable span
function getItemId(processItem) {
const clickableSpan = processItem.querySelector('.item-name.clickable');
if (clickableSpan) {
const hxGet = clickableSpan.getAttribute('hx-get');
if (hxGet) {
const match = hxGet.match(/\/content\/(.+)$/);
return match ? match[1] : null;
}
}
return null;
}
// Handle HTMX before request to save state
document.addEventListener('htmx:beforeSwap', function(event) {
if (event.target.id === 'left-panel-content') {
window.savedExpansionState = saveExpansionState();
}
});
// Handle HTMX after settle to restore state
document.addEventListener('htmx:afterSettle', function(event) {
if (event.target.id === 'left-panel-content' && window.savedExpansionState) {
restoreExpansionState(window.savedExpansionState);
}
});
// Targeted status updates to prevent flickering
function updateStatusIndicators() {
fetch('/status-updates')
.then(response => response.json())
.then(data => {
// Update overall progress
const progressBar = document.getElementById('overall-progress-bar');
const progressText = document.getElementById('overall-progress-text');
if (progressBar) {
progressBar.style.width = data.overall_progress + '%';
}
if (progressText) {
progressText.textContent = data.overall_progress + '% (' + data.overall_status + ')';
}
// Update individual status icons
for (const [itemId, statusInfo] of Object.entries(data.status_updates)) {
const statusIcon = document.querySelector(`[hx-get="/content/${itemId}"] .status-icon`);
if (statusIcon && statusIcon.textContent !== statusInfo.status_icon) {
statusIcon.textContent = statusInfo.status_icon;
// Update the parent item's CSS class
const processItem = statusIcon.closest('.process-item');
if (processItem) {
// Remove old status classes
processItem.classList.remove('status-pending', 'status-in_progress', 'status-completed', 'status-error');
// Add new status class
processItem.classList.add('status-' + statusInfo.status);
}
}
}
})
.catch(error => {
console.log('Status update failed:', error);
});
}
// Start targeted updates when execution tree is present
function startTargetedUpdates() {
if (document.querySelector('.execution-tree')) {
window.statusUpdateInterval = setInterval(updateStatusIndicators, 2000); // Update every 2 seconds
}
}
// Stop targeted updates
function stopTargetedUpdates() {
if (window.statusUpdateInterval) {
clearInterval(window.statusUpdateInterval);
window.statusUpdateInterval = null;
}
}
// Initialize the page
document.addEventListener('DOMContentLoaded', function() {
setCurrentDate();
updateModelOptions(); // Set initial model options for OpenRouter
// Start targeted updates if execution tree is already present
startTargetedUpdates();
});
// Handle when new content is loaded (like when starting a process)
document.addEventListener('htmx:afterSettle', function(event) {
if (event.target.id === 'left-panel-content') {
if (window.savedExpansionState) {
restoreExpansionState(window.savedExpansionState);
}
// Start or restart targeted updates when new content is loaded
stopTargetedUpdates();
startTargetedUpdates();
}
});
</script>
</body>