File size: 7,890 Bytes
cca40c4
 
bf73d26
 
 
 
 
 
 
 
 
 
f97e0c5
bf73d26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cca40c4
 
bf73d26
cca40c4
bf73d26
 
cca40c4
bf73d26
 
 
cca40c4
bf73d26
cca40c4
 
bf73d26
 
 
 
 
 
 
f97e0c5
bf73d26
 
 
 
 
 
 
 
 
 
 
 
 
 
cca40c4
bf73d26
 
 
 
 
 
 
 
 
cca40c4
 
 
 
 
 
 
 
 
 
 
bf73d26
cca40c4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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));
}