diff --git a/docs/trending-paper/daily.css b/docs/trending-paper/daily.css new file mode 100644 index 0000000..898a1e9 --- /dev/null +++ b/docs/trending-paper/daily.css @@ -0,0 +1,310 @@ +:root { + color-scheme: light; + --ink: #0d0d0f; + --ink-2: #3a3a3f; + --muted: #6b6b72; + --paper: #f5f5f6; + --surface: #ffffff; + --surface-soft: #f0f0f2; + --line: rgba(17,17,20,.10); + --blue: #2b50f0; + --blue-dark: #1e3fd0; + --border: #c7c7cc; + --border-soft: #d2d2d7; + --header-bg: rgba(245,245,246,.94); + --poster-start: #e4e4e7; + --poster-end: #f4f4f5; + --rank-bg: rgba(13,13,15,.9); + --disabled: #b4b4ba; + --viewer-stage: #e8e8eb; + --fallback: #f5f5f6; + --body-glow-a: rgba(223,223,223,.38); + --body-glow-b: rgba(90,90,90,.10); + --blue-shadow: rgba(43,80,240,.18); + --focus-ring: rgba(43,80,240,.20); + --card-shadow: rgba(17,17,20,.07); + --hf-icon: #d68700; + --arxiv-icon: #b31b1b; + --nav-accent: #6c5ce7; + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +:root[data-theme="dark"] { + color-scheme: dark; + --ink: #f4f6fa; + --ink-2: #c7cad3; + --muted: #9ba0ad; + --paper: #08090c; + --surface: #14151a; + --surface-soft: #1b1d23; + --line: rgba(240,243,250,.11); + --blue: #5877ff; + --blue-dark: #4463ff; + --border: #3a3e49; + --border-soft: #2c2f38; + --header-bg: rgba(8,9,12,.94); + --poster-start: #1b1d23; + --poster-end: #252832; + --rank-bg: rgba(8,9,12,.9); + --disabled: #626875; + --viewer-stage: #0d0e12; + --fallback: #101116; + --body-glow-a: rgba(59,62,72,.26); + --body-glow-b: rgba(88,119,255,.08); + --blue-shadow: rgba(88,119,255,.24); + --focus-ring: rgba(88,119,255,.30); + --card-shadow: rgba(0,0,0,.28); + --hf-icon: #ffd21e; + --arxiv-icon: #e05a70; +} + +* { box-sizing: border-box; } +.sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; } +body { + margin: 0; + color: var(--ink); + background: + radial-gradient(circle at 8% 0%, var(--body-glow-a), transparent 30rem), + radial-gradient(circle at 94% 22%, var(--body-glow-b), transparent 32rem), + var(--paper); + -webkit-font-smoothing: antialiased; +} +a { color: inherit; } +.topbar-wrap { position: sticky; top: 0; z-index: 50; border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent); background: color-mix(in srgb, var(--paper) 75%, transparent); backdrop-filter: saturate(180%) blur(14px); -webkit-backdrop-filter: saturate(180%) blur(14px); } +.topbar { max-width: 1180px; margin: 0 auto; padding: 14px 24px; display: flex; align-items: center; gap: 24px; } +.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none; font-size: 17px; font-weight: 700; letter-spacing: -.01em; } +.brand:hover { text-decoration: none; } +.brand-logo { display: block; width: 28px; height: 28px; border-radius: 8px; } +.nav-links { display: flex; flex: 1; align-items: center; gap: 22px; margin: 0 0 0 16px; padding: 0; list-style: none; } +.nav-links li { display: inline-flex; } +.nav-links a { padding: 0; color: var(--muted); text-decoration: none; font-size: 14.5px; font-weight: 500; white-space: nowrap; transition: color .15s ease; } +.nav-links a:hover { color: var(--ink); text-decoration: none; } +.nav-links a.active { color: var(--nav-accent); font-weight: 500; } +.nav__cta { display: flex; align-items: center; gap: 10px; margin-left: auto; } +.github-link { display: inline-flex; align-items: center; gap: 8px; padding: 11px 18px; border: 1px solid var(--border); border-radius: 999px; color: var(--ink); background: var(--surface); text-decoration: none; font-size: 14.5px; font-weight: 600; white-space: nowrap; cursor: pointer; transition: transform .15s ease, box-shadow .15s ease, background .15s ease, color .15s ease, border-color .15s ease; } +.github-link:hover { background: var(--surface-soft); text-decoration: none; transform: translateY(-1px); } +.github-link svg { flex-shrink: 0; } +.page-controls { position: fixed; top: 14px; right: 24px; z-index: 100; display: flex; align-items: center; gap: 10px; } +.theme-menu { position: relative; height: 40px; margin: 0; border: 0; color: var(--muted); background: transparent; white-space: nowrap; } +.theme-menu > summary { height: 40px; display: inline-flex; align-items: center; gap: 6px; padding: 0; border: 0; color: var(--muted); background: transparent; font: inherit; font-size: 14.5px; font-weight: 500; list-style: none; cursor: pointer; } +.theme-menu > summary::-webkit-details-marker { display: none; } +.theme-menu > summary:hover, .theme-menu[open] > summary { color: var(--ink); } +.theme-chevron { color: var(--nav-accent); font-size: .8em; transition: transform .15s ease; } +.theme-menu[open] .theme-chevron { transform: rotate(180deg); } +.theme-menu-panel { position: absolute; z-index: 20; top: calc(100% + 5px); right: 0; display: grid; min-width: 126px; padding: 6px; border: 1px solid var(--border-soft); border-radius: 10px; background: var(--surface); box-shadow: 0 14px 34px var(--card-shadow); } +.theme-menu-panel button { padding: 9px 11px; border: 0; border-radius: 7px; color: var(--ink-2); background: transparent; font: inherit; font-size: .82rem; text-align: left; cursor: pointer; } +.theme-menu-panel button:hover { background: var(--surface-soft); } +.theme-menu-panel button.is-active { color: white; background: var(--blue); } +main { width: min(1180px, 90vw); margin: 0 auto; padding: 64px 0 90px; } +.hero { margin-bottom: 38px; } +.title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 32px; } +h1 { margin: 0; max-width: 820px; font-size: clamp(2.4rem, 5vw, 4.8rem); font-weight: 760; line-height: .98; letter-spacing: -.045em; } +.intro { max-width: 800px; margin: 22px 0 0; color: var(--muted); line-height: 1.55; } +.intro p { margin: 0; } +.intro-detail { margin-top: 8px !important; font-size: 1.02rem; } +.intro-detail span { display: block; } +.intro-lead { color: var(--ink-2); font-size: clamp(1.12rem, 2vw, 1.35rem); font-weight: 570; } +.intro-detail strong { color: var(--ink-2); font-weight: 780; } +.reels-accent { color: var(--blue); font-weight: 850; text-decoration: none; text-decoration-thickness: .08em; text-underline-offset: .16em; } +.reels-accent:hover { color: var(--blue-dark); text-decoration-line: underline; } +.count-pill { flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 40px; padding: 8px 14px; border: 1px solid var(--border-soft); border-radius: 999px; color: var(--ink-2); background: var(--surface); box-shadow: 0 1px 2px var(--card-shadow); font-size: .84rem; font-weight: 750; } +.count-pill[hidden], .rank[hidden] { display: none; } +.edition-toolbar { margin-top: 30px; display: flex; align-items: center; gap: 14px; } +.mode-switch { display: inline-flex; flex: 0 0 auto; padding: 4px; border: 1px solid var(--border-soft); border-radius: 11px; background: var(--surface-soft); } +.mode-switch button { min-width: 82px; padding: 8px 14px; border: 0; border-radius: 8px; color: var(--muted); background: transparent; font: inherit; font-size: .86rem; font-weight: 750; cursor: pointer; } +.mode-switch button[aria-pressed="true"] { color: white; background: var(--blue); box-shadow: 0 2px 8px var(--blue-shadow); } +.edition-control { display: inline-grid; grid-template-columns: 40px minmax(170px,auto) 40px; align-items: stretch; overflow: hidden; border: 1px solid var(--border-soft); border-radius: 11px; background: var(--surface); box-shadow: 0 1px 2px var(--card-shadow); } +.edition-step { display: grid; place-items: center; min-height: 40px; padding: 0; border: 0; color: var(--ink-2); background: var(--surface-soft); font: 800 1.15rem/1 inherit; cursor: pointer; } +.edition-step:first-of-type { border-right: 1px solid var(--line); } +.edition-step:last-of-type { border-left: 1px solid var(--line); } +.edition-step:hover:not(:disabled) { color: white; background: var(--blue); } +.edition-step:disabled { color: var(--disabled); cursor: default; } +select { min-width: 180px; padding: 10px 14px; border: 0; border-radius: 0; appearance: none; background: var(--surface); color: var(--ink); font: inherit; font-size: .86rem; font-weight: 750; text-align: center; text-align-last: center; cursor: pointer; } +.mode-switch button:focus-visible, .edition-step:focus-visible, select:focus-visible, .sort-toggle:focus-visible, .theme-menu > summary:focus-visible, .theme-menu-panel button:focus-visible, .brand:focus-visible, .nav-links a:focus-visible, .github-link:focus-visible, .reels-accent:focus-visible, .metadata-chip:focus-visible { position: relative; z-index: 1; outline: 3px solid var(--focus-ring); outline-offset: 2px; } +.catalog-toolbar { display: grid; grid-template-columns: minmax(260px,1fr) auto; gap: 12px; margin-top: 14px; } +.search-control, .sort-control { min-height: 44px; border: 1px solid var(--border-soft); border-radius: 11px; background: var(--surface); box-shadow: 0 1px 2px var(--card-shadow); } +.search-control { display: flex; align-items: center; gap: 9px; padding: 0 13px; } +.search-icon { flex: 0 0 auto; width: 17px; height: 17px; color: var(--muted); } +.search-control input { width: 100%; min-width: 0; padding: 10px 0; border: 0; outline: 0; color: var(--ink); background: transparent; font: inherit; font-size: .9rem; } +.search-control input::placeholder { color: var(--muted); opacity: 1; } +.sort-control { display: inline-flex; align-items: center; gap: 4px; padding: 4px; background: var(--surface-soft); } +.sort-label { flex: 0 0 auto; padding: 0 9px; color: var(--muted); font-size: .7rem; font-weight: 800; letter-spacing: .065em; text-transform: uppercase; } +.sort-toggle { min-width: 128px; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 7px 11px; border: 1px solid var(--line); border-radius: 8px; color: var(--ink-2); background: var(--surface); box-shadow: 0 1px 2px var(--card-shadow); font: inherit; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease, color .15s ease, transform .15s ease; } +.sort-toggle:hover { border-color: var(--border); color: var(--ink); box-shadow: 0 3px 9px var(--card-shadow); transform: translateY(-1px); } +.sort-toggle:active { transform: translateY(0); } +.sort-hf-mark { flex: 0 0 auto; width: 17px; height: 17px; } +.sort-toggle-label { font-size: .82rem; font-weight: 780; letter-spacing: -.01em; } +.sort-arrows { width: 9px; display: inline-grid; justify-items: center; gap: 3px; color: var(--ink); } +.sort-arrow { width: 0; height: 0; opacity: .22; transition: opacity .15s ease, transform .15s ease; } +.sort-arrow-up { border-right: 4px solid transparent; border-bottom: 5px solid currentColor; border-left: 4px solid transparent; } +.sort-arrow-down { border-top: 5px solid currentColor; border-right: 4px solid transparent; border-left: 4px solid transparent; } +.sort-toggle[data-sort="votes-desc"] .sort-arrow-up, +.sort-toggle[data-sort="votes-asc"] .sort-arrow-down { opacity: .92; transform: scale(1.08); } +.search-control:focus-within, .sort-control:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px var(--focus-ring); } +.paper-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 28px; } +.paper-grid[hidden], .paper-card[hidden] { display: none; } +.paper-card { overflow: hidden; display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); box-shadow: 0 8px 28px var(--card-shadow); } +.poster-shell { position: relative; aspect-ratio: 2016 / 1210; overflow: hidden; background: linear-gradient(145deg, var(--poster-start), var(--poster-end)); } +.poster-link { position: absolute; inset: 0; display: block; overflow: hidden; } +.poster { width: 100%; height: 100%; object-fit: contain; display: block; transition: transform .25s ease, filter .2s ease; } +.poster-link:hover .poster { transform: scale(1.015); } +.rank { position: absolute; top: 14px; left: 14px; display: grid; place-items: center; min-width: 38px; height: 38px; padding: 0 9px; border-radius: 999px; color: white; background: var(--rank-bg); font-weight: 800; } +.paper-body { padding: 24px; } +.metadata-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: .76rem; } +.metadata-chip { display: inline-flex; align-items: center; gap: 5px; min-height: 27px; padding: 4px 8px; border: 1px solid var(--border-soft); border-radius: 999px; color: var(--ink-2); background: var(--surface-soft); text-decoration: none; font-weight: 760; font-variant-numeric: tabular-nums; letter-spacing: 0; transition: color .15s ease, border-color .15s ease, background .15s ease; } +.metadata-chip:hover { border-color: var(--blue); color: var(--blue); background: var(--surface); } +.github-chip[hidden] { display: none; } +.brand-inline-icon, .github-mark { flex: 0 0 auto; width: 14px; height: 14px; } +.brand-inline-icon { display: inline-block; background: currentColor; -webkit-mask-position: center; mask-position: center; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-size: contain; mask-size: contain; } +.hf-mark { color: var(--hf-icon); -webkit-mask-image: url("icons/huggingface.svg"); mask-image: url("icons/huggingface.svg"); } +.arxiv-mark { color: var(--arxiv-icon); -webkit-mask-image: url("icons/arxiv.svg"); mask-image: url("icons/arxiv.svg"); } +.metadata-chip:hover .hf-mark, .metadata-chip:hover .arxiv-mark { color: var(--blue); } +.github-star-count, .hf-vote-count, .paper-id { font-size: .74rem; font-weight: 760; } +.paper-title { margin: 12px 0 0; font-size: 1.45rem; font-weight: 760; line-height: 1.22; letter-spacing: -.015em; } +.button { display: inline-flex; align-items: center; justify-content: center; min-height: 38px; padding: 8px 13px; border: 1px solid var(--border); border-radius: 9px; text-decoration: none; font-size: .84rem; font-weight: 700; } +.button:hover { border-color: var(--blue); } +.message-card { padding: 46px; border: 1px dashed var(--border); border-radius: 18px; color: var(--muted); text-align: center; background: var(--surface); } +.paper-viewer { overflow: hidden; width: 100vw; max-width: none; height: 100vh; max-height: none; margin: 0; padding: 0; border: 0; background: var(--paper); } +.paper-viewer::backdrop { background: rgba(13,13,15,.55); } +.viewer-shell { display: grid; grid-template-rows: auto minmax(0,1fr); width: 100%; height: 100%; } +.viewer-header { min-height: 76px; display: grid; grid-template-columns: 110px minmax(0,1fr) auto auto; align-items: center; gap: 20px; padding: 10px 24px; border-bottom: 1px solid var(--line); background: var(--surface); } +.viewer-back { justify-self: start; border: 0; color: var(--blue); background: transparent; font: inherit; font-weight: 720; cursor: pointer; } +.viewer-heading { min-width: 0; text-align: left; } +.viewer-heading span { color: var(--blue); font-size: .7rem; font-weight: 760; text-transform: uppercase; letter-spacing: .12em; } +.viewer-heading h2 { overflow: hidden; margin: 4px 0 0; font-size: 1.12rem; font-weight: 760; white-space: nowrap; text-overflow: ellipsis; } +.viewer-actions { display: flex; align-items: center; justify-content: flex-end; gap: 9px; } +.viewer-external { display: inline-flex; min-height: 38px; align-items: center; padding: 8px 13px; border-radius: 9px; color: white; background: var(--blue); text-decoration: none; font-size: .82rem; font-weight: 720; white-space: nowrap; } +.viewer-tabs { display: inline-flex; padding: 4px; border-radius: 10px; background: var(--surface-soft); } +.viewer-tabs button { min-width: 92px; padding: 8px 18px; border: 0; border-radius: 7px; color: var(--muted); background: transparent; font: inherit; font-size: .86rem; font-weight: 720; cursor: pointer; } +.viewer-tabs button[aria-selected="true"] { color: white; background: var(--blue); } +.viewer-stage { position: relative; min-height: 0; background: var(--viewer-stage); } +.viewer-stage section { width: 100%; height: 100%; border: 0; } +.viewer-stage iframe { display: block; width: 100%; height: 100%; border: 0; } +#paper-panel { display: grid; grid-template-rows: auto minmax(0,1fr); } +#paper-panel[hidden] { display: none; } +.pdf-fallback { padding: 8px 18px; color: var(--muted); background: var(--fallback); border-bottom: 1px solid var(--line); text-align: center; font-size: .78rem; } +.pdf-fallback a { color: var(--blue); font-weight: 700; } +.viewer-loading { position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; color: var(--muted); background: var(--paper); } +.viewer-loading[hidden] { display: none; } +body.viewer-open { overflow: hidden; } +.site-footer { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} +.footer-inner { + width: min(1180px, 90vw); + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: "brand links" "notice notice" "meta meta" "source source"; + align-items: center; + gap: 18px 28px; + margin: 0 auto; + padding: 26px 4px 34px; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: .72rem; +} +.footer-brand { + grid-area: brand; + display: flex; + flex-direction: column; + gap: 5px; +} +.footer-brand span { color: var(--ink-2); font-size: .78rem; font-weight: 800; } +.footer-brand p { margin: 0; color: var(--muted); font-size: .72rem; } +.footer-links { + grid-area: links; + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px 16px; +} +.footer-links a { + color: var(--muted); + font-size: .66rem; + font-weight: 700; + text-decoration: none; + transition: color .15s ease; +} +.footer-links a:hover { color: var(--blue); } +.footer-notice { + grid-area: notice; + margin: 0; + padding-top: 16px; + border-top: 1px solid var(--line); + color: var(--muted); + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif; + font-size: .7rem; + line-height: 1.55; +} +.footer-notice span { margin-right: 6px; color: var(--blue); } +.footer-meta { + grid-area: meta; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px 24px; + color: var(--muted); + font-size: .65rem; +} +.footer-meta a, .footer-source a { + color: var(--muted); + text-decoration: none; + transition: color .15s ease; +} +.footer-meta a:hover, .footer-source a:hover { color: var(--blue); } +.footer-support { text-align: center; } +.footer-source { + grid-area: source; + margin: -8px 0 0; + color: var(--muted); + font-size: .65rem; +} +@media (max-width: 1540px) { + .topbar { padding-right: clamp(24px, calc(720px - 50vw), 112px); } +} +@media (max-width: 1000px) { + .nav-links { display: none; } +} +@media (max-width: 780px) { + .topbar { padding-right: 112px; } + .page-controls { right: 16px; } + main { width: min(92vw, 620px); padding-top: 42px; } + .paper-grid { grid-template-columns: 1fr; } + .title-row { display: block; } + .edition-toolbar { align-items: flex-start; flex-wrap: wrap; } + .mode-switch { width: 100%; } + .mode-switch button { flex: 1; } + .edition-control { flex: 1 1 auto; } + .edition-control select { width: 100%; min-width: 0; } + .catalog-toolbar { grid-template-columns: 1fr; } + .sort-toggle { flex: 1; min-width: 0; } + .theme-menu > summary { font-size: 12px; } + .viewer-header { min-height: 104px; grid-template-columns: auto minmax(0,1fr) auto; grid-template-areas: "back heading actions" "tabs tabs tabs"; gap: 8px; padding: 8px 14px; } + .viewer-back { grid-area: back; } + .viewer-heading { grid-area: heading; text-align: center; } + .viewer-tabs { grid-area: tabs; justify-self: center; } + .viewer-actions { grid-area: actions; } + .viewer-back, .viewer-external { font-size: 0; } + .viewer-back span, .viewer-external span { font-size: 1.15rem; } + .viewer-heading h2 { font-size: .9rem; } + .viewer-actions { gap: 5px; } + .viewer-actions .button { width: 36px; min-height: 36px; padding: 0; font-size: 0; } + .viewer-actions .button::after { content: "Ask"; font-size: .68rem; letter-spacing: .02em; } + .viewer-tabs button { min-width: 72px; padding: 7px 12px; } + .footer-inner { + grid-template-columns: 1fr; + grid-template-areas: "brand" "links" "notice" "meta" "source"; + align-items: flex-start; + } + .footer-links { justify-content: flex-start; } + .footer-meta { flex-direction: column; align-items: flex-start; gap: 6px; } + .footer-support { text-align: left; } +} +@media (max-width: 520px) { + .brand span { display: none; } +} +@media (max-width: 480px) { + .topbar { gap: 12px; padding: 12px 100px 12px 16px; } + .github-link { gap: 0; padding: 8px 10px; } + .github-link > span { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } +} diff --git a/docs/trending-paper/daily.js b/docs/trending-paper/daily.js new file mode 100644 index 0000000..024b1a1 --- /dev/null +++ b/docs/trending-paper/daily.js @@ -0,0 +1,558 @@ +(() => { + "use strict"; + + const API_BASE = "https://researchstudio.site/daily-paper/api"; + const region = document.querySelector("#paper-region"); + const grid = document.querySelector("#paper-grid"); + const loading = document.querySelector("#loading"); + const empty = document.querySelector("#empty"); + const count = document.querySelector("#paper-count"); + const select = document.querySelector("#edition-select"); + const selectLabel = document.querySelector("#edition-label"); + const olderButton = document.querySelector("#edition-older"); + const newerButton = document.querySelector("#edition-newer"); + const modeButtons = [...document.querySelectorAll("[data-mode]")]; + const sourceLink = document.querySelector("#source-link"); + const searchInput = document.querySelector("#paper-search"); + const sortButton = document.querySelector("#paper-sort"); + const template = document.querySelector("#paper-template"); + const viewer = document.querySelector("#paper-viewer"); + const viewerTitle = document.querySelector("#viewer-title"); + const viewerCategory = document.querySelector("#viewer-category"); + const viewerExternal = document.querySelector("#viewer-external"); + const viewerQA = document.querySelector("#viewer-qa"); + const viewerLoading = document.querySelector("#viewer-loading"); + const reelTab = document.querySelector("#reel-tab"); + const paperTab = document.querySelector("#paper-tab"); + const reelPanel = document.querySelector("#reel-panel"); + const paperPanel = document.querySelector("#paper-panel"); + const reelFrame = document.querySelector("#reel-frame"); + const paperFrame = document.querySelector("#paper-frame"); + const pdfFallbackLink = document.querySelector("#pdf-fallback-link"); + + let editions = []; + let activeMode = "weekly"; + let activeSelection = null; + let activeContext = null; + let activeRecords = []; + let selectedWeekly = null; + let selectedDaily = null; + let activePaper = null; + let reelReady = false; + let paperReady = false; + function setLink(element, url) { + element.href = url; + } + + function githubRepoUrl(value) { + if (typeof value !== "string" || !value.trim()) return null; + try { + const raw = value.trim(); + const parsed = new URL( + /^https?:\/\//i.test(raw) ? raw : `https://github.com/${raw.replace(/^\/+/, "")}` + ); + if (parsed.protocol !== "https:" || !["github.com", "www.github.com"].includes(parsed.hostname.toLowerCase())) { + return null; + } + const parts = parsed.pathname.split("/").filter(Boolean); + if (parts.length < 2) return null; + const owner = encodeURIComponent(parts[0]); + const repo = encodeURIComponent(parts[1].replace(/\.git$/i, "")); + return `https://github.com/${owner}/${repo}`; + } catch (_error) { + return null; + } + } + + function compactCount(value) { + return new Intl.NumberFormat("en-US", { + notation: "compact", + maximumFractionDigits: 1 + }).format(value); + } + + function parseUtcDate(value) { + const parsed = new Date(`${value}T00:00:00Z`); + return Number.isNaN(parsed.valueOf()) ? null : parsed; + } + + function formatDate(value, options = {}) { + const parsed = parseUtcDate(value); + if (!parsed) return value || "Unknown date"; + return new Intl.DateTimeFormat("en-US", { + day: "numeric", + month: "short", + year: "numeric", + timeZone: "UTC", + ...options + }).format(parsed); + } + + function formatWeek(edition) { + const start = parseUtcDate(edition?.week_start); + const end = parseUtcDate(edition?.week_end); + if (!start || !end) return edition?.week_id || "Unknown date"; + const startLabel = formatDate(edition.week_start, {year: undefined}); + const endLabel = formatDate(edition.week_end, {year: undefined}); + return `${startLabel} – ${endLabel}`; + } + + function formatDaily(value) { + return formatDate(value, {year: undefined}); + } + + function editionForWeek(weekId) { + return editions.find(edition => edition.week_id === weekId) || null; + } + + function dailyDates() { + const values = new Set(); + for (const edition of editions) { + for (const paper of edition.papers || []) { + if (paper.daily_date) values.add(paper.daily_date); + } + } + return [...values].sort((left, right) => right.localeCompare(left)); + } + + function dailyDatesForWeek(weekId) { + const edition = editionForWeek(weekId); + if (!edition) return []; + return [...new Set((edition.papers || []).map(paper => paper.daily_date).filter(Boolean))] + .sort((left, right) => right.localeCompare(left)); + } + + function weekForDate(date) { + return editions.find(edition => + (edition.papers || []).some(paper => paper.daily_date === date) + ) || editions.find(edition => + edition.week_start <= date && date <= edition.week_end + ) || null; + } + + function selectionValues(mode) { + return mode === "weekly" + ? editions.map(edition => edition.week_id).filter(Boolean) + : dailyDates(); + } + + function populateSelector(mode) { + select.replaceChildren(); + selectLabel.textContent = "Date"; + const values = selectionValues(mode); + for (const value of values) { + const option = document.createElement("option"); + option.value = value; + option.textContent = mode === "weekly" ? formatWeek(editionForWeek(value)) : formatDaily(value); + select.appendChild(option); + } + select.disabled = values.length === 0; + return values; + } + + function recordsFor(mode, key) { + if (mode === "weekly") { + const edition = editionForWeek(key); + if (!edition) return []; + return (edition.papers || []).map(paper => ({edition, paper})); + } + + const records = []; + const seen = new Set(); + for (const edition of editions) { + for (const paper of edition.papers || []) { + if (paper.daily_date !== key) continue; + const identity = paper.token || paper.paper_id; + if (identity && seen.has(identity)) continue; + if (identity) seen.add(identity); + records.push({edition, paper}); + } + } + records.sort((left, right) => { + const votes = (Number(right.paper.hf_upvotes_at_snapshot) || 0) + - (Number(left.paper.hf_upvotes_at_snapshot) || 0); + if (votes) return votes; + return String(left.paper.paper_id).localeCompare(String(right.paper.paper_id)); + }); + return records.map((record, index) => ({ + edition: record.edition, + paper: {...record.paper, rank: index + 1} + })); + } + + function updateEditionNavigation() { + const values = selectionValues(activeMode); + const index = values.indexOf(activeSelection); + olderButton.disabled = index < 0 || index >= values.length - 1; + newerButton.disabled = index <= 0; + } + + function contextFor(mode, key) { + if (mode === "weekly") { + const edition = editionForWeek(key); + return { + mode, + key, + edition, + sourceUrl: edition?.source_url || `https://huggingface.co/papers/week/${encodeURIComponent(key)}` + }; + } + return { + mode, + key, + edition: weekForDate(key), + sourceUrl: `https://huggingface.co/papers/date/${encodeURIComponent(key)}` + }; + } + + function normalizeSearch(value) { + return String(value || "") + .normalize("NFKC") + .toLocaleLowerCase() + .replace(/\s+/g, " ") + .trim(); + } + + function applyCatalogControls() { + const query = normalizeSearch(searchInput.value); + const ascending = sortButton.dataset.sort === "votes-asc"; + const cards = [...grid.querySelectorAll(".paper-card")]; + cards.sort((left, right) => { + const voteDifference = Number(left.dataset.hfVotes) - Number(right.dataset.hfVotes); + if (voteDifference) return ascending ? voteDifference : -voteDifference; + return Number(left.dataset.originalOrder) - Number(right.dataset.originalOrder); + }); + + let visibleCount = 0; + for (const card of cards) { + const matches = !query || card.dataset.searchText.includes(query); + card.hidden = !matches; + if (matches) visibleCount += 1; + grid.appendChild(card); + } + + const totalCount = cards.length; + count.textContent = query + ? `${visibleCount} of ${totalCount} papers` + : `${totalCount} ${totalCount === 1 ? "paper" : "papers"}`; + count.hidden = false; + loading.hidden = true; + empty.textContent = totalCount + ? "No papers match this search." + : "No published papers are available for this selection yet."; + empty.hidden = visibleCount > 0; + grid.hidden = visibleCount === 0; + } + + function setSortDirection(value) { + const ascending = value === "votes-asc"; + sortButton.dataset.sort = ascending ? "votes-asc" : "votes-desc"; + sortButton.title = ascending ? "Least voted first" : "Most voted first"; + sortButton.setAttribute( + "aria-label", + ascending + ? "Sorted by Hugging Face votes, least voted first. Activate for most voted first." + : "Sorted by Hugging Face votes, most voted first. Activate for least voted first." + ); + } + + function toggleSortDirection() { + setSortDirection(sortButton.dataset.sort === "votes-asc" ? "votes-desc" : "votes-asc"); + applyCatalogControls(); + } + + function renderRecords(records, context) { + grid.replaceChildren(); + sourceLink.href = context.sourceUrl; + activeRecords = records; + activeContext = context; + + for (const [recordIndex, {edition, paper}] of records.entries()) { + const fragment = template.content.cloneNode(true); + const card = fragment.querySelector(".paper-card"); + card.dataset.originalOrder = String(recordIndex); + card.dataset.searchText = normalizeSearch(`${paper.title} ${paper.paper_id}`); + card.dataset.paperId = paper.paper_id; + + const rank = fragment.querySelector(".rank"); + if (paper.rank === null || paper.rank === undefined) { + rank.hidden = true; + } else { + rank.textContent = `#${paper.rank}`; + } + + const hfVotes = Number(paper.hf_upvotes_at_snapshot) || 0; + card.dataset.hfVotes = String(hfVotes); + const hfScore = fragment.querySelector(".hf-score"); + hfScore.querySelector(".hf-vote-count").textContent = hfVotes.toLocaleString("en-US"); + hfScore.setAttribute( + "aria-label", + `${hfVotes} Huggingface votes; open this paper on Hugging Face` + ); + setLink( + hfScore, + paper.hf_url || `https://huggingface.co/papers/${encodeURIComponent(paper.paper_id)}` + ); + + const arxivChip = fragment.querySelector(".arxiv-chip"); + arxivChip.querySelector(".paper-id").textContent = paper.paper_id; + arxivChip.setAttribute("aria-label", `Open ${paper.paper_id} on arXiv`); + setLink( + arxivChip, + paper.arxiv_url || `https://arxiv.org/abs/${encodeURIComponent(paper.paper_id)}` + ); + + const githubChip = fragment.querySelector(".github-chip"); + const githubUrl = githubRepoUrl(paper.github_repo); + const githubStarValue = paper.github_stars_at_snapshot; + const githubStars = githubStarValue === null || githubStarValue === undefined + ? Number.NaN + : Number(githubStarValue); + if (githubUrl && Number.isFinite(githubStars) && githubStars >= 0) { + githubChip.href = githubUrl; + githubChip.hidden = false; + githubChip.querySelector(".github-star-count").textContent = compactCount(githubStars); + githubChip.setAttribute( + "aria-label", + `Open the paper repository on GitHub; ${githubStars.toLocaleString("en-US")} stars` + ); + } + fragment.querySelector(".paper-title").textContent = paper.title; + const poster = fragment.querySelector(".poster"); + poster.src = paper.poster_url; + poster.alt = `Preview for ${paper.title}`; + setLink(fragment.querySelector(".poster-link"), paper.reel_url); + + for (const opener of fragment.querySelectorAll("[data-open-view]")) { + opener.addEventListener("click", event => { + event.preventDefault(); + openViewer(edition, paper, opener.dataset.openView); + }); + } + grid.appendChild(fragment); + } + + applyCatalogControls(); + region.setAttribute("aria-busy", "false"); + } + + function writeSelectionUrl(clearViewer = true) { + const url = new URL(window.location.href); + url.searchParams.set("mode", activeMode); + if (activeMode === "weekly") { + if (activeSelection) url.searchParams.set("week", activeSelection); + else url.searchParams.delete("week"); + url.searchParams.delete("date"); + } else { + if (activeSelection) url.searchParams.set("date", activeSelection); + else url.searchParams.delete("date"); + url.searchParams.delete("week"); + } + if (clearViewer) { + url.searchParams.delete("paper"); + url.searchParams.delete("view"); + } + history.replaceState({}, "", url); + } + + function renderSelection(mode, key, updateUrl = true) { + searchInput.value = ""; + activeMode = mode; + activeSelection = key; + select.value = key; + if (mode === "weekly") selectedWeekly = key; + else selectedDaily = key; + updateEditionNavigation(); + renderRecords(recordsFor(mode, key), contextFor(mode, key)); + if (updateUrl) writeSelectionUrl(); + } + + function switchMode(mode, updateUrl = true, preferredKey = null) { + activeMode = mode === "daily" ? "daily" : "weekly"; + for (const button of modeButtons) { + button.setAttribute("aria-pressed", button.dataset.mode === activeMode ? "true" : "false"); + } + const values = populateSelector(activeMode); + const remembered = activeMode === "weekly" ? selectedWeekly : selectedDaily; + const key = values.includes(preferredKey) + ? preferredKey + : values.includes(remembered) + ? remembered + : values[0] || null; + if (key) { + renderSelection(activeMode, key, updateUrl); + return; + } + activeSelection = null; + searchInput.value = ""; + updateEditionNavigation(); + renderRecords([], contextFor(activeMode, "")); + if (updateUrl) writeSelectionUrl(); + } + + function stepEdition(offset) { + const values = selectionValues(activeMode); + const index = values.indexOf(activeSelection); + const nextKey = values[index + offset]; + if (nextKey) renderSelection(activeMode, nextKey); + } + + function paperPdfUrl(url) { + const parsed = new URL(url, window.location.href); + parsed.hash = "view=FitH"; + return parsed.href; + } + + function showView(view, updateUrl = true) { + if (!activePaper) return; + const paperMode = view === "paper"; + reelTab.setAttribute("aria-selected", paperMode ? "false" : "true"); + paperTab.setAttribute("aria-selected", paperMode ? "true" : "false"); + reelPanel.hidden = paperMode; + paperPanel.hidden = !paperMode; + viewerLoading.hidden = paperMode ? paperReady : reelReady; + viewerLoading.textContent = paperMode ? "Loading paper…" : "Loading Reel…"; + if (paperMode && paperFrame.src === "about:blank") paperFrame.src = paperPdfUrl(activePaper.pdf_url); + viewerExternal.href = paperMode ? paperPdfUrl(activePaper.pdf_url) : activePaper.reel_url; + if (updateUrl) setPermalink(paperMode ? "paper" : "reel"); + } + + function setPermalink(view) { + writeSelectionUrl(false); + const url = new URL(window.location.href); + url.searchParams.set("paper", activePaper.paper_id); + url.searchParams.set("view", view); + history.replaceState({}, "", url); + } + + function viewerCategoryText(edition, paper) { + const rank = paper.rank === null || paper.rank === undefined ? "" : ` · #${paper.rank}`; + if (activeMode === "daily") return `${paper.daily_date}${rank}`; + return `${formatWeek(edition)} · ${formatDaily(paper.daily_date)}${rank}`; + } + + function openViewer(edition, paper, view = "reel", updateUrl = true) { + activePaper = paper; + reelReady = false; + paperReady = false; + viewerTitle.textContent = paper.title; + viewerCategory.textContent = viewerCategoryText(edition, paper); + viewerQA.href = paper.qa_url; + pdfFallbackLink.href = paperPdfUrl(paper.pdf_url); + reelFrame.src = paper.reel_url; + paperFrame.src = "about:blank"; + showView(view === "paper" ? "paper" : "reel", updateUrl); + if (!viewer.open) viewer.showModal(); + document.body.classList.add("viewer-open"); + } + + function closeViewer(updateUrl = true) { + viewer.close(); + document.body.classList.remove("viewer-open"); + reelFrame.src = "about:blank"; + paperFrame.src = "about:blank"; + activePaper = null; + reelReady = false; + paperReady = false; + if (updateUrl) writeSelectionUrl(); + } + + function restoreLocation() { + const params = new URLSearchParams(window.location.search); + const requestedMode = params.get("mode"); + const legacyDate = !requestedMode && params.has("date"); + const mode = requestedMode === "daily" || legacyDate ? "daily" : "weekly"; + const key = mode === "daily" ? params.get("date") : params.get("week"); + + switchMode(mode, false, key); + + const paperId = params.get("paper"); + const record = paperId + ? activeRecords.find(item => item.paper.paper_id === paperId) + : null; + if (record) { + openViewer(record.edition, record.paper, params.get("view") === "paper" ? "paper" : "reel", false); + setPermalink(params.get("view") === "paper" ? "paper" : "reel"); + } else { + writeSelectionUrl(); + } + } + + async function requestEditions() { + const response = await fetch(`${API_BASE}/editions?limit=31`, { + headers: {"Accept": "application/json"} + }); + if (!response.ok) throw new Error("edition request failed"); + return ((await response.json()).editions || []) + .filter(edition => edition && edition.week_id) + .sort((left, right) => String(right.week_end).localeCompare(String(left.week_end))); + } + + async function refreshEditions() { + const mode = activeMode; + const selection = activeSelection; + const query = searchInput.value; + const sort = sortButton.dataset.sort; + const updatedEditions = await requestEditions(); + if (!updatedEditions.length) return; + editions = updatedEditions; + + const values = populateSelector(mode); + const nextSelection = values.includes(selection) ? selection : values[0]; + if (!nextSelection) return; + renderSelection(mode, nextSelection, false); + searchInput.value = query; + setSortDirection(sort); + applyCatalogControls(); + } + + async function load() { + try { + editions = await requestEditions(); + if (!editions.length) { + loading.hidden = true; + empty.hidden = false; + count.hidden = true; + region.setAttribute("aria-busy", "false"); + return; + } + select.addEventListener("change", () => renderSelection(activeMode, select.value)); + searchInput.addEventListener("input", applyCatalogControls); + sortButton.addEventListener("click", toggleSortDirection); + olderButton.addEventListener("click", () => stepEdition(1)); + newerButton.addEventListener("click", () => stepEdition(-1)); + for (const button of modeButtons) { + button.addEventListener("click", () => { + const nextMode = button.dataset.mode; + if (nextMode === activeMode) return; + if (nextMode === "daily") { + const dates = dailyDatesForWeek(activeSelection); + switchMode("daily", true, dates.includes(selectedDaily) ? selectedDaily : dates[0]); + } else { + switchMode("weekly", true, weekForDate(activeSelection)?.week_id || selectedWeekly); + } + }); + } + restoreLocation(); + } catch (_error) { + loading.textContent = "Trending Paper is temporarily unavailable. Please try again shortly."; + region.setAttribute("aria-busy", "false"); + } + } + + document.querySelector("#viewer-back").addEventListener("click", () => closeViewer()); + viewer.addEventListener("cancel", event => { event.preventDefault(); closeViewer(); }); + reelTab.addEventListener("click", () => showView("reel")); + paperTab.addEventListener("click", () => showView("paper")); + reelFrame.addEventListener("load", () => { + if (reelFrame.getAttribute("src") === "about:blank") return; + reelReady = true; + if (!reelPanel.hidden) viewerLoading.hidden = true; + }); + paperFrame.addEventListener("load", () => { + if (paperFrame.getAttribute("src") === "about:blank") return; + paperReady = true; + if (!paperPanel.hidden) viewerLoading.hidden = true; + }); + load(); +})(); diff --git a/docs/trending-paper/icons/arxiv.svg b/docs/trending-paper/icons/arxiv.svg new file mode 100644 index 0000000..6d4c8ef --- /dev/null +++ b/docs/trending-paper/icons/arxiv.svg @@ -0,0 +1 @@ + diff --git a/docs/trending-paper/icons/huggingface.svg b/docs/trending-paper/icons/huggingface.svg new file mode 100644 index 0000000..d51054f --- /dev/null +++ b/docs/trending-paper/icons/huggingface.svg @@ -0,0 +1 @@ + diff --git a/docs/trending-paper/index.html b/docs/trending-paper/index.html new file mode 100644 index 0000000..4aa536d --- /dev/null +++ b/docs/trending-paper/index.html @@ -0,0 +1,183 @@ + + + + + + + + Trending Paper · ResearchStudio + + + + + + +
+
+ Theme +
+ + + +
+
+
+
+ +
+ +
+
+
+
+

Trending Paper

+
+

Meet the standout research through ResearchStudio Reels

+

Each paper transformed into a visual poster, a narrated video, and an accessible blog,so you can scan, watch, or read it your way.

+
+
+
+
+
+ + +
+
+ + + + +
+ +
+ +
+ +
+
Loading the latest weekly edition…
+ + +
+
+ + + + + + +
+
+ +
+ Hugging Face Paper +

+
+
+ + +
+ +
+
+
Loading Reel…
+
+ +
+ +
+
+
+ + + + diff --git a/docs/trending-paper/logo.svg b/docs/trending-paper/logo.svg new file mode 100644 index 0000000..c38bee8 --- /dev/null +++ b/docs/trending-paper/logo.svg @@ -0,0 +1,6 @@ + + ResearchStudio + + + + diff --git a/docs/trending-paper/theme.js b/docs/trending-paper/theme.js new file mode 100644 index 0000000..d4de983 --- /dev/null +++ b/docs/trending-paper/theme.js @@ -0,0 +1,91 @@ +(() => { + "use strict"; + + const STORAGE_KEY = "researchstudio:daily-paper-theme"; + const systemThemeQuery = window.matchMedia?.("(prefers-color-scheme: dark)") || null; + let preference = "light"; + + function systemTheme() { + if (systemThemeQuery) return systemThemeQuery.matches ? "dark" : "light"; + const hour = new Date().getHours(); + return hour >= 7 && hour < 19 ? "light" : "dark"; + } + + function storedPreference(value = null) { + try { + const stored = value ?? localStorage.getItem(STORAGE_KEY); + return ["system", "dark", "light"].includes(stored) ? stored : "light"; + } catch (_error) { + return "light"; + } + } + + function syncMenu() { + document.querySelectorAll("[data-theme-option]").forEach(option => { + const selected = option.dataset.themeOption === preference; + option.classList.toggle("is-active", selected); + option.setAttribute("aria-current", selected ? "true" : "false"); + }); + } + + function setTheme(nextPreference, persist = false) { + preference = ["dark", "light"].includes(nextPreference) ? nextPreference : "system"; + const theme = preference === "system" ? systemTheme() : preference; + document.documentElement.dataset.theme = theme; + document.documentElement.dataset.themePreference = preference; + syncMenu(); + if (persist) { + try { + localStorage.setItem(STORAGE_KEY, preference); + } catch (_error) { + // Theme selection still works for this page when storage is unavailable. + } + } + document.dispatchEvent(new CustomEvent("ui-theme-change", { + detail: {theme, preference} + })); + } + + function initializeMenu() { + const menus = [...document.querySelectorAll("[data-theme-menu]")]; + document.querySelectorAll("[data-theme-option]").forEach(option => { + option.addEventListener("click", () => { + setTheme(option.dataset.themeOption, true); + const menu = option.closest("[data-theme-menu]"); + if (menu) menu.open = false; + }); + }); + document.addEventListener("click", event => { + if (event.target.closest("[data-theme-menu]")) return; + menus.forEach(menu => { menu.open = false; }); + }); + document.addEventListener("keydown", event => { + if (event.key !== "Escape") return; + menus.forEach(menu => { menu.open = false; }); + }); + syncMenu(); + } + + setTheme(storedPreference(), false); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initializeMenu, {once: true}); + } else { + initializeMenu(); + } + + const syncSystemTheme = () => { + if (preference === "system") setTheme("system", false); + }; + if (systemThemeQuery?.addEventListener) { + systemThemeQuery.addEventListener("change", syncSystemTheme); + } else if (systemThemeQuery?.addListener) { + systemThemeQuery.addListener(syncSystemTheme); + } + window.addEventListener("pageshow", syncSystemTheme); + document.addEventListener("visibilitychange", () => { + if (!document.hidden) syncSystemTheme(); + }); + window.addEventListener("storage", event => { + if (event.key === STORAGE_KEY) setTheme(storedPreference(event.newValue), false); + }); +})();