Feat : Skills
This commit is contained in:
1 parent
f08fa5f78d
commit
9c6be62a54
52 files changed
+2666
-1581
No files matched your search
@@ -665,7 +665,10 @@ export default function APIPageClient({ machineId }) {
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* Endpoint Card */}
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold mb-4">API Endpoint</h2>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary">api</span>
|
||||
API Endpoint
|
||||
</h2>
|
||||
|
||||
{/* Endpoint rows */}
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -851,7 +854,10 @@ export default function APIPageClient({ machineId }) {
|
||||
{/* Token Saver (RTK + Caveman) */}
|
||||
<Card id="rtk">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-lg font-semibold">Token Saver</h2>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary">bolt</span>
|
||||
Token Saver
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-2 pb-4 border-b border-border gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -922,7 +928,10 @@ export default function APIPageClient({ machineId }) {
|
||||
{/* API Keys */}
|
||||
<Card id="require-api-key">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold">API Keys</h2>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary">vpn_key</span>
|
||||
API Keys
|
||||
</h2>
|
||||
<Button icon="add" onClick={() => setShowAddModal(true)}>
|
||||
Create Key
|
||||
</Button>
|
||||
|
||||
@@ -885,6 +885,10 @@ function GenericExampleCard({ providerId, kind }) {
|
||||
|
||||
// Get models for this kind (e.g., type="image")
|
||||
const kindModels = getModelsByProviderId(providerId).filter((m) => m.type === kind);
|
||||
// Kinds that need a model identifier in the request (image/video/music)
|
||||
const KIND_NEEDS_MODEL = new Set(["image", "video", "music", "stt", "imageToText"]);
|
||||
const needsModel = KIND_NEEDS_MODEL.has(kind);
|
||||
const allowManualModel = needsModel && kindModels.length === 0;
|
||||
const [selectedModel, setSelectedModel] = useState(kindModels[0]?.id ?? "");
|
||||
const selectedModelObj = kindModels.find((m) => m.id === selectedModel);
|
||||
const supportsEdit = !!selectedModelObj?.capabilities?.includes("edit");
|
||||
@@ -935,8 +939,10 @@ function GenericExampleCard({ providerId, kind }) {
|
||||
|
||||
const endpoint = useTunnel ? tunnelEndpoint : localEndpoint;
|
||||
const apiPath = kindConfig.endpoint.path;
|
||||
// For kinds without model concept (webSearch/webFetch), use providerAlias directly
|
||||
const modelFull = kindModels.length === 0 ? providerAlias : (selectedModel ? `${providerAlias}/${selectedModel}` : "");
|
||||
// webSearch/webFetch: use providerAlias only. Other kinds: append model when present.
|
||||
const modelFull = !needsModel
|
||||
? providerAlias
|
||||
: (selectedModel ? `${providerAlias}/${selectedModel}` : (allowManualModel ? "" : providerAlias));
|
||||
|
||||
// Build request body with optional extra fields (only non-empty values)
|
||||
const extraBodyFromFields = Object.entries(extraValues).reduce((acc, [k, v]) => {
|
||||
@@ -1060,8 +1066,8 @@ function GenericExampleCard({ providerId, kind }) {
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold mb-4">Example</h2>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{/* Model selector - only show if models available */}
|
||||
{kindModels.length > 0 && (
|
||||
{/* Model selector — dropdown if presets exist, else manual input for media kinds */}
|
||||
{kindModels.length > 0 ? (
|
||||
<Row label="Model">
|
||||
<select
|
||||
value={selectedModel}
|
||||
@@ -1073,7 +1079,16 @@ function GenericExampleCard({ providerId, kind }) {
|
||||
))}
|
||||
</select>
|
||||
</Row>
|
||||
)}
|
||||
) : allowManualModel ? (
|
||||
<Row label="Model">
|
||||
<input
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
placeholder="Enter model id (provider-specific)"
|
||||
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
|
||||
/>
|
||||
</Row>
|
||||
) : null}
|
||||
|
||||
{/* Endpoint */}
|
||||
<Row label="Endpoint">
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { Card, Badge } from "@/shared/components";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import {
|
||||
SKILLS,
|
||||
SKILLS_REPO_URL,
|
||||
getSkillRawUrl,
|
||||
getSkillBlobUrl,
|
||||
} from "@/shared/constants/skills";
|
||||
|
||||
function CopyButton({ value, label = "Copy link" }) {
|
||||
const { copied, copy } = useCopyToClipboard(2000);
|
||||
return (
|
||||
<button
|
||||
onClick={() => copy(value)}
|
||||
className="px-2 py-1 rounded-md bg-primary text-white text-[11px] font-medium hover:bg-primary/90 transition-colors cursor-pointer shrink-0 inline-flex items-center gap-1"
|
||||
title={value}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">
|
||||
{copied ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copied ? "Copied!" : label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillRow({ skill }) {
|
||||
const url = getSkillRawUrl(skill.id);
|
||||
return (
|
||||
<div
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border transition-colors ${
|
||||
skill.isEntry
|
||||
? "border-primary/40 bg-primary/5"
|
||||
: "border-border-subtle hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`size-9 rounded-lg flex items-center justify-center shrink-0 ${
|
||||
skill.isEntry ? "bg-primary text-white" : "bg-primary/10 text-primary"
|
||||
}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">{skill.icon}</span>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-semibold text-sm text-text-main">{skill.name}</h3>
|
||||
{skill.isEntry && (
|
||||
<Badge variant="primary" size="sm">START HERE</Badge>
|
||||
)}
|
||||
{skill.endpoint && (
|
||||
<Badge variant="default" size="sm">
|
||||
<code className="text-[10px]">{skill.endpoint}</code>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-0.5">{skill.description}</p>
|
||||
<a
|
||||
href={getSkillBlobUrl(skill.id)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[11px] text-text-muted hover:text-primary mt-1 inline-flex items-center gap-1 break-all"
|
||||
>
|
||||
{url}
|
||||
<span className="material-symbols-outlined text-[12px]">open_in_new</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<CopyButton value={url} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SkillsPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<Card padding="md">
|
||||
<div className="text-xs text-text-muted mb-2">Paste this to your AI:</div>
|
||||
<div className="px-3 py-2 rounded bg-surface-2 font-mono text-[12px] text-text-main">
|
||||
Read this skill and use it: {getSkillRawUrl("9router")}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-2">
|
||||
{SKILLS.map((skill) => (
|
||||
<SkillRow key={skill.id} skill={skill} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card padding="md">
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-text-main">More on GitHub</h2>
|
||||
<p className="text-xs text-text-muted mt-0.5">
|
||||
Browse source, README, and examples.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={`${SKILLS_REPO_URL}/tree/main/app/skills`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sm text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">open_in_new</span>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user