const languageButtons = document.querySelectorAll('[data-lang]'); const translatedNodes = document.querySelectorAll('[data-en][data-zh]'); const translatedInputs = document.querySelectorAll('[data-en-placeholder][data-zh-placeholder]'); const form = document.querySelector('#research-form'); const queryInput = document.querySelector('#company-query'); const research = document.querySelector('#research'); const emptyState = document.querySelector('#research-empty'); const loadingState = document.querySelector('#research-loading'); const errorState = document.querySelector('#research-error'); const resultState = document.querySelector('#research-result'); let currentLanguage = 'en'; let currentResult = null; const apiOrigin = window.location.hostname.endsWith('.hf.space') ? 'https://robinhood-web-pi.vercel.app' : ''; const copy = { en: { sector: 'Classification', fiscal: 'Fiscal year end', state: 'Incorporated', facts: 'Latest reported facts', filings: 'Recent material filings', period: 'Period', filed: 'Filed', source: 'Evidence source', limitations: 'Read before using', live: 'LIVE SEC RECORD', noFacts: 'No matching standardized XBRL facts were found for this issuer.', noFilings: 'No recent supported filing was found.', error: 'The public record could not be opened. Try another U.S. ticker or try again shortly.', }, zh: { sector: '行业分类', fiscal: '财年截止', state: '注册地', facts: '最近披露指标', filings: '近期重要申报', period: '数据期', filed: '提交日', source: '证据来源', limitations: '使用前请阅读', live: 'SEC 实时公开记录', noFacts: '未找到该公司的匹配标准化 XBRL 指标。', noFilings: '未找到近期受支持的申报。', error: '暂时无法打开公开记录。请尝试其他美股代码,或稍后重试。', }, }; const element = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; }; const formatMoney = (value) => new Intl.NumberFormat(currentLanguage === 'zh' ? 'zh-CN' : 'en-US', { style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 2, }).format(value); const renderResult = (data) => { const t = copy[currentLanguage]; const company = data.company; resultState.replaceChildren(); const header = element('header', 'result-header'); const headingGroup = element('div'); headingGroup.append(element('p', 'section-label', t.live), element('h2', '', company.name)); const identity = element('div', 'company-identity'); identity.append(element('strong', '', company.ticker), element('span', '', company.exchange || '—'), element('span', '', `CIK ${company.cik}`)); header.append(headingGroup, identity); const profile = element('div', 'company-profile'); [ [t.sector, company.sic_description || '—'], [t.fiscal, company.fiscal_year_end || '—'], [t.state, company.state || '—'], ].forEach(([label, value]) => { const item = element('div'); item.append(element('span', '', label), element('strong', '', value)); profile.append(item); }); const factsSection = element('section', 'result-block'); factsSection.append(element('h3', '', t.facts)); if (data.metrics.length) { const grid = element('div', 'metric-grid'); data.metrics.forEach((metric) => { const card = element('article', 'metric-card'); const period = metric.period_start ? `${metric.period_start}—${metric.period}` : (metric.period || '—'); card.append( element('span', '', currentLanguage === 'zh' ? metric.label_zh : metric.label_en), element('strong', '', formatMoney(metric.value)), element('small', '', `${metric.form} · ${t.period} ${period} · ${t.filed} ${metric.filed}`), ); grid.append(card); }); factsSection.append(grid); } else { factsSection.append(element('p', 'empty-copy', t.noFacts)); } const filingsSection = element('section', 'result-block filings-block'); filingsSection.append(element('h3', '', t.filings)); if (data.filings.length) { const list = element('div', 'filing-list'); data.filings.forEach((filing) => { const link = element('a', 'filing-row'); link.href = filing.url; link.target = '_blank'; link.rel = 'noopener'; link.append( element('strong', '', filing.form), element('span', '', filing.description), element('time', '', filing.filed), element('b', '', '↗'), ); list.append(link); }); filingsSection.append(list); } else { filingsSection.append(element('p', 'empty-copy', t.noFilings)); } const evidence = element('aside', 'evidence-note'); const evidenceLink = element('a', '', data.source); evidenceLink.href = 'https://www.sec.gov/search-filings/edgar-application-programming-interfaces'; evidenceLink.target = '_blank'; evidenceLink.rel = 'noopener'; const retrieved = new Date(data.retrieved_at).toLocaleString(currentLanguage === 'zh' ? 'zh-CN' : 'en-US'); const sourceLine = element('p'); sourceLine.append(element('span', '', `${t.source}: `), evidenceLink, document.createTextNode(` · ${retrieved}`)); evidence.append(sourceLine, element('h3', '', t.limitations), element('p', '', data.limitations[currentLanguage])); resultState.append(header, profile, factsSection, filingsSection, evidence); }; const setLanguage = (language) => { currentLanguage = language; document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en'; translatedNodes.forEach((node) => { node.textContent = node.dataset[language]; }); translatedInputs.forEach((node) => { node.placeholder = node.dataset[`${language}Placeholder`]; }); languageButtons.forEach((button) => { const active = button.dataset.lang === language; button.classList.toggle('active', active); button.setAttribute('aria-pressed', String(active)); }); if (currentResult) renderResult(currentResult); }; const runResearch = async (query) => { research.setAttribute('aria-busy', 'true'); emptyState.hidden = true; errorState.hidden = true; resultState.hidden = true; loadingState.hidden = false; try { const response = await fetch(`${apiOrigin}/api/research?q=${encodeURIComponent(query)}`); if (!response.ok) throw new Error(String(response.status)); currentResult = await response.json(); renderResult(currentResult); resultState.hidden = false; } catch (error) { currentResult = null; errorState.textContent = copy[currentLanguage].error; errorState.hidden = false; } finally { loadingState.hidden = true; research.setAttribute('aria-busy', 'false'); research.scrollIntoView({ behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'start' }); } }; languageButtons.forEach((button) => button.addEventListener('click', () => setLanguage(button.dataset.lang))); document.querySelectorAll('[data-query]').forEach((button) => button.addEventListener('click', () => { queryInput.value = button.dataset.query; runResearch(button.dataset.query); })); form.addEventListener('submit', (event) => { event.preventDefault(); const query = queryInput.value.trim(); if (query) runResearch(query); }); if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) { document.documentElement.classList.add('motion-ready'); const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add('visible'); observer.unobserve(entry.target); }); }, { rootMargin: '0px 0px -10% 0px', threshold: 0.12 }); document.querySelectorAll('.reveal').forEach((node) => observer.observe(node)); }