feat: menu updates
This commit is contained in:
parent
e0848e960c
commit
9201f77c9f
Binary file not shown.
223
webapp/main.py
223
webapp/main.py
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue