diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bd075af..c8468f8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -27,7 +27,7 @@ body: id: version attributes: label: Version - placeholder: "0.5.1" + placeholder: "0.5.3" validations: required: true - type: textarea diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a5ffd08..d30ec9e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,10 +3,32 @@ updates: - package-ecosystem: npm directory: / schedule: - interval: weekly - open-pull-requests-limit: 5 + interval: monthly + open-pull-requests-limit: 2 + groups: + npm-minor-and-patch: + patterns: + - "*" + update-types: + - minor + - patch + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-major - package-ecosystem: github-actions directory: / schedule: interval: monthly - open-pull-requests-limit: 3 + open-pull-requests-limit: 1 + groups: + actions-minor-and-patch: + patterns: + - "*" + update-types: + - minor + - patch + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-major diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 231eaef..968ca9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,5 +64,8 @@ jobs: - name: Audit website production dependencies run: npm run audit:www-dependencies + - name: Audit pitch production dependencies + run: npm run audit:pitch-dependencies + - name: Verify npm package contents run: npm run pack:check --workspace vitalmcp diff --git a/CHANGELOG.md b/CHANGELOG.md index f128555..450ed43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,23 @@ The project coordinates preview releases across the iOS app, `vitalmcp` runtime, ### Added - Open-source license, security policy, contribution guide, support policy, and repository templates. +- MIT license text in the published `vitalmcp` package, enforced by the npm release preflight. +- A release audit guard that rejects Apple Developer Team identifiers in shared Xcode projects. - Full-repository and Git-history secret scanning for the public release gate. ### Changed +- Synchronized the English, Simplified Chinese, and npm package documentation with the `0.5.3` Local Preview flow. +- Expanded the open-source release gate into a reviewable checklist covering repository settings, privacy, asset provenance, package contents, and rollback. - Product scope is now a strict three-phase roadmap: WorkBuddy Local, local Hermes/other Agents, then a user-owned Agent server over Tailscale. - Hosted relay, accounts, billing, App Store launch, public VPS, and marketing are explicitly outside the active roadmap. - Public documentation now leads with the current pinned npm and SkillHub release and separates current guidance from historical relay research. +### Security + +- Updated the MCP and Hono runtime dependency paths to patched releases identified by the public-release audit. +- Pinned the latest stable Next.js 16 release and exported the website and pitch as static artifacts, keeping their framework toolchains out of the deployed production dependency surface. + ## [0.5.2] - 2026-07-19 ### Fixed diff --git a/README.md b/README.md index edfac1a..c204cde 100644 --- a/README.md +++ b/README.md @@ -140,5 +140,6 @@ Use synthetic fixtures only. For component details, release checks, and diagnost - Use the issue templates for reproducible bugs and proposals. - Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md). - Review the public/private boundary in [Open-source scope](docs/open-source-scope.md). +- Complete the [open-source release checklist](docs/open-source-release-checklist.md) before a public release. Vital Agent Sync is licensed under the [MIT License](LICENSE). diff --git a/README.zh-CN.md b/README.zh-CN.md index 9478f7e..2d28a28 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,7 @@ Vital Agent Sync 是面向个人 AI Agent 的开源 Apple Health 本地桥接器 ## 当前路线 -`0.5.2` 是源码分发的 Local Preview,严格按以下顺序推进: +`0.5.3` 是源码分发的 Local Preview,严格按以下顺序推进: | 阶段 | 体验 | 状态 | | --- | --- | --- | @@ -22,22 +22,22 @@ Vital Agent Sync 是面向个人 AI Agent 的开源 Apple Health 本地桥接器 从 [SkillHub](https://skillhub.cn/skills/vital-agent-sync) 安装 Vital Agent Sync,然后对 WorkBuddy 说: -> 安装 Vital Agent Sync,使用局域网连接我的 iPhone。先说明所有持久化修改,等我确认后执行,最后在本机打开配对二维码。 +> 启动 Vital Agent Sync,通过局域网连接我的 iPhone。 Skill 会: -- 无 `sudo` 安装固定版本的 `vitalmcp`; -- 修改文件、服务、网络和 MCP 配置前展示脱敏计划并等待确认; -- 记录 setup 使用的 Node 路径与原生模块 ABI,让 Terminal、launchd 和 MCP 使用同一兼容运行时; -- 遇到 WorkBuddy 沙箱边界时暂停,只让用户在 macOS Terminal 执行一条返回的 launchd 激活命令,不使用 `sudo`; -- 等待用户审批 WorkBuddy MCP、重载并成功调用原生 `vital_agent_status` 后再配对; +- 无 `sudo` 把固定版本的 `vitalmcp` 安装到用户目录; +- 用一次安装确认覆盖固定运行时、私有状态、WorkBuddy MCP 配置、可信局域网监听和本地配对页; +- 记录 setup 使用的 Node 路径与原生模块 ABI,让 CLI、接收端和 MCP 使用同一兼容运行时; +- 在当前 WorkBuddy 会话中启动 Local Preview 接收端,不要求 Terminal 或 `sudo`;WorkBuddy 退出后该预览进程可能停止; +- 在 MCP 审批前即可打开本机私有配对页,但读取健康数据前仍须明确批准 WorkBuddy MCP; - 只在用户本地浏览器打开包含凭据的二维码; - 第一次读取健康数据前说明模型隐私边界并等待确认。 手动备用入口: ```bash -npx -y vitalmcp@0.5.2 setup --agent workbuddy --transport lan +npx -y vitalmcp@0.5.3 setup --agent workbuddy --transport lan ``` 首次同步后检查: @@ -52,9 +52,9 @@ vitalmcp doctor --agent workbuddy --transport lan 所有 Agent 复用同一个接收端、SQLite、setup 状态和 MCP 工具: ```bash -npx -y vitalmcp@0.5.2 setup --agent hermes --transport lan -npx -y vitalmcp@0.5.2 setup --agent generic --transport lan -npx -y vitalmcp@0.5.2 setup --agent openclaw --transport lan +npx -y vitalmcp@0.5.3 setup --agent hermes --transport lan +npx -y vitalmcp@0.5.3 setup --agent generic --transport lan +npx -y vitalmcp@0.5.3 setup --agent openclaw --transport lan ``` Hermes 是第二阶段的第一优先级,通用 stdio MCP 是可移植基线,不依赖任何 Agent 市场上架。 @@ -64,7 +64,7 @@ Hermes 是第二阶段的第一优先级,通用 stdio MCP 是可移植基线 最后阶段让 Agent、`vitalmcp`、SQLite 和 MCP 在同一台用户服务器上运行,Tailscale 只负责 iPhone 到接收端的私有 HTTPS: ```bash -npx -y vitalmcp@0.5.2 setup \ +npx -y vitalmcp@0.5.3 setup \ --agent hermes \ --manager systemd \ --transport tailscale \ @@ -110,6 +110,6 @@ npm run test:ios npm run audit:oss ``` -详细文档见[文档索引](docs/README.md)。提交代码前阅读 [CONTRIBUTING.md](CONTRIBUTING.md),安全问题通过 [SECURITY.md](SECURITY.md) 的私密渠道报告,安装问题使用 [GitHub Discussions](https://github.com/Coooder-Crypto/vital-agent-sync/discussions)。 +详细文档见[文档索引](docs/README.md)。提交代码前阅读 [CONTRIBUTING.md](CONTRIBUTING.md),安全问题通过 [SECURITY.md](SECURITY.md) 的私密渠道报告,安装问题使用 [GitHub Discussions](https://github.com/Coooder-Crypto/vital-agent-sync/discussions)。公开仓库或发布版本前,请完成[开源发布清单](docs/open-source-release-checklist.md)。 项目使用 [MIT License](LICENSE)。 diff --git a/SECURITY.md b/SECURITY.md index fc7ec90..e564642 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -26,4 +26,10 @@ Do not include real Apple Health exports, live pairing artifacts, private keys, - An Agent or model provider may receive the scoped health context returned through MCP. Local storage does not imply local model inference. - Vital Agent Sync is not a medical device and must not be used for diagnosis, treatment, or emergency decisions. +## Static-site build boundary + +`apps/www` and `apps/pitch` are exported as static artifacts. Next.js, React, PostCSS, Sharp, and the other site framework packages are development-only build tools; they are not installed or executed in the deployed static-site runtime. Builds consume repository-controlled source and media only and must not process untrusted stylesheets, source maps, images, or other inputs. + +The root development dependency tree may temporarily report upstream Next.js build-tool advisories even when `audit:www-dependencies` and `audit:pitch-dependencies` report zero production vulnerabilities. This is a narrowly documented exception, not a blanket audit waiver. Maintainers must review those advisories for each release and update to a stable patched framework release when one is available. + See [the direct LAN security model](docs/direct-lan-security.md), [the relay threat model](docs/e2ee-relay-threat-model.md), and [the privacy boundary](docs/e2ee-relay-privacy-boundary.md) for details. diff --git a/apps/ios/VitalAgentSync.xcodeproj/project.pbxproj b/apps/ios/VitalAgentSync.xcodeproj/project.pbxproj index b478260..cb994e8 100644 --- a/apps/ios/VitalAgentSync.xcodeproj/project.pbxproj +++ b/apps/ios/VitalAgentSync.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -45,7 +45,7 @@ 5A7BF3D1A49C2CFC8DA6F100 /* GatewaySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GatewaySettings.swift; sourceTree = ""; }; 5CD7D093F3B454A55990BAC8 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 67808A0519BFAE67E399B083 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 6DEBBD0B8576FD6C78327552 /* VitalAgentSync.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = VitalAgentSync.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6DEBBD0B8576FD6C78327552 /* VitalAgentSync.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VitalAgentSync.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8397B066DD0C035A16337D21 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 853EBEF810560AF6E052E02A /* HealthKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitService.swift; sourceTree = ""; }; 9DD18C251995C1C4C1E01177 /* SyncModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncModelTests.swift; sourceTree = ""; }; @@ -171,7 +171,6 @@ LastUpgradeCheck = 1430; TargetAttributes = { 498AFB73209F99F9DE6CE523 = { - DevelopmentTeam = ""; ProvisioningStyle = Automatic; }; 96749EC2F636F901CCB316F2 = { diff --git a/apps/pitch/.gitignore b/apps/pitch/.gitignore new file mode 100644 index 0000000..c71ca52 --- /dev/null +++ b/apps/pitch/.gitignore @@ -0,0 +1,3 @@ +/.next/ +/out/ +*.tsbuildinfo diff --git a/apps/pitch/app/globals.css b/apps/pitch/app/globals.css new file mode 100644 index 0000000..e08c5d8 --- /dev/null +++ b/apps/pitch/app/globals.css @@ -0,0 +1,1438 @@ +:root { + --sage-700: #659287; + --sage-500: #88bda4; + --sage-300: #b1d3b9; + --sage-100: #e6f2dd; + --ink: #10211a; + --ink-soft: #20372d; + --paper: #f5f8f4; + --paper-bright: #fbfcfa; + --line: #cbd7ce; + --line-dark: #365047; + --muted: #637169; + --coral: #ff6846; + --blue: #2679f6; + --white: #ffffff; + --deck-scale: 1; +} + +* { + box-sizing: border-box; +} + +html, +body { + width: 100%; + height: 100%; + overflow: hidden; + background: #07110d; +} + +body { + margin: 0; + color: var(--ink); + font-family: var(--font-geist-sans), Arial, sans-serif; + letter-spacing: 0; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +button { + font: inherit; + letter-spacing: 0; +} + +button:focus-visible { + outline: 4px solid var(--coral); + outline-offset: 4px; +} + +.deck-viewport { + position: fixed; + inset: 0; + overflow: hidden; + background: #07110d; + touch-action: pan-y; + user-select: none; +} + +.deck-stage { + position: absolute; + z-index: 1; + top: 50%; + left: 50%; + width: 1920px; + height: 1080px; + margin: -540px 0 0 -960px; + overflow: hidden; + background: var(--paper); + box-shadow: 0 40px 120px rgba(0, 0, 0, 0.42); + transform: scale(var(--deck-scale)); + transform-origin: center; +} + +.slide-transition, +.slide { + position: absolute; + inset: 0; +} + +.slide { + isolation: isolate; + overflow: hidden; + padding: 68px 104px 82px; +} + +.slide::before, +.slide::after { + position: absolute; + z-index: -1; + content: ""; + pointer-events: none; +} + +.slide::before { + top: 0; + right: 103px; + bottom: 0; + width: 1px; + background: rgba(16, 33, 26, 0.13); +} + +.slide::after { + top: 0; + bottom: 0; + left: 103px; + width: 1px; + background: rgba(16, 33, 26, 0.13); +} + +.slide-light { + background: var(--paper); + color: var(--ink); +} + +.slide-dark { + background: var(--ink); + color: var(--white); +} + +.slide-dark::before { + background: rgba(230, 242, 221, 0.14); +} + +.slide-dark::after { + background: rgba(230, 242, 221, 0.14); +} + +.slide-sage { + background: var(--sage-100); + color: var(--ink); +} + +.slide-header { + position: relative; + z-index: 3; + display: flex; + align-items: center; + justify-content: space-between; + min-height: 52px; + border-bottom: 1px solid rgba(16, 33, 26, 0.15); +} + +.slide-dark .slide-header { + border-color: rgba(230, 242, 221, 0.17); +} + +.slide-brand { + display: flex; + align-items: center; + gap: 15px; + font-size: 20px; + font-weight: 720; +} + +.slide-brand img { + width: 36px; + height: 36px; + border-radius: 9px; + box-shadow: 0 8px 22px rgba(38, 121, 246, 0.16); +} + +.slide-header > p { + display: flex; + align-items: center; + gap: 17px; + margin: 0; + color: var(--muted); + font-family: var(--font-geist-mono), monospace; + font-size: 16px; + font-weight: 650; + text-transform: uppercase; +} + +.slide-dark .slide-header > p { + color: var(--sage-300); +} + +.slide-header > p span { + color: var(--coral); +} + +.slide-title { + position: relative; + z-index: 2; + max-width: 1530px; + margin: 54px 0 0; + font-size: 68px; + font-weight: 660; + line-height: 1.08; + letter-spacing: 0; +} + +.deck-progress { + position: absolute; + z-index: 50; + right: 104px; + bottom: 43px; + left: 104px; + height: 3px; + overflow: hidden; + background: rgba(136, 189, 164, 0.28); +} + +.deck-progress span { + display: block; + width: 100%; + height: 100%; + background: var(--coral); + transform: scaleX(0); + transform-origin: left center; +} + +.slide-counter { + position: absolute; + z-index: 55; + right: 105px; + top: 72px; + display: flex; + align-items: baseline; + gap: 7px; + color: var(--muted); + font-family: var(--font-geist-mono), monospace; + font-size: 15px; +} + +.slide-counter strong { + color: var(--coral); + font-size: 19px; +} + +.slide-counter small { + font-size: 14px; +} + +.deck-controls { + position: absolute; + z-index: 60; + right: 104px; + bottom: 67px; + display: flex; + align-items: center; + gap: 10px; +} + +.deck-controls button { + display: grid; + width: 48px; + height: 48px; + place-items: center; + padding: 0; + border: 1px solid rgba(16, 33, 26, 0.17); + border-radius: 50%; + background: rgba(251, 252, 250, 0.9); + color: var(--ink); + cursor: pointer; + box-shadow: 0 8px 25px rgba(16, 33, 26, 0.08); + transition: background 160ms ease, color 160ms ease, opacity 160ms ease; +} + +.deck-controls button:hover, +.deck-controls button.active { + background: var(--ink); + color: var(--white); +} + +.deck-controls button:disabled { + cursor: default; + opacity: 0.28; +} + +.speaker-notes { + position: absolute; + z-index: 70; + right: 104px; + bottom: 132px; + width: 570px; + padding: 26px 28px 22px; + border: 1px solid rgba(16, 33, 26, 0.17); + border-radius: 7px; + background: rgba(251, 252, 250, 0.97); + color: var(--ink); + box-shadow: 0 26px 70px rgba(0, 0, 0, 0.22); +} + +.notes-heading { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 14px; + color: var(--sage-700); + font-size: 18px; + font-weight: 700; +} + +.notes-heading strong { + margin-left: auto; + color: var(--coral); + font-family: var(--font-geist-mono), monospace; +} + +.speaker-notes p { + margin: 10px 0 0; + color: var(--ink-soft); + font-size: 18px; + line-height: 1.5; +} + +/* Slide 01 */ + +.title-slide { + display: grid; + grid-template-columns: 180px minmax(0, 1fr) 520px; + gap: 0; + padding: 0; + background: var(--paper); + color: var(--ink); +} + +.title-slide::before { + display: none; +} + +.title-slide::after { + display: none; +} + +.title-accent-column { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + gap: 52px; + padding: 0 0 84px; + background: var(--sage-700); + color: var(--sage-100); +} + +.title-accent-column span { + font-family: var(--font-geist-mono), monospace; + font-size: 15px; + font-weight: 650; + writing-mode: vertical-rl; +} + +.title-copy { + display: flex; + min-width: 0; + flex-direction: column; + justify-content: center; + padding: 58px 54px 58px 78px; +} + +.title-kicker { + display: flex; + align-items: center; + gap: 16px; + margin: 0 0 38px; + color: var(--sage-700); + font-family: var(--font-geist-mono), monospace; + font-size: 18px; + font-weight: 650; + text-transform: uppercase; +} + +.title-kicker span { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--coral); +} + +.title-copy h1 { + margin: 0; + font-size: 132px; + font-weight: 720; + line-height: 0.88; + letter-spacing: 0; +} + +.title-copy h1 strong { + color: var(--sage-700); + font-weight: inherit; +} + +.title-statement { + margin: 52px 0 0; + color: var(--ink-soft); + font-size: 45px; + font-weight: 510; + line-height: 1.16; +} + +.title-meta { + display: flex; + align-items: center; + gap: 18px; + margin-top: 58px; + color: var(--muted); + font-family: var(--font-geist-mono), monospace; + font-size: 14px; + font-weight: 650; +} + +.title-meta i { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--coral); +} + +.title-visual { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + gap: 26px; + overflow: hidden; + padding: 72px 70px; + border-left: 1px solid rgba(16, 33, 26, 0.13); + background: var(--ink); + color: var(--white); +} + +.title-icon-frame { + position: relative; + z-index: 2; + width: 330px; + height: 330px; + margin-bottom: 48px; + padding: 18px; + border: 1px solid rgba(230, 242, 221, 0.22); + background: rgba(230, 242, 221, 0.08); +} + +.title-icon-frame img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 60px; +} + +.title-signal { + position: absolute; + top: 92px; + right: -36px; + width: 205px; + height: 205px; + border: 1px solid rgba(136, 189, 164, 0.27); + border-radius: 50%; +} + +.title-signal span { + position: absolute; + top: 50%; + left: 50%; + display: block; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--coral); +} + +.title-signal span:nth-child(1) { transform: translate(-4px, -98px); } +.title-signal span:nth-child(2) { transform: translate(83px, -28px); } +.title-signal span:nth-child(3) { transform: translate(48px, 70px); } +.title-signal span:nth-child(4) { transform: translate(-68px, 65px); } +.title-signal span:nth-child(5) { transform: translate(-90px, -44px); } + +.title-visual > p { + display: flex; + align-items: center; + gap: 17px; + margin: 0; + padding: 18px 0; + border-top: 1px solid rgba(230, 242, 221, 0.17); + color: var(--sage-100); + font-family: var(--font-geist-mono), monospace; + font-size: 16px; + font-weight: 650; +} + +/* Slide 02 */ + +.problem-layout { + display: grid; + grid-template-columns: 0.94fr 1.06fr; + gap: 82px; + margin-top: 62px; +} + +.problem-question { + display: flex; + min-height: 500px; + flex-direction: column; + justify-content: center; + padding: 0 30px 0 0; +} + +.problem-question > svg { + margin-bottom: 30px; + color: var(--coral); +} + +.problem-question blockquote { + margin: 0; + color: var(--ink); + font-size: 48px; + font-weight: 530; + line-height: 1.35; +} + +.problem-question blockquote strong { + color: var(--sage-700); + font-weight: 690; +} + +.problem-question p { + max-width: 650px; + margin: 42px 0 0; + color: var(--muted); + font-size: 25px; + line-height: 1.45; +} + +.generic-answer { + align-self: center; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 7px; + background: var(--paper-bright); + box-shadow: 0 34px 90px rgba(16, 33, 26, 0.1); +} + +.answer-window-head { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 70px; + padding: 0 30px; + border-bottom: 1px solid var(--line); +} + +.answer-window-head span { + display: flex; + align-items: center; + gap: 11px; + font-size: 20px; + font-weight: 680; +} + +.answer-window-head small { + color: var(--coral); + font-family: var(--font-geist-mono), monospace; + font-size: 13px; + font-weight: 650; +} + +.generic-answer > p { + min-height: 220px; + margin: 0; + padding: 54px 48px; + color: var(--muted); + font-size: 30px; + line-height: 1.55; +} + +.missing-facts { + display: grid; + border-top: 1px solid var(--line); +} + +.missing-facts span { + display: flex; + align-items: center; + gap: 14px; + min-height: 68px; + padding: 0 48px; + border-bottom: 1px solid var(--line); + color: var(--ink-soft); + font-size: 20px; +} + +.missing-facts span:last-child { + border-bottom: 0; +} + +.missing-facts svg { + color: var(--sage-700); +} + +.problem-bottom-line { + display: flex; + align-items: center; + gap: 16px; + margin: 44px 0 0; + color: var(--ink-soft); + font-size: 25px; + font-weight: 590; +} + +.problem-bottom-line svg { + color: var(--coral); +} + +/* Slide 03 */ + +.route-diagram { + position: relative; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 58px; + margin-top: 88px; +} + +.route-line { + position: absolute; + z-index: -1; + top: 77px; + right: 90px; + left: 90px; + height: 2px; + background: var(--sage-300); +} + +.route-line span { + position: absolute; + top: -7px; + width: 16px; + height: 16px; + border: 4px solid var(--paper); + border-radius: 50%; + background: var(--coral); + box-shadow: 0 0 0 1px rgba(255, 104, 70, 0.25); +} + +.route-node { + display: flex; + align-items: center; + flex-direction: column; + text-align: center; +} + +.route-node-icon { + display: grid; + width: 154px; + height: 154px; + place-items: center; + border: 1px solid var(--sage-300); + border-radius: 50%; + background: var(--paper-bright); + color: var(--ink); + box-shadow: 0 22px 50px rgba(16, 33, 26, 0.08); +} + +.route-node strong { + margin-top: 29px; + font-size: 27px; +} + +.route-node small { + margin-top: 10px; + color: var(--muted); + font-family: var(--font-geist-mono), monospace; + font-size: 15px; +} + +.route-principles { + display: grid; + grid-template-columns: repeat(3, 1fr); + margin-top: 70px; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.route-principles p { + display: flex; + align-items: center; + gap: 18px; + min-height: 108px; + margin: 0; + padding: 0 34px; + border-right: 1px solid var(--line); + font-size: 20px; +} + +.route-principles p:first-child { + padding-left: 0; +} + +.route-principles p:last-child { + border-right: 0; +} + +.route-principles svg { + color: var(--sage-700); +} + +.route-principles span { + display: grid; + gap: 5px; + color: var(--muted); +} + +.route-principles strong { + color: var(--ink); + font-size: 24px; +} + +.route-caption { + margin: 38px 0 0; + color: var(--ink-soft); + font-size: 25px; + font-weight: 570; +} + +/* Slide 04 */ + +.demo-slide .slide-title { + margin-top: 38px; +} + +.demo-frame { + position: relative; + height: 670px; + margin-top: 34px; + overflow: hidden; + border: 1px solid rgba(230, 242, 221, 0.22); + border-radius: 7px; + background: #07110d; + box-shadow: 0 35px 90px rgba(0, 0, 0, 0.3); +} + +.demo-frame video { + width: 100%; + height: 100%; + object-fit: cover; + background: #07110d; +} + +.demo-placeholder { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background: var(--ink-soft); +} + +.demo-placeholder::before { + position: absolute; + inset: 74px; + border: 1px solid rgba(230, 242, 221, 0.15); + content: ""; +} + +.demo-placeholder img { + position: absolute; + top: 46px; + left: 46px; + width: 68px; + height: 68px; + border-radius: 15px; +} + +.demo-placeholder > span { + display: grid; + width: 102px; + height: 102px; + place-items: center; + border: 1px solid rgba(230, 242, 221, 0.35); + border-radius: 50%; + color: var(--sage-100); +} + +.demo-placeholder h3 { + margin: 30px 0 0; + font-size: 42px; +} + +.demo-placeholder p { + margin: 14px 0 0; + color: var(--sage-300); + font-family: var(--font-geist-mono), monospace; + font-size: 19px; +} + +.demo-placeholder small { + margin-top: 28px; + color: var(--coral); + font-size: 16px; +} + +.video-toggle { + position: absolute; + right: 34px; + bottom: 34px; + display: grid; + width: 74px; + height: 74px; + place-items: center; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + background: rgba(16, 33, 26, 0.78); + color: var(--white); + cursor: pointer; +} + +.demo-sequence { + position: absolute; + right: 124px; + bottom: 46px; + left: 42px; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 22px; +} + +.demo-sequence span { + padding-top: 15px; + border-top: 2px solid rgba(230, 242, 221, 0.22); + color: rgba(230, 242, 221, 0.56); + font-family: var(--font-geist-mono), monospace; + font-size: 14px; + font-weight: 650; +} + +.demo-sequence span.active { + border-color: var(--coral); + color: var(--white); +} + +.demo-hint { + display: flex; + align-items: center; + gap: 12px; + margin: 22px 0 0; + color: var(--sage-300); + font-size: 17px; +} + +/* Slide 05 */ + +.evidence-layout { + display: grid; + grid-template-columns: 1.45fr 0.55fr; + gap: 70px; + margin-top: 52px; +} + +.agent-capture { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 7px; + background: var(--paper-bright); + box-shadow: 0 34px 90px rgba(16, 33, 26, 0.1); +} + +.capture-head { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 68px; + padding: 0 31px; + border-bottom: 1px solid var(--line); +} + +.capture-head span { + display: flex; + align-items: center; + gap: 12px; + font-size: 19px; + font-weight: 680; +} + +.capture-head small { + color: var(--coral); + font-family: var(--font-geist-mono), monospace; + font-size: 13px; + font-weight: 650; +} + +.capture-question { + margin: 0; + padding: 36px 43px 29px; + border-bottom: 1px solid var(--line); + color: var(--muted); + font-size: 22px; +} + +.capture-answer { + padding: 43px; +} + +.capture-answer > p { + margin: 0; + color: var(--ink); + font-size: 31px; + line-height: 1.55; +} + +.capture-facts { + display: flex; + gap: 0; + margin-top: 42px; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.capture-facts span { + flex: 1; + padding: 22px 24px; + border-right: 1px solid var(--line); + color: var(--muted); + font-size: 17px; +} + +.capture-facts span:first-child { + padding-left: 0; +} + +.capture-facts span:last-child { + border-right: 0; +} + +.capture-facts strong { + display: block; + margin-top: 5px; + color: var(--ink); + font-size: 25px; +} + +.agent-capture footer { + display: flex; + align-items: center; + gap: 32px; + min-height: 65px; + padding: 0 31px; + border-top: 1px solid var(--line); + background: var(--sage-100); + color: var(--sage-700); + font-family: var(--font-geist-mono), monospace; + font-size: 14px; +} + +.agent-capture footer span { + display: flex; + align-items: center; + gap: 9px; +} + +.evidence-notes { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0; +} + +.evidence-note { + display: grid; + grid-template-columns: 54px 1fr; + gap: 20px; + padding: 30px 0; + border-top: 1px solid var(--line); +} + +.evidence-note:last-child { + border-bottom: 1px solid var(--line); +} + +.evidence-note > span { + color: var(--coral); + font-family: var(--font-geist-mono), monospace; + font-size: 17px; +} + +.evidence-note strong { + font-size: 25px; +} + +.evidence-note p { + margin: 9px 0 0; + color: var(--muted); + font-size: 19px; + line-height: 1.5; +} + +.evidence-thesis { + margin: 36px 0 0; + color: var(--sage-700); + font-family: var(--font-geist-mono), monospace; + font-size: 20px; + font-weight: 650; +} + +/* Slide 06 */ + +.why-layout { + display: flex; + min-height: 770px; + flex-direction: column; + justify-content: center; +} + +.why-layout blockquote { + max-width: 1530px; + margin: 0; + font-size: 76px; + font-weight: 560; + line-height: 1.18; +} + +.why-layout blockquote strong { + color: var(--sage-300); + font-weight: 690; +} + +.why-sequence { + display: grid; + grid-template-columns: 1fr 52px 1fr 52px 1fr; + align-items: center; + gap: 26px; + margin-top: 92px; +} + +.why-sequence > svg { + color: var(--coral); +} + +.why-sequence > div { + min-height: 192px; + padding: 28px 28px 24px; + border-top: 1px solid rgba(230, 242, 221, 0.24); +} + +.why-sequence > div.missing-link { + border-top-color: var(--coral); + background: rgba(230, 242, 221, 0.07); +} + +.why-sequence span { + display: flex; + align-items: center; + justify-content: space-between; + color: var(--sage-300); + font-family: var(--font-geist-mono), monospace; + font-size: 15px; +} + +.why-sequence strong { + display: block; + margin-top: 23px; + font-size: 27px; +} + +.why-sequence p { + margin: 10px 0 0; + color: rgba(230, 242, 221, 0.69); + font-size: 19px; + line-height: 1.45; +} + +.why-footer { + position: absolute; + bottom: 92px; + left: 104px; + margin: 0; + color: var(--sage-300); + font-family: var(--font-geist-mono), monospace; + font-size: 19px; +} + +/* Slide 07 */ + +.scaling-slide .slide-title { + margin-top: 42px; +} + +.scaling-layout { + display: grid; + grid-template-columns: 1fr 128px 1fr; + gap: 48px; + margin-top: 54px; +} + +.scaling-column { + position: relative; + min-height: 460px; + padding: 34px 40px 30px; + border-top: 5px solid var(--ink); + background: rgba(251, 252, 250, 0.75); +} + +.context-column { + border-color: var(--sage-700); + background: var(--sage-100); +} + +.scaling-index { + position: absolute; + top: 32px; + right: 38px; + color: var(--coral); + font-family: var(--font-geist-mono), monospace; + font-size: 17px; +} + +.scaling-column h3 { + margin: 0; + font-size: 58px; + line-height: 0.95; +} + +.context-column h3 { + color: var(--sage-700); +} + +.scaling-terms { + display: flex; + flex-wrap: wrap; + gap: 10px 24px; + margin-top: 48px; +} + +.scaling-terms span { + padding: 10px 0; + border-bottom: 1px solid var(--line); + color: var(--ink-soft); + font-size: 20px; +} + +.scaling-column > p { + position: absolute; + bottom: 30px; + left: 40px; + margin: 0; + color: var(--muted); + font-size: 23px; + font-weight: 620; +} + +.scaling-divider { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 15px; + color: var(--sage-700); +} + +.scaling-divider span { + width: 1px; + height: 180px; + background: var(--line); +} + +.scaling-divider svg:first-child { + color: var(--ink); +} + +.scaling-statement { + display: grid; + grid-template-columns: 1fr 1.1fr; + gap: 64px; + align-items: center; + margin-top: 38px; + padding-top: 30px; + border-top: 1px solid var(--line); +} + +.scaling-statement p, +.scaling-statement strong { + margin: 0; + font-size: 27px; + line-height: 1.35; +} + +.scaling-statement p { + color: var(--muted); +} + +.scaling-statement strong { + color: var(--sage-700); + font-weight: 660; +} + +.scaling-definition { + display: block; + max-width: 1480px; + margin-top: 23px; + color: var(--muted); + font-family: var(--font-geist-mono), monospace; + font-size: 14px; + line-height: 1.5; +} + +/* Slide 08 */ + +.ownership-slide .slide-title { + max-width: 1350px; +} + +.ownership-flow { + display: grid; + grid-template-columns: 1fr 66px 410px 66px 1fr; + align-items: center; + gap: 22px; + min-height: 520px; + margin-top: 55px; +} + +.ownership-group { + min-width: 0; +} + +.ownership-group > p { + margin: 0 0 24px; + color: var(--sage-700); + font-family: var(--font-geist-mono), monospace; + font-size: 15px; + font-weight: 650; +} + +.ownership-group > div { + display: grid; + border-top: 1px solid rgba(16, 33, 26, 0.18); +} + +.ownership-group span { + display: flex; + align-items: center; + gap: 18px; + min-height: 92px; + border-bottom: 1px solid rgba(16, 33, 26, 0.18); + font-size: 24px; + font-weight: 620; +} + +.ownership-group svg { + color: var(--sage-700); +} + +.ownership-arrow { + display: grid; + place-items: center; + color: var(--coral); +} + +.context-core { + position: relative; + display: flex; + min-height: 340px; + align-items: center; + justify-content: center; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--sage-700); + background: var(--ink); + color: var(--white); + text-align: center; +} + +.context-core > img { + position: absolute; + top: 24px; + left: 24px; + width: 55px; + height: 55px; + border-radius: 13px; +} + +.context-core > svg { + color: var(--sage-300); +} + +.context-core strong { + margin-top: 22px; + font-size: 31px; + line-height: 1.18; +} + +.context-core small { + margin-top: 19px; + color: var(--sage-300); + font-family: var(--font-geist-mono), monospace; + font-size: 14px; +} + +.ownership-principles { + display: grid; + grid-template-columns: repeat(4, 1fr); + border-top: 1px solid rgba(16, 33, 26, 0.18); + border-bottom: 1px solid rgba(16, 33, 26, 0.18); +} + +.ownership-principles span { + display: flex; + min-height: 88px; + align-items: center; + gap: 12px; + padding: 0 24px; + border-right: 1px solid rgba(16, 33, 26, 0.18); + font-size: 17px; + font-weight: 590; +} + +.ownership-principles span:last-child { + border-right: 0; +} + +.ownership-principles svg { + flex: 0 0 auto; + color: var(--coral); +} + +/* Slide 09 */ + +.vision-layout { + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap: 110px; + align-items: end; + margin-top: 122px; +} + +.vision-copy h2 { + margin: 0; + font-size: 91px; + font-weight: 620; + line-height: 1.02; +} + +.vision-copy h2 strong { + color: var(--sage-300); + font-weight: 690; +} + +.vision-copy > p { + max-width: 820px; + margin: 40px 0 0; + color: rgba(230, 242, 221, 0.68); + font-size: 27px; + line-height: 1.48; +} + +.vision-path { + display: grid; + grid-template-columns: 1fr 48px 1fr; + align-items: center; + gap: 20px; +} + +.vision-path > svg { + color: var(--coral); +} + +.vision-path > div { + min-height: 250px; + padding: 25px 0 0; + border-top: 1px solid rgba(230, 242, 221, 0.22); +} + +.vision-path span { + display: block; + margin-bottom: 40px; + color: var(--coral); + font-family: var(--font-geist-mono), monospace; + font-size: 15px; +} + +.vision-path div > svg { + color: var(--sage-300); +} + +.vision-path strong { + display: block; + margin-top: 18px; + font-size: 25px; +} + +.vision-path p { + margin: 10px 0 0; + color: rgba(230, 242, 221, 0.62); + font-size: 17px; + line-height: 1.45; +} + +.vision-quote { + margin: 88px 0 0; + padding: 35px 0 0; + border-top: 1px solid rgba(230, 242, 221, 0.22); + color: var(--sage-100); + font-size: 35px; + line-height: 1.4; +} + +.vision-quote strong { + color: var(--sage-300); +} + +.vision-signoff { + position: absolute; + right: 390px; + bottom: 88px; + display: grid; + grid-template-columns: 44px auto; + gap: 2px 14px; + align-items: center; +} + +.vision-signoff img { + grid-row: 1 / span 2; + width: 44px; + height: 44px; + border-radius: 10px; +} + +.vision-signoff span { + font-size: 18px; + font-weight: 700; +} + +.vision-signoff small { + color: var(--sage-300); + font-family: var(--font-geist-mono), monospace; + font-size: 11px; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} diff --git a/apps/pitch/app/layout.tsx b/apps/pitch/app/layout.tsx new file mode 100644 index 0000000..2f3968d --- /dev/null +++ b/apps/pitch/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { GeistMono } from "geist/font/mono"; +import { GeistSans } from "geist/font/sans"; +import appIcon from "../../ios/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Vital Agent Sync | Competition Deck", + description: "A presentation about private, user-owned context for AI agents.", + icons: { + icon: [{ url: appIcon.src, type: "image/png", sizes: "1024x1024" }], + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/apps/pitch/app/page.tsx b/apps/pitch/app/page.tsx new file mode 100644 index 0000000..50ae157 --- /dev/null +++ b/apps/pitch/app/page.tsx @@ -0,0 +1,876 @@ +"use client"; + +import { + Activity, + ArrowLeft, + ArrowRight, + Bot, + BrainCircuit, + CalendarDays, + Check, + ChevronLeft, + ChevronRight, + CircleUserRound, + Clock3, + Database, + FileHeart, + Fingerprint, + HeartPulse, + Maximize2, + MessageSquareText, + Minimize2, + Network, + NotebookTabs, + Pause, + Play, + RefreshCcw, + ShieldCheck, + Smartphone, + Sparkles, + Terminal, +} from "lucide-react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { + useCallback, + useEffect, + useRef, + useState, + type CSSProperties, + type ReactNode, + type TouchEvent, +} from "react"; +import appIcon from "../../ios/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png"; + +const STAGE_WIDTH = 1920; +const STAGE_HEIGHT = 1080; + +const slides = [ + { + id: "title", + title: "Vital Agent Sync", + notes: [ + "Vital Agent Sync 让 AI Agent 获得新鲜、经过授权的 Apple Health 上下文,同时让数据继续由用户控制。", + "开场不要介绍功能清单。先让评委记住产品定位:Apple Health context. Private by design.", + ], + }, + { + id: "problem", + title: "AI 可以理解世界,却不知道今天的你", + notes: [ + "用一个具体问题建立冲突:根据我昨晚的睡眠和今天的状态,我适合高强度训练吗?", + "模型可能很强,但缺少当天、可验证的个人状态时,只能给出泛化答案。", + ], + }, + { + id: "route", + title: "一条由用户控制的 Context 路径", + notes: [ + "Agent 不直接访问 HealthKit。iPhone 先生成摘要,再发送到用户自己的 Receiver。", + "Receiver 通过 MCP 提供带范围和新鲜度的上下文。核心词是 Scoped、Fresh、Revocable。", + ], + }, + { + id: "demo", + title: "从连接到第一次回答", + notes: [ + "播放 45–60 秒演示视频。视频应包含 Agent 发起连接、iPhone 确认权限、手动同步和 Agent 回答。", + "不要在视频中暴露真实二维码、Token、服务器地址或可识别的健康信息。", + ], + }, + { + id: "evidence", + title: "Agent 得到证据,而不是 HealthKit 权限", + notes: [ + "用真实 Agent 回答截图替换当前示例界面。", + "重点指向更新时间、观测事实和推断边界。这一页证明产品不是单纯的数据转发。", + ], + }, + { + id: "why", + title: "我不想用交出所有数据,换取一个更懂我的 AI", + notes: [ + "这里讲个人动机,不讲架构。Apple Health 已经记录了真实状态,模型也已经足够聪明,但两者之间缺少一条由用户控制的连接。", + "补充一个真实触发场景,会比抽象观点更有说服力。", + ], + }, + { + id: "scaling", + title: "下一条 Scaling 轴,是 Context", + notes: [ + "Context scaling 不是把 context window 做得更长,也不是把所有私人数据塞给模型。", + "它是持续获得、筛选、验证和安全使用真实世界上下文的能力。", + ], + }, + { + id: "ownership", + title: "Context 应该扩展,但所有权不应转移", + notes: [ + "更懂用户的 AI,不应该自动意味着平台拥有更多用户数据。", + "用户决定数据来源、Agent 权限和撤销时机。Health 是这个架构的第一个压力测试。", + ], + }, + { + id: "vision", + title: "Health 是起点,不是边界", + notes: [ + "收束到长期方向:从 Apple Health 走向用户拥有的 Agent Context 层。", + "最后一句:我想验证的是,私人 Context 能否像模型能力一样扩展,同时仍然属于用户。", + ], + }, +] as const; + +type SlideProps = { + active: boolean; +}; + +type VideoState = "loading" | "ready" | "missing"; + +export default function PitchDeck() { + const prefersReducedMotion = useReducedMotion(); + const [activeSlide, setActiveSlide] = useState(0); + const [direction, setDirection] = useState(1); + const [deckScale, setDeckScale] = useState(1); + const [notesOpen, setNotesOpen] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); + const touchStartX = useRef(null); + + const goToSlide = useCallback((nextIndex: number) => { + const boundedIndex = Math.max(0, Math.min(slides.length - 1, nextIndex)); + setActiveSlide((currentIndex) => { + if (boundedIndex === currentIndex) { + return currentIndex; + } + setDirection(boundedIndex > currentIndex ? 1 : -1); + return boundedIndex; + }); + }, []); + + const goForward = useCallback(() => { + setActiveSlide((currentIndex) => { + const nextIndex = Math.min(slides.length - 1, currentIndex + 1); + if (nextIndex !== currentIndex) { + setDirection(1); + } + return nextIndex; + }); + }, []); + + const goBack = useCallback(() => { + setActiveSlide((currentIndex) => { + const nextIndex = Math.max(0, currentIndex - 1); + if (nextIndex !== currentIndex) { + setDirection(-1); + } + return nextIndex; + }); + }, []); + + const toggleFullscreen = useCallback(async () => { + if (document.fullscreenElement) { + await document.exitFullscreen(); + return; + } + await document.documentElement.requestFullscreen(); + }, []); + + useEffect(() => { + const hashMatch = window.location.hash.match(/^#slide-(\d+)$/); + if (hashMatch) { + const requestedIndex = Number(hashMatch[1]) - 1; + if (requestedIndex >= 0 && requestedIndex < slides.length) { + setActiveSlide(requestedIndex); + } + } + }, []); + + useEffect(() => { + window.history.replaceState(null, "", `#slide-${activeSlide + 1}`); + }, [activeSlide]); + + useEffect(() => { + function updateScale() { + const nextScale = Math.min(window.innerWidth / STAGE_WIDTH, window.innerHeight / STAGE_HEIGHT); + setDeckScale(nextScale); + } + + updateScale(); + window.addEventListener("resize", updateScale); + return () => window.removeEventListener("resize", updateScale); + }, []); + + useEffect(() => { + function handleFullscreenChange() { + setIsFullscreen(Boolean(document.fullscreenElement)); + } + + document.addEventListener("fullscreenchange", handleFullscreenChange); + return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); + }, []); + + useEffect(() => { + function preventWheel(event: WheelEvent) { + event.preventDefault(); + } + + window.addEventListener("wheel", preventWheel, { passive: false }); + return () => window.removeEventListener("wheel", preventWheel); + }, []); + + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (event.repeat) { + return; + } + + if (event.key === "ArrowRight" || event.key === "PageDown" || event.key === " ") { + event.preventDefault(); + goForward(); + return; + } + + if (event.key === "ArrowLeft" || event.key === "PageUp") { + event.preventDefault(); + goBack(); + return; + } + + if (event.key === "Home") { + event.preventDefault(); + goToSlide(0); + return; + } + + if (event.key === "End") { + event.preventDefault(); + goToSlide(slides.length - 1); + return; + } + + if (event.key.toLowerCase() === "f") { + event.preventDefault(); + void toggleFullscreen(); + return; + } + + if (event.key.toLowerCase() === "n") { + event.preventDefault(); + setNotesOpen((isOpen) => !isOpen); + return; + } + + if (event.key.toLowerCase() === "r") { + event.preventDefault(); + goToSlide(0); + return; + } + + if (event.key.toLowerCase() === "v" && activeSlide === 3) { + event.preventDefault(); + window.dispatchEvent(new Event("pitch:toggle-video")); + } + } + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [activeSlide, goBack, goForward, goToSlide, toggleFullscreen]); + + function handleTouchStart(event: TouchEvent) { + touchStartX.current = event.touches[0]?.clientX ?? null; + } + + function handleTouchEnd(event: TouchEvent) { + if (touchStartX.current === null) { + return; + } + + const endX = event.changedTouches[0]?.clientX ?? touchStartX.current; + const deltaX = endX - touchStartX.current; + touchStartX.current = null; + + if (Math.abs(deltaX) < 80) { + return; + } + + if (deltaX < 0) { + goForward(); + } else { + goBack(); + } + } + + const stageStyle = { + "--deck-scale": deckScale, + } as CSSProperties; + + const currentSlide = slides[activeSlide]; + + return ( +
+
+ + + + + + + setNotesOpen((isOpen) => !isOpen)} + onToggleFullscreen={() => void toggleFullscreen()} + /> + + + {notesOpen ? ( + +
+ + 演讲备注 + {String(activeSlide + 1).padStart(2, "0")} +
+ {currentSlide.notes.map((note) => ( +

{note}

+ ))} +
+ ) : null} +
+
+
+ ); +} + +function SlideRenderer({ index }: { index: number }) { + switch (index) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + case 6: + return ; + case 7: + return ; + case 8: + return ; + default: + return null; + } +} + +function DeckChrome({ + activeSlide, + notesOpen, + isFullscreen, + onBack, + onForward, + onToggleNotes, + onToggleFullscreen, +}: { + activeSlide: number; + notesOpen: boolean; + isFullscreen: boolean; + onBack: () => void; + onForward: () => void; + onToggleNotes: () => void; + onToggleFullscreen: () => void; +}) { + return ( + <> + + + {activeSlide === 0 ? ( +
+ {String(activeSlide + 1).padStart(2, "0")} + / + {String(slides.length).padStart(2, "0")} +
+ ) : null} + + + + ); +} + +function SlideShell({ + number, + kicker, + title, + theme = "light", + className = "", + children, +}: { + number: string; + kicker: string; + title?: string; + theme?: "light" | "dark" | "sage"; + className?: string; + children: ReactNode; +}) { + return ( +
+
+
+ + Vital Agent Sync +
+

+ {number} + {kicker} +

+
+ {title ?

{title}

: null} + {children} +
+ ); +} + +function TitleSlide(_: SlideProps) { + return ( +
+ + +
+

Competition deck · Local Preview

+

Vital Agent
Sync

+

Apple Health context.
Private by design.

+
+ USER-OWNED CONTEXT + + MCP NATIVE + + AGENT NEUTRAL +
+
+ +
+
+ Vital Agent Sync app icon +
+ +

HEALTH CONTEXT

+

USER CONTROL

+

AUTHORIZED AGENT

+
+
+ ); +} + +function ProblemSlide(_: SlideProps) { + return ( + +
+
+ +
“根据我昨晚的睡眠和今天的状态,我适合高强度训练吗?”
+

这是一个关于“我”的问题,不是一个关于健康知识的问题。

+
+ +
+
+ Generic AI + NO PERSONAL CONTEXT +
+

一般来说,是否适合高强度训练取决于睡眠、恢复、训练经验和身体感受……

+
+ 不知道数据是否最新 + 不知道昨晚睡了多久 + 无法验证个人状态 +
+
+
+

更强的推理,面对缺失的 Context,仍然只能给出泛化答案。

+
+ ); +} + +const routeNodes = [ + { icon: Smartphone, label: "Apple Health", detail: "authorized on iPhone" }, + { icon: FileHeart, label: "Health summaries", detail: "compact, not raw samples" }, + { icon: Database, label: "Local receiver", detail: "user-owned SQLite" }, + { icon: Bot, label: "Authorized Agent", detail: "fresh context through MCP" }, +]; + +function RouteSlide(_: SlideProps) { + return ( + +
+ + {routeNodes.map((node, index) => { + const Icon = node.icon; + return ( + + + {node.label} + {node.detail} + + ); + })} +
+ +
+

Scoped只暴露用户授权的摘要

+

Fresh每次回答带数据新鲜度

+

Revocable权限可以审计和撤销

+
+

Agent 不直接访问 HealthKit。Context 先经过授权、摘要和本地边界。

+
+ ); +} + +function DemoSlide(_: SlideProps) { + const videoRef = useRef(null); + const [videoState, setVideoState] = useState("loading"); + const [isPlaying, setIsPlaying] = useState(false); + + const toggleVideo = useCallback(() => { + const video = videoRef.current; + if (!video || videoState !== "ready") { + return; + } + + if (video.paused) { + void video.play(); + } else { + video.pause(); + } + }, [videoState]); + + useEffect(() => { + window.addEventListener("pitch:toggle-video", toggleVideo); + return () => window.removeEventListener("pitch:toggle-video", toggleVideo); + }, [toggleVideo]); + + return ( + +
+
+

点击画面或按 V 播放 / 暂停

+
+ ); +} + +function EvidenceSlide(_: SlideProps) { + return ( + +
+
+
+ Agent response + ILLUSTRATIVE OUTPUT +
+

How ready am I for a hard workout today?

+
+

你的恢复信号整体稳定,但昨晚睡眠比七日均值少了 38 分钟。建议今天先保持中等强度,并在热身后重新评估身体感受。

+
+ Sleep 6h 42m + HRV 42 ms + Resting HR 61 +
+
+
+ Synced 2 min ago + health.daily.read +
+
+ +
+ + + +
+
+

Fresh context. Explicit scope. No direct HealthKit access.

+
+ ); +} + +function EvidenceNote({ number, title, body }: { number: string; title: string; body: string }) { + return ( +
+ {number} +
+ {title} +

{body}

+
+
+ ); +} + +function WhySlide(_: SlideProps) { + return ( + +
+
我不想用交出所有数据,换取一个更懂我的 AI。
+ +
+
+ 01 + Apple Health +

已经记录了真实、连续的个人状态。

+
+ +
+ 02 + Better models +

已经具备分析和推理这些信息的能力。

+
+ +
+ 03 + The missing link +

缺少一条持续、安全、由用户控制的连接。

+
+
+
+

The problem was not intelligence. It was access to trustworthy context.

+
+ ); +} + +const modelScaling = ["Reasoning", "Multimodality", "Tool use", "Capability"]; +const contextScaling = ["Identity", "Memory", "Real-world state", "Freshness", "Permission"]; + +function ScalingSlide(_: SlideProps) { + return ( + +
+
+ 01 +

MODEL
SCALING

+
+ {modelScaling.map((term) => {term})} +
+

提升智能的能力上限

+
+ + + +
+ 02 +

CONTEXT
SCALING

+
+ {contextScaling.map((term) => {term})} +
+

让智能进入真实世界

+
+
+ +
+

Model scaling determines what AI can do.

+ Context scaling determines whether it can do it for me. +
+ Context scaling ≠ a longer context window. It is the ability to acquire, verify, retrieve and safely use relevant context over time. +
+ ); +} + +function OwnershipSlide(_: SlideProps) { + return ( + +
+
+

PERSONAL DATA SOURCES

+
+ Health + Calendar + Feedback +
+
+ +
+ +
+ + + User-owned
context layer
+ scope · freshness · audit · revoke +
+ +
+ +
+

AUTHORIZED AGENTS

+
+ WorkBuddy + OpenClaw + Any MCP Agent +
+
+
+ +
+ 用户决定接入什么 + 每个 Agent 获得有限权限 + Context 带来源和新鲜度 + 用户随时审计和撤销 +
+
+ ); +} + +function VisionSlide(_: SlideProps) { + return ( + +
+
+

Health 是起点,
不是边界。

+

从一条可验证的 Apple Health 路径开始,走向用户拥有的 Agent Context 层。

+
+ +
+
+ NOW + + Apple Health +

一台 iPhone · 一条授权路径

+
+ +
+ NEXT + + Personal context +

更多数据源 · 更多授权 Agent

+
+
+
+ +
+ “我想验证的是:私人 Context 能否像模型能力一样扩展,同时仍然属于用户。” +
+ +
+ + Vital Agent Sync + PRIVATE APPLE HEALTH BRIDGE +
+
+ ); +} + +const slideVariants = { + enter: (direction: number) => ({ + opacity: 0, + x: direction > 0 ? 110 : -110, + }), + center: { + opacity: 1, + x: 0, + }, + exit: (direction: number) => ({ + opacity: 0, + x: direction > 0 ? -70 : 70, + }), +}; + +const reducedSlideVariants = { + enter: { opacity: 1, x: 0 }, + center: { opacity: 1, x: 0 }, + exit: { opacity: 1, x: 0 }, +}; diff --git a/apps/pitch/next-env.d.ts b/apps/pitch/next-env.d.ts new file mode 100644 index 0000000..9edff1c --- /dev/null +++ b/apps/pitch/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/pitch/next.config.ts b/apps/pitch/next.config.ts new file mode 100644 index 0000000..dd549b8 --- /dev/null +++ b/apps/pitch/next.config.ts @@ -0,0 +1,11 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "export", + outputFileTracingRoot: new URL("../..", import.meta.url).pathname, + images: { + unoptimized: true, + }, +}; + +export default nextConfig; diff --git a/apps/pitch/package.json b/apps/pitch/package.json new file mode 100644 index 0000000..7a63a2d --- /dev/null +++ b/apps/pitch/package.json @@ -0,0 +1,24 @@ +{ + "name": "vital-agent-sync-pitch", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "node serve.mjs", + "typecheck": "tsc --noEmit" + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^24.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "geist": "^1.7.2", + "lucide-react": "^0.468.0", + "motion": "^12.42.2", + "next": "16.2.12", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.8.0" + } +} diff --git a/apps/pitch/serve.mjs b/apps/pitch/serve.mjs new file mode 100644 index 0000000..d47e60b --- /dev/null +++ b/apps/pitch/serve.mjs @@ -0,0 +1,38 @@ +import { createReadStream, existsSync, statSync } from "node:fs"; +import { createServer } from "node:http"; +import { extname, join, normalize } from "node:path"; + +const root = new URL("./out/", import.meta.url).pathname; +const port = Number(process.env.PORT ?? 3100); + +const mimeTypes = { + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".mp4": "video/mp4", + ".png": "image/png", + ".svg": "image/svg+xml", + ".webm": "video/webm", + ".woff2": "font/woff2", +}; + +createServer((request, response) => { + const requestPath = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname); + const safePath = normalize(requestPath).replace(/^(\.\.[/\\])+/, ""); + let filePath = join(root, safePath === "/" ? "index.html" : safePath); + + if (existsSync(filePath) && statSync(filePath).isDirectory()) { + filePath = join(filePath, "index.html"); + } + + if (!existsSync(filePath)) { + filePath = join(root, "index.html"); + } + + response.setHeader("Content-Type", mimeTypes[extname(filePath)] ?? "application/octet-stream"); + response.setHeader("Cache-Control", "no-store"); + createReadStream(filePath).pipe(response); +}).listen(port, "127.0.0.1", () => { + console.log(`Vital Agent Sync pitch deck: http://127.0.0.1:${port}`); +}); diff --git a/apps/pitch/tsconfig.json b/apps/pitch/tsconfig.json new file mode 100644 index 0000000..d73edca --- /dev/null +++ b/apps/pitch/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/apps/www/app/globals.css b/apps/www/app/globals.css index 6477089..d923ed2 100644 --- a/apps/www/app/globals.css +++ b/apps/www/app/globals.css @@ -201,9 +201,12 @@ code { gap: 3px; } -.header-nav button { +.header-nav button, +.header-nav > a { position: relative; min-height: 34px; + display: inline-flex; + align-items: center; padding: 0 11px; border: 0; background: transparent; @@ -215,7 +218,8 @@ code { transition: opacity 180ms ease, color 180ms ease; } -.header-nav button::after { +.header-nav button::after, +.header-nav > a::after { position: absolute; left: 11px; right: 11px; @@ -229,7 +233,8 @@ code { } .header-nav button:hover, -.header-nav button.active { +.header-nav button.active, +.header-nav > a:hover { opacity: 1; } @@ -1995,6 +2000,556 @@ code { color: var(--white); } +.project-brief { + min-height: 100vh; + background: + linear-gradient(rgba(16, 33, 26, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(16, 33, 26, 0.035) 1px, transparent 1px), + var(--paper); + background-size: 32px 32px; +} + +.project-brief-header { + position: sticky; + z-index: 20; + top: 0; + height: 72px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 28px; + padding: 0 max(32px, calc((100vw - 1160px) / 2)); + border-bottom: 1px solid rgba(16, 33, 26, 0.14); + background: rgba(244, 247, 242, 0.88); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); +} + +.project-brief-brand { + display: inline-flex; + align-items: center; + gap: 11px; + font-size: 14px; +} + +.project-brief-brand span { + width: 34px; + height: 34px; + display: grid; + place-items: center; + border-radius: 7px; + background: var(--ink); + color: var(--sage-100); + font-family: var(--font-geist-mono), monospace; + font-size: 9px; + font-weight: 750; + letter-spacing: 0.08em; +} + +.project-brief-header nav, +.project-brief-footer nav { + display: flex; + align-items: center; + gap: 22px; +} + +.project-brief-header nav a { + color: var(--muted); + font-size: var(--text-ui); + font-weight: 620; +} + +.project-brief-header nav a:hover { + color: var(--coral); +} + +.project-brief article { + width: min(1160px, calc(100% - 64px)); + margin: 0 auto; + padding: 0 0 80px; +} + +.project-brief-hero { + min-height: 620px; + display: flex; + flex-direction: column; + justify-content: center; + padding: 96px 0 72px; +} + +.project-brief-kicker { + margin: 0 0 20px; + color: var(--sage-700); + font-family: var(--font-geist-mono), monospace; + font-size: var(--text-meta); + font-weight: 680; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.project-brief-hero h1 { + max-width: 920px; + margin: 0; + font-size: clamp(52px, 7vw, 88px); + font-weight: 650; + letter-spacing: -0.065em; + line-height: 0.96; +} + +.project-brief-deck { + max-width: 760px; + margin: 34px 0 0; + color: var(--ink-soft); + font-size: clamp(18px, 2vw, 23px); + line-height: 1.5; +} + +.project-brief-thesis { + width: min(760px, 100%); + display: grid; + grid-template-columns: 1fr 1fr; + margin-top: 55px; + border: 1px solid var(--line); + background: var(--paper-bright); + box-shadow: 8px 8px 0 var(--sage-100); +} + +.project-brief-thesis p { + min-height: 94px; + display: flex; + align-items: center; + margin: 0; + padding: 24px; + font-size: 15px; + font-weight: 650; + line-height: 1.45; +} + +.project-brief-thesis p + p { + border-left: 1px solid var(--line); + background: var(--ink); + color: var(--sage-100); +} + +.project-brief-section { + display: grid; + grid-template-columns: minmax(260px, 0.8fr) minmax(0, 1.2fr); + gap: 72px; + padding: 96px 0; + border-top: 1px solid var(--line); +} + +.project-brief-section h2, +.project-brief-footer h2 { + margin: 0; + font-size: clamp(34px, 4.5vw, 54px); + font-weight: 640; + letter-spacing: -0.045em; + line-height: 1.04; +} + +.project-brief-section h3 { + margin: 0; + font-size: 19px; +} + +.project-brief-section p, +.project-brief-section li, +.project-brief-section dd { + color: var(--ink-soft); + font-size: 16px; + line-height: 1.7; +} + +.project-brief-section > div:first-child > p:last-child { + margin-top: 24px; +} + +.project-brief-question { + align-self: center; +} + +.project-brief-question blockquote { + margin: 0 0 28px; + padding: 30px; + border-left: 5px solid var(--coral); + background: var(--ink); + color: var(--white); + font-size: clamp(22px, 2.7vw, 32px); + font-weight: 590; + letter-spacing: -0.025em; + line-height: 1.3; +} + +.project-brief-question p, +.project-brief-proof > div:last-child > p, +.project-brief-origin > p, +.project-brief-future > p { + margin: 0; + font-size: 18px; +} + +.project-loop { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(5, 1fr); + margin: 0; + padding: 0; + border: 1px solid var(--line); + background: var(--paper-bright); + list-style: none; +} + +.project-loop li { + position: relative; + min-height: 235px; + padding: 22px; +} + +.project-loop li + li { + border-left: 1px solid var(--line); +} + +.project-loop li:not(:last-child)::after { + position: absolute; + z-index: 1; + top: 28px; + right: -7px; + width: 13px; + height: 13px; + content: ""; + transform: rotate(45deg); + border-top: 1px solid var(--sage-700); + border-right: 1px solid var(--sage-700); + background: var(--paper-bright); +} + +.project-loop li > span, +.principle-grid li > span { + color: var(--coral); + font-family: var(--font-geist-mono), monospace; + font-size: var(--text-meta); + font-weight: 720; +} + +.project-loop h3 { + margin-top: 38px; +} + +.project-loop p { + margin: 12px 0 0; + font-size: 13px; + line-height: 1.55; +} + +.project-brief-tags { + grid-column: 1 / -1; + display: flex; + gap: 10px; +} + +.project-brief-tags span { + padding: 10px 16px; + border: 1px solid var(--ink); + border-radius: 999px; + font-family: var(--font-geist-mono), monospace; + font-size: var(--text-meta); + font-weight: 680; + text-transform: uppercase; +} + +.project-brief-proof ul, +.status-columns ul { + margin: 25px 0 0; + padding-left: 19px; +} + +.project-brief-proof li, +.status-columns li { + padding-left: 6px; +} + +.project-brief-origin { + padding: 96px 48px; + border-top: 0; + background: var(--ink); + color: var(--white); +} + +.project-brief-origin .project-brief-kicker { + color: var(--sage-300); +} + +.project-brief-origin > p { + color: var(--sage-100); +} + +.scaling-columns, +.status-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; +} + +.scaling-columns > div, +.status-columns > div { + padding: 28px; + border: 1px solid var(--line); + background: var(--paper-bright); +} + +.scaling-columns > div:last-child { + border-color: var(--ink); + background: var(--ink); + color: var(--white); +} + +.scaling-columns ul { + margin: 26px 0 0; + padding: 0; + list-style: none; +} + +.scaling-columns li { + padding: 7px 0; + border-bottom: 1px solid var(--line); +} + +.scaling-columns > div:last-child li { + border-color: var(--line-dark); + color: var(--sage-100); +} + +.project-brief-definition { + grid-column: 1 / -1; + margin: 0; + padding: 24px 28px; + border-left: 5px solid var(--coral); + background: var(--sage-100); + font-size: 18px !important; + font-weight: 630; +} + +.principle-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + margin: 0; + padding: 1px; + background: var(--line); + list-style: none; +} + +.principle-grid li { + min-height: 155px; + padding: 24px; + background: var(--paper-bright); +} + +.principle-grid p { + margin: 30px 0 0; + font-size: 15px; + font-weight: 610; + line-height: 1.5; +} + +.project-brief-status { + grid-template-columns: 1fr; + gap: 50px; +} + +.project-brief-status > div:first-child { + max-width: 720px; +} + +.status-columns > div:first-child { + border-top: 5px solid var(--sage-700); +} + +.status-columns > div:last-child { + border-top: 5px solid var(--coral); +} + +.status-columns h3 { + margin-bottom: 18px; +} + +.status-columns ul { + margin: 0; +} + +.status-columns li { + font-size: 14px; +} + +.project-brief-faq { + grid-template-columns: 1fr; + gap: 48px; +} + +.project-brief-faq dl { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + margin: 0; + padding: 1px; + background: var(--line); +} + +.project-brief-faq dl > div { + min-height: 220px; + padding: 28px; + background: var(--paper-bright); +} + +.project-brief-faq dt { + font-size: 18px; + font-weight: 680; + line-height: 1.35; +} + +.project-brief-faq dd { + margin: 18px 0 0; + font-size: 14px; +} + +.project-brief-footer { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(240px, 0.6fr); + gap: 80px; + margin-top: 30px; + padding: 72px 48px; + background: var(--ink); + color: var(--white); +} + +.project-brief-footer .project-brief-kicker { + color: var(--sage-300); +} + +.project-brief-footer nav { + align-items: flex-start; + flex-direction: column; + justify-content: center; + gap: 15px; +} + +.project-brief-footer nav a { + color: var(--sage-100); + font-size: 14px; +} + +.project-brief-footer nav a:hover { + color: var(--coral); +} + +@media (max-width: 760px) { + .project-brief-header { + height: 64px; + padding: 0 16px; + } + + .project-brief-header nav { + gap: 13px; + } + + .project-brief-header nav a { + font-size: 11px; + } + + .project-brief article { + width: calc(100% - 32px); + padding-bottom: 32px; + } + + .project-brief-hero { + min-height: 610px; + padding: 70px 0 50px; + } + + .project-brief-hero h1 { + font-size: 49px; + } + + .project-brief-deck { + font-size: 17px; + } + + .project-brief-thesis { + grid-template-columns: 1fr; + margin-top: 38px; + } + + .project-brief-thesis p + p { + border-top: 1px solid var(--line); + border-left: 0; + } + + .project-brief-section { + grid-template-columns: 1fr; + gap: 34px; + padding: 64px 0; + } + + .project-brief-section h2, + .project-brief-footer h2 { + font-size: 36px; + } + + .project-brief-question blockquote { + padding: 24px; + font-size: 23px; + } + + .project-loop { + grid-template-columns: 1fr; + } + + .project-loop li { + min-height: 0; + display: grid; + grid-template-columns: 36px 1fr; + padding: 22px; + } + + .project-loop li + li { + border-top: 1px solid var(--line); + border-left: 0; + } + + .project-loop li:not(:last-child)::after { + top: auto; + right: 28px; + bottom: -7px; + transform: rotate(135deg); + } + + .project-loop h3 { + margin-top: 0; + } + + .project-loop p { + margin-top: 8px; + } + + .project-brief-origin { + padding: 58px 24px; + } + + .scaling-columns, + .status-columns, + .principle-grid, + .project-brief-faq dl, + .project-brief-footer { + grid-template-columns: 1fr; + } + + .project-brief-footer { + gap: 46px; + padding: 52px 24px; + } +} + @media (min-width: 1600px) and (min-height: 850px) { :root { --text-micro: 0.6875rem; @@ -2035,7 +2590,8 @@ code { width: min(100% - 48px, 940px); } - .header-nav button { + .header-nav button, + .header-nav > a { padding-inline: 8px; } diff --git a/apps/www/app/layout.tsx b/apps/www/app/layout.tsx index a1bb681..9654461 100644 --- a/apps/www/app/layout.tsx +++ b/apps/www/app/layout.tsx @@ -4,33 +4,96 @@ import { GeistSans } from "geist/font/sans"; import "lenis/dist/lenis.css"; import appIcon from "../../ios/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png"; import "./globals.css"; - -const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"; +import { githubUrl, npmUrl, productSummary, siteUrl, skillHubUrl } from "./site-config"; export const metadata: Metadata = { - title: "Vital Agent Sync | Private Apple Health context for AI agents", - description: - "Run Vital Agent Sync on your machine, pair over trusted LAN, and optionally use your own Tailscale network for private remote access.", + title: { + default: "Vital Agent Sync | Private Apple Health context for AI agents", + template: "%s | Vital Agent Sync", + }, + description: productSummary, + applicationName: "Vital Agent Sync", + category: "technology", + keywords: [ + "Apple Health", + "HealthKit", + "AI agent", + "MCP", + "Model Context Protocol", + "local-first", + "personal context", + "context scaling", + "WorkBuddy", + ], metadataBase: new URL(siteUrl), + alternates: { + canonical: "/", + }, + robots: { + index: true, + follow: true, + }, icons: { icon: [{ url: appIcon.src, type: "image/png", sizes: "1024x1024" }], apple: [{ url: appIcon.src, type: "image/png", sizes: "1024x1024" }], }, openGraph: { title: "Vital Agent Sync | Apple Health context. Private by design.", - description: - "Fresh, scoped Apple Health context for your AI agent through a user-owned LAN or Tailscale receiver.", + description: productSummary, type: "website", + url: "/", + siteName: "Vital Agent Sync", images: [{ url: "/og.png", width: 1730, height: 909, alt: "Vital Agent Sync private Apple Health context" }], }, twitter: { card: "summary_large_image", title: "Vital Agent Sync | Apple Health context. Private by design.", - description: "Fresh, scoped Apple Health context for your AI agent.", + description: productSummary, images: ["/og.png"], }, }; +const structuredData = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "WebSite", + "@id": `${siteUrl}/#website`, + url: siteUrl, + name: "Vital Agent Sync", + description: productSummary, + inLanguage: "en", + }, + { + "@type": "SoftwareApplication", + "@id": `${siteUrl}/#software`, + name: "Vital Agent Sync", + url: siteUrl, + description: productSummary, + applicationCategory: "HealthApplication", + operatingSystem: "iOS and macOS", + isAccessibleForFree: true, + license: `${githubUrl}/blob/main/LICENSE`, + downloadUrl: npmUrl, + sameAs: [githubUrl, skillHubUrl, npmUrl], + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "USD", + }, + featureList: [ + "User-authorized Apple Health summaries", + "Encrypted iPhone pairing and sync", + "User-controlled local SQLite receiver", + "Scoped Model Context Protocol tools", + "Freshness and source metadata", + "Revocable source-device access", + "Trusted LAN and optional user-managed Tailscale routes", + ], + }, + ], +}; + export default function RootLayout({ children, }: Readonly<{ @@ -38,7 +101,13 @@ export default function RootLayout({ }>) { return ( - {children} + +