Add reflection for each analysis

This commit is contained in:
Jenit Jain 2025-08-23 22:01:48 -07:00
parent ce86231796
commit 4c7f636097
6 changed files with 388 additions and 69 deletions

View File

@ -49,7 +49,12 @@ class FinancialSituationMemory:
embeddings=embeddings, embeddings=embeddings,
ids=ids, ids=ids,
) )
def get_latest_situation(self):
"""Retrieve the latest entry from a situation_collection."""
latest_entry = self.situation_collection.query(sort_by="id:descending", limit=1)
return latest_entry
def get_memories(self, current_situation, n_matches=1): def get_memories(self, current_situation, n_matches=1):
"""Find matching recommendations using OpenAI embeddings""" """Find matching recommendations using OpenAI embeddings"""
query_embedding = self.get_embedding(current_situation) query_embedding = self.get_embedding(current_situation)

View File

@ -4,6 +4,7 @@ from pydantic import BaseModel
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import json import json
import os import os
import time
from datetime import datetime from datetime import datetime
import glob import glob
import uuid import uuid
@ -59,10 +60,12 @@ class AnalysisResponse(BaseModel):
class JobStatus(BaseModel): class JobStatus(BaseModel):
job_id: str job_id: str
start_time: Optional[float] = None
status: str status: str
progress: Optional[str] = None progress: Optional[str] = None
result: Optional[Dict[str, Any]] = None result: Optional[Dict[str, Any]] = None
error: Optional[str] = None error: Optional[str] = None
trading_agent: Any = None # Store the trading agent instance here
# In-memory job storage (in production, use Redis or database) # In-memory job storage (in production, use Redis or database)
jobs: Dict[str, JobStatus] = {} jobs: Dict[str, JobStatus] = {}
@ -77,6 +80,15 @@ async def health_check():
async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config_overrides: Dict[str, Any] = None): async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config_overrides: Dict[str, Any] = None):
"""Background task to run the trading analysis without blocking the event loop""" """Background task to run the trading analysis without blocking the event loop"""
def execution_time() -> Optional[str]:
if jobs[job_id].status == "completed" or jobs[job_id].status == "failed":
return f"{time.time() - jobs[job_id].start_time:.2f} seconds"
else:
return None
jobs[job_id].start_time = time.time() # Save the start time in the job
try: try:
jobs[job_id].status = "running" jobs[job_id].status = "running"
jobs[job_id].progress = "Initializing TradingAgents..." jobs[job_id].progress = "Initializing TradingAgents..."
@ -87,16 +99,13 @@ async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config
config.update(config_overrides) config.update(config_overrides)
jobs[job_id].progress = "Setting up trading graph..." jobs[job_id].progress = "Setting up trading graph..."
# Create and store the trading agent instance
# Define blocking work as sync function ta = TradingAgentsGraph(debug=True, config=config)
def _do_work(): jobs[job_id].trading_agent = ta # Store the instance
ta = TradingAgentsGraph(debug=True, config=config) jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
_, decision = ta.propagate(symbol, analysis_date) # Run the propagate method in a threadpool
return decision _, decision = await run_in_threadpool(ta.propagate, symbol, analysis_date)
# Run blocking work in threadpool so the event loop stays responsive
decision = await run_in_threadpool(_do_work)
jobs[job_id].status = "completed" jobs[job_id].status = "completed"
jobs[job_id].result = { jobs[job_id].result = {
@ -104,6 +113,20 @@ async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config
"date": analysis_date, "date": analysis_date,
"decision": decision, "decision": decision,
"completed_at": datetime.now().isoformat(), "completed_at": datetime.now().isoformat(),
"execution_time": execution_time()
}
jobs[job_id].progress = "Analysis completed successfully"
except Exception as e:
jobs[job_id].status = "failed"
jobs[job_id].error = str(e)
jobs[job_id].progress = f"Error: {str(e)}"
jobs[job_id].result = {
"symbol": symbol,
"date": analysis_date,
"decision": decision,
"completed_at": datetime.now().isoformat(),
"execution_time": execution_time()
} }
jobs[job_id].progress = "Analysis completed successfully" jobs[job_id].progress = "Analysis completed successfully"
@ -342,15 +365,81 @@ async def get_jobs():
"""Get all jobs""" """Get all jobs"""
job_lst = [] job_lst = []
for job_id, job in jobs.items(): for job_id, job in jobs.items():
job_lst.append({ job_dict = {
"job_id": job_id, "job_id": job_id,
"status": job.status, "status": job.status,
"progress": job.progress, "progress": job.progress,
"result": job.result, "result": job.result,
"error": job.error "error": job.error,
}) }
# Add execution_time if available
if hasattr(job, 'start_time') and job.start_time is not None:
if job.status in ["completed", "failed"]:
job_dict["execution_time"] = f"{time.time() - job.start_time:.2f} seconds"
job_lst.append(job_dict)
return {"jobs": job_lst} return {"jobs": job_lst}
@app.post("/reflect-on-analysis/{symbol}/{date}")
async def reflect_on_analysis(symbol: str, date: str, request: dict):
"""Get latest financial situation memory for a specific analysis"""
returns_losses = request.get("returns_losses")
if returns_losses is None:
raise HTTPException(status_code=400, detail="returns_losses is required in request body")
# Find the job that matches the symbol and date
matching_job = None
for job_id, job in jobs.items():
if (job.result.get("symbol") == symbol.upper() and
job.result.get("date") == date and
hasattr(job, 'trading_agent') and
job.trading_agent):
matching_job = job
break
if not matching_job:
raise HTTPException(status_code=404, detail=f"No active job found for {symbol} on {date}")
if not hasattr(matching_job.trading_agent, 'memory') or not matching_job.trading_agent.memory:
raise HTTPException(status_code=404, detail="No memory found for this analysis")
matching_job.trading_agent.reflect_and_remember(returns_losses)
try:
bull_memory = matching_job.trading_agent.bull_memory
bear_memory = matching_job.trading_agent.bear_memory
trader_memory = matching_job.trading_agent.trader_memory
invest_judge_memory = matching_job.trading_agent.invest_judge_memory
risk_manager_memory = matching_job.trading_agent.risk_manager_memory
reflections = {}
latest_entry = bull_memory.get_latest_situation()
reflections["bull_memory"] = latest_entry
latest_entry = bear_memory.get_latest_situation()
reflections["bear_memory"] = latest_entry
latest_entry = trader_memory.get_latest_situation()
reflections["trader_memory"] = latest_entry
latest_entry = invest_judge_memory.get_latest_situation()
reflections["invest_judge_memory"] = latest_entry
latest_entry = risk_manager_memory.get_latest_situation()
reflections["risk_manager_memory"] = latest_entry
return {
"symbol": symbol.upper(),
"date": date,
"job_id": matching_job.job_id,
"reflections": reflections
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error retrieving latest situation: {str(e)}")
@app.get("/config") @app.get("/config")
async def get_default_config(): async def get_default_config():
"""Get the default configuration""" """Get the default configuration"""
@ -358,4 +447,4 @@ async def get_default_config():
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

View File

@ -687,7 +687,13 @@ function App() {
{selectedTransformedData ? ( {selectedTransformedData ? (
<TransformedDataAdapter analysisData={selectedTransformedData} /> <TransformedDataAdapter analysisData={selectedTransformedData} />
) : resultDetail ? ( ) : resultDetail ? (
<AnalysisDataAdapter tradingResult={resultDetail} /> <AnalysisDataAdapter
tradingResult={{
...resultDetail,
symbol: selectedResult?.company || resultDetail?.symbol,
date: selectedResult?.date || resultDetail?.date
}}
/>
) : ( ) : (
<div className="p-8 text-center"> <div className="p-8 text-center">
<div className="text-gray-500 mb-2">Loading analysis data...</div> <div className="text-gray-500 mb-2">Loading analysis data...</div>

View File

@ -3,6 +3,7 @@ import AnalysisWidgets from '../pages/AnalysisWidgets.tsx';
interface TradingAgentsResult { interface TradingAgentsResult {
symbol: string; symbol: string;
date?: string;
final_decision?: { final_decision?: {
decision: string; decision: string;
reasoning: string; reasoning: string;
@ -90,7 +91,7 @@ const AnalysisDataAdapter: React.FC<AnalysisDataAdapterProps> = ({ tradingResult
'Based on comprehensive analysis, the stock shows strong fundamentals with reasonable valuation and positive technical momentum, warranting a buy recommendation with appropriate risk management.' 'Based on comprehensive analysis, the stock shows strong fundamentals with reasonable valuation and positive technical momentum, warranting a buy recommendation with appropriate risk management.'
}; };
return <AnalysisWidgets data={transformedData} rawData={tradingResult} />; return <AnalysisWidgets symbol={tradingResult.symbol} date={tradingResult.date} data={transformedData} rawData={tradingResult} />;
}; };
export default AnalysisDataAdapter; export default AnalysisDataAdapter;

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
// New interface for the transformed JSON structure // New interface for the transformed JSON structure
interface TransformedAnalysisData { interface TransformedAnalysisData {
@ -458,12 +458,77 @@ const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysi
const [activeReportTab, setActiveReportTab] = React.useState<string>(availableReports[0]?.key || 'market'); const [activeReportTab, setActiveReportTab] = React.useState<string>(availableReports[0]?.key || 'market');
React.useEffect(() => { setActiveReportTab(availableReports[0]?.key || 'market'); }, [availableReports]); React.useEffect(() => { setActiveReportTab(availableReports[0]?.key || 'market'); }, [availableReports]);
// Reflection functionality state
const [showReflectionModal, setShowReflectionModal] = React.useState(false);
const [lossResults, setLossResults] = React.useState('');
const [isReflecting, setIsReflecting] = React.useState(false);
const [reflectionResults, setReflectionResults] = React.useState(null);
const [reflectionError, setReflectionError] = React.useState('');
// Handle reflection API call
const handleReflection = async () => {
const symbolToUse = data.metadata.company_ticker;
const dateToUse = data.metadata.analysis_date;
console.log('Reflection attempt:', { symbolToUse, dateToUse, lossResults });
if (!lossResults.trim()) {
setReflectionError('Please enter your loss results');
return;
}
setIsReflecting(true);
setReflectionError('');
try {
const response = await fetch(`http://localhost:8000/reflect-on-analysis/${symbolToUse}/${dateToUse}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
returns_losses: lossResults
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to perform reflection');
}
const reflectionData = await response.json();
setReflectionResults(reflectionData);
setReflectionError('');
} catch (error) {
console.error('Reflection error:', error);
setReflectionError(error.message || 'Failed to perform reflection');
} finally {
setIsReflecting(false);
}
};
const handleModalClose = () => {
setShowReflectionModal(false);
setLossResults('');
setReflectionResults(null);
setReflectionError('');
};
return ( return (
<div className="p-6"> <div className="p-6">
<div className="mx-auto max-w-6xl space-y-4"> <div className="mx-auto max-w-6xl space-y-4">
{/* Metadata - Full width */} {/* Metadata - Full width */}
<div className="bg-white rounded-lg shadow p-6"> <div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold mb-3">Company Information</h2> <div className="flex justify-between items-center mb-3">
<h2 className="text-lg font-semibold">Company Information</h2>
<button
onClick={() => setShowReflectionModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
title="Reflect on Analysis"
>
🤔 Reflect on Analysis
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Stat label="Company" value={`${md?.company_name ?? '-'} (${md?.company_ticker ?? '-'})`} /> <Stat label="Company" value={`${md?.company_name ?? '-'} (${md?.company_ticker ?? '-'})`} />
<Stat label="Analysis Date" value={md?.analysis_date ?? '-'} /> <Stat label="Analysis Date" value={md?.analysis_date ?? '-'} />
@ -595,12 +660,85 @@ const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysi
</div> </div>
</div> </div>
</div> </div>
{/* Reflection Modal */}
{showReflectionModal && (
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 flex justify-center items-center z-50">
<div className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md mx-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Reflect on Analysis</h2>
<button
onClick={handleModalClose}
className="text-gray-400 hover:text-gray-600"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={(e) => { e.preventDefault(); handleReflection(); }}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Loss Results:
</label>
<textarea
value={lossResults}
onChange={(e) => setLossResults(e.target.value)}
placeholder="e.g., Lost 15% due to unexpected earnings miss, market volatility, etc."
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
rows={4}
/>
{reflectionError && (
<p className="mt-2 text-sm text-red-500">{reflectionError}</p>
)}
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={handleModalClose}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
>
Cancel
</button>
<button
type="submit"
disabled={isReflecting}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isReflecting ? 'Reflecting...' : 'Submit Reflection'}
</button>
</div>
</form>
{/* Display reflection results */}
{reflectionResults && (
<div className="mt-4 p-4 bg-gray-50 rounded-md">
<h4 className="text-sm font-medium text-gray-900 mb-2">Reflection Results:</h4>
<div className="space-y-2">
{Object.entries(reflectionResults.reflections || {}).map(([agentType, reflection]) => (
<div key={agentType} className="text-xs">
<span className="font-medium capitalize">{agentType.replace('_', ' ')}:</span>
<p className="text-gray-600 ml-2">{typeof reflection === 'string' ? reflection : JSON.stringify(reflection)}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div> </div>
); );
}; };
// Render minimalist dashboard with all keys from the transformed JSON // Render minimalist dashboard with all keys from the transformed JSON
return <MinimalTransformedDashboard data={transformedWithConfig} />; return (
<div>
<MinimalTransformedDashboard data={transformedWithConfig} />
</div>
);
}; };
export default TransformedDataAdapter; export default TransformedDataAdapter;

View File

@ -34,14 +34,24 @@ interface AnalysisWidgetsProps {
rawData?: any; rawData?: any;
onBackWidget?: () => void; onBackWidget?: () => void;
onRefreshWidget?: () => void; onRefreshWidget?: () => void;
symbol?: string;
date?: string;
} }
const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBackWidget, onRefreshWidget }) => { const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBackWidget, onRefreshWidget, symbol, date }) => {
const [activeTab, setActiveTab] = useState('bull'); const [activeTab, setActiveTab] = useState('bull');
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set()); const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
const [positionSize, setPositionSize] = useState(10000); const [positionSize, setPositionSize] = useState(10000);
const [portfolioAllocation, setPortfolioAllocation] = useState(4); const [portfolioAllocation, setPortfolioAllocation] = useState(4);
const [showReflectionModal, setShowReflectionModal] = useState(false);
const [lossResults, setLossResults] = useState('');
const [isReflecting, setIsReflecting] = useState(false);
const [reflectionResults, setReflectionResults] = useState(null);
const [reflectionError, setReflectionError] = useState('');
console.log('AnalysisWidgets props:', { symbol, date, hasRawData: !!rawData });
const handleBack = () => { const handleBack = () => {
if (onBackWidget) return onBackWidget(); if (onBackWidget) return onBackWidget();
if (typeof window !== 'undefined' && window.history) window.history.back(); if (typeof window !== 'undefined' && window.history) window.history.back();
@ -51,6 +61,47 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
if (typeof window !== 'undefined') window.location.reload(); if (typeof window !== 'undefined') window.location.reload();
}; };
const handleReflection = async () => {
const symbolToUse = symbol || data.symbol || 'UNKNOWN';
const dateToUse = date || new Date().toISOString().split('T')[0];
console.log('Reflection attempt:', { symbolToUse, dateToUse, lossResults });
if (!lossResults.trim()) {
setReflectionError('Please enter your loss results');
return;
}
setIsReflecting(true);
setReflectionError('');
try {
const response = await fetch(`http://localhost:8000/reflect-on-analysis/${symbolToUse}/${dateToUse}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
returns_losses: lossResults
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to perform reflection');
}
const reflectionData = await response.json();
setReflectionResults(reflectionData);
setReflectionError('');
} catch (error) {
console.error('Reflection error:', error);
setReflectionError(error.message || 'Failed to perform reflection');
} finally {
setIsReflecting(false);
}
};
const WidgetHeader: React.FC<{ title: string }> = ({ title }) => ( const WidgetHeader: React.FC<{ title: string }> = ({ title }) => (
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<button <button
@ -63,57 +114,33 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
</button> </button>
<h3 className="text-lg font-semibold text-gray-900">{title}</h3> <h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<button <div className="flex items-center space-x-2">
type="button" <button
onClick={handleRefresh} type="button"
className="p-2 rounded hover:bg-gray-100 text-gray-600" onClick={() => setShowReflectionModal(true)}
aria-label="Refresh" className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
title="Refresh" title={`Reflect on Analysis (Symbol: ${symbol || data.symbol || 'N/A'}, Date: ${date || 'N/A'})`}
> >
🤔 Reflect
</button> </button>
<button
type="button"
onClick={handleRefresh}
className="p-2 rounded hover:bg-gray-100 text-gray-600"
aria-label="Refresh"
title="Refresh"
>
</button>
</div>
</div> </div>
); );
// Mock price data for chart const handleModalClose = () => {
const priceData = [ setShowReflectionModal(false);
{ date: '2024-07-01', price: 4.20, volume: 1200000 }, setLossResults('');
{ date: '2024-07-08', price: 4.45, volume: 1500000 }, setReflectionResults(null);
{ date: '2024-07-15', price: 4.80, volume: 1800000 }, setReflectionError('');
{ date: '2024-07-22', price: 5.20, volume: 2100000 },
{ date: '2024-07-26', price: 5.81, volume: 2500000 },
];
const ownershipData = [
{ name: 'Insider', value: data.insiderOwnership, color: '#8884d8' },
{ name: 'Institutional', value: data.institutionalOwnership, color: '#82ca9d' },
{ name: 'Retail', value: data.retailOwnership, color: '#ffc658' },
];
const toggleSection = (section: string) => {
const newExpanded = new Set(expandedSections);
if (newExpanded.has(section)) {
newExpanded.delete(section);
} else {
newExpanded.add(section);
}
setExpandedSections(newExpanded);
};
const calculatePosition = () => {
const allocation = (portfolioAllocation / 100) * positionSize;
const shares = Math.floor(allocation / data.currentPrice);
return { allocation, shares };
};
const getRiskColor = (level: number) => {
if (level < 30) return 'text-green-600';
if (level < 70) return 'text-yellow-600';
return 'text-red-600';
};
const getTrendColor = (current: number, reference: number) => {
return current > reference ? 'text-green-600' : 'text-red-600';
}; };
return ( return (
@ -554,6 +581,59 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
</div> </div>
)} )}
</div> </div>
{/* Reflection Modal */}
{showReflectionModal && (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity">
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg className="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
Reflect on Analysis
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Please enter your loss results to reflect on the analysis.
</p>
<input
type="text"
value={lossResults}
onChange={(e) => setLossResults(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{reflectionError && (
<p className="text-sm text-red-500">{reflectionError}</p>
)}
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
onClick={handleReflection}
disabled={isReflecting}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{isReflecting ? 'Reflecting...' : 'Reflect'}
</button>
<button
type="button"
onClick={handleModalClose}
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };