From 6b07186fc182f6e3e6fa0cf0e374b6e44a77cae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=98=8A=E5=8A=B1?= Date: Sun, 2 Aug 2026 03:51:57 +0800 Subject: [PATCH] fix(docs): keep showcase pages working without the share API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Showcase list, detail and replay pages resolved every record through an external Cloudflare Worker — at build time for the list and at runtime for detail/replay — so any Worker outage blanked the list and broke every detail page. Ship the 17 public shares as a committed snapshot instead, so all three routes render from the bundle and need no network at all. The read API client and the build-time fetch plugin have no callers left and are removed; `pnpm refresh:showcase-data` regenerates the snapshot on demand. --- multimodal/websites/docs/env.d.ts | 7 - multimodal/websites/docs/package.json | 3 +- .../docs/plugins/showcase-data-plugin.ts | 24 -- multimodal/websites/docs/rspress.config.ts | 2 - .../docs/scripts/refresh-showcase-data.mjs | 210 +++++++++++++ .../websites/docs/src/data/showcaseShares.ts | 285 ++++++++++++++++++ .../docs/src/hooks/useShowcaseData.ts | 73 ++--- multimodal/websites/docs/src/services/api.ts | 135 --------- .../docs/src/services/dataProcessor.ts | 4 +- multimodal/websites/docs/src/shared/types.ts | 21 ++ 10 files changed, 552 insertions(+), 212 deletions(-) delete mode 100644 multimodal/websites/docs/plugins/showcase-data-plugin.ts create mode 100644 multimodal/websites/docs/scripts/refresh-showcase-data.mjs create mode 100644 multimodal/websites/docs/src/data/showcaseShares.ts delete mode 100644 multimodal/websites/docs/src/services/api.ts diff --git a/multimodal/websites/docs/env.d.ts b/multimodal/websites/docs/env.d.ts index 257a86adc7..2294e6a33e 100644 --- a/multimodal/websites/docs/env.d.ts +++ b/multimodal/websites/docs/env.d.ts @@ -1,8 +1 @@ /// - -// Virtual module for build-time injected showcase data -declare module 'showcase-data' { - import type { ApiShareItem } from './src/services/api'; - export const showcaseData: ApiShareItem[]; - export const lastUpdated: string; -} diff --git a/multimodal/websites/docs/package.json b/multimodal/websites/docs/package.json index bf79a682bb..6de99c41e3 100644 --- a/multimodal/websites/docs/package.json +++ b/multimodal/websites/docs/package.json @@ -5,7 +5,8 @@ "scripts": { "build": "rspress build", "dev": "rspress dev", - "preview": "rspress preview" + "preview": "rspress preview", + "refresh:showcase-data": "node scripts/refresh-showcase-data.mjs" }, "dependencies": { "@rspress/core": "2.0.0-beta.34", diff --git a/multimodal/websites/docs/plugins/showcase-data-plugin.ts b/multimodal/websites/docs/plugins/showcase-data-plugin.ts deleted file mode 100644 index c749d36902..0000000000 --- a/multimodal/websites/docs/plugins/showcase-data-plugin.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { RspressPlugin } from '@rspress/core'; - -/** - * Rspress plugin to fetch showcase data at build time - */ -export function showcaseDataPlugin(): RspressPlugin { - return { - name: 'showcase-data-plugin', - async addRuntimeModules() { - try { - const response = await fetch('https://agent-tars.toxichl1994.workers.dev/shares/public?page=1&limit=100'); - const data = await response.json(); - - return { - 'showcase-data': `export const showcaseData = ${JSON.stringify(data.success ? data.data : [])};`, - }; - } catch { - return { - 'showcase-data': 'export const showcaseData = [];', - }; - } - }, - }; -} diff --git a/multimodal/websites/docs/rspress.config.ts b/multimodal/websites/docs/rspress.config.ts index 317b1fd541..9276f5feed 100644 --- a/multimodal/websites/docs/rspress.config.ts +++ b/multimodal/websites/docs/rspress.config.ts @@ -3,7 +3,6 @@ import { defineConfig } from '@rspress/core'; import mermaid from 'rspress-plugin-mermaid'; import { SEO_CONFIG } from './src/shared/seoConfig'; -import { showcaseDataPlugin } from './plugins/showcase-data-plugin'; const isProd = process.env.NODE_ENV === 'production'; @@ -82,7 +81,6 @@ export default defineConfig({ fontSize: 16, }, }), - showcaseDataPlugin(), ], themeConfig: { darkMode: false, diff --git a/multimodal/websites/docs/scripts/refresh-showcase-data.mjs b/multimodal/websites/docs/scripts/refresh-showcase-data.mjs new file mode 100644 index 0000000000..2be4556d3e --- /dev/null +++ b/multimodal/websites/docs/scripts/refresh-showcase-data.mjs @@ -0,0 +1,210 @@ +#!/usr/bin/env node +/** + * Rewrites the committed showcase snapshot (`src/data/showcaseShares.ts`) from the + * public shares API. Maintainer-only: it is deliberately kept out of `build` and + * `dev` so the site never needs the API to be up. + * + * Environment: + * SHOWCASE_API_BASE API origin, default is the production worker. + * SHOWCASE_FETCH_VIA Request template containing `{url}`, into which the target + * URL is substituted URL-encoded. Networks that cannot reach + * the worker directly can relay through a CORS/HTTP proxy, + * e.g. 'https://api.allorigins.win/raw?url={url}'. + */ +import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const DEFAULT_API_BASE = 'https://agent-tars.toxichl1994.workers.dev'; +const REQUEST_TIMEOUT_MS = 30_000; + +// Must match the ApiShareItem field order in src/shared/types.ts. +const KNOWN_FIELDS = [ + 'sessionId', + 'slug', + 'url', + 'tags', + 'title', + 'description', + 'imageUrl', + 'languages', + 'author', + 'authorGithub', + 'authorTwitter', + 'date', +]; +const REQUIRED_FIELDS = ['sessionId', 'slug', 'url']; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const targetFile = path.join(scriptDir, '..', 'src', 'data', 'showcaseShares.ts'); + +function fail(message) { + console.error(`refresh-showcase-data: ${message}`); + process.exit(1); +} + +function buildRequestUrl(apiUrl) { + const template = process.env.SHOWCASE_FETCH_VIA; + if (!template) return apiUrl; + if (!template.includes('{url}')) { + fail("SHOWCASE_FETCH_VIA must contain the '{url}' placeholder"); + } + return template.replace('{url}', encodeURIComponent(apiUrl)); +} + +async function fetchShares(apiUrl) { + const requestUrl = buildRequestUrl(apiUrl); + console.log(`fetching ${requestUrl}`); + + const response = await fetch(requestUrl, { + headers: { accept: 'application/json' }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } + + const body = await response.text(); + let payload; + try { + payload = JSON.parse(body); + } catch { + throw new Error(`response is not JSON: ${body.slice(0, 200)}`); + } + + if (payload.success !== true) { + throw new Error( + `API reported failure: ${payload.error ?? JSON.stringify(payload).slice(0, 200)}`, + ); + } + if (!Array.isArray(payload.data) || payload.data.length === 0) { + throw new Error('API returned no records; refusing to overwrite the snapshot'); + } + return payload; +} + +function validateRecords(records) { + const unknownFields = new Set(); + records.forEach((record, index) => { + for (const field of REQUIRED_FIELDS) { + if (typeof record[field] !== 'string' || record[field].length === 0) { + throw new Error(`record #${index} is missing a usable '${field}'`); + } + } + for (const [field, value] of Object.entries(record)) { + if (!KNOWN_FIELDS.includes(field)) { + unknownFields.add(field); + } else if (value !== null && typeof value !== 'string') { + // ApiShareItem models every field as `string | null`; anything else would + // silently break the build instead of failing here. + throw new Error( + `record #${index} field '${field}' is ${typeof value}, expected string or null`, + ); + } + } + }); + + if (unknownFields.size > 0) { + throw new Error( + `API returned unknown fields (${[...unknownFields].join(', ')}); ` + + 'add them to ApiShareItem in src/shared/types.ts and to KNOWN_FIELDS here first', + ); + } +} + +/** Emits records verbatim: no scheme fixing, no reordering, no text rewriting. */ +function renderDataFile(records, { apiUrl, fetchedAt }) { + const entries = records + .map((record) => { + const fields = KNOWN_FIELDS.filter((field) => field in record) + .map((field) => ` ${field}: ${JSON.stringify(record[field])},`) + .join('\n'); + return ` {\n${fields}\n },`; + }) + .join('\n'); + + return `/** + * Showcase share records captured from the public shares API. + * + * Committed on purpose: the showcase list, detail and replay pages read this + * snapshot, so they keep working when the upstream API is down or unreachable. + * Values are stored exactly as the API returns them — notably \`url\` and + * \`imageUrl\` carry no scheme, which \`ensureHttps\` adds at render time. + * + * Regenerate with \`pnpm refresh:showcase-data\`; it never runs during build or dev. + * + * source: ${apiUrl} + * fetchedAt: ${fetchedAt} + * records: ${records.length} + */ +import type { ApiShareItem } from '../shared/types'; + +export const showcaseShares: ApiShareItem[] = [ +${entries} +]; +`; +} + +function readPreviousRecordCount() { + try { + const match = readFileSync(targetFile, 'utf8').match(/^ \* records: (\d+)$/m); + return match ? Number(match[1]) : null; + } catch { + return null; + } +} + +async function format(source) { + try { + const prettier = await import('prettier'); + const config = await prettier.resolveConfig(targetFile); + return await prettier.format(source, { ...config, filepath: targetFile }); + } catch (error) { + console.warn(`skipping prettier (${error.message})`); + return source; + } +} + +async function main() { + const apiBase = (process.env.SHOWCASE_API_BASE ?? DEFAULT_API_BASE).replace(/\/+$/, ''); + const apiUrl = `${apiBase}/shares/public?page=1&limit=100`; + const previousCount = readPreviousRecordCount(); + + const payload = await fetchShares(apiUrl); + validateRecords(payload.data); + + const totalRecords = payload.pagination?.totalRecords; + if (typeof totalRecords === 'number' && totalRecords > payload.data.length) { + throw new Error( + `API reports ${totalRecords} records but only ${payload.data.length} were returned; raise the page limit`, + ); + } + + const source = await format( + renderDataFile(payload.data, { apiUrl, fetchedAt: new Date().toISOString() }), + ); + + // Write beside the target and rename, so a crash can never leave a partial file. + const tempFile = `${targetFile}.tmp`; + try { + mkdirSync(path.dirname(targetFile), { recursive: true }); + writeFileSync(tempFile, source, 'utf8'); + renameSync(tempFile, targetFile); + } catch (error) { + try { + unlinkSync(tempFile); + } catch { + // nothing to clean up + } + throw error; + } + + const newCount = payload.data.length; + const change = + previousCount === null + ? 'new file' + : `was ${previousCount}, ${newCount >= previousCount ? '+' : ''}${newCount - previousCount}`; + console.log(`wrote ${path.relative(process.cwd(), targetFile)}: ${newCount} records (${change})`); +} + +main().catch((error) => fail(error.cause ? `${error.message} (${error.cause})` : error.message)); diff --git a/multimodal/websites/docs/src/data/showcaseShares.ts b/multimodal/websites/docs/src/data/showcaseShares.ts new file mode 100644 index 0000000000..cd280ab75b --- /dev/null +++ b/multimodal/websites/docs/src/data/showcaseShares.ts @@ -0,0 +1,285 @@ +/** + * Showcase share records captured from the public shares API. + * + * Committed on purpose: the showcase list, detail and replay pages read this + * snapshot, so they keep working when the upstream API is down or unreachable. + * Values are stored exactly as the API returns them — notably `url` and + * `imageUrl` carry no scheme, which `ensureHttps` adds at render time. + * + * Regenerate with `pnpm refresh:showcase-data`; it never runs during build or dev. + * + * source: https://agent-tars.toxichl1994.workers.dev/shares/public?page=1&limit=100 + * fetchedAt: 2026-08-01T19:50:41.478Z + * records: 17 + */ +import type { ApiShareItem } from '../shared/types'; + +export const showcaseShares: ApiShareItem[] = [ + { + sessionId: 'RYlOoD54GnHlq6g7cDCxE', + slug: 'analyze-google-network-request-ea86c5', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-RYlOoD54GnHlq6g7cDCxE-1753404574396.html', + tags: 'codeact', + title: 'Analyze Google Network Request', + description: "Use command to help me analyze Google's network request.", + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/analyze-google-network-request-ea86c5.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2026-06-23T18:38:00.210Z', + }, + { + sessionId: '7OKl6U30doK0fidzTM7PI', + slug: 'featagent-respnse-api-3e7e29', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-7OKl6U30doK0fidzTM7PI-1753634591847.html', + tags: 'codeact', + title: 'Use Remote Feat Agent Api Branch', + description: '直接直接使用远程的 feat/agent-respnse-api 分支', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/featagent-respnse-api-3e7e29.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-27T17:28:27.563Z', + }, + { + sessionId: 'PtpHIoKvSJc6T81LabtGL', + slug: 'claude-code-gemini-cli-d3fbf7', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-PtpHIoKvSJc6T81LabtGL-1753404703049.html', + tags: 'research', + title: 'Research on CLI Parameters of Claude Code and Gemini', + description: + '帮我调研一下,claude code 和 gemini 使用 cli 直接运行,输入 prompt 的 cli 参数是什么?', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/claude-code-gemini-cli-d3fbf7.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-27T17:26:28.743Z', + }, + { + sessionId: 'CNhJRab__u5dU64NY48qB', + slug: 'httpsbeianmiitgovcnintegratedrecordquery-httpswwwbytedancec-120388', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-CNhJRab__u5dU64NY48qB-1753636318327.html', + tags: 'ai-browser', + title: 'Query Website Filings On MIIT', + description: + '帮我打开 https://beian.miit.gov.cn/#/Integrated/recordQuery 查看以下网站的备案\r\n\r\n- https://www.bytedance.com\r\n- https://www.douyin.com\r\n- http://toutiao.com/\r\n\r\n整理成表格发给我,注意每次切换 website 要清空输入框', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/httpsbeianmiitgovcnintegratedrecordquery-httpswwwbytedancec-120388.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-27T17:12:37.309Z', + }, + { + sessionId: '6GgWgKN7eqzDl3OV3kvOn', + slug: 'draw-me-a-chart-34bc8d', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-6GgWgKN7eqzDl3OV3kvOn-1753634924390.html', + tags: 'mcp', + title: "Draw Chart of Hangzhou's Weather", + description: "Draw me a chart of Hangzhou's weather for one month", + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/draw-me-a-chart-34bc8d.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-27T16:50:00.471Z', + }, + { + sessionId: 'DBV1DcP9eDBaTRGJZpnl5', + slug: 'another-git-process-seems-b6495e', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-DBV1DcP9eDBaTRGJZpnl5-1753634645808.html', + tags: 'codeact', + title: 'How To Fix Git Process Error', + description: + "如何修复这个报错:Another git process seems to be running in this repository, e.g.\r\nan editor opened by 'git commit'. Please make sure all processes\r\nare terminated then try again. If it still fails, a git process\r\nmay have crashed in this repository earlier:\r\nremove the file manually to continue.\r\nerror: Unable to create '/Users/chenhaoli/workspace/code/UI-TARS-desktop/.git/logs/refs/remotes/origin/release/v0.2.0-beta.1.lock': File exists.", + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/another-git-process-seems-b6495e.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-27T16:44:26.643Z', + }, + { + sessionId: 'D7wFqLQ-3eFFhxFxvlgjW', + slug: 'bytedance-web-infra-1-002133', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-D7wFqLQ-3eFFhxFxvlgjW-1753634327352.html', + tags: 'research', + title: 'In-depth Research on ByteDance Web Infra', + description: + '帮我深度调研一下 ByteDance Web Infra,给出一份详细的调研报告\r\n\r\n我期待覆盖的信息: \r\n\r\n1. 团队介绍\r\n2. 主要的开源项目、贡献者;\r\n3. 应用场景; \r\n4. 项目活跃状态;\r\n5. 社区影响力;\r\n6. 技术蓝图;\r\n7. 你的思考;\r\n\r\n要求报告采用 Markdown 输出中文,最后写入文件,同时,并使用 HTML 绘制一个图文并茂的 Slide,介绍 ByteDance Web Infra', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/bytedance-web-infra-1-002133.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-27T16:40:06.051Z', + }, + { + sessionId: '5BjM8aJCggJ34U2kc1w4K', + slug: 'ui-503a5d', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-5BjM8aJCggJ34U2kc1w4K-1753409812700.html', + tags: 'ai-coding', + title: 'Agent TARS Showcase UI Recreation', + description: 'Write code to completely recreate this UI', + imageUrl: '', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-25T02:17:19.021Z', + }, + { + sessionId: 'ql2oFkTTJsXEIrDIu4IhK', + slug: 'neo-brutalism-poster-agent-bfa30c', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-ql2oFkTTJsXEIrDIu4IhK-1753409327836.html', + tags: 'ai-coding', + title: 'Design Neo - Brutalism Poster For Agent TARS', + description: + '设计一款符合 neo-brutalism 设计风格的海报\r\n\r\n- 主题:Agent TARS\r\n- 标语:开源多模态 AI Agent\r\n- 图标:https://lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/icon.png\r\n- 醒目的 CTA:https://agent-tars.com', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/neo-brutalism-poster-agent-bfa30c.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-25T02:09:11.436Z', + }, + { + sessionId: 'Yq_MRfKkSuPff261ry3rL', + slug: 'solve-problem-theory-python-020cc2', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-Yq_MRfKkSuPff261ry3rL-1753402365712.html', + tags: 'codeact', + title: 'Solve Problem Using Python Theory', + description: + 'Try to solve this problem with theory combined with python command. You should notice that "I" and "H" node are not connected.', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/solve-problem-theory-python-Yq_MRfKkSuPff261ry3rL.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-25T02:04:42.198Z', + }, + { + sessionId: '29xA0iE7NFmzCGC8GJVRB', + slug: 'book-flights-san-jose-3c5d03', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-29xA0iE7NFmzCGC8GJVRB-1753401127685.html', + tags: 'ai-browser', + title: 'Book Flights On Priceline', + description: + 'Please help me book the earliest flight from San Jose to New York on September 1st and the last return flight on September 6th on Priceline\r\n\r\nTip: After switching to Sort, you don’t need to click Search anymore. Please answer me in English', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/book-flights-san-jose-29xA0iE7NFmzCGC8GJVRB.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-25T01:58:46.208Z', + }, + { + sessionId: 'Lu6_7Q0LCLfHzDN9x8WLN', + slug: 'aim-trainer-50-seconds-e2416d', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-Lu6_7Q0LCLfHzDN9x8WLN-1753400827595.html', + tags: 'ai-browser', + title: 'Open, Play And Pass Game', + description: + '1. Open this game: https://cpstest.click/en/aim-trainer#google_vignette\r\n2. Select total sec to 50\r\n3. Play and pass this game', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/aim-trainer-50-seconds-e2416d.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-25T01:56:31.685Z', + }, + { + sessionId: 'PNVJooGmZiJHpD6-5Zsfk', + slug: 'bytedance-seed-1-seed-5e86d4', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-PNVJooGmZiJHpD6-5Zsfk-1752412702751.html', + tags: 'research', + title: 'ByteDance Seed Model Research Report', + description: + '帮我深度调研一下 ByteDance Seed 大模型的发展情况,给出一份完整的报告,我期待覆盖的信息: \r\n\r\n1. Seed 大模型现状;\r\n2. Seed 大模型布局;\r\n3. Seed 相关开源项目;\r\n4. 行业影响力分析;\r\n5. 未来发展分析;\r\n\r\n要求报告采用 Markdown 输出中文,最后写入文件,同时,并使用 HTML 绘制一个图文并茂的 Slide,介绍 ByteDance Seed', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/bytedance-1-2-3-51ee1f.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-13T13:19:05.697Z', + }, + { + sessionId: 'Jr3JfBokFF4xXJRYcuyyM', + slug: 'bytedance-1-2-3-51ee1f', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-Jr3JfBokFF4xXJRYcuyyM-1752412643858.html', + tags: 'research', + title: 'ByteDance Open-source Projects Deep Research', + description: + '帮我深度调研一下 ByteDance 开源项目,给出一份详细的调研报告\r\n\r\n我期待覆盖的信息: \r\n\r\n1. 主要的开源项目、贡献者;\r\n2. 应用场景; \r\n3. 项目活跃状态;\r\n4. 社区影响力;\r\n5. 技术蓝图;\r\n\r\n要求报告采用 Markdown 输出中文,最后写入文件,同时,并使用 HTML 绘制一个图文并茂的 Slide,介绍 ByteDance 开源', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/bytedance-1-2-3-51ee1f.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-13T13:17:56.976Z', + }, + { + sessionId: 'NUk7lAfylgrmBsFLl1cxc', + slug: 'smart-reply-column-fifth-NUk7lAfylgrmBsFLl1cxc', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-NUk7lAfylgrmBsFLl1cxc-1752321229234.html', + tags: 'ai-browser', + title: 'Navigate Document For Smart Reply Value', + description: + 'Navigate to the document below, scroll right to find the "智能回复列", and return the value of the fifth row\r\n\r\nhttps://mcfkdjscz6.feishu.cn/sheets/HWKZsd8Brh0ns6t7di0c4qiYnre', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/navigate-to-the-document-388cbc.jpg', + languages: '', + author: 'ULIVZ', + authorGithub: 'ulivz', + authorTwitter: '_ulivz', + date: '2025-07-12T12:01:19.886Z', + }, + { + sessionId: 'lcUYT3YFmwtzOchsMYRUW', + slug: 'restore-ui-frontend-code-lcUYT3YFmwtzOchsMYRUW', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-lcUYT3YFmwtzOchsMYRUW-1751877983868.html', + tags: 'ai-coding', + title: 'Restore Redbook UI With Front-End Code', + description: + 'Write code to completely restore the UI corresponding to this image and write complete front-end code\r\n\r\nRequirements:\r\n\r\nDo not write Python, write HTML directly to File, and then use browser_nagivate to this local file\r\nALWAYS using following format to generate dummy images: https://picsum.photos/{width}/{height}?random={query}, select appropriate width, height, query as needed.', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/restore-ui-frontend-code-lcUYT3YFmwtzOchsMYRUW.jpg', + languages: '', + author: 'Agent TARS', + authorGithub: null, + authorTwitter: null, + date: '2025-07-09T19:17:32.347Z', + }, + { + sessionId: 'iKE_J1dm4MX2NoXZrAixx', + slug: 'gobang-game-rules-and-iKE_J1dm4MX2NoXZrAixx', + url: 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/shared-conversations/agent-tars-iKE_J1dm4MX2NoXZrAixx-1751877068101.html', + tags: 'general', + title: 'Write a Gomoku game and play it', + description: + '1. 理解五子棋游戏规则:五子棋是一种两人对弈的纯策略型棋类游戏,通常双方分别使用黑白两色的棋子,轮流下在棋盘直线与横线的交叉点上,先在横线、直线或斜对角线上形成5子连线者获胜 2. 基于五子棋游戏规划写一个的五子棋游戏,要求系统的落子算法非常聪明,UI 中有开始游戏、游戏开始(展示当前是谁下棋),游戏结束; 3. 玩一遍这个游戏,你应该时刻铭记游戏规则,持续朝着赢的方向落子; 要求: 1. 落子用红色和黑色,系统是黑子,你是红子,系统先下 2. 不要写 Python,直接写 HTML 到 File,然后用 browser_nagivate 到这个本地文件', + imageUrl: + 'lf3-static.bytednsdoc.com/obj/eden-cn/zyha-aulnh/ljhwZthlaukjlkulzlp/storage/general/gobang-game-rules-and-iKE_J1dm4MX2NoXZrAixx.jpg', + languages: '', + author: 'Agent TARS', + authorGithub: null, + authorTwitter: null, + date: '2025-07-09T18:26:47.826Z', + }, +]; diff --git a/multimodal/websites/docs/src/hooks/useShowcaseData.ts b/multimodal/websites/docs/src/hooks/useShowcaseData.ts index 8d7b791a09..970f98c575 100644 --- a/multimodal/websites/docs/src/hooks/useShowcaseData.ts +++ b/multimodal/websites/docs/src/hooks/useShowcaseData.ts @@ -1,11 +1,11 @@ -import { useState, useEffect, useMemo } from 'react'; -import { shareAPI, ApiShareItem } from '../services/api'; +import { useCallback, useMemo } from 'react'; +import { showcaseShares } from '../data/showcaseShares'; import { processShowcaseData, ProcessedShowcaseData, ShowcaseItem, } from '../services/dataProcessor'; -import { showcaseData } from 'showcase-data'; +import type { ApiShareItem } from '../shared/types'; interface UseShowcaseDataResult { items: ShowcaseItem[]; @@ -21,54 +21,45 @@ interface UseShowcaseDataProps { } /** - * Showcase data hook using build-time data for public shares + * `extractIdFromPath` tells slugs and sessionIds apart by looking for a dash, so a + * sessionId containing one arrives here labelled as a slug. Matching either field + * keeps those links resolvable. + */ +function findShare(id: string): ApiShareItem | undefined { + return showcaseShares.find((share) => share.slug === id || share.sessionId === id); +} + +/** + * Showcase data hook backed by the committed snapshot: resolving a list, a + * sessionId or a slug never touches the network, so the pages survive the share + * API being down. Unknown ids yield an empty result, which callers render as 404. */ export function useShowcaseData({ sessionId, slug, }: UseShowcaseDataProps = {}): UseShowcaseDataResult { - const [apiItems, setApiItems] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const id = sessionId || slug || null; - const processedData = useMemo(() => { - if (apiItems.length === 0) return null; - return processShowcaseData(apiItems); - }, [apiItems]); + const apiItems = useMemo(() => { + if (!id) return showcaseShares; + const match = findShare(id); + return match ? [match] : []; + }, [id]); - const items = processedData?.items || []; - - const fetchData = async () => { - try { - setIsLoading(true); - setError(null); - - if (!sessionId && !slug) { - // Use build-time data for public shares - setApiItems(showcaseData.length > 0 ? showcaseData : await shareAPI.getPublicShares(1, 100).then(r => r.data)); - } else if (sessionId) { - const response = await shareAPI.getShare(sessionId); - setApiItems(response.success ? [response.data] : []); - } else if (slug) { - const response = await shareAPI.getShareBySlug(slug); - setApiItems(response.success ? [response.data] : []); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setIsLoading(false); - } - }; + const processedData = useMemo( + () => (apiItems.length > 0 ? processShowcaseData(apiItems) : null), + [apiItems], + ); - useEffect(() => { - fetchData(); - }, [sessionId, slug]); + // Part of the hook's contract for the retry buttons; the snapshot is bundled, so + // there is nothing left to fetch. + const refetch = useCallback(async () => {}, []); return { - items, + items: processedData?.items || [], processedData, - isLoading, - error, - refetch: fetchData, + isLoading: false, + error: null, + refetch, }; } diff --git a/multimodal/websites/docs/src/services/api.ts b/multimodal/websites/docs/src/services/api.ts deleted file mode 100644 index 2642f946ae..0000000000 --- a/multimodal/websites/docs/src/services/api.ts +++ /dev/null @@ -1,135 +0,0 @@ -interface ApiShareItem { - sessionId: string; - slug: string; - url: string; - tags: string; - title?: string; - description?: string; - imageUrl?: string; - languages?: string; - author?: string; - authorGithub?: string; - authorTwitter?: string; - date?: string; -} - -interface ApiResponse { - success: boolean; - data: T; - error?: string; -} - -interface ApiListResponse extends ApiResponse { - pagination: { - currentPage: number; - totalPages: number; - totalRecords: number; - limit: number; - hasNextPage: boolean; - hasPrevPage: boolean; - }; -} - -interface CreateShareData { - sessionId: string; - slug: string; - url: string; - title?: string; - description?: string; - tags?: string; - imageUrl?: string; - languages?: string; - author?: string; - authorGithub?: string; - authorTwitter?: string; -} - -interface UpdateShareData { - title?: string; - description?: string; - tags?: string; - imageUrl?: string; - languages?: string; - author?: string; - authorGithub?: string; - authorTwitter?: string; -} - -class ShareAPI { - private baseUrl = 'https://agent-tars.toxichl1994.workers.dev'; - - private async request(path: string, options: RequestInit = {}): Promise { - const url = `${this.baseUrl}${path}`; - const response = await fetch(url, { - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, - ...options, - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error || `HTTP ${response.status}`); - } - - return data; - } - - async getShares(page = 1, limit = 100): Promise> { - return this.request>(`/shares?page=${page}&limit=${limit}`); - } - - async getPublicShares(page = 1, limit = 100): Promise> { - return this.request>( - `/shares/public?page=${page}&limit=${limit}`, - ); - } - - async getShare(sessionId: string): Promise> { - const encodedId = encodeURIComponent(sessionId); - return this.request>(`/shares/${encodedId}`); - } - - async getShareBySlug(slug: string): Promise> { - const encodedSlug = encodeURIComponent(slug); - return this.request>(`/shares/slug/${encodedSlug}`); - } - - async createShare(shareData: CreateShareData): Promise> { - return this.request>('/shares', { - method: 'POST', - body: JSON.stringify(shareData), - }); - } - - async updateShare( - sessionId: string, - updateData: UpdateShareData, - ): Promise> { - const encodedId = encodeURIComponent(sessionId); - return this.request>(`/shares/${encodedId}`, { - method: 'PUT', - body: JSON.stringify(updateData), - }); - } - - async updateShareBySlug( - slug: string, - updateData: UpdateShareData, - ): Promise> { - const encodedSlug = encodeURIComponent(slug); - return this.request>(`/shares/slug/${encodedSlug}`, { - method: 'PUT', - body: JSON.stringify(updateData), - }); - } - - async health(): Promise> { - return this.request>('/health'); - } -} - -export const shareAPI = new ShareAPI(); -export type { ApiShareItem, ApiResponse, ApiListResponse, CreateShareData, UpdateShareData }; diff --git a/multimodal/websites/docs/src/services/dataProcessor.ts b/multimodal/websites/docs/src/services/dataProcessor.ts index 31caf9130e..1be183565c 100644 --- a/multimodal/websites/docs/src/services/dataProcessor.ts +++ b/multimodal/websites/docs/src/services/dataProcessor.ts @@ -1,4 +1,4 @@ -import { ApiShareItem } from './api'; +import { ApiShareItem } from '../shared/types'; export type CategoryType = | 'ai-browser' @@ -221,7 +221,7 @@ function transformApiItemToShowcase(apiItem: ApiShareItem): ShowcaseItem { category, imageUrl, link: secureUrl, - date: apiItem.date, + date: apiItem.date ?? undefined, languages, tags, author, diff --git a/multimodal/websites/docs/src/shared/types.ts b/multimodal/websites/docs/src/shared/types.ts index acc6de8f2e..57cf3b5b99 100644 --- a/multimodal/websites/docs/src/shared/types.ts +++ b/multimodal/websites/docs/src/shared/types.ts @@ -2,3 +2,24 @@ export enum DYNAMIC_ROUTE { Showcase = '/showcase', Replay = '/replay', } + +/** + * One record of the public shares API, mirrored field-for-field by the committed + * snapshot in `src/data/showcaseShares.ts`. Everything past the identifiers is + * nullable because the API really does return `null` for unset author handles; + * `refresh-showcase-data.mjs` enforces exactly this shape before writing. + */ +export interface ApiShareItem { + sessionId: string; + slug: string; + url: string; + tags?: string | null; + title?: string | null; + description?: string | null; + imageUrl?: string | null; + languages?: string | null; + author?: string | null; + authorGithub?: string | null; + authorTwitter?: string | null; + date?: string | null; +}