From 652eb91e5d44a18fceac0f86de1f9c185001e295 Mon Sep 17 00:00:00 2001 From: ivancidev Date: Sun, 19 Jul 2026 22:33:28 -0400 Subject: [PATCH] feat: improve dashboard layout, add chat quick-start templates, and secure API key controls --- app/(app)/dashboard/page.tsx | 175 ++++++++++++++++-- app/(marketing)/page.tsx | 263 ++++++++++++++++++++++------ components/chat/chat-interface.tsx | 67 +++++-- components/chat/chat-messages.tsx | 56 +++++- components/chat/skill-preview.tsx | 85 +++++++-- components/dashboard/sidebar.tsx | 37 +++- components/dashboard/skill-card.tsx | 53 ++++-- 7 files changed, 615 insertions(+), 121 deletions(-) diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 6c62920..48c15ff 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -7,7 +7,7 @@ import { SkillGrid } from '@/components/dashboard/skill-grid' import { Skill } from '@/components/dashboard/skill-card' import { Button } from '@/components/ui/button' import Link from 'next/link' -import { Plus, LayoutGrid } from 'lucide-react' +import { Plus, LayoutGrid, Search, Shield, ArrowUpDown } from 'lucide-react' import { useRouter } from 'next/navigation' const MOCK_SKILLS: Skill[] = [ @@ -21,6 +21,7 @@ const MOCK_SKILLS: Skill[] = [ { path: 'scripts/cleanup-unused.js', content: '// script to clean unused classes' } ], isComplete: true, + createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000 * 2).toISOString(), // 2 days ago }, { id: 'mock-2', @@ -32,6 +33,7 @@ const MOCK_SKILLS: Skill[] = [ { path: 'assets/template.html', content: '' } ], isComplete: true, + createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000 * 5).toISOString(), // 5 days ago }, { id: 'mock-3', @@ -43,11 +45,17 @@ const MOCK_SKILLS: Skill[] = [ { path: 'scripts/codegen.ts', content: '// Code generation script' } ], isComplete: true, + createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000 * 8).toISOString(), // 8 days ago } ] export default function DashboardPage() { const [skills, setSkills] = useState([]) + const [searchQuery, setSearchQuery] = useState('') + const [platformFilter, setPlatformFilter] = useState<'all' | 'cursor' | 'claude' | 'windsurf' | 'gpt'>('all') + const [sortBy, setSortBy] = useState<'newest' | 'oldest' | 'name'>('newest') + const [keysCount, setKeysCount] = useState(0) + const router = useRouter() useEffect(() => { @@ -64,6 +72,13 @@ export default function DashboardPage() { setSkills(MOCK_SKILLS) localStorage.setItem('ss_skills', JSON.stringify(MOCK_SKILLS)) } + + // Load active API keys count + let count = 0 + if (localStorage.getItem('ss_api_key_gemini')) count++ + if (localStorage.getItem('ss_api_key_claude')) count++ + if (localStorage.getItem('ss_api_key_gpt')) count++ + setKeysCount(count) }, []) const handleDownload = async (skill: Skill) => { @@ -104,42 +119,170 @@ export default function DashboardPage() { router.push(`/generate?prompt=${encodeURIComponent(`Regenerate ${skill.name} skill for ${skill.platform}`)}`) } + // Calculate Metrics + const cursorCount = skills.filter((s) => s.platform === 'cursor').length + const claudeCount = skills.filter((s) => s.platform === 'claude').length + const windsurfCount = skills.filter((s) => s.platform === 'windsurf').length + const gptCount = skills.filter((s) => s.platform === 'gpt').length + const activePlatformsCount = [cursorCount, claudeCount, windsurfCount, gptCount].filter((c) => c > 0).length + + // Filter & Sort + const filteredAndSortedSkills = skills + .filter((skill) => { + const matchesSearch = skill.name.toLowerCase().includes(searchQuery.toLowerCase()) || + skill.platform.toLowerCase().includes(searchQuery.toLowerCase()) + const matchesPlatform = platformFilter === 'all' || skill.platform === platformFilter + return matchesSearch && matchesPlatform + }) + .sort((a, b) => { + if (sortBy === 'name') { + return a.name.localeCompare(b.name) + } + const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0 + const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0 + return sortBy === 'newest' ? dateB - dateA : dateA - dateB + }) + return ( -
+
{/* Header */} -
+
-

+

Your skills

-

- {skills.length} {skills.length === 1 ? 'skill' : 'skills'} stored in local index +

+ MANAGE CUSTOM AI AGENT INSTRUCTION PACKAGES

