AI / src /components /CustomGptStoreModal.tsx
Harshavardhan
Upload 126 files
31a4127
Raw History Blame Contribute Delete
22.6 kB
import React, { useState } from "react";
import {
Store,
Sparkles,
Plus,
Search,
Star,
Users,
BookOpen,
Code,
FileText,
Image as ImageIcon,
Database,
Lock,
Globe,
ArrowRight,
Check,
Upload,
X,
Sliders,
Share2,
LogIn,
Zap,
ShieldCheck,
Cpu,
Layers,
ExternalLink
} from "lucide-react";
export interface CustomGpt {
id: string;
name: string;
author: string;
description: string;
category: "coding" | "oxford" | "creative" | "productivity" | "analytics";
avatar: string;
rating: number;
useCount: number;
systemPrompt: string;
knowledgeFiles?: string[];
capabilities: {
webSearch: boolean;
codeExecution: boolean;
artifacts: boolean;
computerUse: boolean;
};
customApiActions?: {
endpointUrl: string;
method: "GET" | "POST";
headers: string;
}[];
isCustomCreated?: boolean;
}
interface CustomGptStoreModalProps {
isOpen: boolean;
onClose: () => void;
currentUser: { email?: string; isGuest?: boolean } | null;
onOpenAuthModal: () => void;
onSelectGpt: (gpt: CustomGpt) => void;
}
export default function CustomGptStoreModal({
isOpen,
onClose,
currentUser,
onOpenAuthModal,
onSelectGpt
}: CustomGptStoreModalProps) {
const [activeTab, setActiveTab] = useState<"marketplace" | "create" | "myGpts">("marketplace");
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [searchQuery, setSearchQuery] = useState<string>("");
// New GPT Form State
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [category, setCategory] = useState<CustomGpt["category"]>("coding");
const [avatar, setAvatar] = useState<string>("🤖");
const [systemPrompt, setSystemPrompt] = useState<string>("");
const [knowledgeFiles, setKnowledgeFiles] = useState<string[]>([]);
const [webSearch, setWebSearch] = useState<boolean>(true);
const [codeExecution, setCodeExecution] = useState<boolean>(true);
const [artifacts, setArtifacts] = useState<boolean>(true);
const [computerUse, setComputerUse] = useState<boolean>(false);
const [apiEndpoint, setApiEndpoint] = useState<string>("");
const [apiMethod, setApiMethod] = useState<"GET" | "POST">("POST");
// Default Featured Custom GPT Marketplace Items
const [customGpts, setCustomGpts] = useState<CustomGpt[]>([
{
id: "gpt-oxford",
name: "Oxford Textbook Master",
author: "Oxford Education AI",
description: "Expert tutor for Grade 6-8 Oxford English lessons, literature summaries, and Q&A analysis.",
category: "oxford",
avatar: "📚",
rating: 4.9,
useCount: 14200,
systemPrompt: "You are an expert Oxford English literature & language tutor with full indexed knowledge of Grade 6, Grade 7, and Grade 8 textbooks.",
capabilities: { webSearch: true, codeExecution: false, artifacts: true, computerUse: false }
},
{
id: "gpt-code-architect",
name: "React & Vite Code Architect",
author: "Aura Dev Labs",
description: "Senior full-stack code architect creating production-ready React 19, TypeScript, and Express components with side-by-side Artifacts execution.",
category: "coding",
avatar: "⚡",
rating: 4.95,
useCount: 28900,
systemPrompt: "You are a master React 19 & TypeScript software architect specializing in clean, modern UI components with Tailwind CSS.",
capabilities: { webSearch: true, codeExecution: true, artifacts: true, computerUse: true }
},
{
id: "gpt-image-prompt",
name: "FLUX & Gemini Visual Designer",
author: "Aura Creative",
description: "Crafts ultra-detailed image prompts, vector SVG graphic layouts, and typography palettes for AI Image Studio.",
category: "creative",
avatar: "🎨",
rating: 4.88,
useCount: 9800,
systemPrompt: "You are a master creative director generating photorealistic FLUX image prompts and custom SVG vector art.",
capabilities: { webSearch: false, codeExecution: false, artifacts: true, computerUse: false }
},
{
id: "gpt-data-analyst",
name: "Spreadsheet & SQL Data Analyst",
author: "Analytics Core",
description: "Parses CSV, Excel, and JSON data to generate interactive charts, SQL queries, and financial trend breakdowns.",
category: "analytics",
avatar: "📊",
rating: 4.85,
useCount: 11200,
systemPrompt: "You are an expert data analyst who transforms raw data files into clear visualizations, Recharts charts, and SQL queries.",
capabilities: { webSearch: true, codeExecution: true, artifacts: true, computerUse: false }
}
]);
if (!isOpen) return null;
const isSignedIn = currentUser && !currentUser.isGuest && Boolean(currentUser.email);
const filteredGpts = customGpts.filter(gpt => {
const matchesCategory = selectedCategory === "all" || gpt.category === selectedCategory;
const matchesSearch = gpt.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
gpt.description.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
const handleCreateGpt = (e: React.FormEvent) => {
e.preventDefault();
if (!isSignedIn) {
onOpenAuthModal();
return;
}
if (!name.trim() || !systemPrompt.trim()) return;
const newGpt: CustomGpt = {
id: `custom-gpt-${Date.now()}`,
name,
author: currentUser?.email || "Signed-in Creator",
description: description || "Custom user-built specialized AI micro-app.",
category,
avatar,
rating: 5.0,
useCount: 1,
systemPrompt,
knowledgeFiles,
capabilities: {
webSearch,
codeExecution,
artifacts,
computerUse
},
customApiActions: apiEndpoint ? [{ endpointUrl: apiEndpoint, method: apiMethod, headers: "Content-Type: application/json" }] : [],
isCustomCreated: true
};
setCustomGpts([newGpt, ...customGpts]);
setActiveTab("marketplace");
onSelectGpt(newGpt);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 md:p-6 bg-slate-950/85 backdrop-blur-md animate-fade-in font-sans">
<div
className="relative w-full max-w-5xl max-h-[92vh] bg-slate-900 text-slate-100 rounded-3xl shadow-2xl border border-slate-700/80 flex flex-col overflow-hidden"
onClick={e => e.stopPropagation()}
>
{/* Header Bar */}
<div className="bg-gradient-to-r from-slate-950 via-slate-900 to-indigo-950 p-5 border-b border-slate-800 shrink-0 flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="h-11 w-11 rounded-2xl bg-indigo-500/20 border border-indigo-500/40 flex items-center justify-center text-indigo-400 shrink-0 shadow-inner">
<Store className="h-6 w-6" />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-black tracking-tight text-white">Custom GPT Store & Studio</h3>
<span className="px-2.5 py-0.5 rounded-full bg-purple-500/20 text-purple-300 border border-purple-500/30 text-[10px] font-mono font-bold">
Micro-App Marketplace
</span>
</div>
<p className="text-xs text-slate-400">
Package tailored system prompts, knowledge files, and custom API connections into shareable micro-apps
</p>
</div>
</div>
<button
onClick={onClose}
className="h-8 w-8 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition cursor-pointer shrink-0"
>
<X className="h-4.5 w-4.5" />
</button>
</div>
{/* Tab Navigation */}
<div className="bg-slate-950 border-b border-slate-800 px-5 py-3 flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-2 bg-slate-900 p-1 rounded-2xl border border-slate-800 text-xs font-semibold">
<button
onClick={() => setActiveTab("marketplace")}
className={`px-4 py-2 rounded-xl flex items-center gap-2 transition cursor-pointer ${
activeTab === "marketplace" ? "bg-indigo-600 text-white shadow-md" : "text-slate-400 hover:text-slate-200"
}`}
>
<Store className="h-4 w-4" />
<span>Marketplace ({customGpts.length})</span>
</button>
<button
onClick={() => {
if (!isSignedIn) {
onOpenAuthModal();
} else {
setActiveTab("create");
}
}}
className={`px-4 py-2 rounded-xl flex items-center gap-2 transition cursor-pointer ${
activeTab === "create" ? "bg-indigo-600 text-white shadow-md" : "text-slate-400 hover:text-slate-200"
}`}
>
<Plus className="h-4 w-4 text-amber-400" />
<span>Create Custom GPT</span>
{!isSignedIn && <Lock className="h-3 w-3 text-amber-400 ml-1" />}
</button>
</div>
{/* User Sign-In Badge */}
<div>
{isSignedIn ? (
<div className="px-3 py-1.5 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 text-xs font-mono font-bold flex items-center gap-1.5">
<ShieldCheck className="h-3.5 w-3.5 text-emerald-400" />
<span>Signed In ({currentUser?.email})</span>
</div>
) : (
<button
onClick={onOpenAuthModal}
className="px-3.5 py-1.5 rounded-xl bg-amber-500/20 hover:bg-amber-500/30 text-amber-200 border border-amber-500/40 text-xs font-bold flex items-center gap-1.5 transition cursor-pointer"
>
<LogIn className="h-3.5 w-3.5 text-amber-400" />
<span>Sign In Required for Custom GPT Creation</span>
</button>
)}
</div>
</div>
{/* TAB 1: Marketplace View */}
{activeTab === "marketplace" && (
<div className="flex-1 overflow-y-auto p-5 flex flex-col gap-5 bg-slate-950/60">
{/* Search & Categories Bar */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="relative w-full sm:w-80">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search Custom GPTs by name or skill..."
className="w-full pl-9 pr-4 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
</div>
{/* Category Pills */}
<div className="flex items-center gap-1.5 overflow-x-auto w-full sm:w-auto pb-1">
{[
{ id: "all", label: "All GPTs" },
{ id: "coding", label: "Coding & Dev" },
{ id: "oxford", label: "Oxford Education" },
{ id: "creative", label: "Creative & Design" },
{ id: "analytics", label: "Data & Charts" }
].map(cat => (
<button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
className={`px-3 py-1.5 rounded-xl text-xs font-bold shrink-0 transition cursor-pointer ${
selectedCategory === cat.id
? "bg-indigo-600 text-white"
: "bg-slate-900 text-slate-400 hover:text-slate-200 border border-slate-800"
}`}
>
{cat.label}
</button>
))}
</div>
</div>
{/* Custom GPT Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredGpts.map(gpt => (
<div
key={gpt.id}
className="p-4 rounded-2xl bg-slate-900/90 border border-slate-800 hover:border-indigo-500/50 transition-all shadow-lg flex flex-col justify-between gap-4 group"
>
<div className="flex items-start gap-3">
<div className="h-12 w-12 rounded-2xl bg-gradient-to-br from-indigo-500/20 to-purple-500/20 border border-indigo-500/30 flex items-center justify-center text-2xl shrink-0 shadow-inner group-hover:scale-105 transition-transform">
{gpt.avatar}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<h4 className="text-sm font-bold text-white truncate">{gpt.name}</h4>
<span className="flex items-center gap-1 text-[11px] font-bold text-amber-400">
<Star className="h-3 w-3 fill-amber-400" />
{gpt.rating}
</span>
</div>
<span className="text-[10px] font-mono text-indigo-400 block mb-1">By {gpt.author}</span>
<p className="text-xs text-slate-300 line-clamp-2 leading-relaxed">{gpt.description}</p>
</div>
</div>
{/* Capabilities Badges */}
<div className="flex items-center justify-between pt-2 border-t border-slate-800 text-[10px] font-mono text-slate-400">
<div className="flex items-center gap-1.5 flex-wrap">
{gpt.capabilities.artifacts && <span className="px-2 py-0.5 rounded bg-indigo-500/10 text-indigo-300 border border-indigo-500/20">Artifacts</span>}
{gpt.capabilities.codeExecution && <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-300 border border-emerald-500/20">Code Run</span>}
{gpt.capabilities.computerUse && <span className="px-2 py-0.5 rounded bg-purple-500/10 text-purple-300 border border-purple-500/20">Computer Use</span>}
</div>
<button
onClick={() => {
onSelectGpt(gpt);
onClose();
}}
className="px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs flex items-center gap-1.5 shadow-md cursor-pointer transition active:scale-95 shrink-0"
>
<span>Load GPT</span>
<ArrowRight className="h-3.5 w-3.5" />
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* TAB 2: Create Custom GPT Form */}
{activeTab === "create" && (
<form onSubmit={handleCreateGpt} className="flex-1 overflow-y-auto p-5 flex flex-col gap-5 bg-slate-950/60">
{!isSignedIn && (
<div className="p-4 rounded-2xl bg-amber-500/10 border border-amber-500/30 text-amber-200 text-xs flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Lock className="h-4 w-4 text-amber-400 shrink-0" />
<span>Sign In Required to publish Custom GPT micro-apps to the marketplace.</span>
</div>
<button
type="button"
onClick={onOpenAuthModal}
className="px-3 py-1.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-xs shrink-0 cursor-pointer"
>
Sign In Now
</button>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-slate-300 block mb-1">GPT Name</label>
<input
type="text"
required
value={name}
onChange={e => setName(e.target.value)}
placeholder="e.g. Oxford Grade 7 Math Tutor"
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-xs text-white focus:outline-none focus:border-indigo-500"
/>
</div>
<div>
<label className="text-xs font-bold text-slate-300 block mb-1">Category & Emoji Avatar</label>
<div className="flex gap-2">
<select
value={category}
onChange={e => setCategory(e.target.value as any)}
className="flex-1 px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-xs text-white focus:outline-none focus:border-indigo-500 cursor-pointer"
>
<option value="coding">Coding & Dev</option>
<option value="oxford">Oxford Education</option>
<option value="creative">Creative & Design</option>
<option value="productivity">Productivity</option>
<option value="analytics">Data & Analytics</option>
</select>
<input
type="text"
value={avatar}
onChange={e => setAvatar(e.target.value)}
maxLength={2}
className="w-14 text-center px-2 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-lg font-bold"
/>
</div>
</div>
</div>
<div>
<label className="text-xs font-bold text-slate-300 block mb-1">Short Description</label>
<input
type="text"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="What does this specialized micro-app do?"
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-xs text-white focus:outline-none focus:border-indigo-500"
/>
</div>
<div>
<label className="text-xs font-bold text-slate-300 block mb-1">System Instructions & Custom Persona</label>
<textarea
required
rows={4}
value={systemPrompt}
onChange={e => setSystemPrompt(e.target.value)}
placeholder="Detailed system prompt guiding how the model thinks, acts, formats output, and uses tools..."
className="w-full p-3.5 rounded-xl bg-slate-900 border border-slate-800 text-xs text-white focus:outline-none focus:border-indigo-500 font-mono resize-none leading-relaxed"
/>
</div>
{/* Capability Toggles */}
<div>
<label className="text-xs font-bold text-slate-300 block mb-2">Enabled Capabilities</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{[
{ label: "Web Search", state: webSearch, setter: setWebSearch },
{ label: "Code Run", state: codeExecution, setter: setCodeExecution },
{ label: "Artifacts", state: artifacts, setter: setArtifacts },
{ label: "Computer Use", state: computerUse, setter: setComputerUse }
].map((cap, i) => (
<button
key={i}
type="button"
onClick={() => cap.setter(!cap.state)}
className={`p-2.5 rounded-xl border text-xs font-bold flex items-center justify-between cursor-pointer transition ${
cap.state ? "bg-indigo-600/20 border-indigo-500 text-indigo-300" : "bg-slate-900 border-slate-800 text-slate-500"
}`}
>
<span>{cap.label}</span>
{cap.state && <Check className="h-3.5 w-3.5 text-indigo-400" />}
</button>
))}
</div>
</div>
{/* Custom API Action Connection */}
<div className="p-4 rounded-2xl bg-slate-900 border border-slate-800 flex flex-col gap-3">
<span className="text-xs font-bold text-white flex items-center gap-2">
<Zap className="h-4 w-4 text-amber-400" />
Custom API Action Connection (OpenAPI Spec)
</span>
<div className="flex gap-2">
<select
value={apiMethod}
onChange={e => setApiMethod(e.target.value as any)}
className="px-3 py-2 rounded-xl bg-slate-950 border border-slate-800 text-xs font-mono font-bold text-white"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
</select>
<input
type="text"
value={apiEndpoint}
onChange={e => setApiEndpoint(e.target.value)}
placeholder="https://api.example.com/v1/action"
className="flex-1 px-3 py-2 rounded-xl bg-slate-950 border border-slate-800 text-xs font-mono text-white focus:outline-none"
/>
</div>
</div>
{/* Form Submit */}
<button
type="submit"
disabled={!isSignedIn}
className="w-full py-3 px-6 rounded-2xl bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 disabled:opacity-50 text-white font-bold text-xs flex items-center justify-center gap-2 shadow-xl cursor-pointer transition"
>
<Sparkles className="h-4 w-4 text-amber-400" />
<span>Publish Custom GPT to Marketplace</span>
</button>
</form>
)}
</div>
</div>
);
}