From 8a48f51c9cac71e4fd35a7e7988814a1b9c7d857 Mon Sep 17 00:00:00 2001 From: Gordon YUEN Date: Sat, 1 Aug 2026 15:36:10 +0800 Subject: [PATCH] feat: add per-row copy button and sample code block to API Keys page - Keep the one-time reveal popup as the only source of the full key - Add a Copy button next to each masked API key row (copies key_prefix) - Add a sample-code block below the API keys table with tabs (cURL/Python/Node.js/Go) using a YOUR_API_KEY placeholder - Extract buildCodeByTab into shared lib/sample-code.ts; Usage welcome modal now imports it (no behavior change) - i18n (en/zh): copyKeyPrefix/codeTitle/codeTabs/copyCode* keys --- .../_components/api-key-sample-code.tsx | 130 ++++++++++++++++++ .../api-keys/_components/api-keys-page.tsx | 3 + .../api-keys/_components/api-keys-table.tsx | 50 ++++++- .../usage/_components/usage-welcome-modal.tsx | 96 +------------ i18n/locales/en.json | 8 +- i18n/locales/zh.json | 8 +- lib/sample-code.ts | 89 ++++++++++++ 7 files changed, 285 insertions(+), 99 deletions(-) create mode 100644 app/(dashboard)/api-keys/_components/api-key-sample-code.tsx create mode 100644 lib/sample-code.ts diff --git a/app/(dashboard)/api-keys/_components/api-key-sample-code.tsx b/app/(dashboard)/api-keys/_components/api-key-sample-code.tsx new file mode 100644 index 0000000..aa9d5ff --- /dev/null +++ b/app/(dashboard)/api-keys/_components/api-key-sample-code.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { DashboardCopyIcon } from "@app/(dashboard)/_components/dashboard-modal-primitives"; +import { useToast } from "@hooks/use-toast"; +import { buildCodeByTab, type SampleCodeTab, sampleCodeTabConfig } from "@lib/sample-code"; +import { cn } from "@lib/utils"; +import { copyToClipboard } from "@utils/format"; +import { TerminalSquare } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { Highlight, themes } from "prism-react-renderer"; +import { useId, useState } from "react"; +import { env } from "@/lib/env"; + +const PLACEHOLDER_API_KEY = "YOUR_API_KEY"; + +export const ApiKeySampleCode = () => { + const t = useTranslations("ApiKeys"); + const toast = useToast(); + const tabsId = useId(); + const [activeTab, setActiveTab] = useState("python"); + const apiBaseUrl = env.NEXT_PUBLIC_API_URL.replace(/\/$/, ""); + const codeByTab = buildCodeByTab({ + apiBaseUrl, + apiKey: PLACEHOLDER_API_KEY, + }); + const currentCode = codeByTab[activeTab]; + + const handleCopyCode = async () => { + const isCopied = await copyToClipboard(currentCode); + + if (!isCopied) { + toast.error(t("copyCodeFailed")); + return; + } + + toast.success(t("copyCodeSuccess")); + }; + + return ( +
+
+
+ +
+

+ {t("codeTitle")} +

+
+ +
+
+
+
+ {sampleCodeTabConfig.map((tab) => { + const isActive = activeTab === tab.id; + + return ( + + ); + })} +
+ + +
+ +
+ tab.id === activeTab)?.language ?? "bash"} + theme={themes.vsDark} + > + {({ className, getLineProps, getTokenProps, tokens }) => ( +
+                  {tokens.map((line, lineIndex) => (
+                    
+ {line.map((token, tokenIndex) => ( + + ))} +
+ ))} +
+ )} +
+
+
+
+
+ ); +}; diff --git a/app/(dashboard)/api-keys/_components/api-keys-page.tsx b/app/(dashboard)/api-keys/_components/api-keys-page.tsx index 52ae1bb..da39e02 100644 --- a/app/(dashboard)/api-keys/_components/api-keys-page.tsx +++ b/app/(dashboard)/api-keys/_components/api-keys-page.tsx @@ -2,6 +2,7 @@ import { DashboardActionButton } from "@app/(dashboard)/_components/dashboard-action-button"; import { ApiKeyCreatedDialog } from "@app/(dashboard)/api-keys/_components/api-key-created-dialog"; +import { ApiKeySampleCode } from "@app/(dashboard)/api-keys/_components/api-key-sample-code"; import { ApiKeysEmptyState } from "@app/(dashboard)/api-keys/_components/api-keys-empty-state"; import { ApiKeysTable } from "@app/(dashboard)/api-keys/_components/api-keys-table"; import { CreateApiKeyDialog } from "@app/(dashboard)/api-keys/_components/create-api-key-dialog"; @@ -358,6 +359,8 @@ export const ApiKeysPage = () => { onToggle={handleToggleKey} /> ) : null} + + { + const t = useTranslations("ApiKeys"); + const toast = useToast(); + + const handleCopy = async () => { + const copyValue = apiKey.api_key ?? apiKey.key_prefix ?? ""; + if (!copyValue) { + return; + } + + const isCopied = await copyToClipboard(copyValue); + + if (isCopied) { + toast.success(t("copySuccess")); + return; + } + + toast.error(t("copyFailed")); + }; + + return ( + + ); +}; + export const ApiKeysTable = ({ apiKeys, locale, @@ -128,10 +165,13 @@ export const ApiKeysTable = ({ {key.name} -
- - {apiKeyPreview} - +
+
+ + {apiKeyPreview} + +
+
diff --git a/app/(dashboard)/usage/_components/usage-welcome-modal.tsx b/app/(dashboard)/usage/_components/usage-welcome-modal.tsx index 39cda9c..f0b6aea 100644 --- a/app/(dashboard)/usage/_components/usage-welcome-modal.tsx +++ b/app/(dashboard)/usage/_components/usage-welcome-modal.tsx @@ -6,6 +6,7 @@ import { useUsageWelcome } from "@app/(dashboard)/usage/_hooks/use-usage-welcome import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@components/ui/dialog"; import { useToast } from "@hooks/use-toast"; import { trackFeatureUsage } from "@lib/posthog"; +import { buildCodeByTab, type SampleCodeTab, sampleCodeTabConfig } from "@lib/sample-code"; import { cn } from "@lib/utils"; import { copyToClipboard } from "@utils/format"; import { @@ -24,97 +25,8 @@ import { Highlight, themes } from "prism-react-renderer"; import { useId, useState } from "react"; import { env } from "@/lib/env"; -type WelcomeCodeTab = "curl" | "python" | "node" | "go"; - -const SAMPLE_PDF_URL = "https://arxiv.org/pdf/1706.03762.pdf"; const DOCUMENTATION_URL = "https://docs.knowhereto.ai/"; -const codeTabConfig: Array<{ - id: WelcomeCodeTab; - label: string; - language: "bash" | "go" | "javascript" | "python"; -}> = [ - { id: "python", label: "Python", language: "python" }, - { id: "node", label: "Node.js", language: "javascript" }, - { id: "curl", label: "cURL", language: "bash" }, - { id: "go", label: "Go", language: "go" }, -]; - -const buildCodeByTab = ({ - apiBaseUrl, - apiKey, -}: { - apiBaseUrl: string; - apiKey: string; -}): Record => ({ - curl: `curl -X POST ${apiBaseUrl}/v1/jobs \\ - -H "Authorization: Bearer ${apiKey}" \\ - -H "Content-Type: application/json" \\ - -d '{ - "source_type": "url", - "source_url": "${SAMPLE_PDF_URL}", - "parsing_params": { - "model": "base", - "ocr_enabled": true - } - }'`, - python: `# pip install knowhere-python-sdk -import knowhere - -client = knowhere.Knowhere( - api_key="${apiKey}", - base_url="${apiBaseUrl}", -) - -result = client.parse(url="${SAMPLE_PDF_URL}") - -print(result.statistics.total_chunks) -print(result.full_markdown[:200])`, - node: `// npm install @ontos-ai/knowhere-sdk -import Knowhere from "@ontos-ai/knowhere-sdk"; - -const client = new Knowhere({ - apiKey: "${apiKey}", - baseURL: "${apiBaseUrl}", -}); - -const result = await client.parse({ - url: "${SAMPLE_PDF_URL}", -}); - -console.log("Text chunks:", result.textChunks.length); -console.log(result.textChunks[0]?.content);`, - go: `package main - -import ( - "bytes" - "fmt" - "io" - "net/http" -) - -func main() { - body := []byte(\`{ - "source_type": "url", - "source_url": "${SAMPLE_PDF_URL}", - "parsing_params": { - "model": "base", - "ocr_enabled": true - } - }\`) - - req, _ := http.NewRequest("POST", "${apiBaseUrl}/v1/jobs", bytes.NewBuffer(body)) - req.Header.Set("Authorization", "Bearer ${apiKey}") - req.Header.Set("Content-Type", "application/json") - - resp, _ := http.DefaultClient.Do(req) - defer resp.Body.Close() - - result, _ := io.ReadAll(resp.Body) - fmt.Println(string(result)) -}`, -}); - const FieldLabel = ({ children, icon }: { children: React.ReactNode; icon: React.ReactNode }) => { return (
@@ -130,7 +42,7 @@ export const UsageWelcomeModal = () => { const tabsId = useId(); const { apiKey, dismiss, hasProvisionError, isDismissing, isOpen, isProvisioning } = useUsageWelcome(); - const [activeTab, setActiveTab] = useState("python"); + const [activeTab, setActiveTab] = useState("python"); const apiBaseUrl = env.NEXT_PUBLIC_API_URL.replace(/\/$/, ""); const canDismiss = Boolean(apiKey) || hasProvisionError; const codeByTab = apiKey @@ -283,7 +195,7 @@ export const UsageWelcomeModal = () => { role="tablist" aria-label={t("codeTabs")} > - {codeTabConfig.map((tab) => { + {sampleCodeTabConfig.map((tab) => { const isActive = activeTab === tab.id; return ( @@ -330,7 +242,7 @@ export const UsageWelcomeModal = () => { tab.id === activeTab)?.language ?? "bash" + sampleCodeTabConfig.find((tab) => tab.id === activeTab)?.language ?? "bash" } theme={themes.vsDark} > diff --git a/i18n/locales/en.json b/i18n/locales/en.json index c14bf3b..0ad2fa2 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -374,7 +374,13 @@ "delete": "Delete", "toggleConfirmTitle": "Confirm Disable API Key?", "toggleConfirmDesc": "Disabling this key will prevent any applications using it from accessing the API. You can enable it again at any time.", - "confirmDisable": "Confirm Disable" + "confirmDisable": "Confirm Disable", + "copyKeyPrefix": "Copy key prefix", + "codeTitle": "Run this code to parse your first PDF:", + "codeTabs": "Sample code language", + "copyCode": "Copy", + "copyCodeSuccess": "Sample code copied", + "copyCodeFailed": "Failed to copy sample code" }, "FileUpload": { "status": { diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index ed7526c..4c35ab8 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -375,7 +375,13 @@ "delete": "删除", "toggleConfirmTitle": "确认禁用 API Key?", "toggleConfirmDesc": "禁用后,使用此 Key 的应用将无法访问 API。您随时可以再次启用它。", - "confirmDisable": "确认禁用" + "confirmDisable": "确认禁用", + "copyKeyPrefix": "复制 Key 前缀", + "codeTitle": "运行以下代码解析您的第一个 PDF:", + "codeTabs": "示例代码语言", + "copyCode": "复制", + "copyCodeSuccess": "示例代码已复制", + "copyCodeFailed": "复制示例代码失败" }, "FileUpload": { "status": { diff --git a/lib/sample-code.ts b/lib/sample-code.ts new file mode 100644 index 0000000..0ad51f0 --- /dev/null +++ b/lib/sample-code.ts @@ -0,0 +1,89 @@ +export type SampleCodeTab = "curl" | "python" | "node" | "go"; + +export const SAMPLE_PDF_URL = "https://arxiv.org/pdf/1706.03762.pdf"; + +export const sampleCodeTabConfig: Array<{ + id: SampleCodeTab; + label: string; + language: "bash" | "go" | "javascript" | "python"; +}> = [ + { id: "python", label: "Python", language: "python" }, + { id: "node", label: "Node.js", language: "javascript" }, + { id: "curl", label: "cURL", language: "bash" }, + { id: "go", label: "Go", language: "go" }, +]; + +export const buildCodeByTab = ({ + apiBaseUrl, + apiKey, +}: { + apiBaseUrl: string; + apiKey: string; +}): Record => ({ + curl: `curl -X POST ${apiBaseUrl}/v1/jobs \\ + -H "Authorization: Bearer ${apiKey}" \\ + -H "Content-Type: application/json" \\ + -d '{ + "source_type": "url", + "source_url": "${SAMPLE_PDF_URL}", + "parsing_params": { + "model": "base", + "ocr_enabled": true + } + }'`, + python: `# pip install knowhere-python-sdk +import knowhere + +client = knowhere.Knowhere( + api_key="${apiKey}", + base_url="${apiBaseUrl}", +) + +result = client.parse(url="${SAMPLE_PDF_URL}") + +print(result.statistics.total_chunks) +print(result.full_markdown[:200])`, + node: `// npm install @ontos-ai/knowhere-sdk +import Knowhere from "@ontos-ai/knowhere-sdk"; + +const client = new Knowhere({ + apiKey: "${apiKey}", + baseURL: "${apiBaseUrl}", +}); + +const result = await client.parse({ + url: "${SAMPLE_PDF_URL}", +}); + +console.log("Text chunks:", result.textChunks.length); +console.log(result.textChunks[0]?.content);`, + go: `package main + +import ( + "bytes" + "fmt" + "io" + "net/http" +) + +func main() { + body := []byte(\`{ + "source_type": "url", + "source_url": "${SAMPLE_PDF_URL}", + "parsing_params": { + "model": "base", + "ocr_enabled": true + } + }\`) + + req, _ := http.NewRequest("POST", "${apiBaseUrl}/v1/jobs", bytes.NewBuffer(body)) + req.Header.Set("Authorization", "Bearer ${apiKey}") + req.Header.Set("Content-Type", "application/json") + + resp, _ := http.DefaultClient.Do(req) + defer resp.Body.Close() + + result, _ := io.ReadAll(resp.Body) + fmt.Println(string(result)) +}`, +});