-
+ {/* Metrics Row */} +
+ {/* Metric 1 */} +
+ + Total Skills Stored + +
+ {skills.length} +
+

+ Saved inside browser LocalStorage sandbox +

+
+ + {/* Metric 2 */} +
+ + Target Platforms + +
+ {activePlatformsCount} +
+
+ {cursorCount > 0 && Cursor: {cursorCount}} + {claudeCount > 0 && Claude: {claudeCount}} + {windsurfCount > 0 && Windsurf: {windsurfCount}} + {gptCount > 0 && GPT: {gptCount}} + {activePlatformsCount === 0 && No platforms active} +
+
+ + {/* Metric 3 */} +
+ + Security Status + +
+ +
+ LocalStorage +
+
+

+ {keysCount} / 3 API Keys configured in browser +

+
+
+ + {/* Interactive Controls Bar */} +
+ {/* Search Input */} +
+ + setSearchQuery(e.target.value)} + placeholder="Search skills by name or platform..." + className="w-full bg-slate-50 border border-slate-200 rounded-lg pl-9 pr-4 py-2 text-[13px] text-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-indigo/20 focus:border-brand-indigo/60 transition font-mono" + /> +
+ + {/* Filters and Sort */} +
+ {/* Platform Tabs */} +
+ {(['all', 'cursor', 'claude', 'windsurf'] as const).map((p) => ( + + ))} +
+ + {/* Sort Dropdown */} +
+ + +
+
+
+ {/* Grid or Empty state */} - {skills.length === 0 ? ( -
-

No skills yet

-

- You haven't generated any assistant instructions yet. Describe your workflow to begin. + {filteredAndSortedSkills.length === 0 ? ( +

+

No matching skills found

+

+ Try adjusting your search criteria or platform filters.

- - - +
) : ( diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx index 250e1f6..08bad28 100644 --- a/app/(marketing)/page.tsx +++ b/app/(marketing)/page.tsx @@ -1,89 +1,250 @@ import { Logo } from '@/components/ui/logo' import { Button } from '@/components/ui/button' import Link from 'next/link' +import { + ShieldCheck, + Cpu, + Folder, + FileText, + Database, + Palette, + GitCommit, + Code2, + ArrowRight, + Terminal +} from 'lucide-react' export default function LandingPage() { return ( -
+
{/* Header */}
- - - +
+ + Dashboard + + + + +
{/* Main hero */} -
-

- Build skills. Ship faster. -

-

- Describe what you want. The agent asks questions, generates the files, and packages everything ready to install in Claude, Cursor, Windsurf and more. -

+
+ + {/* Badge */} +
+ + SkillStudio v1.0 +
+ + {/* Hero Typography */} +
+

+ Build custom AI assistant skills.
Master any codebase. +

+

+ Author, refine, and package tailored coding guidelines, design systems, or helper tools into `.skill` bundles ready to install in Cursor, Claude Code, Windsurf, and Continue. +

+
+ {/* Action buttons */}
- - - - - + +
+ + {/* Visual Workspace Mockup inside a Signature Card */} +
+ + {/* Header of Mockup */} +
+
+ + + +
+
+ + Workspace Preview +
+
+ + {/* Editor Grid Split */} +
+ + {/* Sidebar (Folder Structure) */} +
+
Project tree
+
+
+ + my-codebase/ +
+
+ + .agents/ +
+
+ + skills/ +
+
+ + custom-helper/ +
+
+ + SKILL.md +
+
+ + scripts/ +
+
+
+ + {/* Document Content Editor Mock */} +
+
+
SKILL.md - Generated Rules
+
Valid
+
+
+

# Custom Workspace Helper

+

## Rules & Directives

+

1. Always format responses in clean TSX.

+

2. Keep state hooks atomic and use modular imports.

+

3. Do not introduce raw styling utilities; rely on theme variables.

+

## Verification Steps

+

- Run `bun test` after each generated hook.

+
+
+ +
+
+ + {/* Showcase / Skill Types Section */} +
+

+ Build any skill you need +

+

+ Package domain-specific intelligence for your models to make code review and creation incredibly precise. +

+
+ + {/* 4 Step Cards (Signature Cards Style) */} +
+ +
+
+
+ +
+

+ Database Helper +

+

+ Rules for writing Supabase client calls, Postgres constraints, and clean DB migrations. +

+
+ DB & ORM +
+ +
+
+
+ +
+

+ Design System Guide +

+

+ Enforces precise spacing scales, custom design tokens, dark mode guidelines, and typography. +

+
+ UI & STYLING +
+ +
+
+
+ +
+

+ API Architect +

+

+ Standard patterns for routing, payload schema checks, and external API gateways. +

+
+ INTEGRATIONS +
+ +
+
+
+ +
+

+ Git Automator +

+

+ Teaches correct conventional commit formatting, pull request summaries, and git hook scripts. +

+
+ VERSION CONTROL +
+
- {/* 3 Step Cards */} -
-
-
01 / Describe
-

Outline your target behavior

-

- Tell the agent what custom tool, workflow, design system, or rules you want your AI model to master. + {/* Key Features / Benefits List */} +

+
+

100% Client-Side Privacy

+

+ Your model provider API keys are stored strictly in your browser's localStorage and sent directly in-memory to execute calls. We never write keys to a database or server logs.

-
-
02 / Generate
-

Collaborative refinement

-

- The agent asks clarifying questions to perfect the instruction file, generate support scripts, and validate content. +

+

IDE Agnostic Packages

+

+ Generate instructions, helper scripts, and assets exported as structured `.skill` zip bundles. Ready to run in Claude Code, Cursor, Windsurf, Continue, and others.

-
-
03 / Install
-

Package & run

-

- Download the .skill bundle containing SKILL.md and assets. Extract it directly into your project's agent path. +

+

Zero Server Storage Option

+

+ Use our local mock fallback configuration to test and package skills entirely in your browser memory without creating a database profile.

{/* Works with section */} -
-
Works with
+
+
Supported Assistants & IDEs
- Claude - Cursor - Windsurf - GPT Models + Claude Code + Cursor + Windsurf + Continue
{/* Footer */} -
- © {new Date().getFullYear()} SkillStudio. Built for developers. +
+ © {new Date().getFullYear()} SkillStudio. Created for advanced agent custom behavior.
) diff --git a/components/chat/chat-interface.tsx b/components/chat/chat-interface.tsx index 7a4a78c..da295e6 100644 --- a/components/chat/chat-interface.tsx +++ b/components/chat/chat-interface.tsx @@ -9,7 +9,7 @@ import { ChatMessages } from './chat-messages' import { ChatInput } from './chat-input' import { SkillPreview } from './skill-preview' import { ModelProvider } from '@/lib/models/providers' -import { Eye, EyeOff, ShieldCheck } from 'lucide-react' +import { Eye, EyeOff, ShieldCheck, Trash2 } from 'lucide-react' import { useSearchParams } from 'next/navigation' export const ChatInterface = () => { @@ -209,34 +209,73 @@ export const ChatInterface = () => { {/* API Key Input */}
- +
+ + {/* Connection Status Badge */} + {apiKey ? ( +
+ + Connected (Local) +
+ ) : ( +
+ + Server Fallback / Demo +
+ )} +
- +
+ + {apiKey && ( + + )} +
- Keys never touch our database and are sent directly to the LLM endpoint. + Keys never touch our database and are processed strictly in-memory.
{/* Messaging Container */} - + { + sendMessage({ + role: 'user', + content: prompt, + }) + }} + /> {/* Text Area Input */} void } -export const ChatMessages = ({ messages, isLoading }: ChatMessagesProps) => { +export const ChatMessages = ({ messages, isLoading, onSelectTemplate }: ChatMessagesProps) => { return (
{messages.length === 0 && ( -
+
-

+

Generate custom AI skills

-

+

Tell me what behavior, design system, or rules you want your assistant to learn. I will guide you, ask clarifying questions, and package your skill file.

+ + {onSelectTemplate && ( +
+
+ Quick-Start Templates +
+
+ {TEMPLATES.map((tpl) => ( + + ))} +
+
+ )}
)} diff --git a/components/chat/skill-preview.tsx b/components/chat/skill-preview.tsx index 5d10089..f1ae61d 100644 --- a/components/chat/skill-preview.tsx +++ b/components/chat/skill-preview.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { Button } from '../ui/button' -import { FileText, Files, Info, Download, Copy, Check } from 'lucide-react' +import { FileText, Files, Info, Download, Copy, Check, X } from 'lucide-react' import { twMerge } from 'tailwind-merge' interface SkillPreviewProps { @@ -22,6 +22,8 @@ export const SkillPreview = ({ }: SkillPreviewProps) => { const [activeTab, setActiveTab] = useState<'markdown' | 'files' | 'install'>('markdown') const [copied, setCopied] = useState(false) + const [selectedFile, setSelectedFile] = useState<{ path: string; content: string } | null>(null) + const [copiedFile, setCopiedFile] = useState(false) const handleCopy = () => { navigator.clipboard.writeText(skillMd) @@ -171,22 +173,73 @@ export const SkillPreview = ({ )} {activeTab === 'files' && ( -
-
- SKILL.md - Base Instruction Document -
- {files.map((file) => ( -
- {file.path} - - {file.path.endsWith('.sh') ? 'Shell Script' : file.path.endsWith('.py') ? 'Python Script' : 'Template/Config'} - +
+
+
setSelectedFile({ path: 'SKILL.md', content: skillMd })} + className={twMerge( + "font-mono text-[12px] p-3 border rounded-lg flex items-center justify-between transition-all cursor-pointer select-none", + selectedFile?.path === 'SKILL.md' + ? "border-brand-indigo bg-brand-indigo/5 text-brand-indigo" + : "border-slate-200 hover:bg-slate-50 text-slate-700 bg-white" + )} + > + SKILL.md + Base Instruction Document
- ))} - {files.length === 0 && ( -
- This skill only includes the main SKILL.md file. No extra scripts are configured. + {files.map((file) => ( +
setSelectedFile(file)} + className={twMerge( + "font-mono text-[12px] p-3 border rounded-lg flex items-center justify-between transition-all cursor-pointer select-none", + selectedFile?.path === file.path + ? "border-brand-indigo bg-brand-indigo/5 text-brand-indigo" + : "border-slate-200 hover:bg-slate-50 text-slate-700 bg-white" + )} + > + {file.path} + + {file.path.endsWith('.sh') ? 'Shell Script' : file.path.endsWith('.py') ? 'Python Script' : 'Template/Config'} + +
+ ))} + {files.length === 0 && ( +
+ This skill only includes the main SKILL.md file. No extra scripts are configured. +
+ )} +
+ + {/* In-tab Code Inspector */} + {selectedFile && selectedFile.content && ( +
+
+ {selectedFile.path} +
+ + +
+
+
+                  {selectedFile.content}
+                
)}
diff --git a/components/dashboard/sidebar.tsx b/components/dashboard/sidebar.tsx index 594aaa1..c5eb188 100644 --- a/components/dashboard/sidebar.tsx +++ b/components/dashboard/sidebar.tsx @@ -2,6 +2,7 @@ import { Logo } from '@/components/ui/logo' import { LayoutDashboard, Plus, Settings, Menu, X } from 'lucide-react' +import { Button } from '@/components/ui/button' import Link from 'next/link' import { usePathname } from 'next/navigation' import { useState } from 'react' @@ -109,19 +110,51 @@ export const Sidebar = () => { })} -
+
Settings + + {/* User profile / Sync Widget */} +
+ {/* Sync status */} +
+ Local Sandbox +
+ + Offline +
+
+ + {/* Profile Detail */} +
+ {/* Fake Avatar */} +
+ ID +
+
+
Ivan Dev
+
demo_account
+
+
+ + {/* Sign in prompt button */} + +
diff --git a/components/dashboard/skill-card.tsx b/components/dashboard/skill-card.tsx index 863b640..ea1059e 100644 --- a/components/dashboard/skill-card.tsx +++ b/components/dashboard/skill-card.tsx @@ -1,5 +1,5 @@ import { Button } from '../ui/button' -import { Download, RefreshCw, FileCode } from 'lucide-react' +import { Download, RefreshCw, FileCode, Calendar, Code2 } from 'lucide-react' export interface Skill { id: string @@ -19,8 +19,13 @@ interface SkillCardProps { } export const SkillCard = ({ skill, onDownload, onRegenerate }: SkillCardProps) => { + const lineCount = skill.skillMd ? skill.skillMd.split('\n').length : 0 + const formattedDate = skill.createdAt + ? new Date(skill.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + : 'Local Draft' + return ( -
+
{/* Top Details */}
@@ -29,29 +34,29 @@ export const SkillCard = ({ skill, onDownload, onRegenerate }: SkillCardProps) = {skill.platform} {skill.isComplete && ( -
+
- Ready + Ready
)}
{/* Title */} -

+

{skill.name}

- {/* Files */} -
- - + {/* Files Badges */} +
+ + SKILL.md {skill.files.map((file) => { const fileName = file.path.split('/').pop() || file.path return ( - - + + {fileName} ) @@ -59,23 +64,35 @@ export const SkillCard = ({ skill, onDownload, onRegenerate }: SkillCardProps) =
+ {/* Meta Indicators */} +
+
+ + {lineCount} lines +
+
+ + {formattedDate} +
+
+ {/* Footer Controls */} -
+