608 lines
28 KiB
TypeScript
608 lines
28 KiB
TypeScript
import React from 'react';
|
|
|
|
// New interface for the transformed JSON structure
|
|
interface TransformedAnalysisData {
|
|
metadata: {
|
|
company_ticker: string;
|
|
company_name: string;
|
|
analysis_date: string;
|
|
final_recommendation: 'BUY' | 'SELL' | 'HOLD';
|
|
confidence_level: 'HIGH' | 'MEDIUM' | 'LOW';
|
|
};
|
|
|
|
financial_data: {
|
|
current_price: number;
|
|
price_change: number;
|
|
price_change_percent: number;
|
|
market_cap: string;
|
|
enterprise_value: string;
|
|
shares_outstanding: string;
|
|
trading_range: {
|
|
high: number;
|
|
low: number;
|
|
open: number;
|
|
};
|
|
volume: number;
|
|
valuation_ratios: {
|
|
current_ps_ratio: number;
|
|
fair_value_ps_ratio: number;
|
|
forward_pe: number;
|
|
forward_ps: number;
|
|
forward_pcf: number;
|
|
forward_pocf: number;
|
|
};
|
|
ownership: {
|
|
insider_percent: number;
|
|
institutional_percent: number;
|
|
};
|
|
analyst_data: {
|
|
consensus_rating: string;
|
|
price_target: number;
|
|
forecast_price: number;
|
|
};
|
|
};
|
|
|
|
technical_indicators: {
|
|
sma_50: number;
|
|
sma_200: number;
|
|
ema_10: number;
|
|
macd: number;
|
|
macd_signal: number;
|
|
rsi: number;
|
|
atr: number;
|
|
trend_directions: {
|
|
sma_50: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
|
|
sma_200: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
|
|
ema_10: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
|
|
macd: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
|
|
rsi_condition: 'OVERSOLD' | 'OVERBOUGHT' | 'NEUTRAL';
|
|
};
|
|
};
|
|
|
|
investment_strategy: {
|
|
position_sizing: {
|
|
total_allocation_percent: string;
|
|
entry_strategy: string;
|
|
tranche_1_percent: string;
|
|
tranche_2_percent: string;
|
|
};
|
|
risk_management: {
|
|
initial_stop_loss: number;
|
|
stop_loss_percent: number;
|
|
breakeven_strategy: string;
|
|
};
|
|
profit_targets: Array<{
|
|
target_price: number;
|
|
action: string;
|
|
rationale: string;
|
|
}>;
|
|
monitoring_points: string[];
|
|
};
|
|
|
|
debate_summary: {
|
|
bull_key_points: string[];
|
|
bear_key_points: string[];
|
|
neutral_perspective: string;
|
|
final_decision_rationale: string;
|
|
};
|
|
|
|
text_content: {
|
|
market_report: {
|
|
title: string;
|
|
content: string;
|
|
key_takeaways: string[];
|
|
};
|
|
sentiment_report: {
|
|
title: string;
|
|
content: string;
|
|
recent_developments: string[];
|
|
};
|
|
fundamentals_report: {
|
|
title: string;
|
|
content: string;
|
|
financial_highlights: string[];
|
|
};
|
|
news_report: {
|
|
title: string;
|
|
content: string;
|
|
key_developments: Array<{
|
|
date: string;
|
|
event: string;
|
|
impact: string;
|
|
}>;
|
|
};
|
|
investment_plan_full: {
|
|
title: string;
|
|
content: string;
|
|
};
|
|
debate_transcripts: {
|
|
bull_analysis: string;
|
|
bear_analysis: string;
|
|
neutral_analysis: string;
|
|
risk_discussion: string;
|
|
};
|
|
};
|
|
|
|
widgets_config: {
|
|
charts_needed: Array<{
|
|
type: string;
|
|
data_source: string;
|
|
timeframe: string;
|
|
}>;
|
|
text_widgets: Array<{
|
|
type: string;
|
|
title: string;
|
|
content_source: string;
|
|
}>;
|
|
};
|
|
}
|
|
|
|
// Legacy interface for backward compatibility
|
|
interface LegacyTradingAgentsResult {
|
|
symbol: string;
|
|
final_decision?: {
|
|
decision: string;
|
|
reasoning: string;
|
|
};
|
|
technical_analysis?: {
|
|
current_price: number;
|
|
rsi: number;
|
|
macd: number;
|
|
moving_averages: {
|
|
ma_50: number;
|
|
ma_200: number;
|
|
};
|
|
};
|
|
fundamental_analysis?: {
|
|
market_cap: string;
|
|
ps_ratio: number;
|
|
forward_pe: number;
|
|
analyst_target: number;
|
|
};
|
|
bull_arguments?: string[];
|
|
bear_arguments?: string[];
|
|
neutral_perspective?: string;
|
|
risk_assessment?: {
|
|
overall_risk: number;
|
|
};
|
|
sentiment_analysis?: {
|
|
overall_score: number;
|
|
};
|
|
ownership_structure?: {
|
|
insider_ownership: number;
|
|
institutional_ownership: number;
|
|
retail_ownership: number;
|
|
};
|
|
investment_plan?: {
|
|
stop_loss: number;
|
|
profit_targets: number[];
|
|
};
|
|
earnings_date?: string;
|
|
}
|
|
|
|
interface TransformedDataAdapterProps {
|
|
analysisData: TransformedAnalysisData | LegacyTradingAgentsResult;
|
|
}
|
|
|
|
const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysisData }) => {
|
|
// Check if this is the new transformed format or legacy format
|
|
const isTransformedFormat = (data: any): data is TransformedAnalysisData => {
|
|
return data.metadata && data.financial_data && data.technical_indicators;
|
|
};
|
|
|
|
// Convert legacy format to new format for backward compatibility
|
|
const convertLegacyToTransformed = (legacyData: LegacyTradingAgentsResult): TransformedAnalysisData => {
|
|
return {
|
|
metadata: {
|
|
company_ticker: legacyData.symbol || 'UNKNOWN',
|
|
company_name: legacyData.symbol || 'Unknown Company',
|
|
analysis_date: new Date().toISOString().split('T')[0],
|
|
final_recommendation: (legacyData.final_decision?.decision?.toUpperCase() as 'BUY' | 'SELL' | 'HOLD') || 'HOLD',
|
|
confidence_level: 'MEDIUM'
|
|
},
|
|
financial_data: {
|
|
current_price: legacyData.technical_analysis?.current_price || 0,
|
|
price_change: 0,
|
|
price_change_percent: 0,
|
|
market_cap: legacyData.fundamental_analysis?.market_cap || 'N/A',
|
|
enterprise_value: 'N/A',
|
|
shares_outstanding: 'N/A',
|
|
trading_range: {
|
|
high: 0,
|
|
low: 0,
|
|
open: 0
|
|
},
|
|
volume: 0,
|
|
valuation_ratios: {
|
|
current_ps_ratio: legacyData.fundamental_analysis?.ps_ratio || 0,
|
|
fair_value_ps_ratio: 0,
|
|
forward_pe: legacyData.fundamental_analysis?.forward_pe || 0,
|
|
forward_ps: 0,
|
|
forward_pcf: 0,
|
|
forward_pocf: 0
|
|
},
|
|
ownership: {
|
|
insider_percent: legacyData.ownership_structure?.insider_ownership || 0,
|
|
institutional_percent: legacyData.ownership_structure?.institutional_ownership || 0
|
|
},
|
|
analyst_data: {
|
|
consensus_rating: 'N/A',
|
|
price_target: legacyData.fundamental_analysis?.analyst_target || 0,
|
|
forecast_price: 0
|
|
}
|
|
},
|
|
technical_indicators: {
|
|
sma_50: legacyData.technical_analysis?.moving_averages?.ma_50 || 0,
|
|
sma_200: legacyData.technical_analysis?.moving_averages?.ma_200 || 0,
|
|
ema_10: 0,
|
|
macd: legacyData.technical_analysis?.macd || 0,
|
|
macd_signal: 0,
|
|
rsi: legacyData.technical_analysis?.rsi || 50,
|
|
atr: 0,
|
|
trend_directions: {
|
|
sma_50: 'NEUTRAL',
|
|
sma_200: 'NEUTRAL',
|
|
ema_10: 'NEUTRAL',
|
|
macd: 'NEUTRAL',
|
|
rsi_condition: 'NEUTRAL'
|
|
}
|
|
},
|
|
investment_strategy: {
|
|
position_sizing: {
|
|
total_allocation_percent: '0%',
|
|
entry_strategy: 'N/A',
|
|
tranche_1_percent: '0%',
|
|
tranche_2_percent: '0%'
|
|
},
|
|
risk_management: {
|
|
initial_stop_loss: legacyData.investment_plan?.stop_loss || 0,
|
|
stop_loss_percent: 0,
|
|
breakeven_strategy: 'N/A'
|
|
},
|
|
profit_targets: legacyData.investment_plan?.profit_targets?.map(target => ({
|
|
target_price: target,
|
|
action: 'SELL',
|
|
rationale: 'Profit target'
|
|
})) || [],
|
|
monitoring_points: []
|
|
},
|
|
debate_summary: {
|
|
bull_key_points: legacyData.bull_arguments || [],
|
|
bear_key_points: legacyData.bear_arguments || [],
|
|
neutral_perspective: legacyData.neutral_perspective || 'No neutral perspective available',
|
|
final_decision_rationale: legacyData.final_decision?.reasoning || 'No decision rationale available'
|
|
},
|
|
text_content: {
|
|
market_report: {
|
|
title: 'Technical Analysis Report',
|
|
content: 'Legacy data - detailed technical analysis not available',
|
|
key_takeaways: []
|
|
},
|
|
sentiment_report: {
|
|
title: 'Company Sentiment Analysis',
|
|
content: 'Legacy data - detailed sentiment analysis not available',
|
|
recent_developments: []
|
|
},
|
|
fundamentals_report: {
|
|
title: 'Fundamental Analysis',
|
|
content: 'Legacy data - detailed fundamental analysis not available',
|
|
financial_highlights: []
|
|
},
|
|
news_report: {
|
|
title: 'Macroeconomic Context',
|
|
content: 'Legacy data - news report not available',
|
|
key_developments: []
|
|
},
|
|
investment_plan_full: {
|
|
title: 'Complete Investment Strategy',
|
|
content: 'Legacy data - detailed investment plan not available'
|
|
},
|
|
debate_transcripts: {
|
|
bull_analysis: legacyData.bull_arguments?.join('\n') || '',
|
|
bear_analysis: legacyData.bear_arguments?.join('\n') || '',
|
|
neutral_analysis: legacyData.neutral_perspective || '',
|
|
risk_discussion: ''
|
|
}
|
|
},
|
|
widgets_config: {
|
|
charts_needed: [
|
|
{ type: 'price_chart', data_source: 'financial_data.current_price', timeframe: '30_days' },
|
|
{ type: 'technical_indicators', data_source: 'technical_indicators', timeframe: '30_days' }
|
|
],
|
|
text_widgets: [
|
|
{ type: 'expandable_report', title: 'Technical Analysis', content_source: 'text_content.market_report' }
|
|
]
|
|
}
|
|
};
|
|
};
|
|
|
|
// Get the transformed data (either already transformed or converted from legacy)
|
|
const transformedData: TransformedAnalysisData = isTransformedFormat(analysisData)
|
|
? analysisData
|
|
: convertLegacyToTransformed(analysisData);
|
|
|
|
// Builds a default widgets_config for transformed analyses when missing
|
|
const buildTransformedWidgetsConfig = (data: any) => {
|
|
const confidenceMap: Record<string, number> = { LOW: 33, MEDIUM: 66, HIGH: 90 };
|
|
return {
|
|
layout: [
|
|
["kpi_recommendation", "kpi_confidence", "kpi_price_change", "kpi_marketcap", "kpi_ev"],
|
|
["ownership_donut", "range_bar", "volume_chip"],
|
|
["rsi_gauge", "macd_mini", "ma_trends", "atr_chip"],
|
|
["strategy_ladder", "position_sizing", "monitoring_points"],
|
|
["analyst_target_vs_price"],
|
|
["report_technical", "report_sentiment"],
|
|
["report_fundamentals", "report_investment_plan"],
|
|
["debate_summary"]
|
|
],
|
|
widgets: [
|
|
{ id: "kpi_recommendation", type: "kpi_badge", title: "Recommendation", dataPath: "metadata.final_recommendation", colorMapping: { BUY: "green", HOLD: "yellow", SELL: "red" } },
|
|
{ id: "kpi_confidence", type: "radial_gauge", title: "Confidence", value: confidenceMap[(data?.metadata?.confidence_level || "MEDIUM").toUpperCase()] || 66 },
|
|
{ id: "kpi_price_change", type: "price_kpi", title: "Current Price", valuePath: "financial_data.current_price", deltaPath: "financial_data.price_change_percent", format: { value: "currency", delta: "percent" } },
|
|
{ id: "kpi_marketcap", type: "kpi_text", title: "Market Cap", dataPath: "financial_data.market_cap" },
|
|
{ id: "kpi_ev", type: "kpi_text", title: "Enterprise Value", dataPath: "financial_data.enterprise_value" },
|
|
{ id: "ownership_donut", type: "donut", title: "Ownership", series: [
|
|
{ label: "Insider %", valuePath: "financial_data.ownership.insider_percent" },
|
|
{ label: "Institutional %", valuePath: "financial_data.ownership.institutional_percent" }
|
|
], valueFormat: "percent" },
|
|
{ id: "range_bar", type: "range_bar", title: "Trading Range", lowPath: "financial_data.trading_range.low", highPath: "financial_data.trading_range.high", openPath: "financial_data.trading_range.open", currentPath: "financial_data.current_price" },
|
|
{ id: "volume_chip", type: "kpi_text", title: "Volume", dataPath: "financial_data.volume", format: { value: "number_compact" } },
|
|
{ id: "rsi_gauge", type: "linear_gauge", title: "RSI", dataPath: "technical_indicators.rsi", min: 0, max: 100, zones: [ { to: 30, color: "blue", label: "Oversold" }, { from: 70, to: 100, color: "red", label: "Overbought" } ] },
|
|
{ id: "macd_mini", type: "macd_snapshot", title: "MACD", macdPath: "technical_indicators.macd", signalPath: "technical_indicators.macd_signal", histogramCompute: "macd - macd_signal" },
|
|
{ id: "ma_trends", type: "badges", title: "MA & Trend", badges: [
|
|
{ label: "SMA 50", valuePath: "technical_indicators.sma_50", trendPath: "technical_indicators.trend_directions.sma_50" },
|
|
{ label: "SMA 200", valuePath: "technical_indicators.sma_200", trendPath: "technical_indicators.trend_directions.sma_200" },
|
|
{ label: "EMA 10", valuePath: "technical_indicators.ema_10", trendPath: "technical_indicators.trend_directions.ema_10" }
|
|
], trendColors: { BULLISH: "green", BEARISH: "red", NEUTRAL: "gray" } },
|
|
{ id: "atr_chip", type: "kpi_text", title: "ATR (Volatility)", dataPath: "technical_indicators.atr" },
|
|
{ id: "strategy_ladder", type: "price_ladder", title: "Entry, Stop, Targets", entryStrategyPath: "investment_strategy.position_sizing.entry_strategy", stopPricePath: "investment_strategy.risk_management.initial_stop_loss", stopPercentPath: "investment_strategy.risk_management.stop_loss_percent", targetsPath: "investment_strategy.profit_targets", targetMapping: { price: "target_price", label: "action" }, currentPricePath: "financial_data.current_price" },
|
|
{ id: "position_sizing", type: "chips", title: "Position Sizing", items: [
|
|
{ label: "Total Allocation", valuePath: "investment_strategy.position_sizing.total_allocation_percent" },
|
|
{ label: "Tranche 1", valuePath: "investment_strategy.position_sizing.tranche_1_percent" },
|
|
{ label: "Tranche 2", valuePath: "investment_strategy.position_sizing.tranche_2_percent" }
|
|
] },
|
|
{ id: "monitoring_points", type: "bullet_list", title: "Monitoring Points", itemsPath: "investment_strategy.monitoring_points" },
|
|
{ id: "analyst_target_vs_price", type: "bar_compare", title: "Analyst Target vs Current", series: [ { label: "Current", valuePath: "financial_data.current_price" }, { label: "Target", valuePath: "financial_data.analyst_data.price_target" } ], yFormat: "currency" },
|
|
{ id: "report_technical", type: "expandable_report", titlePath: "text_content.market_report.title", contentPath: "text_content.market_report.content", bulletsPath: "text_content.market_report.key_takeaways" },
|
|
{ id: "report_sentiment", type: "expandable_report", titlePath: "text_content.sentiment_report.title", contentPath: "text_content.sentiment_report.content", bulletsPath: "text_content.sentiment_report.recent_developments" },
|
|
{ id: "report_fundamentals", type: "expandable_report", titlePath: "text_content.fundamentals_report.title", contentPath: "text_content.fundamentals_report.content", bulletsPath: "text_content.fundamentals_report.financial_highlights" },
|
|
{ id: "report_investment_plan", type: "expandable_report", titlePath: "text_content.investment_plan_full.title", contentPath: "text_content.investment_plan_full.content" },
|
|
{ id: "debate_summary", type: "debate_viewer", bullPath: "debate_summary.bull_key_points", bearPath: "debate_summary.bear_key_points", neutralPath: "debate_summary.neutral_perspective", finalPath: "debate_summary.final_decision_rationale" }
|
|
]
|
|
};
|
|
};
|
|
|
|
// Ensure we have a widgets_config; if missing, build a sensible default
|
|
const transformedWithConfig = {
|
|
...transformedData,
|
|
widgets_config: transformedData?.widgets_config || buildTransformedWidgetsConfig(transformedData)
|
|
};
|
|
|
|
// Minimalist dashboard that shows ALL main sections of transformed JSON
|
|
const MinimalTransformedDashboard: React.FC<{ data: TransformedAnalysisData }> = ({ data }) => {
|
|
const md = (data?.metadata as any) || {};
|
|
const fd = (data?.financial_data as any) || {};
|
|
const ti = (data?.technical_indicators as any) || {};
|
|
const istrat = (data?.investment_strategy as any) || {};
|
|
const ds = React.useMemo(() => ((data?.debate_summary as any) || {}), [data]);
|
|
const txt = React.useMemo(() => ((data?.text_content as any) || {}), [data]);
|
|
|
|
const trends = ti?.trend_directions || {};
|
|
const fmt = (v: any, d = 0) => {
|
|
const n = Number(v);
|
|
if (v === null || v === undefined || Number.isNaN(n)) return '-';
|
|
return Number.isFinite(n) ? n.toFixed(d) : String(v);
|
|
};
|
|
const fmtNumber = (v: any, d = 0) => {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return '-';
|
|
return n.toLocaleString(undefined, { maximumFractionDigits: d, minimumFractionDigits: d });
|
|
};
|
|
const fmtCurrency = (v: any, d = 2) => {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return '-';
|
|
return n.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: d, minimumFractionDigits: d });
|
|
};
|
|
const fmtPercent = (v: any, d = 2) => {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return '-';
|
|
return `${n.toFixed(d)}%`;
|
|
};
|
|
const toneFromNumber = (v: any): 'pos' | 'neg' | 'neutral' => {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return 'neutral';
|
|
if (n > 0) return 'pos';
|
|
if (n < 0) return 'neg';
|
|
return 'neutral';
|
|
};
|
|
const Stat: React.FC<{ label: string; value: React.ReactNode; sub?: React.ReactNode; tone?: 'pos' | 'neg' | 'neutral' }>
|
|
= ({ label, value, sub, tone = 'neutral' }) => {
|
|
const toneCls = tone === 'pos' ? 'text-green-600' : tone === 'neg' ? 'text-red-600' : 'text-gray-900';
|
|
const ringCls = tone === 'pos' ? 'ring-green-200' : tone === 'neg' ? 'ring-red-200' : 'ring-gray-200';
|
|
return (
|
|
<div className={`rounded-lg border border-gray-200 ring-1 ${ringCls} p-4 bg-white shadow-sm`}>
|
|
<p className="text-xs text-gray-500">{label}</p>
|
|
<p className={`text-xl font-semibold ${toneCls}`}>{value}</p>
|
|
{sub && <p className="text-xs text-gray-500 mt-0.5">{sub}</p>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const chip = (label: string, val?: string) => (
|
|
<span className="px-2 py-1 rounded bg-gray-100 text-gray-700 text-xs font-medium">{label}: {val ?? '-'}</span>
|
|
);
|
|
|
|
// Tabs state and memoized content for text-heavy sections
|
|
const debateTabs = React.useMemo(() => ([
|
|
{ key: 'bull', label: 'Bull', content: Array.isArray(ds?.bull_key_points) ? ds.bull_key_points : [] as any[] },
|
|
{ key: 'bear', label: 'Bear', content: Array.isArray(ds?.bear_key_points) ? ds.bear_key_points : [] as any[] },
|
|
{ key: 'neutral', label: 'Neutral', content: ds?.neutral_perspective ? [ds.neutral_perspective] : [] as any[] },
|
|
{ key: 'final', label: 'Final', content: ds?.final_decision_rationale ? [ds.final_decision_rationale] : [] as any[] },
|
|
]), [ds]);
|
|
const firstDebateWithContent = React.useMemo(
|
|
() => debateTabs.find(t => (t.content?.length ?? 0) > 0)?.key || 'bull',
|
|
[debateTabs]
|
|
);
|
|
const [activeDebateTab, setActiveDebateTab] = React.useState<string>(firstDebateWithContent);
|
|
React.useEffect(() => { setActiveDebateTab(firstDebateWithContent); }, [firstDebateWithContent]);
|
|
|
|
const reportTabsAll = React.useMemo(() => ([
|
|
{ key: 'market', title: txt?.market_report?.title || 'Market Report', bullets: txt?.market_report?.key_takeaways, content: txt?.market_report?.content },
|
|
{ key: 'sentiment', title: txt?.sentiment_report?.title || 'Sentiment Report', bullets: txt?.sentiment_report?.recent_developments, content: txt?.sentiment_report?.content },
|
|
{ key: 'fundamentals', title: txt?.fundamentals_report?.title || 'Fundamentals Report', bullets: txt?.fundamentals_report?.financial_highlights, content: txt?.fundamentals_report?.content },
|
|
]), [txt]);
|
|
const availableReports = React.useMemo(
|
|
() => reportTabsAll.filter(t => t.title || (Array.isArray(t.bullets) && t.bullets.length) || t.content),
|
|
[reportTabsAll]
|
|
);
|
|
const [activeReportTab, setActiveReportTab] = React.useState<string>(availableReports[0]?.key || 'market');
|
|
React.useEffect(() => { setActiveReportTab(availableReports[0]?.key || 'market'); }, [availableReports]);
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="mx-auto max-w-6xl space-y-4">
|
|
{/* Metadata - Full width */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold mb-3">Company Information</h2>
|
|
<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="Analysis Date" value={md?.analysis_date ?? '-'} />
|
|
<Stat label="Final Recommendation" value={md?.final_recommendation ?? '-'} />
|
|
<Stat label="Confidence" value={md?.confidence_level ?? '-'} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Two column layout for remaining sections */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{/* Debate Summary with sub-tabs */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold mb-3">Debate Summary</h2>
|
|
<div>
|
|
<div className="flex gap-2 border-b border-gray-200 mb-3">
|
|
{debateTabs.map(t => (
|
|
<button
|
|
key={t.key}
|
|
onClick={() => setActiveDebateTab(t.key)}
|
|
className={`px-3 py-1.5 text-sm rounded-t ${activeDebateTab === t.key ? 'bg-gray-100 text-gray-900' : 'text-gray-500 hover:text-gray-700'}`}
|
|
>{t.label}</button>
|
|
))}
|
|
</div>
|
|
<div className="text-sm text-gray-800 space-y-2 max-h-64 overflow-auto pr-1">
|
|
{debateTabs.find(t => t.key === activeDebateTab)?.content?.length ? (
|
|
<ul className="list-disc list-inside">
|
|
{(debateTabs.find(t => t.key === activeDebateTab)?.content as any[]).map((c, i) => <li key={i}>{c}</li>)}
|
|
</ul>
|
|
) : (
|
|
<p className="text-gray-500">No content.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Text Content with sub-tabs */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold mb-3">Reports</h2>
|
|
{!availableReports.length ? (
|
|
<p className="text-sm text-gray-500">No reports available.</p>
|
|
) : (
|
|
<div>
|
|
<div className="flex gap-2 border-b border-gray-200 mb-3">
|
|
{availableReports.map(t => (
|
|
<button
|
|
key={t.key}
|
|
onClick={() => setActiveReportTab(t.key)}
|
|
className={`px-3 py-1.5 text-sm rounded-t ${activeReportTab === t.key ? 'bg-gray-100 text-gray-900' : 'text-gray-500 hover:text-gray-700'}`}
|
|
>{t.title}</button>
|
|
))}
|
|
</div>
|
|
{(() => {
|
|
const activeTab = availableReports.find(t => t.key === activeReportTab);
|
|
if (!activeTab) return null;
|
|
return (
|
|
<div className="space-y-3 max-h-72 overflow-auto pr-1">
|
|
{Array.isArray(activeTab.bullets) && activeTab.bullets.length > 0 && (
|
|
<ul className="list-disc list-inside text-sm text-gray-700">
|
|
{activeTab.bullets.map((k: string, i: number) => <li key={i}>{k}</li>)}
|
|
</ul>
|
|
)}
|
|
{activeTab.content && (
|
|
<p className="text-sm text-gray-700 whitespace-pre-wrap">{activeTab.content}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Financial Data */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold mb-3">Financial Data</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<Stat label="Current Price" value={fmtCurrency(fd?.current_price)} />
|
|
<Stat label="Target Price" value={fmtCurrency(fd?.analyst_data?.price_target ?? fd?.target_price)} />
|
|
<Stat label="Price Change" value={fmtPercent(fd?.price_change)} tone={toneFromNumber(fd?.price_change)} />
|
|
<Stat label="Price Change %" value={fmtPercent(fd?.price_change_percent)} tone={toneFromNumber(fd?.price_change_percent)} />
|
|
<Stat label="Market Cap" value={fd?.market_cap ?? '-'} />
|
|
<Stat label="Enterprise Value" value={fd?.enterprise_value ?? '-'} />
|
|
<Stat label="Shares Outstanding" value={fd?.shares_outstanding ?? '-'} />
|
|
<Stat label="Volume" value={fmtNumber(fd?.volume, 0)} />
|
|
<Stat label="P/E Ratio" value={fmtNumber(fd?.pe_ratio ?? fd?.valuation_ratios?.forward_pe, 2)} />
|
|
<Stat label="P/S Ratio" value={fmtNumber(fd?.valuation_ratios?.current_ps_ratio, 2)} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Technical Indicators */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold mb-3">Technical Indicators</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<Stat label="RSI" value={fmtNumber(ti?.rsi, 2)} tone={toneFromNumber((Number(ti?.rsi) - 50))} />
|
|
<Stat label="MACD" value={fmtNumber(ti?.macd, 3)} />
|
|
<Stat label="Signal" value={fmtNumber(ti?.macd_signal, 3)} />
|
|
<Stat label="SMA 50" value={fmtCurrency((ti?.sma_50 ?? ti?.moving_avg_50), 2)} />
|
|
<Stat label="SMA 200" value={fmtCurrency((ti?.sma_200 ?? ti?.moving_avg_200), 2)} />
|
|
<Stat label="MA 20" value={fmtCurrency(ti?.moving_avg_20, 2)} />
|
|
<Stat label="EMA 10" value={fmtCurrency(ti?.ema_10, 2)} />
|
|
<Stat label="ATR" value={fmtNumber(ti?.atr, 3)} />
|
|
<Stat label="Bollinger Upper" value={fmtCurrency(ti?.bollinger_upper, 2)} />
|
|
<Stat label="Bollinger Lower" value={fmtCurrency(ti?.bollinger_lower, 2)} />
|
|
<Stat label="Support" value={fmtCurrency(ti?.support_level, 2)} />
|
|
<Stat label="Resistance" value={fmtCurrency(ti?.resistance_level, 2)} />
|
|
</div>
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
{chip('Trend SMA 50', trends?.sma_50)}
|
|
{chip('Trend SMA 200', trends?.sma_200)}
|
|
{trends?.ema_10 !== undefined && chip('Trend EMA 10', trends?.ema_10)}
|
|
{trends?.price_action !== undefined && chip('Price', trends?.price_action)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Investment Strategy */}
|
|
<div className="bg-white rounded-lg shadow p-6 lg:col-span-2">
|
|
<h2 className="text-lg font-semibold mb-3">Investment Strategy</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<Stat label="Risk Level (SL%)" value={fmtPercent(istrat?.risk_management?.stop_loss_percent ?? md?.risk_level)} />
|
|
<Stat label="Initial Stop Loss" value={fmtCurrency(istrat?.risk_management?.initial_stop_loss)} />
|
|
<Stat label="Profit Targets" value={istrat.profit_targets.map((t: any, idx: number) => (
|
|
<li key={idx} className="flex gap-4">
|
|
<span className="font-semibold">{fmtCurrency(t?.target_price)}</span>
|
|
{t?.action && <span className="text-xs text-gray-500">{t.action}</span>}
|
|
{t?.rationale && <span className="text-xs text-gray-500">{t.rationale}</span>}
|
|
<hr />
|
|
</li>
|
|
))} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Render minimalist dashboard with all keys from the transformed JSON
|
|
return <MinimalTransformedDashboard data={transformedWithConfig} />;
|
|
};
|
|
|
|
export default TransformedDataAdapter;
|
|
export type { TransformedAnalysisData, LegacyTradingAgentsResult };
|