增加提示词管理

This commit is contained in:
2025-10-21 15:08:31 +08:00
parent 0e32c6e64c
commit 7021ab6bec
17 changed files with 2286 additions and 6 deletions

View File

@@ -0,0 +1,237 @@
import React, { useState, useEffect } from 'react';
import { Loader2, Save, Eye, EyeOff, X, Tag as TagIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog';
import MonacoEditor from '@monaco-editor/react';
import ReactMarkdown from 'react-markdown';
import { usePromptFilesStore } from '@/stores/promptFilesStore';
import type { PromptFile } from '@/lib/api';
interface PromptFileEditorProps {
open: boolean;
onOpenChange: (open: boolean) => void;
file?: PromptFile;
onSuccess: () => void;
}
export const PromptFileEditor: React.FC<PromptFileEditorProps> = ({
open,
onOpenChange,
file,
onSuccess,
}) => {
const { createFile, updateFile } = usePromptFilesStore();
const [saving, setSaving] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [content, setContent] = useState('');
const [tags, setTags] = useState<string[]>([]);
const [tagInput, setTagInput] = useState('');
useEffect(() => {
if (file) {
setName(file.name);
setDescription(file.description || '');
setContent(file.content);
setTags(file.tags);
} else {
setName('');
setDescription('');
setContent('');
setTags([]);
}
}, [file, open]);
const handleAddTag = () => {
const trimmed = tagInput.trim().toLowerCase();
if (trimmed && !tags.includes(trimmed)) {
setTags([...tags, trimmed]);
setTagInput('');
}
};
const handleRemoveTag = (tag: string) => {
setTags(tags.filter((t) => t !== tag));
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
}
};
const handleSave = async () => {
if (!name.trim()) return;
setSaving(true);
try {
if (file) {
await updateFile({
id: file.id,
name: name.trim(),
description: description.trim() || undefined,
content: content,
tags,
});
} else {
await createFile({
name: name.trim(),
description: description.trim() || undefined,
content: content,
tags,
});
}
onSuccess();
} catch (error) {
// Error handling is done in the store
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{file ? '编辑提示词文件' : '创建提示词文件'}</DialogTitle>
<DialogDescription>
{file ? '修改提示词文件的内容和信息' : '创建一个新的提示词文件模板'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Basic Info */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
placeholder="例如: React 项目指南"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Input
id="description"
placeholder="简短描述..."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
</div>
{/* Tags */}
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-2 mb-2">
{tags.map((tag) => (
<Badge key={tag} variant="secondary" className="flex items-center gap-1">
{tag}
<X
className="h-3 w-3 cursor-pointer hover:text-destructive"
onClick={() => handleRemoveTag(tag)}
/>
</Badge>
))}
</div>
<div className="flex gap-2">
<Input
placeholder="添加标签(按 Enter"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
<Button type="button" variant="outline" onClick={handleAddTag}>
<TagIcon className="h-4 w-4" />
</Button>
</div>
</div>
{/* Content Editor */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label> *</Label>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowPreview(!showPreview)}
>
{showPreview ? (
<>
<EyeOff className="mr-2 h-4 w-4" />
</>
) : (
<>
<Eye className="mr-2 h-4 w-4" />
</>
)}
</Button>
</div>
{showPreview ? (
<div className="border rounded-lg p-4 max-h-[400px] overflow-y-auto prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
) : (
<div className="border rounded-lg overflow-hidden" style={{ height: '400px' }}>
<MonacoEditor
language="markdown"
theme="vs-dark"
value={content}
onChange={(value) => setContent(value || '')}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
wordWrap: 'on',
lineNumbers: 'on',
automaticLayout: true,
}}
/>
</div>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
</Button>
<Button onClick={handleSave} disabled={!name.trim() || !content.trim() || saving}>
{saving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default PromptFileEditor;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { Edit, Play, Tag as TagIcon, Clock, Calendar } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog';
import ReactMarkdown from 'react-markdown';
import type { PromptFile } from '@/lib/api';
interface PromptFilePreviewProps {
open: boolean;
onOpenChange: (open: boolean) => void;
file: PromptFile;
onEdit: () => void;
onApply: () => void;
}
export const PromptFilePreview: React.FC<PromptFilePreviewProps> = ({
open,
onOpenChange,
file,
onEdit,
onApply,
}) => {
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl">{file.name}</DialogTitle>
{file.description && (
<DialogDescription className="text-base mt-2">{file.description}</DialogDescription>
)}
</DialogHeader>
{/* Metadata */}
<div className="flex flex-wrap gap-4 py-4 border-y text-sm text-muted-foreground">
{file.tags.length > 0 && (
<div className="flex items-center gap-2">
<TagIcon className="h-4 w-4" />
<div className="flex gap-1 flex-wrap">
{file.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
</div>
)}
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
: {formatDate(file.created_at)}
</div>
{file.updated_at !== file.created_at && (
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
: {formatDate(file.updated_at)}
</div>
)}
{file.last_used_at && (
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
使: {formatDate(file.last_used_at)}
</div>
)}
</div>
{/* Content */}
<div className="prose prose-sm dark:prose-invert max-w-none py-4">
<ReactMarkdown>{file.content}</ReactMarkdown>
</div>
<DialogFooter className="flex items-center justify-between">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<div className="flex gap-2">
<Button variant="outline" onClick={onEdit}>
<Edit className="mr-2 h-4 w-4" />
</Button>
{!file.is_active && (
<Button onClick={onApply}>
<Play className="mr-2 h-4 w-4" />
使
</Button>
)}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default PromptFilePreview;

View File

@@ -0,0 +1,589 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
FileText,
Plus,
Search,
Upload,
ArrowLeft,
Check,
Edit,
Trash2,
Eye,
Play,
AlertCircle,
Loader2,
Tag,
Clock,
CheckCircle2,
RefreshCw,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from '@/components/ui/dialog';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { usePromptFilesStore } from '@/stores/promptFilesStore';
import { useTranslation } from '@/hooks/useTranslation';
import type { PromptFile } from '@/lib/api';
import { cn } from '@/lib/utils';
import { PromptFileEditor } from './PromptFileEditor';
import { PromptFilePreview } from './PromptFilePreview';
interface PromptFilesManagerProps {
onBack?: () => void;
className?: string;
}
export const PromptFilesManager: React.FC<PromptFilesManagerProps> = ({ onBack, className }) => {
const { t } = useTranslation();
const {
files,
isLoading,
error,
loadFiles,
deleteFile,
applyFile,
deactivateAll,
importFromClaudeMd,
clearError,
} = usePromptFilesStore();
const [searchQuery, setSearchQuery] = useState('');
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const [showPreviewDialog, setShowPreviewDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showImportDialog, setShowImportDialog] = useState(false);
const [selectedFile, setSelectedFile] = useState<PromptFile | null>(null);
const [applyingFileId, setApplyingFileId] = useState<string | null>(null);
const [syncingFileId, setSyncingFileId] = useState<string | null>(null);
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
useEffect(() => {
loadFiles();
}, [loadFiles]);
useEffect(() => {
if (error) {
showToast(error, 'error');
clearError();
}
}, [error, clearError]);
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
setToast({ message, type });
setTimeout(() => setToast(null), 3000);
};
const handleApply = async (file: PromptFile) => {
setApplyingFileId(file.id);
try {
const path = await applyFile(file.id);
showToast(`已应用到: ${path}`, 'success');
} catch (error) {
showToast('应用失败', 'error');
} finally {
setApplyingFileId(null);
}
};
const handleDeactivate = async () => {
try {
await deactivateAll();
showToast('已取消使用', 'success');
} catch (error) {
showToast('取消失败', 'error');
}
};
const handleSync = async (file: PromptFile) => {
setSyncingFileId(file.id);
try {
// 同步当前激活的文件到 ~/.claude/CLAUDE.md
const path = await applyFile(file.id);
showToast(`文件已同步到: ${path}`, 'success');
await loadFiles(); // 重新加载以更新状态
} catch (error) {
showToast('同步失败', 'error');
} finally {
setSyncingFileId(null);
}
};
const handleDelete = async () => {
if (!selectedFile) return;
try {
await deleteFile(selectedFile.id);
setShowDeleteDialog(false);
setSelectedFile(null);
showToast('删除成功', 'success');
} catch (error) {
showToast('删除失败', 'error');
}
};
const handleImportFromClaudeMd = async (name: string, description?: string) => {
try {
await importFromClaudeMd(name, description);
setShowImportDialog(false);
showToast('导入成功', 'success');
} catch (error) {
showToast('导入失败', 'error');
}
};
const openPreview = (file: PromptFile) => {
setSelectedFile(file);
setShowPreviewDialog(true);
};
const openEdit = (file: PromptFile) => {
setSelectedFile(file);
setShowEditDialog(true);
};
const openDelete = (file: PromptFile) => {
setSelectedFile(file);
setShowDeleteDialog(true);
};
const filteredFiles = files.filter((file) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
file.name.toLowerCase().includes(query) ||
file.description?.toLowerCase().includes(query) ||
file.tags.some((tag) => tag.toLowerCase().includes(query))
);
});
const activeFiles = filteredFiles.filter((f) => f.is_active);
const inactiveFiles = filteredFiles.filter((f) => !f.is_active);
return (
<div className={cn('h-full flex flex-col overflow-hidden', className)}>
<div className="flex-1 overflow-y-auto min-h-0">
<div className="container mx-auto p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{onBack && (
<Button variant="ghost" size="sm" onClick={onBack} className="flex items-center gap-2">
<ArrowLeft className="h-4 w-4" />
{t('app.back')}
</Button>
)}
<div>
<h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground"> Claude </p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setShowImportDialog(true)}>
<Upload className="mr-2 h-4 w-4" />
CLAUDE.md
</Button>
<Button onClick={() => setShowCreateDialog(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索提示词文件..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
{/* Error Display */}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Loading State */}
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
{/* Active File */}
{!isLoading && activeFiles.length > 0 && (
<div>
<h2 className="text-lg font-semibold mb-3 flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-600" />
使
</h2>
{activeFiles.map((file) => (
<Card key={file.id} className="border-green-200 dark:border-green-900 bg-green-50/50 dark:bg-green-950/20">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
{file.name}
<Badge variant="secondary" className="bg-green-100 dark:bg-green-900">
使
</Badge>
</CardTitle>
{file.description && (
<CardDescription className="mt-2">{file.description}</CardDescription>
)}
</div>
</div>
<div className="flex items-center gap-4 mt-3 text-sm text-muted-foreground">
{file.tags.length > 0 && (
<div className="flex items-center gap-1">
<Tag className="h-3 w-3" />
{file.tags.slice(0, 3).map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{file.tags.length > 3 && <span className="text-xs">+{file.tags.length - 3}</span>}
</div>
)}
{file.last_used_at && (
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{new Date(file.last_used_at * 1000).toLocaleString('zh-CN')}
</div>
)}
</div>
</CardHeader>
<CardContent>
<div className="flex gap-2 flex-wrap">
<Button
variant="default"
size="sm"
onClick={() => handleSync(file)}
disabled={syncingFileId === file.id}
>
{syncingFileId === file.id ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
</>
)}
</Button>
<Button variant="outline" size="sm" onClick={() => openPreview(file)}>
<Eye className="mr-2 h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => openEdit(file)}>
<Edit className="mr-2 h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={handleDeactivate}>
使
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* All Prompt Files */}
{!isLoading && (
<div>
<h2 className="text-lg font-semibold mb-3">
({inactiveFiles.length})
</h2>
{inactiveFiles.length === 0 ? (
<Card className="p-12">
<div className="text-center">
<FileText className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4">
{searchQuery ? '没有找到匹配的提示词文件' : '还没有提示词文件'}
</p>
{!searchQuery && (
<Button onClick={() => setShowCreateDialog(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
)}
</div>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{inactiveFiles.map((file) => (
<Card key={file.id} className="hover:shadow-md transition-shadow flex flex-col">
<CardHeader className="flex-1">
<CardTitle className="flex items-center gap-2 text-base">
<FileText className="h-4 w-4 flex-shrink-0" />
<span className="truncate">{file.name}</span>
</CardTitle>
<CardDescription className="text-sm line-clamp-2 min-h-[1.25rem]">
{file.description || ' '}
</CardDescription>
{file.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{file.tags.slice(0, 3).map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
{file.tags.length > 3 && (
<span className="text-xs text-muted-foreground">
+{file.tags.length - 3}
</span>
)}
</div>
)}
</CardHeader>
<CardContent className="space-y-2 pt-4">
<Button
className="w-full"
size="sm"
onClick={() => handleApply(file)}
disabled={applyingFileId === file.id}
>
{applyingFileId === file.id ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Play className="mr-2 h-4 w-4 flex-shrink-0" />
使
</>
)}
</Button>
<div className="flex gap-2 justify-center">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => openPreview(file)}
title="查看内容"
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => openEdit(file)}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => openDelete(file)}
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
)}
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{selectedFile && (
<div className="py-4">
<p className="font-medium">{selectedFile.name}</p>
{selectedFile.description && (
<p className="text-sm text-muted-foreground mt-1">{selectedFile.description}</p>
)}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowDeleteDialog(false)}>
</Button>
<Button variant="destructive" onClick={handleDelete}>
<Trash2 className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Import Dialog */}
<ImportFromClaudeMdDialog
open={showImportDialog}
onOpenChange={setShowImportDialog}
onImport={handleImportFromClaudeMd}
/>
{/* Create/Edit Dialogs */}
{showCreateDialog && (
<PromptFileEditor
open={showCreateDialog}
onOpenChange={setShowCreateDialog}
onSuccess={() => {
setShowCreateDialog(false);
showToast('创建成功', 'success');
}}
/>
)}
{showEditDialog && selectedFile && (
<PromptFileEditor
open={showEditDialog}
onOpenChange={setShowEditDialog}
file={selectedFile}
onSuccess={() => {
setShowEditDialog(false);
setSelectedFile(null);
showToast('更新成功', 'success');
}}
/>
)}
{/* Preview Dialog */}
{showPreviewDialog && selectedFile && (
<PromptFilePreview
open={showPreviewDialog}
onOpenChange={setShowPreviewDialog}
file={selectedFile}
onEdit={() => {
setShowPreviewDialog(false);
openEdit(selectedFile);
}}
onApply={() => {
setShowPreviewDialog(false);
handleApply(selectedFile);
}}
/>
)}
</div>
</div>
{/* Toast */}
<AnimatePresence>
{toast && (
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }}
className="fixed bottom-4 right-4 z-50"
>
<Alert variant={toast.type === 'error' ? 'destructive' : 'default'} className="shadow-lg">
{toast.type === 'success' ? (
<Check className="h-4 w-4" />
) : (
<AlertCircle className="h-4 w-4" />
)}
<AlertDescription>{toast.message}</AlertDescription>
</Alert>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
// Import from CLAUDE.md Dialog
const ImportFromClaudeMdDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
onImport: (name: string, description?: string) => Promise<void>;
}> = ({ open, onOpenChange, onImport }) => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [importing, setImporting] = useState(false);
const handleImport = async () => {
if (!name.trim()) return;
setImporting(true);
try {
await onImport(name, description || undefined);
setName('');
setDescription('');
} finally {
setImporting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle> CLAUDE.md </DialogTitle>
<DialogDescription> CLAUDE.md </DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input
placeholder="例如: 我的项目指南"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input
placeholder="简短描述这个提示词文件的用途"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleImport} disabled={!name.trim() || importing}>
{importing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default PromptFilesManager;

View File

@@ -30,6 +30,7 @@ import { ClaudeVersionSelector } from "./ClaudeVersionSelector";
import { StorageTab } from "./StorageTab";
import { HooksEditor } from "./HooksEditor";
import { SlashCommandsManager } from "./SlashCommandsManager";
import PromptFilesManager from "./PromptFilesManager";
import { ProxySettings } from "./ProxySettings";
import { AnalyticsConsent } from "./AnalyticsConsent";
import { useTheme, useTrackEvent, useTranslation } from "@/hooks";
@@ -457,13 +458,14 @@ export const Settings: React.FC<SettingsProps> = ({
<div className="flex-1 overflow-y-auto min-h-0">
<div className="p-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid grid-cols-9 w-full sticky top-0 z-10 bg-background">
<TabsList className="grid grid-cols-10 w-full sticky top-0 z-10 bg-background">
<TabsTrigger value="general">{t('settings.general')}</TabsTrigger>
<TabsTrigger value="permissions">{t('settings.permissionsTab')}</TabsTrigger>
<TabsTrigger value="environment">{t('settings.environmentTab')}</TabsTrigger>
<TabsTrigger value="advanced">{t('settings.advancedTab')}</TabsTrigger>
<TabsTrigger value="hooks">{t('settings.hooksTab')}</TabsTrigger>
<TabsTrigger value="commands">{t('settings.commands')}</TabsTrigger>
<TabsTrigger value="prompts">{t('promptFiles.title')}</TabsTrigger>
<TabsTrigger value="storage">{t('settings.storage')}</TabsTrigger>
<TabsTrigger value="proxy">{t('settings.proxy')}</TabsTrigger>
<TabsTrigger value="analytics">{t('settings.analyticsTab')}</TabsTrigger>
@@ -1019,6 +1021,13 @@ export const Settings: React.FC<SettingsProps> = ({
</Card>
</TabsContent>
{/* Prompt Files Tab */}
<TabsContent value="prompts" className="mt-6">
<Card className="p-6">
<PromptFilesManager className="p-0" />
</Card>
</TabsContent>
{/* Storage Tab */}
<TabsContent value="storage" className="mt-6">
<StorageTab />

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { Circle, FileText, Settings, ExternalLink, BarChart3, Network, Info, Bot } from "lucide-react";
import { Circle, FileText, Settings, ExternalLink, BarChart3, Network, Info, Bot, Files } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Popover } from "@/components/ui/popover";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
@@ -34,6 +34,10 @@ interface TopbarProps {
* Callback when Agents is clicked
*/
onAgentsClick?: () => void;
/**
* Callback when Prompt Files is clicked
*/
onPromptFilesClick?: () => void;
/**
* Optional className for styling
*/
@@ -58,6 +62,7 @@ export const Topbar: React.FC<TopbarProps> = ({
onMCPClick,
onInfoClick,
onAgentsClick,
onPromptFilesClick,
className,
}) => {
const { t } = useTranslation();
@@ -218,6 +223,18 @@ export const Topbar: React.FC<TopbarProps> = ({
CLAUDE.md
</Button>
{onPromptFilesClick && (
<Button
variant="ghost"
size="sm"
onClick={onPromptFilesClick}
className="text-xs"
>
<Files className="mr-2 h-3 w-3" />
{t('navigation.promptFiles')}
</Button>
)}
<Button
variant="ghost"
size="sm"

View File

@@ -74,13 +74,13 @@ export function WelcomePage({ onNavigate, onNewSession, onSmartQuickStart }: Wel
view: "ccr-router"
},
{
id: "claude-md",
id: "prompt-files",
icon: FileText,
title: t("welcome.claudeMd"),
subtitle: t("welcome.claudeMdDesc"),
title: t("welcome.promptFiles"),
subtitle: t("welcome.promptFilesDesc"),
color: "text-orange-500",
bgColor: "bg-orange-500/10",
view: "editor"
view: "prompt-files"
},
{
id: "settings",

View File

@@ -26,6 +26,9 @@ export * from "./ui/toast";
export * from "./ui/tooltip";
export * from "./SlashCommandPicker";
export * from "./SlashCommandsManager";
export { default as PromptFilesManager } from "./PromptFilesManager";
export { default as PromptFileEditor } from "./PromptFileEditor";
export { default as PromptFilePreview } from "./PromptFilePreview";
export * from "./ui/popover";
export * from "./ui/pagination";
export * from "./ui/split-pane";