add stable media support and sync skillhub UI
This commit is contained in:
@@ -1,5 +1,34 @@
|
||||
import Link from "next/link";
|
||||
import { getDictionary, type Locale } from "@/lib/site-content";
|
||||
import { CopyButton } from "@/components/copy-button";
|
||||
import { PopiartApiError, getBudgetSummary, getBudgetUsage, getPopiartEndpoint, getViewerSession } from "@/lib/popiart-api";
|
||||
import { getLiveCopy } from "@/lib/live-copy";
|
||||
import { type Locale } from "@/lib/site-content";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function formatNumber(locale: Locale, value: number) {
|
||||
return new Intl.NumberFormat(locale === "zh" ? "zh-CN" : "en-US").format(value);
|
||||
}
|
||||
|
||||
function maskSecret(value?: string) {
|
||||
if (!value) {
|
||||
return "Unavailable";
|
||||
}
|
||||
if (value.length <= 10) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)} • • • • ${value.slice(-4)}`;
|
||||
}
|
||||
|
||||
function formatError(error: unknown) {
|
||||
if (error instanceof PopiartApiError) {
|
||||
return error.message;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export default async function ConsolePage({
|
||||
params,
|
||||
@@ -7,109 +36,167 @@ export default async function ConsolePage({
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
const dictionary = getDictionary(locale as Locale);
|
||||
const typedLocale = locale as Locale;
|
||||
const liveCopy = getLiveCopy(typedLocale);
|
||||
const session = await getViewerSession();
|
||||
const isZh = typedLocale === "zh";
|
||||
|
||||
const pageTitle = isZh ? "控制台" : "Console";
|
||||
const pageSubtitle = isZh
|
||||
? "管理产品层密钥、查看用量、快速接入 CLI"
|
||||
: "Manage product-layer keys, usage, and quick CLI onboarding.";
|
||||
const remainingLabel = isZh ? "剩余 Credit" : "Remaining credits";
|
||||
const remainingHint = isZh ? "总计可用额度" : "Total available quota";
|
||||
const usedLabel = isZh ? "已使用" : "Used";
|
||||
const usedHint = isZh ? "本月累计消耗" : "Consumed this month";
|
||||
const callsLabel = isZh ? "调用次数" : "API calls";
|
||||
const callsHint = isZh ? "本月 CLI / API 调用" : "CLI / API calls this month";
|
||||
const keysTitle = isZh ? "API 密钥" : "API keys";
|
||||
const quickTitle = isZh ? "快速安装指令" : "Quick install commands";
|
||||
const quickBody = isZh
|
||||
? "把下面这段命令复制到终端,完成 CLI 安装、登录和验证。"
|
||||
: "Copy these commands into your terminal to install the CLI, sign in, and verify the workflow.";
|
||||
const sessionKeyLabel = "POPIART_SESSION_KEY";
|
||||
const endpointLabel = "POPIART_ENDPOINT";
|
||||
const copyLabel = isZh ? "复制" : "Copy";
|
||||
const copiedLabel = isZh ? "已复制" : "Copied";
|
||||
const loginCta = isZh ? "去登录" : "Sign in";
|
||||
const docsCta = isZh ? "查看文档" : "Open docs";
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<section className="section-panel console-hero">
|
||||
<div className="section-heading">
|
||||
<h1>{pageTitle}</h1>
|
||||
<p>{pageSubtitle}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section-panel console-surface-card">
|
||||
<div className="section-heading compact">
|
||||
<h2>{liveCopy.console.unauthenticatedTitle}</h2>
|
||||
<p>{liveCopy.console.unauthenticatedBody}</p>
|
||||
</div>
|
||||
<div className="hero-actions">
|
||||
<Link className="button button-dark" href={`/${locale}/login`}>
|
||||
{loginCta}
|
||||
</Link>
|
||||
<Link className="button button-light" href={`/${locale}/docs`}>
|
||||
{docsCta}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [budgetResult, usageResult] = await Promise.allSettled([getBudgetSummary(), getBudgetUsage()]);
|
||||
const budget = budgetResult.status === "fulfilled" ? budgetResult.value : null;
|
||||
const usage = usageResult.status === "fulfilled" ? usageResult.value : null;
|
||||
const loadErrors = [budgetResult, usageResult]
|
||||
.filter((result) => result.status === "rejected")
|
||||
.map((result) => formatError((result as PromiseRejectedResult).reason));
|
||||
|
||||
const metrics = [
|
||||
{
|
||||
label: remainingLabel,
|
||||
value: budget ? formatNumber(typedLocale, budget.remaining.tokens) : "--",
|
||||
hint: remainingHint,
|
||||
},
|
||||
{
|
||||
label: usedLabel,
|
||||
value: budget ? formatNumber(typedLocale, budget.used.tokens) : "--",
|
||||
hint: usedHint,
|
||||
},
|
||||
{
|
||||
label: callsLabel,
|
||||
value: usage ? formatNumber(typedLocale, usage.total.job_count) : "--",
|
||||
hint: callsHint,
|
||||
},
|
||||
];
|
||||
|
||||
const quickStart = [
|
||||
"brew tap wtgoku-create/popi",
|
||||
"brew install wtgoku-create/popi/popiart",
|
||||
`export POPIART_ENDPOINT=${getPopiartEndpoint()}`,
|
||||
"popiart auth login --key <your-popinewapi-key>",
|
||||
"popiart skills list",
|
||||
].join("\n");
|
||||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<section className="section-panel">
|
||||
<div className="dashboard-top">
|
||||
<div>
|
||||
<div className="eyebrow">{dictionary.console.tag}</div>
|
||||
<h1>{dictionary.console.title}</h1>
|
||||
<p>{dictionary.console.subtitle}</p>
|
||||
</div>
|
||||
<div className="dashboard-actions">
|
||||
<Link className="button button-light" href={`/${locale}/pricing`}>
|
||||
{dictionary.console.billingCta}
|
||||
</Link>
|
||||
<Link className="button button-dark" href={`/${locale}/login`}>
|
||||
{dictionary.console.loginCta}
|
||||
</Link>
|
||||
</div>
|
||||
<section className="section-panel console-hero">
|
||||
<div className="section-heading">
|
||||
<h1>{pageTitle}</h1>
|
||||
<p>{pageSubtitle}</p>
|
||||
</div>
|
||||
{loadErrors.length > 0 ? (
|
||||
<div className="status-banner status-banner-error">
|
||||
<strong>{liveCopy.console.loadErrorPrefix}</strong>
|
||||
<span>{loadErrors.join(" | ")}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<div className="stats-grid">
|
||||
{dictionary.console.metrics.map((metric) => (
|
||||
<article className="stat-card" key={metric.label}>
|
||||
<strong>{metric.value}</strong>
|
||||
<span>{metric.label}</span>
|
||||
</article>
|
||||
))}
|
||||
<section className="console-metrics-grid">
|
||||
{metrics.map((metric) => (
|
||||
<article className="console-metric-card" key={metric.label}>
|
||||
<span>{metric.label}</span>
|
||||
<strong>{metric.value}</strong>
|
||||
<small>{metric.hint}</small>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="console-surface-card">
|
||||
<div className="section-heading compact">
|
||||
<h2>{keysTitle}</h2>
|
||||
</div>
|
||||
<div className="console-key-list">
|
||||
<div className="console-key-row">
|
||||
<div className="console-key-copy">
|
||||
<div>
|
||||
<strong>{sessionKeyLabel}</strong>
|
||||
<span>{maskSecret(session.session_key)}</span>
|
||||
</div>
|
||||
{session.session_key ? (
|
||||
<CopyButton
|
||||
className="button button-light button-small console-copy-button"
|
||||
copiedLabel={copiedLabel}
|
||||
copyLabel={copyLabel}
|
||||
value={session.session_key}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="console-key-row">
|
||||
<div className="console-key-copy">
|
||||
<div>
|
||||
<strong>{endpointLabel}</strong>
|
||||
<span>{getPopiartEndpoint()}</span>
|
||||
</div>
|
||||
<CopyButton
|
||||
className="button button-light button-small console-copy-button"
|
||||
copiedLabel={copiedLabel}
|
||||
copyLabel={copyLabel}
|
||||
value={getPopiartEndpoint()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="dashboard-grid">
|
||||
<article className="dashboard-card">
|
||||
<div className="section-heading compact">
|
||||
<div className="eyebrow">{dictionary.console.skillsTag}</div>
|
||||
<h2>{dictionary.console.skillsTitle}</h2>
|
||||
</div>
|
||||
<div className="table-list">
|
||||
{dictionary.console.officialSkills.map((skill) => (
|
||||
<div className="table-row" key={skill.name}>
|
||||
<div>
|
||||
<strong>{skill.name}</strong>
|
||||
<span>{skill.routeKey}</span>
|
||||
</div>
|
||||
<p>{skill.status}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="dashboard-card">
|
||||
<div className="section-heading compact">
|
||||
<div className="eyebrow">{dictionary.console.keysTag}</div>
|
||||
<h2>{dictionary.console.keysTitle}</h2>
|
||||
</div>
|
||||
<div className="table-list">
|
||||
{dictionary.console.apiKeys.map((key) => (
|
||||
<div className="table-row" key={key.name}>
|
||||
<div>
|
||||
<strong>{key.name}</strong>
|
||||
<span>{key.masked}</span>
|
||||
</div>
|
||||
<p>{key.scope}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="dashboard-grid">
|
||||
<article className="dashboard-card">
|
||||
<div className="section-heading compact">
|
||||
<div className="eyebrow">{dictionary.console.usageTag}</div>
|
||||
<h2>{dictionary.console.usageTitle}</h2>
|
||||
</div>
|
||||
<div className="table-list">
|
||||
{dictionary.console.usageRows.map((row) => (
|
||||
<div className="table-row" key={row.name}>
|
||||
<div>
|
||||
<strong>{row.name}</strong>
|
||||
<span>{row.count}</span>
|
||||
</div>
|
||||
<p>{row.cost}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="dashboard-card billing-card">
|
||||
<div className="section-heading compact">
|
||||
<div className="eyebrow">{dictionary.console.planTag}</div>
|
||||
<h2>{dictionary.console.planTitle}</h2>
|
||||
</div>
|
||||
<p className="billing-copy">{dictionary.console.planDescription}</p>
|
||||
<ul className="bullet-list">
|
||||
{dictionary.console.planBenefits.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link className="button button-dark" href={`/${locale}/pricing`}>
|
||||
{dictionary.console.planCta}
|
||||
</Link>
|
||||
</article>
|
||||
<section className="console-surface-card">
|
||||
<div className="section-heading compact">
|
||||
<h2>{quickTitle}</h2>
|
||||
<p>{quickBody}</p>
|
||||
</div>
|
||||
<div className="console-code-block">
|
||||
<pre>
|
||||
<code>{quickStart}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user