feat(web): community site for deepseek-tui.com (mobile + color refresh) (#1108)
First commit of the Next.js community site that powers deepseek-tui.com, deployed via Cloudflare Workers / OpenNext. This commit lands the scaffold and applies the visual + correctness pass requested by community feedback: - Palette: drop the cream/Anthropic-feel paper (#F4F1E8) for a DeepSeek-aligned cool white + soft gray (#FFFFFF / #F4F6FB), with indigo accents kept. Soften default hairlines so a pure-white background reads clean instead of harsh. - Mobile: add a hamburger menu (mobile-menu.tsx) so phones can reach Install / Docs / Activity / Roadmap / Contribute — previously the link list was hidden on phones with no replacement. Tighter hero, flexible button row, viewport-safe code blocks, columnar grids collapse cleanly under 768px, and the printed-almanac center rule is desktop-only now (it sliced through narrow viewports). - "How it works" diagram: replace the hand-rolled ASCII art (which misaligned under CJK monospace because Han characters take 2 columns vs Latin's 1, per dhh's note in WeChat) with a real mermaid diagram rendered client-side via dynamic import. Uses the mermaid.live standard syntax 庄表伟 recommended. - Issue #1104: the docs listed a `deepseek-cn` provider that the v0.8.16 binary doesn't accept (`ProviderArg` in crates/cli only has 9 variants; the 10th lives only in the legacy tui/config.rs). derive-facts.mjs now omits `deepseek-cn` until that variant is wired through the shared ProviderKind, and the install page's China-network recipe uses `base_url` / `DEEPSEEK_BASE_URL` (which actually works on v0.8.16) instead of the unsupported provider. Auto-deploys via .github/workflows/deploy-web.yml on push to main. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import Link from "next/link";
|
||||
import type { FeedItem } from "@/lib/types";
|
||||
import { relativeTime } from "@/lib/github";
|
||||
|
||||
const KIND_LABEL: Record<FeedItem["kind"], { label: string; cn: string }> = {
|
||||
issue: { label: "Issue", cn: "议题" },
|
||||
pull: { label: "Pull", cn: "合并" },
|
||||
release: { label: "Release", cn: "发布" },
|
||||
discussion: { label: "Talk", cn: "讨论" },
|
||||
};
|
||||
|
||||
function statePill(state: FeedItem["state"]) {
|
||||
const map: Record<FeedItem["state"], string> = {
|
||||
open: "pill pill-jade",
|
||||
closed: "pill pill-ghost",
|
||||
merged: "pill pill-hot",
|
||||
draft: "pill pill-ghost",
|
||||
published: "pill pill-ochre",
|
||||
};
|
||||
return <span className={map[state]}>{state}</span>;
|
||||
}
|
||||
|
||||
export function FeedCard({ item, dense = false }: { item: FeedItem; dense?: boolean }) {
|
||||
const k = KIND_LABEL[item.kind];
|
||||
return (
|
||||
<article className={`hairline-b py-4 ${dense ? "" : "px-1"}`}>
|
||||
<div className="flex items-baseline gap-3 mb-1.5">
|
||||
<span className="font-mono text-[0.66rem] uppercase tracking-widest text-indigo">
|
||||
{k.label} <span className="font-cjk text-ink-mute normal-case tracking-normal ml-1">{k.cn}</span>
|
||||
</span>
|
||||
<span className="font-mono text-[0.7rem] text-ink-mute tabular">#{item.number}</span>
|
||||
<span className="ml-auto font-mono text-[0.7rem] text-ink-mute tabular">{relativeTime(item.updatedAt)}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="font-display text-base leading-snug">
|
||||
<Link href={item.url} className="hover:text-indigo transition-colors">
|
||||
{item.title}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-3 mt-2 flex-wrap">
|
||||
{statePill(item.state)}
|
||||
{item.labels.slice(0, 3).map((l) => (
|
||||
<span
|
||||
key={l.name}
|
||||
className="pill pill-ghost"
|
||||
style={{ borderColor: `#${l.color}`, color: `#${l.color}` }}
|
||||
>
|
||||
{l.name}
|
||||
</span>
|
||||
))}
|
||||
<span className="ml-auto flex items-center gap-2 font-mono text-[0.7rem] text-ink-mute">
|
||||
<span>@{item.author}</span>
|
||||
{item.comments > 0 && <span className="tabular">· {item.comments} reply{item.comments === 1 ? "" : "s"}</span>}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import Link from "next/link";
|
||||
import { GITEE_ENABLED, type Locale } from "@/lib/i18n/config";
|
||||
import { Seal } from "./seal";
|
||||
|
||||
const EN_COLS = [
|
||||
{
|
||||
title: "Product",
|
||||
cn: "产品",
|
||||
items: [
|
||||
{ label: "Install", href: "/install" },
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "Roadmap", href: "/roadmap" },
|
||||
{ label: "Releases", href: "https://github.com/Hmbown/deepseek-tui/releases" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Community",
|
||||
cn: "社区",
|
||||
items: [
|
||||
{ label: "Issues", href: "https://github.com/Hmbown/deepseek-tui/issues" },
|
||||
{ label: "Pull Requests", href: "https://github.com/Hmbown/deepseek-tui/pulls" },
|
||||
{ label: "Discussions", href: "https://github.com/Hmbown/deepseek-tui/discussions" },
|
||||
{ label: "Contribute", href: "/contribute" },
|
||||
{ label: "Support DeepSeek TUI", href: "https://buymeacoffee.com/hmbown" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Resources",
|
||||
cn: "资源",
|
||||
items: [
|
||||
{ label: "Activity Feed", href: "/feed" },
|
||||
{ label: "Code of Conduct", href: "https://github.com/Hmbown/deepseek-tui/blob/main/CODE_OF_CONDUCT.md" },
|
||||
{ label: "Security", href: "https://github.com/Hmbown/deepseek-tui/blob/main/SECURITY.md" },
|
||||
{ label: "License (MIT)", href: "https://github.com/Hmbown/deepseek-tui/blob/main/LICENSE" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const ZH_COLS = [
|
||||
{
|
||||
title: "产品",
|
||||
items: [
|
||||
{ label: "安装指南", href: "/zh/install" },
|
||||
{ label: "使用文档", href: "/zh/docs" },
|
||||
{ label: "路线图", href: "/zh/roadmap" },
|
||||
{ label: "版本发布", href: "https://github.com/Hmbown/deepseek-tui/releases" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "社区",
|
||||
items: [
|
||||
{ label: "议题", href: "https://github.com/Hmbown/deepseek-tui/issues" },
|
||||
{ label: "合并请求", href: "https://github.com/Hmbown/deepseek-tui/pulls" },
|
||||
{ label: "讨论区", href: "https://github.com/Hmbown/deepseek-tui/discussions" },
|
||||
{ label: "参与贡献", href: "/zh/contribute" },
|
||||
{ label: "支持 DeepSeek TUI", href: "https://buymeacoffee.com/hmbown" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "资源",
|
||||
items: [
|
||||
{ label: "活动动态", href: "/zh/feed" },
|
||||
{ label: "行为准则", href: "https://github.com/Hmbown/deepseek-tui/blob/main/CODE_OF_CONDUCT.md" },
|
||||
{ label: "安全策略", href: "https://github.com/Hmbown/deepseek-tui/blob/main/SECURITY.md" },
|
||||
{ label: "MIT 许可证", href: "https://github.com/Hmbown/deepseek-tui/blob/main/LICENSE" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Footer({ locale = "en" }: { locale?: Locale }) {
|
||||
const isZh = locale === "zh";
|
||||
const cols = isZh ? ZH_COLS : EN_COLS;
|
||||
|
||||
return (
|
||||
<footer className="hairline-t mt-24 bg-paper-deep">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-12 grid grid-cols-2 md:grid-cols-5 gap-10">
|
||||
<div className="col-span-2 md:col-span-2 space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Seal char="深" size="md" />
|
||||
<div>
|
||||
<div className="font-display text-xl font-semibold">DeepSeek TUI</div>
|
||||
<div className="font-cjk text-[0.7rem] text-ink-mute tracking-widest">
|
||||
{isZh ? "深度求索 · 终端智能体" : "深度求索 · 终端代理"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-ink-soft max-w-md leading-relaxed">
|
||||
{isZh
|
||||
? "基于 DeepSeek V4 的开源终端编程智能体。MIT 许可证。由一位维护者从得克萨斯独立维护。欢迎提交 Pull Request。"
|
||||
: "Open-source terminal-native coding agent built on DeepSeek V4. MIT licensed. Maintained from a small workshop in Texas. Pull requests welcome."}
|
||||
</p>
|
||||
<div className="font-mono text-[0.7rem] text-ink-mute uppercase tracking-widest">
|
||||
{isZh ? "用心制作 · Made with care" : "Made with care · 用心制作"}
|
||||
</div>
|
||||
{/* Mirror sources — prominent on zh */}
|
||||
{isZh && (
|
||||
<div className="pt-2 border-t border-paper-line/20">
|
||||
<div className="eyebrow mb-2 text-ink-mute">镜像源 / Mirror</div>
|
||||
<div className="flex flex-wrap gap-3 text-xs">
|
||||
{GITEE_ENABLED && <a href="https://gitee.com/Hmbown/deepseek-tui" className="text-indigo hover:underline" target="_blank" rel="noopener">Gitee 镜像</a>}
|
||||
<a href="https://npmmirror.com/package/deepseek-tui" className="text-indigo hover:underline" target="_blank" rel="noopener">npmmirror</a>
|
||||
<a href="https://mirrors.tuna.tsinghua.edu.cn/help/crates.io-index.html" className="text-indigo hover:underline" target="_blank" rel="noopener">Tuna crates.io</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{cols.map((c) => (
|
||||
<div key={c.title}>
|
||||
<div className="eyebrow mb-3">
|
||||
{isZh ? c.title : `${c.title} · `}
|
||||
{!isZh && "cn" in c && <span className="font-cjk normal-case tracking-normal">{(c as { cn?: string }).cn}</span>}
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{c.items.map((it) => (
|
||||
<li key={it.href}>
|
||||
<Link href={it.href} className="text-sm text-ink hover:text-indigo transition-colors">
|
||||
{it.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hairline-t">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-4 flex flex-col gap-2 text-[0.78rem] text-ink-soft">
|
||||
<div>
|
||||
{isZh ? "咨询、投资、研究合作、媒体采访 — " : "For consulting, investors, researchers, or press — "}
|
||||
<a href="mailto:hunter@shannonlabs.dev" className="font-mono text-ink hover:text-indigo">hunter@shannonlabs.dev</a>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 font-mono text-[0.7rem] text-ink-mute uppercase tracking-widest">
|
||||
<span>© {new Date().getFullYear()} · DeepSeek TUI · Hmbown</span>
|
||||
<span className="font-cjk normal-case tracking-normal">
|
||||
{isZh ? "本网站由 DeepSeek V4-Flash 协助维护" : "本网站由 DeepSeek V4-Flash 协同维护"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type OS = "macos" | "linux" | "windows" | "any";
|
||||
|
||||
interface Method {
|
||||
id: string;
|
||||
os: OS;
|
||||
label: string;
|
||||
cn: string;
|
||||
recommended?: boolean;
|
||||
comingSoon?: boolean;
|
||||
prereq: string;
|
||||
cmd: string;
|
||||
}
|
||||
|
||||
const METHODS: Method[] = [
|
||||
// ─── macOS ────────────────────────────────────────────────
|
||||
{
|
||||
id: "cargo-mac",
|
||||
os: "macos",
|
||||
label: "Cargo (recommended)",
|
||||
cn: "Cargo · 推荐",
|
||||
recommended: true,
|
||||
prereq: "Rust 1.88+ — install via rustup.rs if needed",
|
||||
cmd: `# Install the dispatcher (provides \`deepseek\`)
|
||||
cargo install deepseek-tui-cli --locked
|
||||
|
||||
# Optional: also install the raw TUI binary (\`deepseek-tui\`)
|
||||
cargo install deepseek-tui --locked
|
||||
|
||||
# Set your API key (one-time)
|
||||
export DEEPSEEK_API_KEY=sk-...
|
||||
echo 'export DEEPSEEK_API_KEY=sk-...' >> ~/.zshrc
|
||||
|
||||
# Run it
|
||||
deepseek`,
|
||||
},
|
||||
{
|
||||
id: "npm-mac",
|
||||
os: "macos",
|
||||
label: "npm wrapper",
|
||||
cn: "npm 包",
|
||||
prereq: "Node.js 18+",
|
||||
cmd: `npm install -g deepseek-tui
|
||||
|
||||
# Provides both binaries on PATH:
|
||||
deepseek # canonical dispatcher
|
||||
deepseek-tui # raw TUI binary`,
|
||||
},
|
||||
{
|
||||
id: "binary-mac",
|
||||
os: "macos",
|
||||
label: "Pre-built binary",
|
||||
cn: "二进制",
|
||||
prereq: "Apple Silicon (arm64) or Intel (x64). Releases ship raw binaries — no archive to extract.",
|
||||
cmd: `# Apple Silicon
|
||||
curl -fsSL -o deepseek \\
|
||||
https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-macos-arm64
|
||||
chmod +x deepseek
|
||||
xattr -d com.apple.quarantine deepseek 2>/dev/null || true
|
||||
sudo mv deepseek /usr/local/bin/
|
||||
|
||||
# Intel
|
||||
curl -fsSL -o deepseek \\
|
||||
https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-macos-x64
|
||||
chmod +x deepseek
|
||||
xattr -d com.apple.quarantine deepseek 2>/dev/null || true
|
||||
sudo mv deepseek /usr/local/bin/
|
||||
|
||||
# Verify checksum (optional but recommended)
|
||||
curl -fsSL -O https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-artifacts-sha256.txt
|
||||
shasum -a 256 -c deepseek-artifacts-sha256.txt --ignore-missing
|
||||
|
||||
deepseek`,
|
||||
},
|
||||
{
|
||||
id: "brew",
|
||||
os: "macos",
|
||||
label: "Homebrew",
|
||||
cn: "Homebrew",
|
||||
prereq: "Homebrew on macOS or Linux; installs the dispatcher and companion TUI from the official Hmbown tap.",
|
||||
cmd: `brew tap Hmbown/deepseek-tui
|
||||
brew install deepseek-tui
|
||||
|
||||
deepseek --version
|
||||
deepseek`,
|
||||
},
|
||||
|
||||
// ─── Linux ────────────────────────────────────────────────
|
||||
{
|
||||
id: "cargo-linux",
|
||||
os: "linux",
|
||||
label: "Cargo (recommended)",
|
||||
cn: "Cargo · 推荐",
|
||||
recommended: true,
|
||||
prereq: "Rust 1.88+; on Debian/Ubuntu: apt install build-essential pkg-config libssl-dev",
|
||||
cmd: `cargo install deepseek-tui-cli --locked
|
||||
export DEEPSEEK_API_KEY=sk-...
|
||||
deepseek`,
|
||||
},
|
||||
{
|
||||
id: "npm-linux",
|
||||
os: "linux",
|
||||
label: "npm wrapper",
|
||||
cn: "npm 包",
|
||||
prereq: "Node.js 18+",
|
||||
cmd: `npm install -g deepseek-tui
|
||||
deepseek`,
|
||||
},
|
||||
{
|
||||
id: "binary-linux",
|
||||
os: "linux",
|
||||
label: "Pre-built binary",
|
||||
cn: "二进制",
|
||||
prereq: "x86_64 or aarch64 glibc. Releases ship raw binaries — no archive to extract.",
|
||||
cmd: `# x86_64
|
||||
curl -fsSL -o deepseek \\
|
||||
https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-linux-x64
|
||||
chmod +x deepseek
|
||||
sudo mv deepseek /usr/local/bin/
|
||||
|
||||
# arm64
|
||||
curl -fsSL -o deepseek \\
|
||||
https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-linux-arm64
|
||||
chmod +x deepseek
|
||||
sudo mv deepseek /usr/local/bin/
|
||||
|
||||
# Verify checksum (optional but recommended)
|
||||
curl -fsSL -O https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-artifacts-sha256.txt
|
||||
sha256sum -c deepseek-artifacts-sha256.txt --ignore-missing
|
||||
|
||||
deepseek`,
|
||||
},
|
||||
|
||||
// ─── Windows ──────────────────────────────────────────────
|
||||
{
|
||||
id: "cargo-win",
|
||||
os: "windows",
|
||||
label: "Cargo (recommended)",
|
||||
cn: "Cargo · 推荐",
|
||||
recommended: true,
|
||||
prereq: "Rust 1.88+ via rustup-init.exe",
|
||||
cmd: `cargo install deepseek-tui-cli --locked
|
||||
$env:DEEPSEEK_API_KEY = "sk-..."
|
||||
deepseek`,
|
||||
},
|
||||
{
|
||||
id: "npm-win",
|
||||
os: "windows",
|
||||
label: "npm wrapper",
|
||||
cn: "npm 包",
|
||||
prereq: "Node.js 18+",
|
||||
cmd: `npm install -g deepseek-tui
|
||||
deepseek`,
|
||||
},
|
||||
{
|
||||
id: "binary-win",
|
||||
os: "windows",
|
||||
label: "Pre-built binary",
|
||||
cn: "二进制",
|
||||
prereq: "Windows 10+ x64. Releases ship a raw .exe — no archive to extract.",
|
||||
cmd: `# PowerShell
|
||||
$ErrorActionPreference = "Stop"
|
||||
$dest = "$Env:USERPROFILE\\bin"
|
||||
New-Item -ItemType Directory -Force $dest | Out-Null
|
||||
|
||||
Invoke-WebRequest \`
|
||||
-Uri https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-windows-x64.exe \`
|
||||
-OutFile "$dest\\deepseek.exe"
|
||||
|
||||
# Add to PATH for this session (persist via System Properties → Environment Variables)
|
||||
$Env:Path = "$dest;$Env:Path"
|
||||
|
||||
$Env:DEEPSEEK_API_KEY = "sk-..."
|
||||
deepseek`,
|
||||
},
|
||||
{
|
||||
id: "scoop",
|
||||
os: "windows",
|
||||
label: "Scoop",
|
||||
cn: "Scoop",
|
||||
comingSoon: true,
|
||||
prereq: "Scoop manifest not yet published — use Cargo or the pre-built .exe above.",
|
||||
cmd: `# Coming soon — no Scoop manifest yet.
|
||||
# Working alternatives on Windows:
|
||||
# - Cargo (recommended above)
|
||||
# - Pre-built deepseek-windows-x64.exe (above)
|
||||
#
|
||||
# Track progress:
|
||||
# https://github.com/Hmbown/deepseek-tui/issues`,
|
||||
},
|
||||
|
||||
// ─── Any (cross-platform) ────────────────────────────────
|
||||
{
|
||||
id: "docker",
|
||||
os: "any",
|
||||
label: "Docker",
|
||||
cn: "Docker",
|
||||
prereq: "Dockerfile ships with the repo (multi-arch buildx). No prebuilt image is published to a registry yet.",
|
||||
cmd: `git clone https://github.com/Hmbown/deepseek-tui
|
||||
cd deepseek-tui
|
||||
|
||||
# Build for your local arch
|
||||
docker build -t deepseek-tui .
|
||||
|
||||
# Or multi-arch via buildx
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t deepseek-tui .
|
||||
|
||||
# Run interactively, mounting your config + a project
|
||||
docker run --rm -it \\
|
||||
-e DEEPSEEK_API_KEY=$DEEPSEEK_API_KEY \\
|
||||
-v ~/.deepseek:/home/deepseek/.deepseek \\
|
||||
-v "$PWD:/work" -w /work \\
|
||||
deepseek-tui`,
|
||||
},
|
||||
{
|
||||
id: "from-source",
|
||||
os: "any",
|
||||
label: "Build from source",
|
||||
cn: "源码编译",
|
||||
prereq: "Rust 1.88+ and a git checkout — useful for hacking on the workspace itself.",
|
||||
cmd: `git clone https://github.com/Hmbown/deepseek-tui
|
||||
cd deepseek-tui
|
||||
|
||||
# Builds both \`deepseek\` and \`deepseek-tui\` into ./target/release/
|
||||
cargo build --release --locked
|
||||
|
||||
# Run without installing
|
||||
./target/release/deepseek
|
||||
|
||||
# Or install both binaries from your local checkout
|
||||
cargo install --path crates/cli --locked # provides \`deepseek\`
|
||||
cargo install --path crates/tui --locked # provides \`deepseek-tui\``,
|
||||
},
|
||||
];
|
||||
|
||||
const OS_LABEL: Record<OS, { en: string; cn: string }> = {
|
||||
macos: { en: "macOS", cn: "苹果" },
|
||||
linux: { en: "Linux", cn: "Linux" },
|
||||
windows: { en: "Windows", cn: "视窗" },
|
||||
any: { en: "Any platform", cn: "通用" },
|
||||
};
|
||||
|
||||
function detectOS(): OS {
|
||||
if (typeof navigator === "undefined") return "macos";
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
if (ua.includes("mac")) return "macos";
|
||||
if (ua.includes("win")) return "windows";
|
||||
if (ua.includes("linux")) return "linux";
|
||||
return "macos";
|
||||
}
|
||||
|
||||
export function InstallTabs() {
|
||||
const [os, setOS] = useState<OS>("macos");
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => { setOS(detectOS()); }, []);
|
||||
|
||||
// Show OS-specific methods + universal ones (Docker status / source build).
|
||||
// On the "Any" tab, only show universal ones.
|
||||
const methods = METHODS.filter((m) => (os === "any" ? m.os === "any" : m.os === os || m.os === "any"));
|
||||
|
||||
const copy = (id: string, text: string) => {
|
||||
navigator.clipboard?.writeText(text);
|
||||
setCopied(id);
|
||||
setTimeout(() => setCopied(null), 1400);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* OS selector */}
|
||||
<div className="hairline-t hairline-b grid grid-cols-4">
|
||||
{(["macos", "linux", "windows", "any"] as OS[]).map((o) => {
|
||||
const active = os === o;
|
||||
return (
|
||||
<button
|
||||
key={o}
|
||||
onClick={() => setOS(o)}
|
||||
className={`px-4 py-4 text-left transition-colors hairline-l first:border-l-0 ${
|
||||
active ? "bg-ink text-paper" : "bg-paper hover:bg-paper-deep"
|
||||
}`}
|
||||
>
|
||||
<div className={`eyebrow mb-1 ${active ? "text-paper-deep/70" : ""}`}>
|
||||
{active ? "▼ " : ""}Detected · {o === detectOS() ? "auto" : "switch"}
|
||||
</div>
|
||||
<div className="font-display text-lg leading-tight">{OS_LABEL[o].en}</div>
|
||||
<div className={`font-cjk text-xs ${active ? "text-paper-deep/80" : "text-ink-mute"}`}>
|
||||
{OS_LABEL[o].cn}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* methods */}
|
||||
<div className="hairline-b">
|
||||
{methods.map((m, i) => (
|
||||
<div key={m.id} className={i > 0 ? "hairline-t" : ""}>
|
||||
<div className="grid lg:grid-cols-12 gap-0 min-w-0">
|
||||
<div className={`min-w-0 lg:col-span-4 p-6 hairline-r-0 lg:hairline-r bg-paper ${m.comingSoon ? "opacity-70" : ""}`}>
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
{m.recommended && <span className="pill pill-hot">Recommended</span>}
|
||||
{m.comingSoon && <span className="pill pill-ghost">Coming soon</span>}
|
||||
<span className="eyebrow">Method 0{i + 1}</span>
|
||||
</div>
|
||||
<h3 className="font-display text-xl mb-1">{m.label}</h3>
|
||||
<div className="font-cjk text-sm text-ink-mute mb-3">{m.cn}</div>
|
||||
<div className="text-xs text-ink-soft leading-relaxed">
|
||||
<strong className="text-ink">Prereq:</strong> {m.prereq}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`min-w-0 lg:col-span-8 p-6 bg-paper-deep relative ${m.comingSoon ? "opacity-80" : ""}`}>
|
||||
{!m.comingSoon && (
|
||||
<button
|
||||
onClick={() => copy(m.id, m.cmd)}
|
||||
className="absolute top-7 right-7 z-10 px-3 py-1 bg-paper hairline-t hairline-b hairline-l hairline-r font-mono text-[0.7rem] uppercase tracking-wider hover:bg-indigo hover:text-paper transition-colors"
|
||||
>
|
||||
{copied === m.id ? "Copied ✓" : "Copy"}
|
||||
</button>
|
||||
)}
|
||||
<pre className="code-block text-[0.78rem] m-0 max-w-full">{m.cmd}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
|
||||
const LABELS: Record<Locale, string> = { en: "EN", zh: "中文" };
|
||||
|
||||
export function LocaleSwitcher({ current }: { current: string }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const switchTo = current === "zh" ? "en" : "zh";
|
||||
const other = LABELS[switchTo as Locale];
|
||||
|
||||
const handleClick = () => {
|
||||
// Replace locale segment in path
|
||||
const segments = pathname.split("/");
|
||||
if (locales.includes(segments[1] as Locale)) {
|
||||
segments[1] = switchTo;
|
||||
} else {
|
||||
segments.splice(1, 0, switchTo);
|
||||
}
|
||||
const newPath = segments.join("/") || `/${switchTo}`;
|
||||
document.cookie = `NEXT_LOCALE=${switchTo};path=/;max-age=${60 * 60 * 24 * 365}`;
|
||||
router.push(newPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 hairline-t hairline-b hairline-l hairline-r font-mono text-[0.7rem] uppercase tracking-wider hover:bg-paper-deep transition-colors"
|
||||
title={current === "zh" ? "Switch to English" : "切换到中文"}
|
||||
>
|
||||
<span className="font-cjk normal-case tracking-normal">{other}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
chart: string;
|
||||
label?: string;
|
||||
fallback?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function MermaidDiagram({ chart, label, fallback }: Props) {
|
||||
const [svg, setSvg] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const idRef = useRef(`mermaid-${Math.random().toString(36).slice(2, 9)}`);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const mermaid = (await import("mermaid")).default;
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "strict",
|
||||
theme: "base",
|
||||
fontFamily: '"JetBrains Mono", ui-monospace, Menlo, monospace',
|
||||
flowchart: {
|
||||
curve: "basis",
|
||||
padding: 14,
|
||||
htmlLabels: false,
|
||||
useMaxWidth: true,
|
||||
},
|
||||
themeVariables: {
|
||||
background: "#ffffff",
|
||||
primaryColor: "#ffffff",
|
||||
primaryTextColor: "#0e0e10",
|
||||
primaryBorderColor: "#0e0e10",
|
||||
lineColor: "#4d6bfe",
|
||||
secondaryColor: "#e9eefe",
|
||||
tertiaryColor: "#f4f6fb",
|
||||
edgeLabelBackground: "#ffffff",
|
||||
clusterBkg: "#f4f6fb",
|
||||
clusterBorder: "#0e0e10",
|
||||
nodeBorder: "#0e0e10",
|
||||
mainBkg: "#ffffff",
|
||||
},
|
||||
});
|
||||
const { svg: rendered } = await mermaid.render(idRef.current, chart);
|
||||
if (!cancelled) setSvg(rendered);
|
||||
} catch (e) {
|
||||
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [chart]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="mermaid-frame" role="img" aria-label={label}>
|
||||
<pre className="code-block text-[0.78rem]">{chart}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!svg) {
|
||||
return (
|
||||
<div className="mermaid-frame" role="img" aria-label={label} aria-busy="true">
|
||||
{fallback ?? (
|
||||
<pre className="code-block text-[0.78rem] opacity-70">{chart}</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mermaid-frame"
|
||||
role="img"
|
||||
aria-label={label}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type MobileLink = { href: string; label: string; cn?: string };
|
||||
|
||||
export function MobileMenu({
|
||||
links,
|
||||
installHref,
|
||||
installLabel,
|
||||
}: {
|
||||
links: MobileLink[];
|
||||
installHref: string;
|
||||
installLabel: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.body.style.overflow = prev;
|
||||
window.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="md:hidden inline-flex items-center justify-center w-9 h-9 hairline-t hairline-b hairline-l hairline-r hover:bg-paper-deep transition-colors"
|
||||
aria-label={open ? "Close menu" : "Open menu"}
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
{open ? (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden>
|
||||
<path d="M2 2L12 12M12 2L2 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" aria-hidden>
|
||||
<path d="M0 1H16M0 6H16M0 11H16" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
id="mobile-menu"
|
||||
className="md:hidden fixed inset-x-0 top-[5.7rem] bottom-0 z-40 bg-paper hairline-t overflow-y-auto"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<nav className="px-6 py-4">
|
||||
<ul className="divide-y divide-[rgba(14,14,16,0.18)]">
|
||||
{links.map((l) => (
|
||||
<li key={l.href}>
|
||||
<Link
|
||||
href={l.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex items-baseline gap-3 py-4 hover:text-indigo transition-colors"
|
||||
>
|
||||
<span className="font-display text-lg">{l.label}</span>
|
||||
{l.cn && (
|
||||
<span className="font-cjk text-sm text-ink-mute">{l.cn}</span>
|
||||
)}
|
||||
<span className="ml-auto font-mono text-xs text-ink-mute">→</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link
|
||||
href={installHref}
|
||||
onClick={() => setOpen(false)}
|
||||
className="mt-6 block w-full text-center px-5 py-3 bg-indigo text-paper font-mono text-sm uppercase tracking-wider hover:bg-indigo-deep transition-colors"
|
||||
>
|
||||
{installLabel}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import Link from "next/link";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { Seal } from "./seal";
|
||||
import { Whale } from "./whale";
|
||||
import { LocaleSwitcher } from "./locale-switcher";
|
||||
import { MobileMenu } from "./mobile-menu";
|
||||
|
||||
const EN_LINKS = [
|
||||
{ href: "/install", label: "Install", cn: "安装" },
|
||||
{ href: "/docs", label: "Docs", cn: "文档" },
|
||||
{ href: "/feed", label: "Activity", cn: "动态" },
|
||||
{ href: "/roadmap", label: "Roadmap", cn: "路线" },
|
||||
{ href: "/contribute", label: "Contribute", cn: "参与" },
|
||||
];
|
||||
|
||||
const ZH_LINKS = [
|
||||
{ href: "/zh/install", label: "安装", cn: "" },
|
||||
{ href: "/zh/docs", label: "文档", cn: "" },
|
||||
{ href: "/zh/feed", label: "动态", cn: "" },
|
||||
{ href: "/zh/roadmap", label: "路线图", cn: "" },
|
||||
{ href: "/zh/contribute", label: "参与贡献", cn: "" },
|
||||
];
|
||||
|
||||
export function Nav({ locale = "en" }: { locale?: Locale }) {
|
||||
const isZh = locale === "zh";
|
||||
const links = isZh ? ZH_LINKS : EN_LINKS;
|
||||
|
||||
return (
|
||||
<header className="hairline-b bg-paper/85 backdrop-blur sticky top-0 z-30">
|
||||
{/* date / build strip */}
|
||||
<div className="hairline-b">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-1.5 flex items-center justify-between text-[0.66rem] font-mono uppercase tracking-[0.18em] text-ink-mute">
|
||||
<div className="flex items-center gap-4">
|
||||
<span>{isZh ? `第 ${new Date().toISOString().slice(0, 10)} 期` : `第 ${new Date().toISOString().slice(0, 10)} 期`}</span>
|
||||
<span className="hidden sm:inline">· {isZh ? new Date().toLocaleDateString("zh-CN", { weekday: "long", month: "long", day: "numeric" }) : new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" })}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="hidden md:inline">deepseek-tui.com</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 bg-jade rounded-full inline-block animate-pulse" />
|
||||
<span>{isZh ? "API · 在线" : "API · 在线"}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* main nav */}
|
||||
<div className="mx-auto max-w-[1400px] px-4 sm:px-6 py-3 flex items-center justify-between gap-3 sm:gap-6">
|
||||
<Link href={isZh ? "/zh" : "/"} className="flex items-center gap-3 group min-w-0">
|
||||
<Seal char="深" size="md" />
|
||||
<div className="leading-tight min-w-0">
|
||||
<div className="font-display text-[1.2rem] sm:text-[1.35rem] font-semibold tracking-crisp flex items-center gap-2 truncate">
|
||||
DeepSeek TUI
|
||||
<Whale size={20} className="text-indigo hidden sm:inline-block" />
|
||||
</div>
|
||||
<div className="font-cjk text-[0.65rem] sm:text-[0.7rem] text-ink-mute tracking-widest truncate">
|
||||
{isZh ? "深度求索 · 终端智能体" : "深度求索 · 终端代理"}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-7">
|
||||
{links.map((l) => (
|
||||
<Link key={l.href} href={l.href} className="nav-link group">
|
||||
<span>{l.label}</span>
|
||||
{!isZh && "cn" in l && l.cn && (
|
||||
<span className="font-cjk text-[0.66rem] ml-1.5 text-ink-mute">{l.cn}</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<LocaleSwitcher current={locale} />
|
||||
<Link
|
||||
href="https://github.com/Hmbown/deepseek-tui"
|
||||
className="hidden sm:inline-flex items-center gap-2 px-3 py-1.5 hairline-t hairline-b hairline-l hairline-r font-mono text-[0.7rem] uppercase tracking-wider hover:bg-paper-deep transition-colors"
|
||||
>
|
||||
<span>★ GitHub</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={isZh ? "/zh/install" : "/install"}
|
||||
className="hidden md:inline-flex items-center gap-2 px-3 py-1.5 bg-indigo text-paper font-mono text-[0.72rem] uppercase tracking-wider hover:bg-indigo-deep transition-colors"
|
||||
>
|
||||
{isZh ? "安装 →" : "Install →"}
|
||||
</Link>
|
||||
<MobileMenu
|
||||
installHref={isZh ? "/zh/install" : "/install"}
|
||||
installLabel={isZh ? "安装 30 秒搞定 →" : "Install in 30 seconds →"}
|
||||
links={links.map((l) => ({
|
||||
href: l.href,
|
||||
label: l.label,
|
||||
cn: !isZh && "cn" in l ? l.cn : undefined,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function Seal({
|
||||
char = "深",
|
||||
size = "md",
|
||||
variant = "ink",
|
||||
}: {
|
||||
char?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
variant?: "ink" | "indigo";
|
||||
}) {
|
||||
const dim = size === "sm" ? "w-7 h-7 text-sm" : size === "lg" ? "w-12 h-12 text-2xl" : "w-10 h-10 text-lg";
|
||||
const cls = variant === "indigo" ? "seal seal-indigo" : "seal";
|
||||
return (
|
||||
<span className={`${cls} ${dim}`} aria-hidden>
|
||||
{char}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { RepoStats } from "@/lib/types";
|
||||
|
||||
function fmt(n: number): string {
|
||||
if (n >= 1000) return (n / 1000).toFixed(1) + "k";
|
||||
return n.toString();
|
||||
}
|
||||
|
||||
export function StatGrid({ stats }: { stats: RepoStats }) {
|
||||
const cells = [
|
||||
{ label: "Stars", cn: "星标", value: fmt(stats.stars) },
|
||||
{ label: "Forks", cn: "复刻", value: fmt(stats.forks) },
|
||||
{ label: "Contributors", cn: "贡献者", value: fmt(stats.contributors) },
|
||||
{
|
||||
label: "Latest",
|
||||
cn: "版本",
|
||||
value: stats.latestRelease?.tag ?? "—",
|
||||
mono: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="hairline-t hairline-b grid grid-cols-2 sm:grid-cols-4 col-rule">
|
||||
{cells.map((c) => (
|
||||
<div key={c.label} className="px-5 py-5">
|
||||
<div className="eyebrow mb-2">
|
||||
{c.label} · <span className="font-cjk normal-case tracking-normal">{c.cn}</span>
|
||||
</div>
|
||||
<div className={c.mono ? "font-mono text-2xl tabular text-ink" : "bignum text-ink"}>{c.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { FeedItem } from "@/lib/types";
|
||||
import { relativeTime } from "@/lib/github";
|
||||
|
||||
export function Ticker({ items }: { items: FeedItem[] }) {
|
||||
if (!items.length) return null;
|
||||
const doubled = [...items, ...items]; // seamless loop
|
||||
return (
|
||||
<div className="hairline-t hairline-b bg-paper-deep overflow-hidden">
|
||||
<div className="mx-auto max-w-[1400px] flex items-stretch">
|
||||
<div className="bg-ink text-paper px-4 py-2 flex items-center shrink-0 gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-indigo rounded-full inline-block animate-pulse" />
|
||||
<span className="font-cjk text-sm font-semibold tracking-wider">实 时</span>
|
||||
<span className="font-mono text-[0.7rem] uppercase tracking-widest text-paper-deep">LIVE</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<div className="ticker-track py-2 font-mono text-[0.78rem]">
|
||||
{doubled.map((item, i) => (
|
||||
<span key={`${item.url}-${i}`} className="inline-flex items-center gap-2">
|
||||
<span className="text-indigo uppercase tracking-wider">{item.kind === "pull" ? "PR" : "ISS"}</span>
|
||||
<span className="tabular text-ink-mute">#{item.number}</span>
|
||||
<span className="text-ink">{item.title.slice(0, 78)}{item.title.length > 78 ? "…" : ""}</span>
|
||||
<span className="text-ink-mute tabular">· {relativeTime(item.updatedAt)}</span>
|
||||
<span className="text-paper-line">◆</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Stylized whale mark — a nod to DeepSeek's cetacean motif.
|
||||
* Kept minimal and geometric so it reads as a wordmark accent, not an illustration.
|
||||
*/
|
||||
export function Whale({ size = 22, className = "" }: { size?: number; className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 64 32"
|
||||
width={size}
|
||||
height={(size * 32) / 64}
|
||||
className={className}
|
||||
aria-hidden
|
||||
fill="currentColor"
|
||||
>
|
||||
{/* body */}
|
||||
<path d="M2 18 C 2 10, 14 4, 28 4 C 42 4, 50 10, 50 16 C 50 22, 42 28, 28 28 C 18 28, 8 24, 2 18 Z" />
|
||||
{/* tail flukes */}
|
||||
<path d="M48 12 L 62 4 L 58 16 L 62 28 L 48 20 Z" />
|
||||
{/* eye */}
|
||||
<circle cx="14" cy="14" r="1.4" fill="#FFFFFF" />
|
||||
{/* spout */}
|
||||
<path d="M22 4 L 22 0 M 26 4 L 28 0 M 18 4 L 16 0" stroke="currentColor" strokeWidth="1.2" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user