Skip to content
Open
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
130 changes: 130 additions & 0 deletions app/(dashboard)/api-keys/_components/api-key-sample-code.tsx
Original file line number Diff line number Diff line change
@@ -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<SampleCodeTab>("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 (
<section className="overflow-hidden border border-[#e6defe] bg-[#f5f3ff]">
<div className="flex items-center gap-2 px-4 pb-4 pt-[14px] lg:gap-3 lg:px-5 lg:pt-4">
<div className="flex size-8 items-center justify-center border border-[#ddd6fe] bg-[#ede9fe] lg:size-9">
<TerminalSquare className="h-3.5 w-3.5 text-[#7f22fe]" strokeWidth={1.8} />
</div>
<p className="text-[14px] font-medium leading-[22px] text-[#09090b] dark:text-[#fafafa] lg:text-[16px] lg:leading-6">
{t("codeTitle")}
</p>
</div>

<div className="px-4 pb-6 pt-0 lg:px-5 lg:pb-7">
<div className="overflow-hidden bg-[#27272a]">
<div className="relative flex items-start gap-[10px] border-b border-[#3f3f46] px-[14px] py-[14px] sm:flex-wrap sm:items-center sm:gap-3 sm:px-4 sm:py-4 lg:gap-2">
<div
className="flex min-w-0 flex-nowrap items-center gap-[6px] overflow-x-auto pr-16 sm:flex-wrap sm:pr-0"
role="tablist"
aria-label={t("codeTabs")}
>
{sampleCodeTabConfig.map((tab) => {
const isActive = activeTab === tab.id;

return (
<button
key={tab.id}
type="button"
id={`${tabsId}-${tab.id}-tab`}
role="tab"
aria-controls={`${tabsId}-${tab.id}-panel`}
aria-selected={isActive}
tabIndex={isActive ? 0 : -1}
className={cn(
"min-h-[26px] shrink-0 px-[10px] py-[6px] font-mono-display text-[12px] leading-4 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#a684ff] sm:min-h-9 sm:px-3 sm:py-2 sm:text-[14px] sm:leading-5 lg:min-h-8 lg:text-[12px] lg:leading-4",
isActive ? "bg-[#fafafa] text-[#09090b]" : "bg-[#3f3f46] text-[#fafafa]"
)}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
);
})}
</div>

<button
type="button"
className="absolute right-[14px] top-1/2 inline-flex -translate-y-1/2 items-center gap-1 rounded-full bg-[#27272a] px-[14px] py-[6px] font-mono-display text-[12px] leading-4 text-[#a684ff] transition-colors hover:bg-[#3f3f46] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#a684ff] sm:static sm:ml-auto sm:h-9 sm:translate-y-0 sm:px-4 sm:py-2 sm:text-[14px] sm:leading-5 lg:h-auto lg:px-4 lg:py-2 lg:text-[12px] lg:leading-4"
onClick={() => {
void handleCopyCode();
}}
>
<DashboardCopyIcon className="size-4" />
<span>{t("copyCode")}</span>
</button>
</div>

<div
id={`${tabsId}-${activeTab}-panel`}
role="tabpanel"
aria-labelledby={`${tabsId}-${activeTab}-tab`}
className="overflow-x-auto p-[14px] sm:px-4 sm:py-4 lg:px-5 lg:py-5"
>
<Highlight
code={currentCode}
language={sampleCodeTabConfig.find((tab) => tab.id === activeTab)?.language ?? "bash"}
theme={themes.vsDark}
>
{({ className, getLineProps, getTokenProps, tokens }) => (
<pre
className={cn(
className,
"min-w-max bg-transparent p-0 font-mono-readable text-[12px] leading-[18px] text-[#fafafa] sm:text-[14px] sm:leading-5 lg:text-[13px]"
)}
>
{tokens.map((line, lineIndex) => (
<div key={`line-${lineIndex + 1}`} {...getLineProps({ line })}>
{line.map((token, tokenIndex) => (
<span
key={`token-${lineIndex + 1}-${tokenIndex + 1}`}
{...getTokenProps({ token })}
/>
))}
</div>
))}
</pre>
)}
</Highlight>
</div>
</div>
</div>
</section>
);
};
3 changes: 3 additions & 0 deletions app/(dashboard)/api-keys/_components/api-keys-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -358,6 +359,8 @@ export const ApiKeysPage = () => {
onToggle={handleToggleKey}
/>
) : null}

