Commit 7d1ef9de authored by MD. SHAHIDUL ISLAM's avatar MD. SHAHIDUL ISLAM

feat: replace static report sections with an interactive, expandable AI company analysis component

parent d7ecdbd0
......@@ -66,6 +66,11 @@ DSE_PASSWORD=
#TRADINGAGENTS_DEEP_THINK_LLM=google/gemini-3.1-pro-preview
#TRADINGAGENTS_QUICK_THINK_LLM=google/gemini-3.5-flash
#TRADINGAGENTS_LLM_BACKEND_URL=
# Optional dedicated provider for the structured ten-question company analysis.
# Direct OpenAI example (requires OPENAI_API_KEY above):
#TRADINGAGENTS_RESEARCH_LLM_PROVIDER=openai
#TRADINGAGENTS_RESEARCH_LLM_MODEL=gpt-5.4
#TRADINGAGENTS_RESEARCH_LLM_BACKEND_URL=
#TRADINGAGENTS_OUTPUT_LANGUAGE=English # Supported: English or Bangla
#TRADINGAGENTS_MAX_DEBATE_ROUNDS=1
#TRADINGAGENTS_MAX_RISK_ROUNDS=1
......
......@@ -358,6 +358,27 @@ the research, and produces the trader view. The first UI launch may still need
time to install/build the Angular frontend. Use `--no-open-ui` to generate and
save the AI payload without starting the server, or `--no-ai` for the original
calculation-only fallback.
The score screen places a collapsible company analysis immediately below the
score. Its structured prompt always follows the same ten questions: company,
revenue model, profitability, financial safety, valuation method, shareholder
rewards, competitive edge, upside case, risks, and investor suitability. The
model receives the date-bounded company profile, annual/quarterly results,
balance sheet, ownership, dividends, cash flow, price history, calculated
valuation anchors, and any completed agent reports. Unexpected evidence keys
are not forwarded.
To send this one structured research call directly to OpenAI while leaving the
multi-agent graph on another provider, set:
```dotenv
OPENAI_API_KEY=your-key
TRADINGAGENTS_RESEARCH_LLM_PROVIDER=openai
TRADINGAGENTS_RESEARCH_LLM_MODEL=gpt-5.4
```
If the dedicated research settings are omitted, the dashboard uses the main
`TRADINGAGENTS_LLM_PROVIDER` and `TRADINGAGENTS_DEEP_THINK_LLM` values.
The equivalent source-tree command is
`python -m dohasecuritiesstockai.dashboard_cli GP --date 2026-08-10`.
......
......@@ -43,8 +43,8 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "30kb",
"maximumError": "40kb"
"maximumWarning": "45kb",
"maximumError": "55kb"
}
]
},
......
......@@ -120,6 +120,72 @@
</aside>
</section>
@if (report.ai_research; as ai) {
<details id="company-analysis" class="company-analysis section-anchor">
<summary>
<span class="analysis-summary-icon">AI</span>
<span class="analysis-summary-copy">
<small>{{ language() === 'bn' ? 'স্কোরের পেছনের সম্পূর্ণ বিশ্লেষণ' : 'THE FULL ANALYSIS BEHIND THE SCORE' }}</small>
<strong>{{ language() === 'bn' ? 'কোম্পানির ১০-ধাপের বিশ্লেষণ দেখুন' : 'Read the 10-question company analysis' }}</strong>
<span>{{ copy(report.headline) }}</span>
</span>
<span class="analysis-summary-action">
{{ language() === 'bn' ? 'বিশ্লেষণ খুলুন' : 'Open analysis' }}
<i aria-hidden="true"></i>
</span>
</summary>
<div class="company-analysis-body">
<header class="analysis-introduction">
<div>
<p class="eyebrow">AI COMPANY ANALYSIS · {{ ai.provider }} / {{ ai.model }}</p>
<h2>{{ copy(report.in_depth_title) }}</h2>
<p>{{ copy(report.in_depth_snippet) }}</p>
</div>
<span>Data as of {{ report.analysis_date | date: 'mediumDate' }}</span>
</header>
<div class="analysis-chapters">
@for (section of report.report_sections; track section.key; let index = $index) {
<article class="analysis-chapter" [class.bull-chapter]="section.key === 'bull_case'" [class.risk-chapter]="section.key === 'risks'">
<span class="chapter-number">{{ (index + 1).toString().padStart(2, '0') }}</span>
<div class="chapter-content">
<h3>{{ copy(section.title) }}</h3>
<p class="chapter-answer">{{ copy(section.summary) }}</p>
@for (paragraph of section.body; track $index) {
<p class="chapter-paragraph">{{ copy(paragraph) }}</p>
}
@if (section.key === 'valuation') {
<div class="analysis-value-callout">
<div><small>VALUE TODAY</small><strong>{{ copy(report.valuation.verdict_label) }}</strong></div>
<div><small>CURRENT</small><strong>{{ formatTaka(report.valuation.current_price) }}</strong></div>
<div><small>ROUGH ESTIMATE</small><strong>{{ formatTaka(report.valuation.rough_estimate) }}</strong></div>
<div><small>FAIR RANGE</small><strong>{{ formatTaka(report.valuation.fair_range_low) }}–{{ formatTaka(report.valuation.fair_range_high) }}</strong></div>
</div>
}
@if (section.bullets.length) {
<ul class="chapter-bullets">
@for (bullet of section.bullets; track bullet.en) {
<li>{{ copy(bullet) }}</li>
}
</ul>
}
</div>
</article>
}
</div>
<footer class="analysis-method-note">
<span></span>
<p><strong>Grounded structured analysis.</strong> Generated from date-bounded DSE evidence and validated against a fixed ten-question format. {{ copy(report.disclaimer) }}</p>
</footer>
</div>
</details>
}
<section class="market-ribbon" aria-label="Market snapshot">
<div class="price-cell">
<span>{{ language() === 'bn' ? 'সর্বশেষ মূল্য' : 'Latest price' }}</span>
......@@ -151,10 +217,11 @@
</section>
<nav class="section-nav" aria-label="Report sections">
<button type="button" (click)="jumpTo('decision-summary')">01 · Decision</button>
<button type="button" (click)="jumpTo('valuation')">02 · Valuation</button>
<button type="button" (click)="jumpTo('factor-scorecard')">03 · Factors</button>
<button type="button" (click)="jumpTo('research-dossier')">04 · Full dossier</button>
@if (report.ai_research) { <button type="button" (click)="openCompanyAnalysis()">01 · Analysis</button> }
<button type="button" (click)="jumpTo('decision-summary')">02 · Decision</button>
<button type="button" (click)="jumpTo('valuation')">03 · Valuation</button>
<button type="button" (click)="jumpTo('factor-scorecard')">04 · Factors</button>
<button type="button" (click)="jumpTo('research-dossier')">05 · Full dossier</button>
<span>{{ reportDocumentCount() }} agent documents</span>
</nav>
......@@ -327,23 +394,6 @@
</div>
</section>
<section class="summary-reports">
<header class="section-heading compact">
<div><p class="eyebrow">DETERMINISTIC SUMMARY</p><h2>{{ copy(report.in_depth_title) }}</h2></div>
</header>
<div class="summary-report-grid">
@for (section of report.report_sections; track section.key) {
<article [class]="getSectionStatus(section.key)">
<header><span>{{ getSectionStatus(section.key) === 'positive' ? '✓' : getSectionStatus(section.key) === 'negative' ? '×' : getSectionStatus(section.key) === 'caution' ? '!' : '•' }}</span><h3>{{ copy(section.title) }}</h3></header>
<p>{{ copy(section.summary) }}</p>
@if (section.bullets.length) {
<ul>@for (bullet of section.bullets; track bullet.en) { <li>{{ copy(bullet) }}</li> }</ul>
}
</article>
}
</div>
</section>
<section id="research-dossier" class="dossier-section section-anchor">
<header class="dossier-heading">
<div>
......
......@@ -247,6 +247,69 @@
.score-copy strong { margin-top: 0.28rem; font-size: 1.1rem; }
.score-copy small { margin-top: 0.25rem; color: var(--muted); font-size: 0.68rem; }
.company-analysis {
max-width: 1500px;
margin: 1rem auto 0;
overflow: hidden;
border: 1px solid var(--line-strong);
border-radius: 1rem;
background: var(--surface);
box-shadow: var(--shadow-sm);
}
.company-analysis > summary {
min-height: 7rem;
padding: 1.25rem 1.5rem;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 1rem;
cursor: pointer;
list-style: none;
background: linear-gradient(105deg, #f4f8f4 0%, #fff 58%, #f7faf8 100%);
}
.company-analysis > summary::-webkit-details-marker { display: none; }
.company-analysis > summary:hover { background: linear-gradient(105deg, #edf6f0 0%, #fff 58%, #f1f7f3 100%); }
.analysis-summary-icon { width: 3.1rem; height: 3.1rem; display: grid; place-items: center; border-radius: .8rem; color: white; background: var(--green); font-family: 'Plus Jakarta Sans', sans-serif; font-size: .72rem; font-weight: 850; letter-spacing: .06em; }
.analysis-summary-copy { min-width: 0; display: flex; flex-direction: column; }
.analysis-summary-copy small { color: var(--green); font-size: .55rem; font-weight: 850; letter-spacing: .12em; }
.analysis-summary-copy strong { margin-top: .2rem; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 1.05rem; }
.analysis-summary-copy > span { margin-top: .25rem; overflow: hidden; color: var(--muted); font-size: .7rem; text-overflow: ellipsis; white-space: nowrap; }
.analysis-summary-action { display: flex; align-items: center; gap: .75rem; color: var(--ink-soft); font-size: .67rem; font-weight: 800; text-transform: uppercase; }
.analysis-summary-action i { position: relative; width: 1.9rem; height: 1.9rem; border: 1px solid var(--line-strong); border-radius: 50%; background: white; }
.analysis-summary-action i::before,
.analysis-summary-action i::after { content: ''; position: absolute; top: 50%; left: 50%; width: .65rem; height: 1px; background: var(--ink); transform: translate(-50%, -50%); transition: transform 160ms ease; }
.analysis-summary-action i::after { transform: translate(-50%, -50%) rotate(90deg); }
.company-analysis[open] .analysis-summary-action i::after { transform: translate(-50%, -50%) rotate(0); }
.company-analysis-body { padding: clamp(1.4rem, 4vw, 3.6rem); border-top: 1px solid var(--line); }
.analysis-introduction { padding-bottom: 2.2rem; display: flex; align-items: flex-start; justify-content: space-between; gap: 2rem; border-bottom: 1px solid var(--line-strong); }
.analysis-introduction h2 { max-width: 920px; margin: 0; font-family: 'Plus Jakarta Sans', 'Noto Sans Bengali', sans-serif; font-size: clamp(1.8rem, 3.4vw, 3.15rem); line-height: 1.1; letter-spacing: -.045em; }
.analysis-introduction div > p:last-child { max-width: 980px; margin: 1rem 0 0; color: var(--ink-soft); font-size: .92rem; line-height: 1.72; }
.analysis-introduction > span { flex: 0 0 auto; padding: .48rem .65rem; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); background: var(--surface-soft); font-size: .6rem; font-weight: 750; white-space: nowrap; }
.analysis-chapters { max-width: 1120px; margin: 0 auto; }
.analysis-chapter { padding: 2.6rem 0; display: grid; grid-template-columns: 3.2rem minmax(0, 1fr); gap: 1rem; border-bottom: 1px solid var(--line); }
.chapter-number { padding-top: .24rem; color: var(--green); font-family: 'Plus Jakarta Sans', sans-serif; font-size: .74rem; font-weight: 850; letter-spacing: .08em; }
.chapter-content h3 { margin: 0; font-family: 'Plus Jakarta Sans', 'Noto Sans Bengali', sans-serif; font-size: clamp(1.35rem, 2.6vw, 2rem); letter-spacing: -.035em; }
.chapter-answer { margin: .7rem 0 1.3rem; color: var(--green); font-size: 1rem; font-weight: 700; line-height: 1.55; }
.risk-chapter .chapter-number,
.risk-chapter .chapter-answer { color: var(--red); }
.chapter-paragraph { margin: .9rem 0; color: var(--ink-soft); font-size: .88rem; line-height: 1.78; }
.chapter-bullets { margin: 1.3rem 0 0; padding: 0; display: grid; gap: .8rem; list-style: none; }
.chapter-bullets li { position: relative; padding: .9rem 1rem .9rem 2.45rem; border: 1px solid var(--green-line); border-radius: .65rem; background: var(--green-soft); color: var(--ink-soft); font-size: .82rem; line-height: 1.62; }
.chapter-bullets li::before { content: '✓'; position: absolute; top: .9rem; left: .9rem; width: 1rem; height: 1rem; display: grid; place-items: center; border-radius: 50%; color: white; background: var(--green); font-size: .58rem; font-weight: 850; }
.risk-chapter .chapter-bullets li { border-color: var(--red-line); background: var(--red-soft); }
.risk-chapter .chapter-bullets li::before { content: '!'; background: var(--red); }
.analysis-value-callout { margin: 1.5rem 0; display: grid; grid-template-columns: repeat(4, 1fr); overflow: hidden; border: 1px solid var(--line-strong); border-radius: .75rem; }
.analysis-value-callout > div { padding: 1rem; display: flex; flex-direction: column; border-right: 1px solid var(--line); background: var(--surface-soft); }
.analysis-value-callout > div:last-child { border-right: 0; }
.analysis-value-callout small { color: var(--muted); font-size: .52rem; font-weight: 850; letter-spacing: .08em; }
.analysis-value-callout strong { margin-top: .3rem; font-size: .82rem; }
.analysis-method-note { margin-top: 2rem; padding: 1rem 1.1rem; display: grid; grid-template-columns: auto 1fr; gap: .8rem; border: 1px solid #bad0db; border-radius: .7rem; background: var(--blue-soft); }
.analysis-method-note > span { width: 1.45rem; height: 1.45rem; display: grid; place-items: center; border-radius: 50%; color: white; background: var(--blue); font-size: .65rem; font-weight: 850; }
.analysis-method-note p { margin: 0; color: #486675; font-size: .7rem; line-height: 1.55; }
.market-ribbon {
max-width: 1500px;
margin: 0 auto;
......@@ -575,6 +638,10 @@
.score-chart { width: 8rem; height: 8rem; }
.score-center strong { font-size: 2rem; }
.score-copy { margin-top: 0; align-items: flex-start; text-align: left; }
.analysis-introduction { flex-direction: column; gap: 1rem; }
.analysis-value-callout { grid-template-columns: repeat(2, 1fr); }
.analysis-value-callout > div:nth-child(2) { border-right: 0; }
.analysis-value-callout > div:nth-child(n+3) { border-top: 1px solid var(--line); }
.section-nav { top: .5rem; overflow-x: auto; }
.section-nav button { white-space: nowrap; }
.section-nav > span { display: none; }
......@@ -603,6 +670,15 @@
.decision-copy strong { font-size: 1.25rem; }
.decision-copy small { margin-top: .28rem; }
.hero-score-card { justify-content: center; }
.company-analysis > summary { min-height: 0; padding: 1rem; grid-template-columns: auto 1fr; }
.analysis-summary-icon { width: 2.5rem; height: 2.5rem; }
.analysis-summary-copy > span { white-space: normal; }
.analysis-summary-action { grid-column: 2; justify-self: start; }
.company-analysis-body { padding: 1.1rem; }
.analysis-chapter { padding: 2rem 0; grid-template-columns: 2rem minmax(0, 1fr); gap: .4rem; }
.analysis-value-callout { grid-template-columns: 1fr; }
.analysis-value-callout > div { border-right: 0; border-top: 1px solid var(--line); }
.analysis-value-callout > div:first-child { border-top: 0; }
.market-ribbon { grid-template-columns: repeat(2, 1fr); }
.market-ribbon > div { min-height: 5.5rem; padding: 1rem; border-top: 1px solid var(--line); }
.market-ribbon > div:nth-child(even) { border-right: 0; }
......
......@@ -135,6 +135,15 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
this.loadLatest(this.selectedSymbol);
}
openCompanyAnalysis(): void {
const analysis = document.getElementById(
'company-analysis',
) as HTMLDetailsElement | null;
if (!analysis) return;
analysis.open = true;
analysis.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
runAnalysis(force = false): void {
this.loadingAnalysis.set(true);
this.errorMessage.set('');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment