feat: separate guides into system and custom sections
Add system/custom guide separation with section headers, lock icons, delete functionality for custom guides, and usage counter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7bf94d4c61
commit
549476274d
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { readGuide } from '@/services/guides/guideService';
|
||||
import { readGuide, deleteGuide } from '@/services/guides/guideService';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -25,3 +25,26 @@ export async function GET(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ name: string }> }
|
||||
) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const result = deleteGuide(name);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true, message: `Guide "${name}" deleted` });
|
||||
} catch (error) {
|
||||
console.error('[API /guides/[name] DELETE] error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to delete guide' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { ArrowLeft, Lock, Trash2 } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import PaneHeader from './PaneHeader';
|
||||
@@ -10,6 +10,7 @@ import type { BasePaneProps } from './types';
|
||||
interface GuideMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
immutable: boolean;
|
||||
}
|
||||
|
||||
interface Guide extends GuideMeta {
|
||||
@@ -27,6 +28,7 @@ export default function GuidesPane({
|
||||
const [guides, setGuides] = useState<GuideMeta[]>([]);
|
||||
const [selectedGuide, setSelectedGuide] = useState<Guide | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGuides();
|
||||
@@ -62,10 +64,34 @@ export default function GuidesPane({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteGuide = async (name: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm(`Delete guide "${name}"?`)) return;
|
||||
|
||||
setDeleting(name);
|
||||
try {
|
||||
const res = await fetch(`/api/guides/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
fetchGuides();
|
||||
if (selectedGuide?.name === name) {
|
||||
setSelectedGuide(null);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[GuidesPane] Failed to delete guide:', err);
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setSelectedGuide(null);
|
||||
};
|
||||
|
||||
const systemGuides = guides.filter(g => g.immutable);
|
||||
const userGuides = guides.filter(g => !g.immutable);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
@@ -75,7 +101,7 @@ export default function GuidesPane({
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes} tabBar={tabBar}>
|
||||
{selectedGuide && (
|
||||
{selectedGuide ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<button
|
||||
onClick={handleBack}
|
||||
@@ -94,10 +120,15 @@ export default function GuidesPane({
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<span style={{ color: '#ccc', fontSize: '13px', fontWeight: 500 }}>
|
||||
<span style={{ color: '#ccc', fontSize: '13px', fontWeight: 500, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
{selectedGuide.immutable && <Lock size={12} style={{ color: '#22c55e' }} />}
|
||||
{selectedGuide.name}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: '#666', fontSize: '11px' }}>
|
||||
{userGuides.length} of 10 custom guides
|
||||
</span>
|
||||
)}
|
||||
</PaneHeader>
|
||||
|
||||
@@ -188,14 +219,62 @@ export default function GuidesPane({
|
||||
No guides found
|
||||
</div>
|
||||
) : (
|
||||
guides.map((guide) => (
|
||||
<button
|
||||
<>
|
||||
{systemGuides.length > 0 && (
|
||||
<div style={{ color: '#555', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.5px', padding: '4px 0' }}>
|
||||
System Guides
|
||||
</div>
|
||||
)}
|
||||
{systemGuides.map((guide) => (
|
||||
<GuideCard
|
||||
key={guide.name}
|
||||
onClick={() => handleSelectGuide(guide.name)}
|
||||
guide={guide}
|
||||
onSelect={handleSelectGuide}
|
||||
onDelete={handleDeleteGuide}
|
||||
deleting={deleting}
|
||||
/>
|
||||
))}
|
||||
{userGuides.length > 0 && (
|
||||
<div style={{ color: '#555', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.5px', padding: '8px 0 4px 0' }}>
|
||||
Custom Guides
|
||||
</div>
|
||||
)}
|
||||
{userGuides.map((guide) => (
|
||||
<GuideCard
|
||||
key={guide.name}
|
||||
guide={guide}
|
||||
onSelect={handleSelectGuide}
|
||||
onDelete={handleDeleteGuide}
|
||||
deleting={deleting}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GuideCard({
|
||||
guide,
|
||||
onSelect,
|
||||
onDelete,
|
||||
deleting,
|
||||
}: {
|
||||
guide: GuideMeta;
|
||||
onSelect: (name: string) => void;
|
||||
onDelete: (name: string, e: React.MouseEvent) => void;
|
||||
deleting: string | null;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onSelect(guide.name)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '12px',
|
||||
background: '#161616',
|
||||
border: '1px solid #222',
|
||||
@@ -213,18 +292,39 @@ export default function GuidesPane({
|
||||
e.currentTarget.style.borderColor = '#222';
|
||||
}}
|
||||
>
|
||||
{guide.immutable && (
|
||||
<Lock size={12} style={{ color: '#22c55e', flexShrink: 0 }} />
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<span style={{ color: '#ddd', fontSize: '13px', fontWeight: 500 }}>
|
||||
{guide.name}
|
||||
</span>
|
||||
<span style={{ color: '#777', fontSize: '12px', lineHeight: '1.4' }}>
|
||||
<span style={{ color: '#777', fontSize: '12px', lineHeight: '1.4', display: 'block', marginTop: '2px' }}>
|
||||
{guide.description}
|
||||
</span>
|
||||
</div>
|
||||
{!guide.immutable && (
|
||||
<button
|
||||
onClick={(e) => onDelete(guide.name, e)}
|
||||
disabled={deleting === guide.name}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#555',
|
||||
cursor: 'pointer',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
opacity: deleting === guide.name ? 0.3 : 1,
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.color = '#ef4444'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.color = '#555'; }}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user