Initial release v1.0.0

Open-source marketplace for AI Agent skills.

Features:
- Next.js 15 web app with i18n (en/fa)
- CLI tool for skill installation (npx skillhub)
- GitHub crawler/indexer with multi-strategy discovery
- Security scanning for all indexed skills
- Self-hostable with Docker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-12 06:08:51 +03:30
commit 97b427831a
227 changed files with 48411 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
'use client';
import { useState, useCallback } from 'react';
import { Copy, Check } from 'lucide-react';
interface DiscoveryMode {
key: string;
letter: string;
title: string;
description: string;
latency: string;
quality: string;
bestFor: string;
recommended?: boolean;
prompt: string;
}
interface DiscoveryModesProps {
modes: DiscoveryMode[];
labels: {
latency: string;
quality: string;
bestFor: string;
recommended: string;
copyPrompt: string;
copied: string;
selectMode: string;
addToFile: string;
};
}
export function DiscoveryModes({ modes, labels }: DiscoveryModesProps) {
const [selectedMode, setSelectedMode] = useState<string>('standard');
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}, []);
const selected = modes.find((m) => m.key === selectedMode) || modes[0];
return (
<div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 my-6 not-prose">
{modes.map((mode) => (
<button
key={mode.key}
onClick={() => setSelectedMode(mode.key)}
className={`glass-card p-4 text-start transition-all cursor-pointer ${
selectedMode === mode.key
? 'ring-2 ring-primary-500 shadow-lg'
: 'hover:ring-1 hover:ring-primary-300 opacity-70 hover:opacity-100'
}`}
>
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-text-primary">
{mode.letter}. {mode.title}
</span>
{mode.recommended && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300 font-medium">
{labels.recommended}
</span>
)}
</div>
<p className="text-sm text-text-secondary mb-3">{mode.description}</p>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-tertiary">
<span>
<span className="font-medium">{labels.latency}:</span> {mode.latency}
</span>
<span>
<span className="font-medium">{labels.quality}:</span> {mode.quality}
</span>
</div>
</button>
))}
</div>
{/* Selected mode details */}
<div className="glass-card p-5 my-6 not-prose">
<div className="flex items-center justify-between mb-3">
<div>
<h4 className="font-semibold text-text-primary text-lg">
{selected.letter}. {selected.title}
</h4>
<p className="text-sm text-text-secondary">
{labels.bestFor}: {selected.bestFor}
</p>
</div>
<button
onClick={() => handleCopy(selected.prompt)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/50"
>
{copied ? (
<>
<Check className="w-4 h-4" />
{labels.copied}
</>
) : (
<>
<Copy className="w-4 h-4" />
{labels.copyPrompt}
</>
)}
</button>
</div>
<p className="text-sm text-text-secondary mb-3">{labels.addToFile}</p>
<div className="bg-surface-secondary rounded-lg p-4 overflow-x-auto" dir="ltr">
<pre className="text-sm font-mono text-left whitespace-pre-wrap">{selected.prompt}</pre>
</div>
</div>
</div>
);
}