diff --git a/app/academy/[[...slug]]/not-found.tsx b/app/academy/(en)/[[...slug]]/not-found.tsx similarity index 100% rename from app/academy/[[...slug]]/not-found.tsx rename to app/academy/(en)/[[...slug]]/not-found.tsx diff --git a/app/academy/[[...slug]]/page.tsx b/app/academy/(en)/[[...slug]]/page.tsx similarity index 100% rename from app/academy/[[...slug]]/page.tsx rename to app/academy/(en)/[[...slug]]/page.tsx diff --git a/app/academy/layout.tsx b/app/academy/(en)/layout.tsx similarity index 100% rename from app/academy/layout.tsx rename to app/academy/(en)/layout.tsx diff --git a/app/academy/japan/[[...slug]]/not-found.tsx b/app/academy/japan/[[...slug]]/not-found.tsx new file mode 100644 index 0000000000..2e0a57e9de --- /dev/null +++ b/app/academy/japan/[[...slug]]/not-found.tsx @@ -0,0 +1,15 @@ +import Link from "next/link"; + +export default function JaAcademyNotFound() { + return ( +
+

ページが見つかりません

+

+ お探しのページは存在しないか、移動されました。 +

+ + Academy トップへ + +
+ ); +} diff --git a/app/academy/japan/[[...slug]]/page.tsx b/app/academy/japan/[[...slug]]/page.tsx new file mode 100644 index 0000000000..2434031add --- /dev/null +++ b/app/academy/japan/[[...slug]]/page.tsx @@ -0,0 +1,52 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { academyJaSource } from "@/lib/source"; +import { DocsChromePage } from "@/components/DocsChromePage"; +import { buildSectionMetadata } from "@/lib/mdx-page"; + +type PageProps = { + params: Promise<{ slug?: string[] }>; +}; + +export default async function JaAcademyPage({ params }: PageProps) { + const { slug = [] } = await params; + const page = academyJaSource.getPage(slug); + if (!page) notFound(); + // Only the Academy intro page (empty slug) shows the translation credit. + const isIntroPage = slug.length === 0; + return ( + + Translation by{" "} + + GAO, Inc. + + + ) : undefined + } + /> + ); +} + +export async function generateMetadata({ + params, +}: PageProps): Promise { + const { slug = [] } = await params; + const page = academyJaSource.getPage(slug); + if (!page) return { title: "Not Found" }; + return buildSectionMetadata(page, "academy/japan", "Academy", slug); +} + +export function generateStaticParams() { + return academyJaSource + .generateParams() + .map((p) => (p.slug.length > 0 ? { slug: p.slug } : {})); +} diff --git a/app/academy/japan/layout.tsx b/app/academy/japan/layout.tsx new file mode 100644 index 0000000000..d72046697c --- /dev/null +++ b/app/academy/japan/layout.tsx @@ -0,0 +1,14 @@ +import { academyJaSource } from "@/lib/source"; +import { SharedDocsLayout } from "@/components/layout"; + +export default function JaAcademyLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/components/DocsChromePage.tsx b/components/DocsChromePage.tsx index f1921cf3c0..cb7cf5a18f 100644 --- a/components/DocsChromePage.tsx +++ b/components/DocsChromePage.tsx @@ -1,5 +1,5 @@ import "server-only"; -import type { ComponentProps, ComponentType } from "react"; +import type { ComponentProps, ComponentType, ReactNode } from "react"; import { DocsPage } from "fumadocs-ui/page"; import type { TOCItemType } from "fumadocs-core/toc"; @@ -32,10 +32,13 @@ const getIsoDate = (value: unknown): string | undefined => { export async function DocsChromePage({ page, bodyChromeProps, + bottomSuffix, }: { page: LoadedPage; /** Extra props forwarded to `DocBodyChrome` (e.g. `versionLabel` on self-hosting). */ bodyChromeProps?: BodyChromeProps; + /** Optional node rendered inside DocBodyChrome, after the MDX body. */ + bottomSuffix?: ReactNode; }) { const data = page.data; const loaded = @@ -63,6 +66,7 @@ export async function DocsChromePage({ > + {bottomSuffix} ); diff --git a/components/academy/japan/AgentPromptCallout.tsx b/components/academy/japan/AgentPromptCallout.tsx new file mode 100644 index 0000000000..2b7209b690 --- /dev/null +++ b/components/academy/japan/AgentPromptCallout.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useState } from "react"; + +export interface AgentPromptCalloutProps { + /** Ribbon label, e.g. "Run with your agent". */ + ribbon?: string; + /** Title shown above the lede. */ + title?: string; + /** Lede paragraph beneath the title. */ + lede?: React.ReactNode; + /** The exact text written to the clipboard. */ + prompt: string; +} + +export function AgentPromptCallout({ + ribbon = "エージェントで実行する", + title, + lede, + prompt, +}: AgentPromptCalloutProps) { + const [copied, setCopied] = useState(false); + + const onCopy = () => { + if (typeof navigator === "undefined" || !navigator.clipboard) return; + navigator.clipboard.writeText(prompt.trim()).then( + () => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }, + () => {}, + ); + }; + + return ( +
+
+ + {ribbon} +
+ + {(title || lede) && ( +
+ {title &&

{title}

} + {lede &&

{lede}

} +
+ )} + +
+
{prompt.trim()}
+ +
+ + +
+ ); +} diff --git a/components/academy/japan/DatasetFieldsDiagram.tsx b/components/academy/japan/DatasetFieldsDiagram.tsx new file mode 100644 index 0000000000..6415724389 --- /dev/null +++ b/components/academy/japan/DatasetFieldsDiagram.tsx @@ -0,0 +1,379 @@ +"use client"; + +import { useLayoutEffect, useRef } from "react"; + +const INNER_W = 980; +const INNER_H = 386; + +function estimateInitialScale(): number { + if (typeof window === "undefined") return 0.69; + return Math.min( + 1, + Math.max(0.25, (document.documentElement.clientWidth - 32) / INNER_W), + ); +} + +function StageLabel({ + y, + num, + name, +}: { + y: number; + num: string; + name: string; +}) { + return ( +
+ {num} + {name} +
+ ); +} + +function Node({ + x, + y, + width, + children, + variant, +}: { + x: number; + y: number; + width: number; + children: React.ReactNode; + variant?: "live" | "stripe" | "terminal"; +}) { + return ( +
+ {children} +
+ ); +} + +export function DatasetFieldsDiagram() { + const wrapRef = useRef(null); + const innerRef = useRef(null); + const estScale = estimateInitialScale(); + + useLayoutEffect(() => { + const wrap = wrapRef.current; + const inner = innerRef.current; + if (!wrap || !inner) return; + + const fit = () => { + const scale = Math.min(1, wrap.clientWidth / INNER_W); + inner.style.transform = `scale(${scale})`; + wrap.style.height = `${INNER_H * scale}px`; + }; + + fit(); + let rafId = 0; + const ro = new ResizeObserver(() => { + cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(fit); + }); + ro.observe(wrap); + + return () => { + ro.disconnect(); + cancelAnimationFrame(rafId); + }; + }, []); + + return ( +
+
+
+
+
+
+
+ + + + + + + + + 入力 + + + タスク実行 + + + 実出力 + + + + メタデータ + + + レビュー用の追加情報 + + + + 期待出力 + + + スコア / 比較 + +
+
+ + +
+ ); +} diff --git a/components/academy/japan/ErrorAnalysisProcessDiagram.tsx b/components/academy/japan/ErrorAnalysisProcessDiagram.tsx new file mode 100644 index 0000000000..268690935d --- /dev/null +++ b/components/academy/japan/ErrorAnalysisProcessDiagram.tsx @@ -0,0 +1,212 @@ +"use client"; + +import { useLayoutEffect, useRef } from "react"; + +const INNER_W = 1100; +const INNER_H = 150; + +const STEPS = [ + { num: "01", label: "収集", title: "トレースを\n集める" }, + { num: "02", label: "記録", title: "オープン\nコーディング" }, + { num: "03", label: "グループ化", title: "カテゴリに\nクラスタリング" }, + { num: "04", label: "定量化", title: "ラベル付けと\n計測" }, + { num: "05", label: "実行", title: "判断と実行", accent: true }, +]; + +function estimateInitialScale(): number { + if (typeof window === "undefined") return 0.65; + const vw = document.documentElement.clientWidth; + return Math.min(1, Math.max(0.3, (vw - 32) / INNER_W)); +} + +export function ErrorAnalysisProcessDiagram() { + const wrapRef = useRef(null); + const innerRef = useRef(null); + const estScale = estimateInitialScale(); + + useLayoutEffect(() => { + const wrap = wrapRef.current; + const inner = innerRef.current; + if (!wrap || !inner) return; + + const fit = () => { + const scale = Math.min(1, wrap.clientWidth / INNER_W); + inner.style.transform = `scale(${scale})`; + wrap.style.height = `${INNER_H * scale}px`; + }; + + fit(); + let rafId = 0; + const ro = new ResizeObserver(() => { + cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(fit); + }); + ro.observe(wrap); + + return () => { + ro.disconnect(); + cancelAnimationFrame(rafId); + }; + }, []); + + return ( +
+
+
+ {STEPS.map((step, i) => ( +
+
+
+ {step.num} + + {step.label} + +
+

{step.title}

+
+ {i < STEPS.length - 1 && ( + + )} +
+ ))} +
+
+ + +
+ ); +} diff --git a/components/academy/japan/EvaluationEvolutionDiagram.tsx b/components/academy/japan/EvaluationEvolutionDiagram.tsx new file mode 100644 index 0000000000..2373190f04 --- /dev/null +++ b/components/academy/japan/EvaluationEvolutionDiagram.tsx @@ -0,0 +1,599 @@ +import type { ReactNode } from "react"; + +function Connector() { + return ( +