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 { NextRequest, NextResponse } from 'next/server';
|
||||||
import { readGuide } from '@/services/guides/guideService';
|
import { readGuide, deleteGuide } from '@/services/guides/guideService';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft, Lock, Trash2 } from 'lucide-react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import PaneHeader from './PaneHeader';
|
import PaneHeader from './PaneHeader';
|
||||||
@@ -10,6 +10,7 @@ import type { BasePaneProps } from './types';
|
|||||||
interface GuideMeta {
|
interface GuideMeta {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
immutable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Guide extends GuideMeta {
|
interface Guide extends GuideMeta {
|
||||||
@@ -27,6 +28,7 @@ export default function GuidesPane({
|
|||||||
const [guides, setGuides] = useState<GuideMeta[]>([]);
|
const [guides, setGuides] = useState<GuideMeta[]>([]);
|
||||||
const [selectedGuide, setSelectedGuide] = useState<Guide | null>(null);
|
const [selectedGuide, setSelectedGuide] = useState<Guide | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [deleting, setDeleting] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchGuides();
|
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 = () => {
|
const handleBack = () => {
|
||||||
setSelectedGuide(null);
|
setSelectedGuide(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const systemGuides = guides.filter(g => g.immutable);
|
||||||
|
const userGuides = guides.filter(g => !g.immutable);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -75,7 +101,7 @@ export default function GuidesPane({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}>
|
}}>
|
||||||
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes} tabBar={tabBar}>
|
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes} tabBar={tabBar}>
|
||||||
{selectedGuide && (
|
{selectedGuide ? (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<button
|
<button
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
@@ -94,10 +120,15 @@ export default function GuidesPane({
|
|||||||
>
|
>
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
</button>
|
</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}
|
{selectedGuide.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: '#666', fontSize: '11px' }}>
|
||||||
|
{userGuides.length} of 10 custom guides
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</PaneHeader>
|
</PaneHeader>
|
||||||
|
|
||||||
@@ -188,39 +219,36 @@ export default function GuidesPane({
|
|||||||
No guides found
|
No guides found
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
guides.map((guide) => (
|
<>
|
||||||
<button
|
{systemGuides.length > 0 && (
|
||||||
key={guide.name}
|
<div style={{ color: '#555', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.5px', padding: '4px 0' }}>
|
||||||
onClick={() => handleSelectGuide(guide.name)}
|
System Guides
|
||||||
style={{
|
</div>
|
||||||
display: 'flex',
|
)}
|
||||||
flexDirection: 'column',
|
{systemGuides.map((guide) => (
|
||||||
gap: '4px',
|
<GuideCard
|
||||||
padding: '12px',
|
key={guide.name}
|
||||||
background: '#161616',
|
guide={guide}
|
||||||
border: '1px solid #222',
|
onSelect={handleSelectGuide}
|
||||||
borderRadius: '8px',
|
onDelete={handleDeleteGuide}
|
||||||
cursor: 'pointer',
|
deleting={deleting}
|
||||||
textAlign: 'left',
|
/>
|
||||||
transition: 'all 0.15s ease',
|
))}
|
||||||
}}
|
{userGuides.length > 0 && (
|
||||||
onMouseEnter={e => {
|
<div style={{ color: '#555', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.5px', padding: '8px 0 4px 0' }}>
|
||||||
e.currentTarget.style.background = '#1a1a1a';
|
Custom Guides
|
||||||
e.currentTarget.style.borderColor = '#333';
|
</div>
|
||||||
}}
|
)}
|
||||||
onMouseLeave={e => {
|
{userGuides.map((guide) => (
|
||||||
e.currentTarget.style.background = '#161616';
|
<GuideCard
|
||||||
e.currentTarget.style.borderColor = '#222';
|
key={guide.name}
|
||||||
}}
|
guide={guide}
|
||||||
>
|
onSelect={handleSelectGuide}
|
||||||
<span style={{ color: '#ddd', fontSize: '13px', fontWeight: 500 }}>
|
onDelete={handleDeleteGuide}
|
||||||
{guide.name}
|
deleting={deleting}
|
||||||
</span>
|
/>
|
||||||
<span style={{ color: '#777', fontSize: '12px', lineHeight: '1.4' }}>
|
))}
|
||||||
{guide.description}
|
</>
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -228,3 +256,75 @@ export default function GuidesPane({
|
|||||||
</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',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '12px',
|
||||||
|
background: '#161616',
|
||||||
|
border: '1px solid #222',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textAlign: 'left',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.background = '#1a1a1a';
|
||||||
|
e.currentTarget.style.borderColor = '#333';
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.background = '#161616';
|
||||||
|
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', 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>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user