-
Notifications
You must be signed in to change notification settings - Fork 0
feat: improve dashboard layout, add chat quick-start templates, and s… #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: '<!-- HTML slide template -->' } | ||
| ], | ||
| 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<Skill[]>([]) | ||
| 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 ( | ||
| <div className="flex-1 p-6 md:p-10 max-w-7xl mx-auto w-full"> | ||
| <div className="flex-1 p-6 md:p-10 max-w-7xl mx-auto w-full bg-[#F8FAFC]"> | ||
| {/* Header */} | ||
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-10 pb-6 border-b border-slate-200/50"> | ||
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8 pb-6 border-b border-slate-200/50"> | ||
| <div> | ||
| <h1 className="font-display font-semibold text-[22px] text-brand-slate leading-tight flex items-center gap-2"> | ||
| <h1 className="font-display font-bold text-[22px] text-brand-slate tracking-[-0.02em] leading-tight flex items-center gap-2"> | ||
| <LayoutGrid className="w-5 h-5 text-brand-indigo" /> | ||
| Your skills | ||
| </h1> | ||
| <p className="font-mono text-[13px] text-brand-slate-light mt-1"> | ||
| {skills.length} {skills.length === 1 ? 'skill' : 'skills'} stored in local index | ||
| <p className="font-mono text-[11px] text-brand-slate-light mt-1"> | ||
| MANAGE CUSTOM AI AGENT INSTRUCTION PACKAGES | ||
| </p> | ||
| </div> | ||
|
|
||
| <Link href="/generate"> | ||
| <Button variant="accent" className="flex items-center gap-2"> | ||
| <Button variant="accent" className="flex items-center gap-2 shadow-sm shadow-brand-indigo/10 cursor-pointer"> | ||
| <Plus className="w-4 h-4" /> | ||
| Generate skill | ||
| </Button> | ||
| </Link> | ||
| </div> | ||
|
|
||
| {/* Metrics Row */} | ||
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> | ||
| {/* Metric 1 */} | ||
| <div className="bg-white border border-slate-200/50 rounded-xl p-5 shadow-sm"> | ||
| <span className="font-mono text-[10px] text-brand-slate-light uppercase tracking-wider font-bold block select-none"> | ||
| Total Skills Stored | ||
| </span> | ||
| <div className="text-3xl font-display font-bold mt-1.5 text-brand-slate"> | ||
| {skills.length} | ||
| </div> | ||
| <p className="text-[12px] text-slate-500 mt-1 select-none font-sans"> | ||
| Saved inside browser LocalStorage sandbox | ||
| </p> | ||
| </div> | ||
|
|
||
| {/* Metric 2 */} | ||
| <div className="bg-white border border-slate-200/50 rounded-xl p-5 shadow-sm"> | ||
| <span className="font-mono text-[10px] text-brand-slate-light uppercase tracking-wider font-bold block select-none"> | ||
| Target Platforms | ||
| </span> | ||
| <div className="text-3xl font-display font-bold mt-1.5 text-brand-slate"> | ||
| {activePlatformsCount} | ||
| </div> | ||
| <div className="text-[11px] text-slate-500 mt-2 select-none flex flex-wrap gap-1"> | ||
| {cursorCount > 0 && <span className="bg-slate-50 border border-slate-200 px-1.5 py-0.5 rounded font-mono font-medium">Cursor: {cursorCount}</span>} | ||
| {claudeCount > 0 && <span className="bg-slate-50 border border-slate-200 px-1.5 py-0.5 rounded font-mono font-medium">Claude: {claudeCount}</span>} | ||
| {windsurfCount > 0 && <span className="bg-slate-50 border border-slate-200 px-1.5 py-0.5 rounded font-mono font-medium">Windsurf: {windsurfCount}</span>} | ||
| {gptCount > 0 && <span className="bg-slate-50 border border-slate-200 px-1.5 py-0.5 rounded font-mono font-medium">GPT: {gptCount}</span>} | ||
| {activePlatformsCount === 0 && <span className="text-slate-400 font-sans italic">No platforms active</span>} | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Metric 3 */} | ||
| <div className="bg-white border border-slate-200/50 rounded-xl p-5 shadow-sm"> | ||
| <span className="font-mono text-[10px] text-brand-slate-light uppercase tracking-wider font-bold block select-none"> | ||
| Security Status | ||
| </span> | ||
| <div className="flex items-center gap-1.5 mt-1"> | ||
| <Shield className="w-5 h-5 text-brand-green animate-pulse" /> | ||
| <div className="text-xl font-display font-semibold text-slate-800"> | ||
| LocalStorage | ||
| </div> | ||
| </div> | ||
| <p className="text-[12px] text-slate-500 mt-2 font-mono"> | ||
| {keysCount} / 3 API Keys configured in browser | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Interactive Controls Bar */} | ||
| <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 bg-white border border-slate-200/50 rounded-xl p-4 shadow-sm"> | ||
| {/* Search Input */} | ||
| <div className="relative flex-1 max-w-md"> | ||
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> | ||
| <input | ||
| type="text" | ||
| value={searchQuery} | ||
| onChange={(e) => 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" | ||
| /> | ||
| </div> | ||
|
|
||
| {/* Filters and Sort */} | ||
| <div className="flex flex-wrap items-center gap-3.5"> | ||
| {/* Platform Tabs */} | ||
| <div className="flex items-center bg-slate-50 border border-slate-200 rounded-lg p-0.5 gap-0.5"> | ||
| {(['all', 'cursor', 'claude', 'windsurf'] as const).map((p) => ( | ||
| <button | ||
| key={p} | ||
| onClick={() => setPlatformFilter(p)} | ||
| className={`font-mono text-[10px] uppercase tracking-wider font-bold px-2.5 py-1.5 rounded-md transition-colors cursor-pointer select-none ${ | ||
| platformFilter === p | ||
| ? 'bg-white text-brand-indigo shadow-sm border border-slate-200/60' | ||
| : 'text-slate-500 hover:text-slate-850 hover:bg-slate-100/50' | ||
| }`} | ||
| > | ||
| {p} | ||
| </button> | ||
| ))} | ||
| </div> | ||
|
|
||
| {/* Sort Dropdown */} | ||
| <div className="relative flex items-center bg-white border border-slate-200 rounded-lg px-2.5 py-1.5 gap-1.5 text-slate-700"> | ||
| <ArrowUpDown className="w-3.5 h-3.5 text-slate-400" /> | ||
| <select | ||
| value={sortBy} | ||
| onChange={(e) => setSortBy(e.target.value as any)} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. setsortby uses as any The new sort dropdown casts e.target.value to any without documenting why any is unavoidable, which erases type safety and makes future refactors riskier. This violates the strict TypeScript requirement to avoid any unless explicitly justified with an explanatory comment. Agent Prompt
|
||
| className="bg-transparent text-[11px] font-mono uppercase tracking-wider font-bold outline-none cursor-pointer pr-1" | ||
| > | ||
| <option value="newest">Newest</option> | ||
| <option value="oldest">Oldest</option> | ||
| <option value="name">Name</option> | ||
| </select> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Grid or Empty state */} | ||
| {skills.length === 0 ? ( | ||
| <div className="text-center py-20 bg-white border border-slate-200/50 rounded-2xl p-8 max-w-md mx-auto"> | ||
| <p className="font-display font-semibold text-[18px] text-slate-800 mb-2">No skills yet</p> | ||
| <p className="font-sans text-[14px] text-slate-500 mb-6"> | ||
| You haven't generated any assistant instructions yet. Describe your workflow to begin. | ||
| {filteredAndSortedSkills.length === 0 ? ( | ||
| <div className="text-center py-20 bg-white border border-slate-200/50 rounded-2xl p-8 max-w-md mx-auto shadow-sm select-none"> | ||
| <p className="font-display font-semibold text-[18px] text-slate-800 mb-2">No matching skills found</p> | ||
| <p className="font-sans text-[13px] text-slate-500 mb-6"> | ||
| Try adjusting your search criteria or platform filters. | ||
| </p> | ||
| <Link href="/generate"> | ||
| <Button variant="accent">Generate your first skill →</Button> | ||
| </Link> | ||
| <button | ||
| onClick={() => { | ||
| setSearchQuery('') | ||
| setPlatformFilter('all') | ||
| }} | ||
| className="text-[12px] font-mono text-brand-indigo uppercase tracking-wider font-bold border border-slate-250 hover:bg-slate-50 px-4 py-2 rounded-lg cursor-pointer" | ||
| > | ||
| Clear Filters | ||
| </button> | ||
| </div> | ||
| ) : ( | ||
| <SkillGrid | ||
| skills={skills} | ||
| skills={filteredAndSortedSkills} | ||
| onDownload={handleDownload} | ||
| onRegenerate={handleRegenerate} | ||
| /> | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4. Missing gpt filter tab
🐞 Bug⚙ MaintainabilityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools