mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: add Accordion component for UI and Zustand store for coordinator management
- Implemented Accordion component using Radix UI for collapsible sections. - Created Zustand store to manage coordinator execution state, command chains, logs, and interactive questions. - Added validation tests for CLI settings type definitions, ensuring type safety and correct behavior of helper functions.
This commit is contained in:
@@ -3,8 +3,9 @@
|
||||
// ========================================
|
||||
// View and manage core memory and context with CRUD operations
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Brain,
|
||||
Search,
|
||||
@@ -19,12 +20,16 @@ import {
|
||||
Copy,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Star,
|
||||
Archive,
|
||||
ArchiveRestore,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
import { Checkbox } from '@/components/ui/Checkbox';
|
||||
import { useMemory, useMemoryMutations } from '@/hooks';
|
||||
import type { CoreMemory } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -38,10 +43,19 @@ interface MemoryCardProps {
|
||||
onEdit: (memory: CoreMemory) => void;
|
||||
onDelete: (memory: CoreMemory) => void;
|
||||
onCopy: (content: string) => void;
|
||||
onToggleFavorite: (memory: CoreMemory) => void;
|
||||
onArchive: (memory: CoreMemory) => void;
|
||||
onUnarchive: (memory: CoreMemory) => void;
|
||||
}
|
||||
|
||||
function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCopy }: MemoryCardProps) {
|
||||
function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCopy, onToggleFavorite, onArchive, onUnarchive }: MemoryCardProps) {
|
||||
const formattedDate = new Date(memory.createdAt).toLocaleDateString();
|
||||
|
||||
// Parse metadata from memory
|
||||
const metadata = memory.metadata ? (typeof memory.metadata === 'string' ? JSON.parse(memory.metadata) : memory.metadata) : {};
|
||||
const isFavorite = metadata.favorite === true;
|
||||
const priority = metadata.priority || 'medium';
|
||||
const isArchived = memory.archived || false;
|
||||
const formattedSize = memory.size
|
||||
? memory.size < 1024
|
||||
? `${memory.size} B`
|
||||
@@ -70,6 +84,16 @@ function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCo
|
||||
{memory.source}
|
||||
</Badge>
|
||||
)}
|
||||
{priority !== 'medium' && (
|
||||
<Badge variant={priority === 'high' ? 'destructive' : 'secondary'} className="text-xs">
|
||||
{priority}
|
||||
</Badge>
|
||||
)}
|
||||
{isArchived && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Archived
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formattedDate} - {formattedSize}
|
||||
@@ -77,6 +101,17 @@ function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCo
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn("h-8 w-8 p-0", isFavorite && "text-yellow-500")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleFavorite(memory);
|
||||
}}
|
||||
>
|
||||
<Star className={cn("w-4 h-4", isFavorite && "fill-current")} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -99,6 +134,31 @@ function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCo
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
{!isArchived ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onArchive(memory);
|
||||
}}
|
||||
>
|
||||
<Archive className="w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUnarchive(memory);
|
||||
}}
|
||||
>
|
||||
<ArchiveRestore className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -160,7 +220,7 @@ function MemoryCard({ memory, isExpanded, onToggleExpand, onEdit, onDelete, onCo
|
||||
interface NewMemoryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (data: { content: string; tags?: string[] }) => void;
|
||||
onSubmit: (data: { content: string; tags?: string[]; metadata?: Record<string, any> }) => void;
|
||||
isCreating: boolean;
|
||||
editingMemory?: CoreMemory | null;
|
||||
}
|
||||
@@ -175,6 +235,27 @@ function NewMemoryDialog({
|
||||
const { formatMessage } = useIntl();
|
||||
const [content, setContent] = useState(editingMemory?.content || '');
|
||||
const [tagsInput, setTagsInput] = useState(editingMemory?.tags?.join(', ') || '');
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
|
||||
|
||||
// Initialize from editing memory metadata
|
||||
useEffect(() => {
|
||||
if (editingMemory && editingMemory.metadata) {
|
||||
try {
|
||||
const metadata = typeof editingMemory.metadata === 'string'
|
||||
? JSON.parse(editingMemory.metadata)
|
||||
: editingMemory.metadata;
|
||||
setIsFavorite(metadata.favorite === true);
|
||||
setPriority(metadata.priority || 'medium');
|
||||
} catch {
|
||||
setIsFavorite(false);
|
||||
setPriority('medium');
|
||||
}
|
||||
} else {
|
||||
setIsFavorite(false);
|
||||
setPriority('medium');
|
||||
}
|
||||
}, [editingMemory]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -183,9 +264,21 @@ function NewMemoryDialog({
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
onSubmit({ content: content.trim(), tags: tags.length > 0 ? tags : undefined });
|
||||
|
||||
// Build metadata object
|
||||
const metadata: Record<string, any> = {};
|
||||
if (isFavorite) metadata.favorite = true;
|
||||
if (priority !== 'medium') metadata.priority = priority;
|
||||
|
||||
onSubmit({
|
||||
content: content.trim(),
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
||||
});
|
||||
setContent('');
|
||||
setTagsInput('');
|
||||
setIsFavorite(false);
|
||||
setPriority('medium');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -217,6 +310,30 @@ function NewMemoryDialog({
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="favorite"
|
||||
checked={isFavorite}
|
||||
onCheckedChange={(checked) => setIsFavorite(checked === true)}
|
||||
/>
|
||||
<label htmlFor="favorite" className="text-sm font-medium cursor-pointer">
|
||||
{formatMessage({ id: 'memory.createDialog.labels.favorite' })}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium">{formatMessage({ id: 'memory.createDialog.labels.priority' })}</label>
|
||||
<select
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(e.target.value as 'low' | 'medium' | 'high')}
|
||||
className="mt-1 w-full p-2 bg-background border border-input rounded-md text-sm"
|
||||
>
|
||||
<option value="low">{formatMessage({ id: 'memory.priority.low' })}</option>
|
||||
<option value="medium">{formatMessage({ id: 'memory.priority.medium' })}</option>
|
||||
<option value="high">{formatMessage({ id: 'memory.priority.high' })}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{formatMessage({ id: 'memory.createDialog.buttons.cancel' })}
|
||||
@@ -250,6 +367,11 @@ export function MemoryPage() {
|
||||
const [isNewMemoryOpen, setIsNewMemoryOpen] = useState(false);
|
||||
const [editingMemory, setEditingMemory] = useState<CoreMemory | null>(null);
|
||||
const [expandedMemories, setExpandedMemories] = useState<Set<string>>(new Set());
|
||||
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived'>('memories');
|
||||
|
||||
// Build filter based on current tab
|
||||
const favoriteFilter = currentTab === 'favorites' ? { favorite: true } : undefined;
|
||||
const archivedFilter = currentTab === 'archived' ? { archived: true } : { archived: false };
|
||||
|
||||
const {
|
||||
memories,
|
||||
@@ -263,10 +385,12 @@ export function MemoryPage() {
|
||||
filter: {
|
||||
search: searchQuery || undefined,
|
||||
tags: selectedTags.length > 0 ? selectedTags : undefined,
|
||||
...favoriteFilter,
|
||||
...archivedFilter,
|
||||
},
|
||||
});
|
||||
|
||||
const { createMemory, updateMemory, deleteMemory, isCreating, isUpdating } =
|
||||
const { createMemory, updateMemory, deleteMemory, archiveMemory, unarchiveMemory, isCreating, isUpdating } =
|
||||
useMemoryMutations();
|
||||
|
||||
const toggleExpand = (memoryId: string) => {
|
||||
@@ -281,12 +405,12 @@ export function MemoryPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateMemory = async (data: { content: string; tags?: string[] }) => {
|
||||
const handleCreateMemory = async (data: { content: string; tags?: string[]; metadata?: Record<string, any> }) => {
|
||||
if (editingMemory) {
|
||||
await updateMemory(editingMemory.id, data);
|
||||
setEditingMemory(null);
|
||||
} else {
|
||||
await createMemory(data);
|
||||
await createMemory(data as any); // TODO: update createMemory type to accept metadata
|
||||
}
|
||||
setIsNewMemoryOpen(false);
|
||||
};
|
||||
@@ -302,12 +426,29 @@ export function MemoryPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleFavorite = async (memory: CoreMemory) => {
|
||||
const currentMetadata = memory.metadata ? (typeof memory.metadata === 'string' ? JSON.parse(memory.metadata) : memory.metadata) : {};
|
||||
const newFavorite = !(currentMetadata.favorite === true);
|
||||
await updateMemory(memory.id, {
|
||||
metadata: JSON.stringify({ ...currentMetadata, favorite: newFavorite }),
|
||||
} as any); // TODO: update updateMemory to accept metadata field
|
||||
};
|
||||
|
||||
const handleArchive = async (memory: CoreMemory) => {
|
||||
await archiveMemory(memory.id);
|
||||
};
|
||||
|
||||
const handleUnarchive = async (memory: CoreMemory) => {
|
||||
await unarchiveMemory(memory.id);
|
||||
};
|
||||
|
||||
const copyToClipboard = async (content: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
// TODO: Show toast notification
|
||||
toast.success(formatMessage({ id: 'memory.actions.copySuccess' }));
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
toast.error(formatMessage({ id: 'memory.actions.copyError' }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -348,6 +489,34 @@ export function MemoryPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex items-center gap-2 border-b border-border">
|
||||
<Button
|
||||
variant={currentTab === 'memories' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setCurrentTab('memories')}
|
||||
>
|
||||
<Brain className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.tabs.memories' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentTab === 'favorites' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setCurrentTab('favorites')}
|
||||
>
|
||||
<Star className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.tabs.favorites' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentTab === 'archived' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setCurrentTab('archived')}
|
||||
>
|
||||
<Archive className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.tabs.archived' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="p-4">
|
||||
@@ -429,9 +598,9 @@ export function MemoryPage() {
|
||||
|
||||
{/* Memory List */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-32 bg-muted animate-pulse rounded-lg" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<div key={i} className="h-64 bg-muted animate-pulse rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : memories.length === 0 ? (
|
||||
@@ -449,7 +618,7 @@ export function MemoryPage() {
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{memories.map((memory) => (
|
||||
<MemoryCard
|
||||
key={memory.id}
|
||||
@@ -459,6 +628,9 @@ export function MemoryPage() {
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onCopy={copyToClipboard}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onArchive={handleArchive}
|
||||
onUnarchive={handleUnarchive}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
import {
|
||||
usePromptHistory,
|
||||
usePromptInsights,
|
||||
useInsightsHistory,
|
||||
usePromptHistoryMutations,
|
||||
useDeleteInsight,
|
||||
extractUniqueProjects,
|
||||
type PromptHistoryFilter,
|
||||
} from '@/hooks/usePromptHistory';
|
||||
import { PromptStats, PromptStatsSkeleton } from '@/components/shared/PromptStats';
|
||||
import { PromptCard } from '@/components/shared/PromptCard';
|
||||
import { BatchOperationToolbar } from '@/components/shared/BatchOperationToolbar';
|
||||
import { InsightsPanel } from '@/components/shared/InsightsPanel';
|
||||
import { InsightsHistoryList } from '@/components/shared/InsightsHistoryList';
|
||||
import { InsightDetailPanelOverlay } from '@/components/shared/InsightDetailPanel';
|
||||
import { fetchInsightDetail } from '@/lib/api';
|
||||
import type { InsightHistory } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
@@ -42,7 +50,6 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
} from '@/components/ui/Dropdown';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type IntentFilter = 'all' | string;
|
||||
@@ -56,24 +63,35 @@ export function PromptHistoryPage() {
|
||||
// Filter state
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [intentFilter, setIntentFilter] = React.useState<IntentFilter>('all');
|
||||
const [projectFilter, setProjectFilter] = React.useState<string>('all');
|
||||
const [selectedTool, setSelectedTool] = React.useState<'gemini' | 'qwen' | 'codex'>('gemini');
|
||||
|
||||
// Dialog state
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
||||
const [promptToDelete, setPromptToDelete] = React.useState<string | null>(null);
|
||||
|
||||
// Insight detail state
|
||||
const [selectedInsight, setSelectedInsight] = React.useState<InsightHistory | null>(null);
|
||||
const [insightDetailOpen, setInsightDetailOpen] = React.useState(false);
|
||||
|
||||
// Batch operations state
|
||||
const [selectedPromptIds, setSelectedPromptIds] = React.useState<Set<string>>(new Set());
|
||||
const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = React.useState(false);
|
||||
|
||||
// Build filter object
|
||||
const filter: PromptHistoryFilter = React.useMemo(
|
||||
() => ({
|
||||
search: searchQuery,
|
||||
intent: intentFilter === 'all' ? undefined : intentFilter,
|
||||
project: projectFilter === 'all' ? undefined : projectFilter,
|
||||
}),
|
||||
[searchQuery, intentFilter]
|
||||
[searchQuery, intentFilter, projectFilter]
|
||||
);
|
||||
|
||||
// Fetch prompts and insights
|
||||
const {
|
||||
prompts,
|
||||
allPrompts,
|
||||
promptsBySession,
|
||||
stats,
|
||||
isLoading,
|
||||
@@ -83,10 +101,18 @@ export function PromptHistoryPage() {
|
||||
} = usePromptHistory({ filter });
|
||||
|
||||
const { data: insightsData, isLoading: insightsLoading } = usePromptInsights();
|
||||
const { data: insightsHistoryData, isLoading: insightsHistoryLoading } = useInsightsHistory({ limit: 20 });
|
||||
|
||||
const { analyzePrompts, deletePrompt, isAnalyzing } = usePromptHistoryMutations();
|
||||
const { analyzePrompts, deletePrompt, batchDeletePrompts, isAnalyzing, isBatchDeleting } = usePromptHistoryMutations();
|
||||
const { deleteInsight: deleteInsightMutation, isDeleting: isDeletingInsight } = useDeleteInsight();
|
||||
|
||||
const isMutating = isAnalyzing;
|
||||
const isMutating = isAnalyzing || isBatchDeleting;
|
||||
|
||||
// Extract unique projects from all prompts
|
||||
const uniqueProjects = React.useMemo(
|
||||
() => extractUniqueProjects(allPrompts),
|
||||
[allPrompts]
|
||||
);
|
||||
|
||||
// Handlers
|
||||
const handleAnalyze = async () => {
|
||||
@@ -118,6 +144,45 @@ export function PromptHistoryPage() {
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
const handleInsightSelect = async (insightId: string) => {
|
||||
try {
|
||||
const insight = await fetchInsightDetail(insightId);
|
||||
setSelectedInsight(insight);
|
||||
setInsightDetailOpen(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch insight detail:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteInsight = async (insightId: string) => {
|
||||
const locale = useIntl().locale;
|
||||
const confirmMessage = locale === 'zh'
|
||||
? '确定要删除此分析吗?此操作无法撤销。'
|
||||
: 'Are you sure you want to delete this insight? This action cannot be undone.';
|
||||
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteInsightMutation(insightId);
|
||||
setInsightDetailOpen(false);
|
||||
setSelectedInsight(null);
|
||||
// Show success toast
|
||||
const successMessage = locale === 'zh' ? '洞察已删除' : 'Insight deleted';
|
||||
if (window.showToast) {
|
||||
window.showToast(successMessage, 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete insight:', err);
|
||||
// Show error toast
|
||||
const errorMessage = locale === 'zh' ? '删除洞察失败' : 'Failed to delete insight';
|
||||
if (window.showToast) {
|
||||
window.showToast(errorMessage, 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleIntentFilter = (intent: string) => {
|
||||
setIntentFilter((prev) => (prev === intent ? 'all' : intent));
|
||||
};
|
||||
@@ -125,9 +190,53 @@ export function PromptHistoryPage() {
|
||||
const clearFilters = () => {
|
||||
setSearchQuery('');
|
||||
setIntentFilter('all');
|
||||
setProjectFilter('all');
|
||||
};
|
||||
|
||||
const hasActiveFilters = searchQuery.length > 0 || intentFilter !== 'all';
|
||||
// Batch operations handlers
|
||||
const handleSelectPrompt = (promptId: string, selected: boolean) => {
|
||||
setSelectedPromptIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (selected) {
|
||||
next.add(promptId);
|
||||
} else {
|
||||
next.delete(promptId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = (selected: boolean) => {
|
||||
if (selected) {
|
||||
setSelectedPromptIds(new Set(prompts.map((p) => p.id)));
|
||||
} else {
|
||||
setSelectedPromptIds(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearSelection = () => {
|
||||
setSelectedPromptIds(new Set());
|
||||
};
|
||||
|
||||
const handleBatchDeleteClick = () => {
|
||||
if (selectedPromptIds.size > 0) {
|
||||
setBatchDeleteDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmBatchDelete = async () => {
|
||||
if (selectedPromptIds.size === 0) return;
|
||||
|
||||
try {
|
||||
await batchDeletePrompts(Array.from(selectedPromptIds));
|
||||
setBatchDeleteDialogOpen(false);
|
||||
setSelectedPromptIds(new Set());
|
||||
} catch (err) {
|
||||
console.error('Failed to batch delete prompts:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const hasActiveFilters = searchQuery.length > 0 || intentFilter !== 'all' || projectFilter !== 'all';
|
||||
|
||||
// Group prompts for timeline view
|
||||
const timelineGroups = React.useMemo(() => {
|
||||
@@ -247,9 +356,9 @@ export function PromptHistoryPage() {
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Filter className="h-4 w-4" />
|
||||
{formatMessage({ id: 'common.actions.filter' })}
|
||||
{intentFilter !== 'all' && (
|
||||
{(intentFilter !== 'all' || projectFilter !== 'all') && (
|
||||
<Badge variant="secondary" className="ml-1 h-5 min-w-5 px-1">
|
||||
{intentFilter}
|
||||
{(intentFilter !== 'all' ? 1 : 0) + (projectFilter !== 'all' ? 1 : 0)}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
@@ -274,6 +383,26 @@ export function PromptHistoryPage() {
|
||||
{intentFilter === intent && <span className="text-primary">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{formatMessage({ id: 'prompts.filterByProject' })}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setProjectFilter('all')}
|
||||
className="justify-between"
|
||||
>
|
||||
<span>{formatMessage({ id: 'prompts.projects.all' })}</span>
|
||||
{projectFilter === 'all' && <span className="text-primary">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
{uniqueProjects.map((project) => (
|
||||
<DropdownMenuItem
|
||||
key={project}
|
||||
onClick={() => setProjectFilter(project)}
|
||||
className="justify-between"
|
||||
>
|
||||
<span className="truncate max-w-32" title={project}>{project}</span>
|
||||
{projectFilter === project && <span className="text-primary">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{hasActiveFilters && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -300,6 +429,16 @@ export function PromptHistoryPage() {
|
||||
<X className="ml-1 h-3 w-3" />
|
||||
</Badge>
|
||||
)}
|
||||
{projectFilter !== 'all' && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="cursor-pointer"
|
||||
onClick={() => setProjectFilter('all')}
|
||||
>
|
||||
{formatMessage({ id: 'prompts.projects.project' })}: {projectFilter}
|
||||
<X className="ml-1 h-3 w-3" />
|
||||
</Badge>
|
||||
)}
|
||||
{searchQuery && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
@@ -316,6 +455,16 @@ export function PromptHistoryPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Batch operations toolbar */}
|
||||
<BatchOperationToolbar
|
||||
selectedCount={selectedPromptIds.size}
|
||||
allSelected={prompts.length > 0 && selectedPromptIds.size === prompts.length}
|
||||
onSelectAll={handleSelectAll}
|
||||
onClearSelection={handleClearSelection}
|
||||
onDelete={handleBatchDeleteClick}
|
||||
isDeleting={isBatchDeleting}
|
||||
/>
|
||||
|
||||
{/* Timeline */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
@@ -366,6 +515,9 @@ export function PromptHistoryPage() {
|
||||
prompt={prompt}
|
||||
onDelete={handleDeleteClick}
|
||||
actionsDisabled={isMutating}
|
||||
selected={selectedPromptIds.has(prompt.id)}
|
||||
onSelectChange={handleSelectPrompt}
|
||||
selectionMode={selectedPromptIds.size > 0 || prompts.some(p => selectedPromptIds.has(p.id))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -376,7 +528,7 @@ export function PromptHistoryPage() {
|
||||
</div>
|
||||
|
||||
{/* Insights panel */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="lg:col-span-1 space-y-4">
|
||||
<InsightsPanel
|
||||
insights={insightsData?.insights}
|
||||
patterns={insightsData?.patterns}
|
||||
@@ -387,6 +539,11 @@ export function PromptHistoryPage() {
|
||||
isAnalyzing={isAnalyzing || insightsLoading}
|
||||
className="sticky top-4"
|
||||
/>
|
||||
<InsightsHistoryList
|
||||
insights={insightsHistoryData?.insights}
|
||||
isLoading={insightsHistoryLoading}
|
||||
onInsightSelect={handleInsightSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -419,6 +576,48 @@ export function PromptHistoryPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Batch Delete Confirmation Dialog */}
|
||||
<Dialog open={batchDeleteDialogOpen} onOpenChange={setBatchDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formatMessage({ id: 'prompts.dialog.batchDeleteTitle' })}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{formatMessage({ id: 'prompts.dialog.batchDeleteConfirm' }, { count: selectedPromptIds.size })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setBatchDeleteDialogOpen(false);
|
||||
}}
|
||||
disabled={isBatchDeleting}
|
||||
>
|
||||
{formatMessage({ id: 'common.actions.cancel' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleConfirmBatchDelete}
|
||||
disabled={isBatchDeleting}
|
||||
>
|
||||
{isBatchDeleting ? formatMessage({ id: 'common.actions.deleting' }, { defaultValue: 'Deleting...' }) : formatMessage({ id: 'common.actions.delete' })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Insight Detail Panel Overlay */}
|
||||
<InsightDetailPanelOverlay
|
||||
insight={selectedInsight}
|
||||
onClose={() => {
|
||||
setInsightDetailOpen(false);
|
||||
setSelectedInsight(null);
|
||||
}}
|
||||
onDelete={handleDeleteInsight}
|
||||
isDeleting={isDeletingInsight}
|
||||
showOverlay={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user