const { useState, useEffect, useCallback, useRef } = React; const EMPTY_TEASER = { companyName: "", ticker: "", exchange: "", sector: "", tagline: "", profile: "", asOfDate: "", metrics: [ { label: "Market Cap", value: "", unit: "" }, { label: "Revenue (LTM)", value: "", unit: "" }, { label: "Net Profit", value: "", unit: "" }, { label: "Revenue Growth", value: "", unit: "" }, { label: "ROE", value: "", unit: "" }, { label: "Net Margin", value: "", unit: "" }, ], shareholders: [], financialTrend: [], operational: [], highlights: [], peers: [], dealInfo: { type: "", size: "", useOfProceeds: "" }, }; async function apiJSON(method, path, body) { const opts = { method, headers: { "Content-Type": "application/json" }, credentials: "same-origin", }; if (body !== undefined) opts.body = JSON.stringify(body); const r = await fetch(path, opts); const data = await r.json().catch(() => ({})); if (!r.ok) { const err = new Error(data.error || `HTTP ${r.status}`); err.status = r.status; throw err; } return data; } function App() { const [me, setMe] = useState(null); // null = not loaded, false = not authed, object = authed const [booting, setBooting] = useState(true); const bootstrap = useCallback(async () => { try { const data = await apiJSON("GET", "/api/me"); setMe(data); } catch (e) { if (e.status === 401) setMe(false); else setMe(false); } finally { setBooting(false); } }, []); useEffect(() => { bootstrap(); }, [bootstrap]); if (booting) return
Đang tải...
; if (!me) return setMe(u)} />; return ; } function AuthScreen({ onAuthed }) { const [mode, setMode] = useState("login"); // login | register const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [err, setErr] = useState(""); const [busy, setBusy] = useState(false); const submit = async (e) => { e.preventDefault(); setErr(""); setBusy(true); try { const path = mode === "login" ? "/api/auth/login" : "/api/auth/register"; const data = await apiJSON("POST", path, { username: username.trim(), password }); onAuthed(data); } catch (e) { setErr(e.message); } finally { setBusy(false); } }; return (
TCBSECM Teaser Generator
setUsername(e.target.value)} autoFocus autoComplete="username" placeholder="3-32 ký tự: chữ, số, . _ -" required /> setPassword(e.target.value)} autoComplete={mode === "login" ? "current-password" : "new-password"} placeholder="tối thiểu 8 ký tự" required /> {err &&
{err}
}
); } function MainApp({ me, setMe }) { const [apiKey, setApiKey] = useState(""); const [urlsInput, setUrlsInput] = useState(""); const [sources, setSources] = useState([]); const [teaser, setTeaser] = useState(EMPTY_TEASER); const [teaserId, setTeaserId] = useState(null); const [status, setStatus] = useState(""); const [statusErr, setStatusErr] = useState(false); const [history, setHistory] = useState([]); const [tab, setTab] = useState("sources"); const [loading, setLoading] = useState(false); const fileRef = useRef(null); const previewRef = useRef(null); const setMsg = (m, err = false) => { setStatus(m); setStatusErr(err); }; const refreshMe = useCallback(async () => { try { setMe(await apiJSON("GET", "/api/me")); } catch (e) { /* */ } }, [setMe]); const loadHistory = useCallback(async () => { try { const data = await apiJSON("GET", "/api/teasers"); setHistory(data.teasers || []); } catch (e) { /* ignore */ } }, []); useEffect(() => { loadHistory(); }, [loadHistory]); const logout = async () => { try { await apiJSON("POST", "/api/auth/logout"); } catch (e) { /* ignore */ } setMe(false); }; const saveAPIKey = async () => { if (!apiKey.trim()) return; try { await apiJSON("PUT", "/api/me/apikey", { apiKey: apiKey.trim() }); setApiKey(""); setMsg("API key đã lưu (mã hoá AES-GCM)."); refreshMe(); } catch (e) { setMsg(e.message, true); } }; const extractSources = async () => { const urls = urlsInput.split(/\s+/).map(s => s.trim()).filter(Boolean); const files = fileRef.current?.files || []; if (urls.length === 0 && files.length === 0) { setMsg("Nhập URL hoặc chọn file PDF.", true); return; } setLoading(true); setMsg("Đang trích text từ PDF..."); try { const fd = new FormData(); if (urls.length > 0) fd.append("urls", JSON.stringify(urls)); for (const f of files) fd.append("files", f); const r = await fetch("/api/pdf/extract", { method: "POST", body: fd, credentials: "same-origin" }); const data = await r.json(); if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`); setSources(data.results || []); const ok = (data.results || []).filter(x => !x.error).length; setMsg(`Trích ${ok}/${(data.results || []).length} nguồn thành công.`); if (fileRef.current) fileRef.current.value = ""; } catch (e) { setMsg(e.message, true); } finally { setLoading(false); } }; const extractTeaser = async () => { const texts = sources.filter(s => !s.error && s.text).map(s => `=== ${s.sourceRef} ===\n${s.text}`); if (texts.length === 0) { setMsg("Không có nguồn text nào để gọi Claude.", true); return; } setLoading(true); setMsg("Đang gọi Claude Opus 4.7 (adaptive thinking)..."); try { const combinedText = texts.join("\n\n").slice(0, 60000); const sourceRefs = sources.filter(s => !s.error).map(s => s.sourceRef); const data = await apiJSON("POST", "/api/teasers/extract", { combinedText, sourceRefs }); setTeaser({ ...EMPTY_TEASER, ...data.payload }); setTeaserId(data.id); const u = data.usage || {}; setMsg(`Trích teaser OK. tokens in/out=${u.InputTokens}/${u.OutputTokens}, latency=${u.LatencyMS}ms`); loadHistory(); } catch (e) { setMsg(e.message, true); } finally { setLoading(false); } }; const saveEdit = async () => { if (!teaserId) return; try { await apiJSON("PUT", `/api/teasers/${teaserId}`, teaser); setMsg("Đã lưu chỉnh sửa."); loadHistory(); } catch (e) { setMsg(e.message, true); } }; const loadTeaser = async (id) => { try { const data = await apiJSON("GET", `/api/teasers/${id}`); setTeaser({ ...EMPTY_TEASER, ...data.payload }); setTeaserId(data.id); setMsg(`Đã tải teaser #${id}.`); setTab("edit"); } catch (e) { setMsg(e.message, true); } }; const deleteTeaser = async (id) => { if (!confirm(`Xoá teaser #${id}?`)) return; try { await apiJSON("DELETE", `/api/teasers/${id}`); if (teaserId === id) { setTeaser(EMPTY_TEASER); setTeaserId(null); } loadHistory(); } catch (e) { setMsg(e.message, true); } }; const exportPDF = async () => { const el = previewRef.current; if (!el) return; setLoading(true); setMsg("Đang render PDF..."); try { const canvas = await html2canvas(el, { scale: 2, backgroundColor: "#ffffff" }); const imgData = canvas.toDataURL("image/png"); const { jsPDF } = window.jspdf; const pdf = new jsPDF({ unit: "mm", format: "a4", orientation: "portrait" }); const pageW = pdf.internal.pageSize.getWidth(); const pageH = pdf.internal.pageSize.getHeight(); const ratio = Math.min(pageW / canvas.width, pageH / canvas.height); const w = canvas.width * ratio; const h = canvas.height * ratio; pdf.addImage(imgData, "PNG", (pageW - w) / 2, 0, w, h); const fname = `${(teaser.ticker || "TEASER").toUpperCase()}_ECM_Teaser.pdf`; pdf.save(fname); setMsg("PDF tải xong."); } catch (e) { setMsg("Export PDF lỗi: " + e.message, true); } finally { setLoading(false); } }; return (

Người dùng

{me?.name} {me?.hasApiKey ? " • API key ✓" : " • chưa có API key"}

Anthropic API Key

setApiKey(e.target.value)} placeholder={me?.hasApiKey ? "(đã có — nhập để thay)" : "sk-ant-..."} />
Lưu mã hoá AES-GCM trong SQLite.
{tab === "sources" && (

PDF nguồn