This commit is contained in:
hemangjoshi37a 2026-03-05 16:32:02 +11:00
parent 2418f11142
commit 791e3c0ec8
69 changed files with 209 additions and 41 deletions

BIN
asianpaint-detail-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

BIN
asianpaint-final-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

BIN
audit-dashboard-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
audit-dashboard-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

BIN
audit-history-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

BIN
audit-howItWorks-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

BIN
audit-howItWorks-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

BIN
audit-stock-datasources.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
audit-stock-debates.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

BIN
audit-stock-detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

BIN
audit-stock-pipeline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
audit2-dashboard-fixed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

BIN
audit2-dashboard-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

BIN
audit2-history-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

BIN
audit2-howitworks-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
audit2-stockdetail-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

BIN
audit3-dashboard-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

BIN
audit3-dashboard-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
audit3-history-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

BIN
audit3-mobile-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

BIN
dashboard-dark-audit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

BIN
dashboard-dark-bottom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
dashboard-dark-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
dashboard-dark-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
dashboard-full-audit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

BIN
dashboard-improved-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

BIN
dashboard-light-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

BIN
desktop-dark-grid-final.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@ -8,6 +8,118 @@ from typing import Optional
DB_PATH = Path(__file__).parent / "recommendations.db"
NIFTY_50_NAMES = {
'RELIANCE': 'Reliance Industries Ltd',
'TCS': 'Tata Consultancy Services Ltd',
'HDFCBANK': 'HDFC Bank Ltd',
'INFY': 'Infosys Ltd',
'ICICIBANK': 'ICICI Bank Ltd',
'HINDUNILVR': 'Hindustan Unilever Ltd',
'ITC': 'ITC Ltd',
'SBIN': 'State Bank of India',
'BHARTIARTL': 'Bharti Airtel Ltd',
'KOTAKBANK': 'Kotak Mahindra Bank Ltd',
'LT': 'Larsen & Toubro Ltd',
'AXISBANK': 'Axis Bank Ltd',
'ASIANPAINT': 'Asian Paints Ltd',
'MARUTI': 'Maruti Suzuki India Ltd',
'HCLTECH': 'HCL Technologies Ltd',
'SUNPHARMA': 'Sun Pharmaceutical Industries Ltd',
'TITAN': 'Titan Company Ltd',
'BAJFINANCE': 'Bajaj Finance Ltd',
'WIPRO': 'Wipro Ltd',
'ULTRACEMCO': 'UltraTech Cement Ltd',
'NESTLEIND': 'Nestle India Ltd',
'NTPC': 'NTPC Ltd',
'POWERGRID': 'Power Grid Corporation of India Ltd',
'M&M': 'Mahindra & Mahindra Ltd',
'TATAMOTORS': 'Tata Motors Ltd',
'ONGC': 'Oil & Natural Gas Corporation Ltd',
'JSWSTEEL': 'JSW Steel Ltd',
'TATASTEEL': 'Tata Steel Ltd',
'ADANIENT': 'Adani Enterprises Ltd',
'ADANIPORTS': 'Adani Ports and SEZ Ltd',
'COALINDIA': 'Coal India Ltd',
'BAJAJFINSV': 'Bajaj Finserv Ltd',
'TECHM': 'Tech Mahindra Ltd',
'HDFCLIFE': 'HDFC Life Insurance Company Ltd',
'SBILIFE': 'SBI Life Insurance Company Ltd',
'GRASIM': 'Grasim Industries Ltd',
'DIVISLAB': "Divi's Laboratories Ltd",
'DRREDDY': "Dr. Reddy's Laboratories Ltd",
'CIPLA': 'Cipla Ltd',
'BRITANNIA': 'Britannia Industries Ltd',
'EICHERMOT': 'Eicher Motors Ltd',
'APOLLOHOSP': 'Apollo Hospitals Enterprise Ltd',
'INDUSINDBK': 'IndusInd Bank Ltd',
'HEROMOTOCO': 'Hero MotoCorp Ltd',
'TATACONSUM': 'Tata Consumer Products Ltd',
'BPCL': 'Bharat Petroleum Corporation Ltd',
'UPL': 'UPL Ltd',
'HINDALCO': 'Hindalco Industries Ltd',
'BAJAJ-AUTO': 'Bajaj Auto Ltd',
'LTIM': 'LTIMindtree Ltd',
}
def _truncate_at_word(text: str, max_len: int) -> str:
"""Truncate text at a word boundary to avoid mid-word cuts."""
if len(text) <= max_len:
return text
truncated = text[:max_len]
last_space = truncated.rfind(' ')
if last_space > max_len * 0.6:
return truncated[:last_space] + '...'
return truncated + '...'
def _extract_reason(raw_analysis: str) -> str:
"""Extract clean reason text from raw LLM analysis output.
Strips markdown bold markers and skips the metadata prefix
(FINAL DECISION, HOLD_DAYS, CONFIDENCE, RISK_LEVEL) to return
the actual analytical content, truncated at word boundaries.
"""
if not raw_analysis:
return ''
text = raw_analysis.replace('**', '').strip()
# Look for "RISK ASSESSMENT:" or "RISK_ASSESSMENT:" and return text after it
for marker in ('RISK ASSESSMENT:', 'RISK_ASSESSMENT:'):
idx = text.upper().find(marker.upper())
if idx != -1:
after = text[idx + len(marker):].strip()
if after:
return _truncate_at_word(after, 500)
# Look for the last substantive paragraph after metadata lines
lines = text.split('\n')
content_lines = []
past_metadata = False
for line in lines:
stripped = line.strip()
if not stripped:
continue
upper_line = stripped.upper()
is_metadata = (
upper_line.startswith('FINAL DECISION:')
or upper_line.startswith('HOLD_DAYS:')
or upper_line.startswith('CONFIDENCE:')
or upper_line.startswith('RISK_LEVEL:')
)
if is_metadata:
past_metadata = True
continue
if past_metadata or not is_metadata:
content_lines.append(stripped)
if content_lines:
return _truncate_at_word(' '.join(content_lines), 500)
# Fallback: return original text truncated
return _truncate_at_word(text, 500)
def sanitize_decision(raw: str) -> str:
"""Extract BUY/SELL/HOLD from potentially noisy LLM output.
@ -1470,18 +1582,18 @@ def update_daily_recommendation_summary(date: str):
buy_count += 1
buy_stocks.append({
'symbol': row['symbol'],
'company_name': row['company_name'] or row['symbol'],
'company_name': NIFTY_50_NAMES.get(row['symbol'], row['company_name'] or row['symbol']),
'confidence': row['confidence'] or 'MEDIUM',
'reason': (row['raw_analysis'] or '')[:200],
'reason': _extract_reason(row['raw_analysis'] or ''),
'rank': row['rank']
})
elif decision == 'SELL':
sell_count += 1
sell_stocks.append({
'symbol': row['symbol'],
'company_name': row['company_name'] or row['symbol'],
'company_name': NIFTY_50_NAMES.get(row['symbol'], row['company_name'] or row['symbol']),
'confidence': row['confidence'] or 'MEDIUM',
'reason': (row['raw_analysis'] or '')[:200],
'reason': _extract_reason(row['raw_analysis'] or ''),
'rank': row['rank']
})
else:

Binary file not shown.

View File

@ -1,6 +1,13 @@
import { Link } from 'react-router-dom';
import { TrendingUp, TrendingDown, Minus, ChevronRight, Clock } from 'lucide-react';
import type { StockAnalysis, Decision } from '../types';
import { NIFTY_50_STOCKS } from '../types';
/** Resolve the display name for a stock, falling back to NIFTY_50_STOCKS when the backend stored the ticker as company_name */
function getCompanyName(symbol: string, providedName?: string): string {
if (providedName && providedName !== symbol) return providedName;
return NIFTY_50_STOCKS.find(s => s.symbol === symbol)?.company_name || symbol;
}
interface StockCardProps {
stock: StockAnalysis;
@ -151,7 +158,7 @@ export default function StockCard({ stock, showDetails = true, compact = false }
}`} aria-hidden="true" />
<span className="font-semibold text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="text-gray-300 dark:text-gray-600 text-xs hidden sm:inline" aria-hidden="true">&middot;</span>
<span className="text-xs text-gray-500 dark:text-gray-400 truncate hidden sm:inline">{stock.company_name}</span>
<span className="text-xs text-gray-500 dark:text-gray-400 truncate hidden sm:inline">{getCompanyName(stock.symbol, stock.company_name)}</span>
</div>
<div className="flex items-center gap-2">
<DecisionBadge decision={stock.decision} />
@ -172,7 +179,7 @@ export default function StockCard({ stock, showDetails = true, compact = false }
<h3 className="font-semibold text-gray-900 dark:text-gray-100 truncate text-sm">{stock.symbol}</h3>
<DecisionBadge decision={stock.decision} />
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{stock.company_name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{getCompanyName(stock.symbol, stock.company_name)}</p>
{showDetails && (
<div className="flex items-center gap-2 mt-1.5">
<ConfidenceBadge confidence={stock.confidence} />
@ -201,7 +208,7 @@ export function StockCardCompact({ stock }: { stock: StockAnalysis }) {
<div>
<span className="font-semibold text-gray-900 dark:text-gray-100">{stock.symbol}</span>
<span className="text-gray-300 dark:text-gray-600 mx-2">&middot;</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{stock.company_name}</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{getCompanyName(stock.symbol, stock.company_name)}</span>
</div>
</div>
<DecisionBadge decision={stock.decision} />

View File

@ -3,6 +3,21 @@ import { Trophy, AlertTriangle, TrendingUp, TrendingDown, ChevronRight } from 'l
import type { TopPick, StockToAvoid } from '../types';
import { RankBadge } from './StockCard';
/** Extract just the risk assessment reasoning from raw LLM output */
function cleanReason(reason?: string): string {
if (!reason) return '';
let text = reason.replace(/\*\*/g, '').replace(/\*/g, '');
// Try to extract text after "RISK ASSESSMENT:" or "RISK_ASSESSMENT:"
const riskMatch = text.match(/RISK[_ ]ASSESSMENT:\s*([\s\S]*)/i);
if (riskMatch) return riskMatch[1].trim();
// Fallback: strip known metadata prefixes
text = text.replace(/FINAL DECISION:\s*\w+\s*/i, '');
text = text.replace(/HOLD_DAYS:\s*\S+\s*/i, '');
text = text.replace(/CONFIDENCE:\s*\w+\s*/i, '');
text = text.replace(/RISK_LEVEL:\s*\w+\s*/i, '');
return text.trim();
}
interface TopPicksProps {
picks: TopPick[];
}
@ -44,7 +59,7 @@ export default function TopPicks({ picks }: TopPicksProps) {
BUY
</span>
</div>
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{pick.reason?.replace(/\*\*/g, '').replace(/\*/g, '')}</p>
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{cleanReason(pick.reason)}</p>
<div className="flex items-center justify-between">
<span className={`text-[11px] px-2 py-0.5 rounded-md font-medium border ${
pick.risk_level === 'LOW' ? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 border-emerald-200/50 dark:border-emerald-800/30' :
@ -81,7 +96,7 @@ export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{stocks.map((stock) => {
return (
<Link
@ -91,14 +106,17 @@ export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
style={{ background: 'linear-gradient(135deg, rgba(239,68,68,0.04), rgba(220,38,38,0.01))' }}
>
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center justify-between mb-1">
<span className="font-bold text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold text-white" style={{ background: 'linear-gradient(135deg, #ef4444, #dc2626)' }}>
<TrendingDown className="w-3 h-3" />
SELL
</span>
</div>
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{stock.reason?.replace(/\*\*/g, '').replace(/\*/g, '')}</p>
{stock.company_name && stock.company_name !== stock.symbol && (
<p className="text-[10px] text-gray-400 dark:text-gray-500 mb-1.5 truncate">{stock.company_name}</p>
)}
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{cleanReason(stock.reason)}</p>
<ChevronRight className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 group-hover:text-red-600 dark:group-hover:text-red-400 transition-colors" />
</div>
</Link>

View File

@ -264,7 +264,7 @@ export default function Dashboard() {
// Normal mode: only show analyzed stocks
return (recommendation ? Object.values(recommendation.analysis) : []).map(s => ({
symbol: s.symbol,
company_name: s.company_name,
company_name: NIFTY_50_STOCKS.find(n => n.symbol === s.symbol)?.company_name || s.company_name,
liveState: 'completed' as StockLiveState,
analysis: s,
}));
@ -538,8 +538,8 @@ export default function Dashboard() {
{/* How It Works Section */}
<HowItWorks collapsed={true} />
{/* Top Picks and Avoid Section - Side by Side Compact */}
<div className="grid lg:grid-cols-2 gap-4">
{/* Top Picks and Avoid Section - Side by Side when both present */}
<div className={`grid gap-4 ${recommendation.top_picks.length > 0 ? 'lg:grid-cols-2' : ''}`}>
{recommendation.top_picks.length > 0 && (
<TopPicks picks={recommendation.top_picks} />
)}
@ -561,10 +561,10 @@ export default function Dashboard() {
<button
onClick={() => setFilter('ALL')}
aria-pressed={filter === 'ALL'}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
filter === 'ALL'
? 'bg-nifty-600 text-white shadow-sm'
: 'bg-gray-100 dark:bg-slate-600 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-slate-500'
: 'bg-gray-100 dark:bg-slate-600 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-slate-500 active:scale-95'
}`}
>
All ({total})
@ -572,10 +572,10 @@ export default function Dashboard() {
<button
onClick={() => setFilter('BUY')}
aria-pressed={filter === 'BUY'}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
filter === 'BUY'
? 'bg-green-600 text-white shadow-sm'
: 'bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900/50'
: 'bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900/50 active:scale-95'
}`}
>
Buy ({buy})
@ -583,10 +583,10 @@ export default function Dashboard() {
<button
onClick={() => setFilter('HOLD')}
aria-pressed={filter === 'HOLD'}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
filter === 'HOLD'
? 'bg-amber-600 text-white shadow-sm'
: 'bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/50'
: 'bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/50 active:scale-95'
}`}
>
Hold ({hold})
@ -594,10 +594,10 @@ export default function Dashboard() {
<button
onClick={() => setFilter('SELL')}
aria-pressed={filter === 'SELL'}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
filter === 'SELL'
? 'bg-red-600 text-white shadow-sm'
: 'bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/50'
: 'bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/50 active:scale-95'
}`}
>
Sell ({sell})
@ -605,7 +605,7 @@ export default function Dashboard() {
<span className="mx-0.5 text-gray-300 dark:text-gray-600">|</span>
<button
onClick={() => setSortBy(sortBy === 'rank' ? 'symbol' : 'rank')}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 active:scale-95 ${
sortBy === 'rank'
? 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400'
: 'bg-gray-100 dark:bg-slate-600 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-slate-500'
@ -623,7 +623,7 @@ export default function Dashboard() {
placeholder="Search by symbol or company name..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-9 py-2 text-sm rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:border-transparent"
className="w-full pl-9 pr-9 py-2 text-sm rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:border-nifty-400 dark:focus:border-nifty-500 transition-all duration-200"
/>
{searchQuery && (
<button
@ -637,7 +637,7 @@ export default function Dashboard() {
</div>
</div>
<div className="p-2 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2 max-h-[400px] overflow-y-auto" role="list" aria-label="Stock recommendations list">
<div className="p-2 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 max-h-[400px] overflow-y-auto" role="list" aria-label="Stock recommendations list">
{filteredItems.map((item) => {
// COMPLETED with analysis data: clickable link
if (item.liveState === 'completed' && item.analysis) {
@ -645,14 +645,14 @@ export default function Dashboard() {
<Link
key={item.symbol}
to={`/stock/${item.symbol}`}
className="card-hover p-2 group relative overflow-hidden"
className="card-hover p-2 group relative overflow-hidden transition-all duration-200 hover:shadow-lg hover:border-nifty-300 dark:hover:border-nifty-600 hover:scale-[1.02]"
role="listitem"
>
<div className="relative z-10">
<div className="flex items-center gap-1.5 mb-0.5">
<div className="flex flex-wrap items-center gap-1 sm:gap-1.5 mb-0.5 min-w-0">
<RankBadge rank={item.analysis.rank} size="small" />
<span className="font-semibold text-sm text-gray-900 dark:text-gray-100">{item.symbol}</span>
<DecisionBadge decision={item.analysis.decision} size="small" />
<span className="flex-shrink-0"><DecisionBadge decision={item.analysis.decision} size="small" /></span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{item.company_name}</p>
{item.analysis.hold_days != null && item.analysis.hold_days > 0 && item.analysis.decision !== 'SELL' && (
@ -755,7 +755,7 @@ export default function Dashboard() {
{/* Compact CTA */}
<Link
to="/history"
className="flex items-center justify-between p-4 rounded-xl text-white group focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-2 transition-all hover:shadow-lg"
className="flex items-center justify-between p-4 rounded-xl text-white group focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-2 transition-all duration-200 hover:shadow-lg hover:scale-[1.02]"
style={{
background: 'linear-gradient(135deg, #0284c7, #0369a1)',
boxShadow: '0 2px 8px rgba(2, 132, 199, 0.25)',

View File

@ -2,6 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { Link } from 'react-router-dom';
import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, ChevronDown, BarChart3, Target, HelpCircle, Activity, Calculator, LineChart, PieChart, Shield, Filter, Clock, Zap, Award, ArrowUpRight, ArrowDownRight, Play, Loader2, FileText, MessageSquare, Search, XCircle, AlertTriangle } from 'lucide-react';
import type { ReturnBreakdown } from '../types';
import { NIFTY_50_STOCKS } from '../types';
import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard';
import Sparkline from '../components/Sparkline';
import AccuracyExplainModal from '../components/AccuracyExplainModal';
@ -1390,7 +1391,7 @@ export default function History() {
<div className="flex items-center gap-3 flex-1 min-w-0">
<RankBadge rank={stock.rank} size="small" />
<span className="font-semibold text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="text-xs text-gray-500 dark:text-gray-400 hidden sm:inline truncate">{stock.company_name}</span>
<span className="text-xs text-gray-500 dark:text-gray-400 hidden sm:inline truncate">{(stock.company_name && stock.company_name !== stock.symbol) ? stock.company_name : (NIFTY_50_STOCKS.find(n => n.symbol === stock.symbol)?.company_name || stock.company_name)}</span>
</div>
<div className="flex items-center gap-3">
<DecisionBadge decision={stock.decision} size="small" />

View File

@ -20,6 +20,21 @@ import { api } from '../services/api';
import { useSettings } from '../contexts/SettingsContext';
import type { FullPipelineData, PipelineStep, PipelineStepStatus } from '../types/pipeline';
/** Extract just the risk assessment reasoning from raw LLM output */
function cleanReason(reason?: string): string {
if (!reason) return '';
let text = reason.replace(/\*\*/g, '').replace(/\*/g, '');
// Try to extract text after "RISK ASSESSMENT:" or "RISK_ASSESSMENT:"
const riskMatch = text.match(/RISK[_ ]ASSESSMENT:\s*([\s\S]*)/i);
if (riskMatch) return riskMatch[1].trim();
// Fallback: strip known metadata prefixes
text = text.replace(/FINAL DECISION:\s*\w+\s*/i, '');
text = text.replace(/HOLD_DAYS:\s*\S+\s*/i, '');
text = text.replace(/CONFIDENCE:\s*\w+\s*/i, '');
text = text.replace(/RISK_LEVEL:\s*\w+\s*/i, '');
return text.trim();
}
// Type for real backtest data from API
interface BacktestResult {
date: string;
@ -940,17 +955,31 @@ export default function StockDetail() {
</div>
) : backtestResults.length > 0 ? (
<div className="divide-y divide-gray-100 dark:divide-slate-700 max-h-[320px] overflow-y-auto">
{backtestResults.map((entry, idx) => (
<div key={idx} className="px-3 py-2.5 flex items-center gap-3 hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
{backtestResults.map((entry, idx) => {
const isWeekend = [0, 6].includes(new Date(entry.date).getDay());
return (
<div key={idx} className={`px-3 py-2.5 flex items-center gap-3 transition-colors ${
isWeekend
? 'bg-gray-50/50 dark:bg-slate-800/30 opacity-60 hover:opacity-80'
: 'hover:bg-gray-50 dark:hover:bg-slate-700/50'
}`}>
{/* Date */}
<div className="w-16 flex-shrink-0">
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">
<div className={`text-xs font-medium ${
[0, 6].includes(new Date(entry.date).getDay())
? 'text-gray-400 dark:text-gray-500'
: 'text-gray-700 dark:text-gray-300'
}`}>
{new Date(entry.date).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
})}
</div>
<div className="text-[10px] text-gray-400 dark:text-gray-500">
<div className={`text-[10px] ${
[0, 6].includes(new Date(entry.date).getDay())
? 'text-amber-500 dark:text-amber-400'
: 'text-gray-400 dark:text-gray-500'
}`}>
{new Date(entry.date).toLocaleDateString('en-IN', { weekday: 'short' })}
</div>
</div>
@ -1005,7 +1034,8 @@ export default function StockDetail() {
</div>
)}
</div>
))}
);
})}
</div>
) : history.length > 0 ? (
<div className="p-8 text-center">
@ -1077,10 +1107,10 @@ export default function StockDetail() {
<section className="card p-3 bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
<div className="flex items-center gap-3">
<TrendingUp className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0" />
<div>
<div className="min-w-0">
<span className="font-semibold text-green-800 dark:text-green-300 text-sm">Top Pick: </span>
<span className="text-sm text-green-700 dark:text-green-400">
{latestRecommendation.top_picks.find(p => p.symbol === symbol)?.reason?.replace(/\*\*/g, '').replace(/\*/g, '')}
<span className="text-sm text-green-700 dark:text-green-400 line-clamp-3">
{cleanReason(latestRecommendation.top_picks.find(p => p.symbol === symbol)?.reason)}
</span>
</div>
</div>
@ -1091,10 +1121,10 @@ export default function StockDetail() {
<section className="card p-3 bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800">
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" />
<div>
<div className="min-w-0">
<span className="font-semibold text-red-800 dark:text-red-300 text-sm">Avoid: </span>
<span className="text-sm text-red-700 dark:text-red-400">
{latestRecommendation.stocks_to_avoid.find(s => s.symbol === symbol)?.reason?.replace(/\*\*/g, '').replace(/\*/g, '')}
<span className="text-sm text-red-700 dark:text-red-400 line-clamp-3">
{cleanReason(latestRecommendation.stocks_to_avoid.find(s => s.symbol === symbol)?.reason)}
</span>
</div>
</div>

BIN
github-final-check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
github-readme-check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
github-readme-footer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
github-readme-header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
github-readme-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
history-audit-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

BIN
history-light-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
history-page-audit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

BIN
history-test-1-initial.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

BIN
history-test-dark-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
how-it-works-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

BIN
how-it-works-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

BIN
mobile-dark-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
mobile-dark-grid-fixed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
mobile-dark-grid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
stock-detail-dark-audit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
verify-dashboard-fixed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB