Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/home/HomeHero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const zh = lang === "zh-CN";
>
{c.h1a} <span class="text-shimmer">{c.h1b}</span>
</h1>
<p class="reveal text-pretty text-[17px] leading-relaxed text-[#cbd5e1] sm:text-xl" style="animation-delay: 260ms;">{c.sub}</p>
{/* 背景图形从这一段后面穿过:一圈深色阴影让字压得住它 */}
<p class="reveal text-pretty text-[17px] leading-relaxed text-[#dbe3ee] [text-shadow:0_0_14px_rgba(11,18,32,0.95),0_0_4px_rgba(11,18,32,0.9)] sm:text-xl" style="animation-delay: 260ms;">{c.sub}</p>

<div class="reveal mt-2 grid gap-4 sm:grid-cols-2" style="animation-delay: 400ms;">
<a href={localePath(lang, "/thinkwatch")} class="door group flex h-full flex-col gap-2.5 rounded-[14px] border border-[var(--color-border-strong)] bg-[rgba(17,26,46,0.85)] p-6 backdrop-blur-sm">
Expand Down
15 changes: 10 additions & 5 deletions src/components/home/LiteBand.astro
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,19 @@ const zh = lang === "zh-CN";
lang === "zh-CN" ? "text-[28px] leading-[1.35] sm:text-[34px] lg:text-[40px]" : "text-[32px] leading-[1.15] sm:text-[40px] lg:text-[46px]",
]}
>{c.title}</h2>
<div class="mt-2 flex flex-col gap-4">
<div class="mt-2 grid gap-x-8 gap-y-5 sm:grid-cols-2">
{c.points.map((p) => (
<p class="text-base leading-relaxed text-[#cbd5e1] sm:text-[17px]"><span class="font-semibold text-white">{p.t}</span> {p.b}</p>
<div class="flex flex-col gap-1.5">
<h3 class="m-0 text-[17px] font-semibold text-white">{p.t}</h3>
<p class="m-0 text-[15px] leading-relaxed text-[#cbd5e1]">{p.b}</p>
</div>
))}
</div>
<div class="flex flex-wrap items-center gap-3">
{c.pills.map((pill) => (
<span class="rounded-full border border-[var(--color-border-strong)] px-3 py-1.5 text-[13px] text-[#cbd5e1]">{pill}</span>
{/* 第一项是状态,单独做成徽标;其余是平台与许可证 */}
<div class="flex flex-wrap items-center gap-2.5">
<span class="rounded-full bg-[var(--color-brand-1)] px-3 py-1 text-[13px] font-medium text-[var(--color-bg)]">{c.pills[0]}</span>
{c.pills.slice(1).map((pill) => (
<span class="rounded-full border border-[var(--color-border-strong)] px-3 py-1 text-[13px] text-[#cbd5e1]">{pill}</span>
))}
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-5">
Expand Down
13 changes: 10 additions & 3 deletions src/components/mocks/RateLimitMock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ import { useInView } from "../hooks/useInView";

const BUCKETS = 30;
const LIMIT = 100;
const TYPICAL = 62;

function genBucket(seed: number, base: number) {
/**
* One bucket's request count: a step from the previous one that drifts back
* towards the typical load, capped at the limit, since a limiter never lets a
* bucket past it.
*/
function genBucket(seed: number, prev: number) {
seed = (seed * 9301 + 49297) % 233280;
const r = seed / 233280;
return Math.max(0, Math.round(base + (r - 0.4) * 35));
const base = prev * 0.6 + TYPICAL * 0.4;
return Math.min(LIMIT, Math.max(0, Math.round(base + (r - 0.5) * 40)));
}

export default function RateLimitMock() {
Expand Down Expand Up @@ -60,7 +67,7 @@ export default function RateLimitMock() {
const pct = Math.min(100, (b / LIMIT) * 100);
const over = b > LIMIT * 0.9;
return (
<div key={i} className="flex-1 flex flex-col justify-end">
<div key={i} className="flex h-full flex-1 flex-col justify-end">
<div
className="w-full rounded-sm transition-all duration-500"
style={{
Expand Down
59 changes: 33 additions & 26 deletions src/components/pages/LitePage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ const h2 = zh
: "font-display m-0 text-[30px] leading-[1.15] sm:text-[36px] lg:text-[40px]";
const textLink =
"inline-flex min-h-11 items-center text-[17px] font-medium text-[var(--color-brand-1)] transition-colors hover:text-[var(--color-brand-2)]";
const nodes = [c.built.lite, c.built.core, c.built.upstreams];
/** 截图外的一圈:和窗口自带的阴影配合,不再加边框 */
const shotClass = "w-full rounded-[10px]";
const archLabel = { x64: "x64", arm64: "ARM64" } as const;
Expand Down Expand Up @@ -234,51 +233,59 @@ const archLabel = { x64: "x64", arm64: "ARM64" } as const;
</section>

<section class="bg-[var(--color-surface)] py-16 sm:py-20 lg:py-[88px]" aria-labelledby="lite-built">
<div class="container-page">
<h2 id="lite-built" class:list={[eyebrow, "m-0 font-normal"]}>{c.built.eyebrow}</h2>
<div class="mt-8 grid items-stretch lg:mt-10 lg:grid-cols-[minmax(0,1fr)_216px_minmax(0,1fr)_96px_minmax(0,1fr)] lg:items-center">
{nodes.map((node, i) => (
<div class="container-page flex flex-col gap-10">
<div class="flex max-w-[46rem] flex-col gap-3">
<p class:list={[eyebrow, "m-0"]}>{c.built.eyebrow}</p>
<h2 id="lite-built" class={h2}>{c.built.title}</h2>
<p class="m-0 text-base leading-relaxed text-[var(--color-muted)] sm:text-[17px]">{c.built.intro}</p>
</div>
{/* 三个节点等高;连线上各一个标签,第一条(应用到 core)带流动的点 */}
<div class="grid items-stretch lg:grid-cols-[minmax(0,1fr)_minmax(140px,180px)_minmax(0,1fr)_minmax(120px,150px)_minmax(0,1fr)]">
{c.built.nodes.map((node, i) => (
<Fragment>
{i > 0 && (
<div
class="flow-link relative flex flex-col items-center justify-center gap-3 py-3 lg:py-0"
data-active={i === 1 ? "true" : undefined}
aria-hidden={i === 1 ? undefined : "true"}
>
<div class="flow-link relative flex items-center justify-center py-8 lg:py-0" aria-hidden={i === 1 ? undefined : "true"}>
<div
class:list={[
"flow-line relative h-10 w-px lg:h-px lg:w-full",
"flow-line absolute left-1/2 top-0 h-full w-px -translate-x-1/2 lg:left-0 lg:top-1/2 lg:h-px lg:w-full lg:translate-x-0 lg:-translate-y-1/2",
i === 1 ? "bg-[var(--color-brand-1)]" : "bg-[var(--color-border-strong)]",
]}
>
{i === 1 && <span class="flow-dot" aria-hidden="true"></span>}
</div>
{/* The channel between the app and core: under the line when the nodes sit
side by side, below the vertical line when they are stacked. */}
{i === 1 && (
<div class="text-center font-mono text-xs leading-relaxed text-[var(--color-muted)] lg:absolute lg:inset-x-2 lg:top-[calc(50%+12px)]">
<p class="m-0 text-[var(--color-text)]">{c.built.link.title}</p>
<ul class="m-0 list-none p-0">
{c.built.link.lines.map((line) => (
<li>{line}</li>
))}
</ul>
</div>
)}
<span
class:list={[
"relative whitespace-nowrap rounded-full border bg-[var(--color-surface)] px-3 py-1 text-[13px]",
i === 1
? "border-[var(--color-brand-1)]/60 text-[var(--color-brand-1)]"
: "border-[var(--color-border-strong)] text-[var(--color-muted)]",
]}
>
{c.built.links[i - 1]}
</span>
</div>
)}
<div
class:list={[
"flex flex-col gap-2.5 rounded-[14px] border bg-[var(--color-bg)]/30 p-6 sm:p-7",
i === 0 ? "border-[var(--color-brand-1)]" : "border-[var(--color-border-strong)]",
"flex flex-col gap-3 rounded-[14px] border bg-[var(--color-bg)]/40 p-6 sm:p-7",
i === 0 ? "border-[var(--color-brand-1)]/70" : "border-[var(--color-border-strong)]",
]}
>
<p class="m-0 text-[13px] text-[var(--color-muted)]">{node.role}</p>
<h3 class="m-0 text-xl font-semibold sm:text-[22px]">{node.title}</h3>
<p class="m-0 text-[15px] leading-normal text-[var(--color-muted)]">{node.body}</p>
<ul class="m-0 flex list-none flex-col gap-2 p-0">
{node.items.map((item) => (
<li class="flex items-start gap-2.5 text-[15px] leading-snug text-[#cbd5e1]">
<span class="mt-[0.45em] size-1.5 shrink-0 rounded-full bg-[var(--color-brand-1)]/80" aria-hidden="true"></span>
{item}
</li>
))}
</ul>
</div>
</Fragment>
))}
</div>
<p class="m-0 max-w-[52rem] text-[15px] leading-relaxed text-[var(--color-muted)]">{c.built.note}</p>
</div>
</section>

Expand Down
50 changes: 18 additions & 32 deletions src/i18n/pages/lite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,22 +139,15 @@ export const liteCopy = {
},
built: {
eyebrow: "Architecture",
lite: {
title: "ThinkWatch Lite",
body: "A Tauri 2 shell with a React 19 window. It starts and supervises the local core or connects to a core on a server, and renders the menu bar on macOS and the tray menu on Windows and Linux.",
},
link: {
title: "Control channel",
lines: ["macOS and Linux: Unix socket", "Windows: loopback port", "Remote core: TCP port", "Every connection: encrypted handshake"],
},
core: {
title: "ThinkWatch Core",
body: "The gateway, twcore: routing, forwarding, format conversion, cost accounting and the security protections. It runs beside the app or as a systemd service on a Linux server; Lite contains none of this logic.",
},
upstreams: {
title: "Upstreams",
body: "The Anthropic, OpenAI and Gemini APIs, accounts signed in from the app, relays and local models.",
},
title: "The app is the interface; the gateway is ThinkWatch Core",
intro: "Lite holds no routing, forwarding or accounting logic. All of it is in ThinkWatch Core, which runs beside the app or on a Linux server.",
nodes: [
{ role: "Desktop app", title: "ThinkWatch Lite", items: ["Main window, menu bar and tray", "Starts and supervises the local core", "Or connects to a core on a server"] },
{ role: "Gateway", title: "ThinkWatch Core", items: ["Routing, failover and format conversion", "Cost accounting and request records", "The five security protections"] },
{ role: "Upstreams", title: "Model services", items: ["Anthropic, OpenAI and Gemini APIs", "Amazon Bedrock and signed-in accounts", "Relays and local models"] },
],
links: ["Encrypted control channel", "Forwards requests"],
note: "The control channel is a Unix socket on macOS and Linux, a loopback port on Windows and a TCP port to a core on a server; every connection is encrypted and authenticated by a Noise handshake.",
},
install: {
eyebrow: "Install",
Expand Down Expand Up @@ -324,22 +317,15 @@ export const liteCopy = {
},
built: {
eyebrow: "架构",
lite: {
title: "ThinkWatch Lite",
body: "Tauri 2 外壳与 React 19 窗口。负责启动并托管本机的 core,或连接服务器上的 core;在 macOS 上渲染菜单栏,在 Windows 与 Linux 上渲染托盘菜单。",
},
link: {
title: "控制通道",
lines: ["macOS、Linux:unix socket", "Windows:本机回环端口", "远程 core:TCP 端口", "每条连接都经加密握手"],
},
core: {
title: "ThinkWatch Core",
body: "网关本体 twcore,负责路由、转发、格式转换、成本核算与安全防护。它随应用在本机运行,也可以作为 systemd 服务运行在 Linux 服务器上;Lite 不包含这些逻辑。",
},
upstreams: {
title: "上游服务",
body: "Anthropic、OpenAI 与 Gemini API,在应用内登录的账号,以及中转服务与本机模型。",
},
title: "应用负责界面,网关是 ThinkWatch Core",
intro: "Lite 不含路由、转发与计费逻辑,这些都在 ThinkWatch Core 中;core 随应用在本机运行,也可以部署在 Linux 服务器上。",
nodes: [
{ role: "桌面应用", title: "ThinkWatch Lite", items: ["主窗口、菜单栏与托盘", "启动并守护本机的 core", "或连接服务器上的 core"] },
{ role: "网关", title: "ThinkWatch Core", items: ["路由、故障转移与格式转换", "费用核算与请求记录", "五项安全防护"] },
{ role: "上游", title: "模型服务", items: ["Anthropic、OpenAI、Gemini API", "Amazon Bedrock 与登录的账号", "中转服务与本机模型"] },
],
links: ["加密控制通道", "转发请求"],
note: "控制通道在 macOS 与 Linux 上是 unix socket,在 Windows 上是本机回环端口,连接服务器时是 TCP 端口;每条连接都经 Noise 握手加密与认证。",
},
install: {
eyebrow: "安装",
Expand Down
Loading