<ApiKeySampleCode />
</div>

<CreateApiKeyDialog
Expand Down
50 changes: 45 additions & 5 deletions app/(dashboard)/api-keys/_components/api-keys-table.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"use client";

import { DashboardCopyIcon } from "@app/(dashboard)/_components/dashboard-modal-primitives";
import { useToast } from "@hooks/use-toast";
import { cn } from "@lib/utils";
import type { APIKey } from "@server/external-api/api-keys";
import { formatDate } from "@utils/format";
import { copyToClipboard, formatDate } from "@utils/format";
import Image from "next/image";
import { useTranslations } from "next-intl";

Expand Down Expand Up @@ -70,6 +72,41 @@ const ToggleButton = ({
);
};

const CopyKeyButton = ({ apiKey }: { apiKey: APIKey }) => {
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 (
<button
type="button"
onClick={() => {
void handleCopy();
}}
aria-label={`${t("copyKeyPrefix")} ${apiKey.name}`}
title={t("copyKeyPrefix")}
className="ml-1 flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-[#71717b] transition-colors hover:bg-[#ede9fe] hover:text-[#7f22fe] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#7f22fe]/25"
>
<DashboardCopyIcon className="size-4" />
</button>
);
};

export const ApiKeysTable = ({
apiKeys,
locale,
Expand Down Expand Up @@ -128,10 +165,13 @@ export const ApiKeysTable = ({
{key.name}
</td>
<td className="h-[52px] px-[14px] lg:px-4">
<div className="inline-flex max-w-full items-center bg-[#f5f3ff] px-[6px] py-0.5 lg:px-2 lg:py-1">
<code className="block truncate font-mono-readable text-xs leading-[18px] text-[#4d179a] lg:text-sm lg:leading-5">
{apiKeyPreview}
</code>
<div className="flex max-w-full items-center">
<div className="inline-flex min-w-0 items-center bg-[#f5f3ff] px-[6px] py-0.5 lg:px-2 lg:py-1">
<code className="block truncate font-mono-readable text-xs leading-[18px] text-[#4d179a] lg:text-sm lg:leading-5">
{apiKeyPreview}
</code>
</div>
<CopyKeyButton apiKey={key} />
</div>
</td>
<td className="h-[52px] px-[14px] lg:px-4">
Expand Down
96 changes: 4 additions & 92 deletions app/(dashboard)/usage/_components/usage-welcome-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<WelcomeCodeTab, string> => ({
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 (
<div className="flex items-center gap-1.5 text-[12px] leading-[18px] text-[#09090b] lg:gap-2 lg:text-[14px] lg:leading-5">
Expand All @@ -130,7 +42,7 @@ export const UsageWelcomeModal = () => {
const tabsId = useId();
const { apiKey, dismiss, hasProvisionError, isDismissing, isOpen, isProvisioning } =
useUsageWelcome();
const [activeTab, setActiveTab] = useState<WelcomeCodeTab>("python");
const [activeTab, setActiveTab] = useState<SampleCodeTab>("python");
const apiBaseUrl = env.NEXT_PUBLIC_API_URL.replace(/\/$/, "");
const canDismiss = Boolean(apiKey) || hasProvisionError;
const codeByTab = apiKey
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -330,7 +242,7 @@ export const UsageWelcomeModal = () => {
<Highlight
code={currentCode}
language={
codeTabConfig.find((tab) => tab.id === activeTab)?.language ?? "bash"
sampleCodeTabConfig.find((tab) => tab.id === activeTab)?.language ?? "bash"
}
theme={themes.vsDark}
>
Expand Down
8 changes: 7 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
8 changes: 7 additions & 1 deletion i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,13 @@
"delete": "删除",
"toggleConfirmTitle": "确认禁用 API Key?",
"toggleConfirmDesc": "禁用后,使用此 Key 的应用将无法访问 API。您随时可以再次启用它。",
"confirmDisable": "确认禁用"
"confirmDisable": "确认禁用",
"copyKeyPrefix": "复制 Key 前缀",
"codeTitle": "运行以下代码解析您的第一个 PDF:",
"codeTabs": "示例代码语言",
"copyCode": "复制",
"copyCodeSuccess": "示例代码已复制",
"copyCodeFailed": "复制示例代码失败"
},
"FileUpload": {
"status": {
Expand Down
Loading