mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
Add comprehensive tests for ast-grep and tree-sitter relationship extraction
- Introduced test suite for AstGrepPythonProcessor covering pattern definitions, parsing, and relationship extraction. - Added comparison tests between tree-sitter and ast-grep for consistency in relationship extraction. - Implemented tests for ast-grep binding module to verify functionality and availability. - Ensured tests cover various scenarios including inheritance, function calls, and imports.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// Memory Page
|
||||
// ========================================
|
||||
// View and manage core memory and context with CRUD operations
|
||||
// Includes unified vector search across all memory categories
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -22,6 +23,11 @@ import {
|
||||
Archive,
|
||||
ArchiveRestore,
|
||||
AlertCircle,
|
||||
Layers,
|
||||
Zap,
|
||||
Terminal,
|
||||
GitBranch,
|
||||
Hash,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -30,9 +36,39 @@ import { Badge } from '@/components/ui/Badge';
|
||||
import { TabsNavigation } from '@/components/ui/TabsNavigation';
|
||||
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';
|
||||
import { useMemory, useMemoryMutations, useUnifiedSearch, useUnifiedStats, useRecommendations, useReindex } from '@/hooks';
|
||||
import type { CoreMemory, UnifiedSearchResult } from '@/lib/api';
|
||||
import { cn, parseMemoryMetadata } from '@/lib/utils';
|
||||
|
||||
// ========== Source Type Helpers ==========
|
||||
|
||||
const SOURCE_TYPE_COLORS: Record<string, string> = {
|
||||
core_memory: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
cli_history: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
|
||||
workflow: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
entity: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
pattern: 'bg-pink-100 text-pink-800 dark:bg-pink-900/30 dark:text-pink-300',
|
||||
};
|
||||
|
||||
const SOURCE_TYPE_ICONS: Record<string, React.ReactNode> = {
|
||||
core_memory: <Brain className="w-3 h-3" />,
|
||||
cli_history: <Terminal className="w-3 h-3" />,
|
||||
workflow: <GitBranch className="w-3 h-3" />,
|
||||
entity: <Hash className="w-3 h-3" />,
|
||||
pattern: <Layers className="w-3 h-3" />,
|
||||
};
|
||||
|
||||
function SourceTypeBadge({ sourceType }: { sourceType: string }) {
|
||||
const colorClass = SOURCE_TYPE_COLORS[sourceType] || 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300';
|
||||
const icon = SOURCE_TYPE_ICONS[sourceType] || <Database className="w-3 h-3" />;
|
||||
|
||||
return (
|
||||
<span className={cn('inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium', colorClass)}>
|
||||
{icon}
|
||||
{sourceType}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Memory Card Component ==========
|
||||
|
||||
@@ -51,7 +87,7 @@ function MemoryCard({ memory, onView, onEdit, onDelete, onCopy, onToggleFavorite
|
||||
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 metadata = parseMemoryMetadata(memory.metadata);
|
||||
const isFavorite = metadata.favorite === true;
|
||||
const priority = metadata.priority || 'medium';
|
||||
const isArchived = memory.archived || false;
|
||||
@@ -197,6 +233,138 @@ function MemoryCard({ memory, onView, onEdit, onDelete, onCopy, onToggleFavorite
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Unified Search Result Card ==========
|
||||
|
||||
interface UnifiedResultCardProps {
|
||||
result: UnifiedSearchResult;
|
||||
onCopy: (content: string) => void;
|
||||
}
|
||||
|
||||
function UnifiedResultCard({ result, onCopy }: UnifiedResultCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const scorePercent = (result.score * 100).toFixed(1);
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-3 min-w-0 flex-1">
|
||||
<SourceTypeBadge sourceType={result.source_type} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium text-foreground truncate">
|
||||
{result.source_id}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs shrink-0">
|
||||
{formatMessage({ id: 'memory.unified.score' })}: {scorePercent}%
|
||||
</Badge>
|
||||
</div>
|
||||
{/* Rank sources */}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{result.rank_sources.vector_rank != null && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'memory.unified.vectorRank' }, { rank: result.rank_sources.vector_rank })}
|
||||
</span>
|
||||
)}
|
||||
{result.rank_sources.fts_rank != null && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'memory.unified.ftsRank' }, { rank: result.rank_sources.fts_rank })}
|
||||
</span>
|
||||
)}
|
||||
{result.rank_sources.heat_score != null && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'memory.unified.heatScore' }, { score: result.rank_sources.heat_score.toFixed(2) })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 shrink-0"
|
||||
onClick={() => onCopy(result.content)}
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content preview */}
|
||||
<p className="text-sm text-muted-foreground mt-2 line-clamp-3">
|
||||
{result.content}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Recommendations Panel ==========
|
||||
|
||||
interface RecommendationsPanelProps {
|
||||
memoryId: string;
|
||||
onCopy: (content: string) => void;
|
||||
}
|
||||
|
||||
function RecommendationsPanel({ memoryId, onCopy }: RecommendationsPanelProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { recommendations, isLoading } = useRecommendations({
|
||||
memoryId,
|
||||
limit: 5,
|
||||
enabled: !!memoryId,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-muted-foreground py-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span className="text-sm">{formatMessage({ id: 'memory.unified.searching' })}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
{formatMessage({ id: 'memory.unified.noRecommendations' })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{recommendations.map((rec) => (
|
||||
<div
|
||||
key={rec.source_id}
|
||||
className="flex items-start gap-2 p-2 rounded-md bg-muted/30 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<SourceTypeBadge sourceType={rec.source_type} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-foreground truncate">
|
||||
{rec.source_id}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{(rec.score * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">
|
||||
{rec.content}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 shrink-0"
|
||||
onClick={() => onCopy(rec.content)}
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== View Memory Dialog ==========
|
||||
|
||||
interface ViewMemoryDialogProps {
|
||||
@@ -211,7 +379,7 @@ function ViewMemoryDialog({ memory, open, onOpenChange, onEdit, onCopy }: ViewMe
|
||||
const { formatMessage } = useIntl();
|
||||
if (!memory) return null;
|
||||
|
||||
const metadata = memory.metadata ? (typeof memory.metadata === 'string' ? JSON.parse(memory.metadata) : memory.metadata) : {};
|
||||
const metadata = parseMemoryMetadata(memory.metadata);
|
||||
const priority = metadata.priority || 'medium';
|
||||
const formattedDate = new Date(memory.createdAt).toLocaleDateString();
|
||||
const formattedSize = memory.size
|
||||
@@ -264,6 +432,15 @@ function ViewMemoryDialog({ memory, open, onOpenChange, onEdit, onCopy }: ViewMe
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Recommendations */}
|
||||
<div className="pt-2 border-t border-border">
|
||||
<h4 className="text-sm font-medium text-foreground flex items-center gap-1.5 mb-2">
|
||||
<Zap className="w-4 h-4 text-primary" />
|
||||
{formatMessage({ id: 'memory.unified.recommendations' })}
|
||||
</h4>
|
||||
<RecommendationsPanel memoryId={memory.id} onCopy={onCopy} />
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-border">
|
||||
<Button variant="outline" size="sm" onClick={() => onCopy(memory.content)}>
|
||||
@@ -311,21 +488,9 @@ function NewMemoryDialog({
|
||||
setTagsInput(editingMemory.tags?.join(', ') || '');
|
||||
|
||||
// Sync metadata
|
||||
if (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');
|
||||
}
|
||||
const metadata = parseMemoryMetadata(editingMemory.metadata);
|
||||
setIsFavorite(metadata.favorite === true);
|
||||
setPriority(metadata.priority || 'medium');
|
||||
} else {
|
||||
// New mode: reset all state
|
||||
setContent('');
|
||||
@@ -436,6 +601,17 @@ function NewMemoryDialog({
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Category Filter ==========
|
||||
|
||||
const CATEGORY_OPTIONS = [
|
||||
{ value: '', labelId: 'memory.filters.categoryAll' },
|
||||
{ value: 'core_memory', labelId: 'memory.filters.categoryCoreMemory' },
|
||||
{ value: 'cli_history', labelId: 'memory.filters.categoryCliHistory' },
|
||||
{ value: 'workflow', labelId: 'memory.filters.categoryWorkflow' },
|
||||
{ value: 'entity', labelId: 'memory.filters.categoryEntity' },
|
||||
{ value: 'pattern', labelId: 'memory.filters.categoryPattern' },
|
||||
];
|
||||
|
||||
// ========== Main Page Component ==========
|
||||
|
||||
export function MemoryPage() {
|
||||
@@ -445,9 +621,13 @@ export function MemoryPage() {
|
||||
const [isNewMemoryOpen, setIsNewMemoryOpen] = useState(false);
|
||||
const [editingMemory, setEditingMemory] = useState<CoreMemory | null>(null);
|
||||
const [viewingMemory, setViewingMemory] = useState<CoreMemory | null>(null);
|
||||
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived'>('memories');
|
||||
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived' | 'unifiedSearch'>('memories');
|
||||
const [unifiedQuery, setUnifiedQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('');
|
||||
|
||||
// Build filter based on current tab
|
||||
const isUnifiedTab = currentTab === 'unifiedSearch';
|
||||
|
||||
// Build filter based on current tab (for non-unified tabs)
|
||||
const favoriteFilter = currentTab === 'favorites' ? { favorite: true } : undefined;
|
||||
const archivedFilter = currentTab === 'archived' ? { archived: true } : { archived: false };
|
||||
|
||||
@@ -467,8 +647,34 @@ export function MemoryPage() {
|
||||
...favoriteFilter,
|
||||
...archivedFilter,
|
||||
},
|
||||
enabled: !isUnifiedTab,
|
||||
});
|
||||
|
||||
// Unified search
|
||||
const {
|
||||
results: unifiedResults,
|
||||
total: unifiedTotal,
|
||||
isLoading: unifiedLoading,
|
||||
isFetching: unifiedFetching,
|
||||
error: unifiedError,
|
||||
refetch: refetchUnified,
|
||||
} = useUnifiedSearch({
|
||||
query: unifiedQuery,
|
||||
categories: selectedCategory || undefined,
|
||||
topK: 20,
|
||||
enabled: isUnifiedTab && unifiedQuery.trim().length > 0,
|
||||
});
|
||||
|
||||
// Unified stats
|
||||
const {
|
||||
stats: unifiedStats,
|
||||
isLoading: statsLoading,
|
||||
refetch: refetchStats,
|
||||
} = useUnifiedStats();
|
||||
|
||||
// Reindex mutation
|
||||
const { reindex, isReindexing } = useReindex();
|
||||
|
||||
const { createMemory, updateMemory, deleteMemory, archiveMemory, unarchiveMemory, isCreating, isUpdating } =
|
||||
useMemoryMutations();
|
||||
|
||||
@@ -495,9 +701,7 @@ export function MemoryPage() {
|
||||
|
||||
const handleToggleFavorite = async (memory: CoreMemory) => {
|
||||
try {
|
||||
const currentMetadata = memory.metadata
|
||||
? (typeof memory.metadata === 'string' ? JSON.parse(memory.metadata) : memory.metadata)
|
||||
: {};
|
||||
const currentMetadata = parseMemoryMetadata(memory.metadata);
|
||||
const newFavorite = !(currentMetadata.favorite === true);
|
||||
await updateMemory(memory.id, {
|
||||
content: memory.content,
|
||||
@@ -544,6 +748,17 @@ export function MemoryPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReindex = async () => {
|
||||
try {
|
||||
await reindex();
|
||||
toast.success(formatMessage({ id: 'memory.unified.reindexSuccess' }));
|
||||
refetchStats();
|
||||
} catch (err) {
|
||||
console.error('Failed to reindex:', err);
|
||||
toast.error(formatMessage({ id: 'memory.unified.reindexError' }));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
setSelectedTags((prev) =>
|
||||
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
|
||||
@@ -556,6 +771,18 @@ export function MemoryPage() {
|
||||
? `${(totalSize / 1024).toFixed(1)} KB`
|
||||
: `${(totalSize / (1024 * 1024)).toFixed(1)} MB`;
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (isUnifiedTab) {
|
||||
refetchUnified();
|
||||
refetchStats();
|
||||
} else {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const isRefreshing = isUnifiedTab ? unifiedFetching : isFetching;
|
||||
const activeError = isUnifiedTab ? unifiedError : error;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
@@ -570,21 +797,37 @@ export function MemoryPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
|
||||
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
||||
{isUnifiedTab && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReindex}
|
||||
disabled={isReindexing}
|
||||
>
|
||||
{isReindexing ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{formatMessage({ id: isReindexing ? 'memory.unified.reindexing' : 'memory.unified.reindex' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={handleRefresh} disabled={isRefreshing}>
|
||||
<RefreshCw className={cn('w-4 h-4 mr-2', isRefreshing && 'animate-spin')} />
|
||||
{formatMessage({ id: 'common.actions.refresh' })}
|
||||
</Button>
|
||||
<Button onClick={() => { setEditingMemory(null); setIsNewMemoryOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.actions.add' })}
|
||||
</Button>
|
||||
{!isUnifiedTab && (
|
||||
<Button onClick={() => { setEditingMemory(null); setIsNewMemoryOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.actions.add' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation - styled like LiteTasksPage */}
|
||||
{/* Tab Navigation */}
|
||||
<TabsNavigation
|
||||
value={currentTab}
|
||||
onValueChange={(v) => setCurrentTab(v as 'memories' | 'favorites' | 'archived')}
|
||||
onValueChange={(v) => setCurrentTab(v as typeof currentTab)}
|
||||
tabs={[
|
||||
{
|
||||
value: 'memories',
|
||||
@@ -601,141 +844,285 @@ export function MemoryPage() {
|
||||
label: formatMessage({ id: 'memory.tabs.archived' }),
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
value: 'unifiedSearch',
|
||||
label: formatMessage({ id: 'memory.tabs.unifiedSearch' }),
|
||||
icon: <Search className="h-4 w-4" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Error alert */}
|
||||
{error && (
|
||||
{activeError && (
|
||||
<div className="flex items-center gap-2 p-4 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive">
|
||||
<AlertCircle className="h-5 w-5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{formatMessage({ id: 'common.errors.loadFailed' })}</p>
|
||||
<p className="text-xs mt-0.5">{error.message}</p>
|
||||
<p className="text-xs mt-0.5">{activeError.message}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||
{formatMessage({ id: 'home.errors.retry' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Database className="w-5 h-5 text-primary" />
|
||||
{isUnifiedTab ? (
|
||||
/* Unified Stats Cards */
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Database className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{statsLoading ? '-' : (unifiedStats?.core_memories.total ?? 0)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.count' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">{memories.length}</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.count' })}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-orange-500/10">
|
||||
<Hash className="w-5 h-5 text-orange-500" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{statsLoading ? '-' : (unifiedStats?.entities ?? 0)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.entities' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-info/10">
|
||||
<FileText className="w-5 h-5 text-info" />
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||
<Layers className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{statsLoading ? '-' : (unifiedStats?.vector_index.total_chunks ?? 0)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.vectorChunks' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">{claudeMdCount}</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.claudeMdCount' })}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"p-2 rounded-lg",
|
||||
unifiedStats?.vector_index.hnsw_available ? "bg-green-500/10" : "bg-muted"
|
||||
)}>
|
||||
<Zap className={cn(
|
||||
"w-5 h-5",
|
||||
unifiedStats?.vector_index.hnsw_available ? "text-green-500" : "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{statsLoading ? '-' : (unifiedStats?.vector_index.hnsw_available ? unifiedStats.vector_index.hnsw_count : 'N/A')}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.hnswStatus' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-success/10">
|
||||
<Brain className="w-5 h-5 text-success" />
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
/* Standard Stats Cards */
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Database className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">{memories.length}</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.count' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">{formattedTotalSize}</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.totalSize' })}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-info/10">
|
||||
<FileText className="w-5 h-5 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">{claudeMdCount}</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.claudeMdCount' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-success/10">
|
||||
<Brain className="w-5 h-5 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-foreground">{formattedTotalSize}</div>
|
||||
<p className="text-sm text-muted-foreground">{formatMessage({ id: 'memory.stats.totalSize' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={formatMessage({ id: 'memory.filters.search' })}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags Filter */}
|
||||
{allTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="text-sm text-muted-foreground py-1">{formatMessage({ id: 'memory.card.tags' })}:</span>
|
||||
{allTags.map((tag) => (
|
||||
<Button
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={() => toggleTag(tag)}
|
||||
>
|
||||
<Tag className="w-3 h-3 mr-1" />
|
||||
{tag}
|
||||
</Button>
|
||||
))}
|
||||
{selectedTags.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={() => setSelectedTags([])}
|
||||
>
|
||||
{formatMessage({ id: 'memory.filters.clear' })}
|
||||
</Button>
|
||||
)}
|
||||
{isUnifiedTab ? (
|
||||
/* Unified Search Input + Category Filter */
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={formatMessage({ id: 'memory.filters.searchUnified' })}
|
||||
value={unifiedQuery}
|
||||
onChange={(e) => setUnifiedQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="px-3 py-2 bg-background border border-input rounded-md text-sm min-w-[160px]"
|
||||
>
|
||||
{CATEGORY_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{formatMessage({ id: opt.labelId })}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Memory List */}
|
||||
{isLoading ? (
|
||||
<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" />
|
||||
))}
|
||||
{unifiedQuery.trim().length > 0 && !unifiedLoading && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'memory.unified.resultCount' }, { count: unifiedTotal })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : memories.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Brain className="w-12 h-12 mx-auto text-muted-foreground/50" />
|
||||
<h3 className="mt-4 text-lg font-medium text-foreground">
|
||||
{formatMessage({ id: 'memory.emptyState.title' })}
|
||||
</h3>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{formatMessage({ id: 'memory.emptyState.message' })}
|
||||
</p>
|
||||
<Button className="mt-4" onClick={() => { setEditingMemory(null); setIsNewMemoryOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.emptyState.createFirst' })}
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{memories.map((memory) => (
|
||||
<MemoryCard
|
||||
key={memory.id}
|
||||
memory={memory}
|
||||
onView={setViewingMemory}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onCopy={copyToClipboard}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onArchive={handleArchive}
|
||||
onUnarchive={handleUnarchive}
|
||||
/* Standard Search + Tag Filters */
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={formatMessage({ id: 'memory.filters.search' })}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tags Filter */}
|
||||
{allTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="text-sm text-muted-foreground py-1">{formatMessage({ id: 'memory.card.tags' })}:</span>
|
||||
{allTags.map((tag) => (
|
||||
<Button
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={() => toggleTag(tag)}
|
||||
>
|
||||
<Tag className="w-3 h-3 mr-1" />
|
||||
{tag}
|
||||
</Button>
|
||||
))}
|
||||
{selectedTags.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={() => setSelectedTags([])}
|
||||
>
|
||||
{formatMessage({ id: 'memory.filters.clear' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Area */}
|
||||
{isUnifiedTab ? (
|
||||
/* Unified Search Results */
|
||||
unifiedLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-primary mr-2" />
|
||||
<span className="text-muted-foreground">
|
||||
{formatMessage({ id: 'memory.unified.searching' })}
|
||||
</span>
|
||||
</div>
|
||||
) : unifiedQuery.trim().length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Search className="w-12 h-12 mx-auto text-muted-foreground/50" />
|
||||
<h3 className="mt-4 text-lg font-medium text-foreground">
|
||||
{formatMessage({ id: 'memory.tabs.unifiedSearch' })}
|
||||
</h3>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{formatMessage({ id: 'memory.filters.searchUnified' })}
|
||||
</p>
|
||||
</Card>
|
||||
) : unifiedResults.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Search className="w-12 h-12 mx-auto text-muted-foreground/50" />
|
||||
<h3 className="mt-4 text-lg font-medium text-foreground">
|
||||
{formatMessage({ id: 'memory.unified.noResults' })}
|
||||
</h3>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{unifiedResults.map((result) => (
|
||||
<UnifiedResultCard
|
||||
key={`${result.source_type}-${result.source_id}`}
|
||||
result={result}
|
||||
onCopy={copyToClipboard}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
/* Standard Memory List */
|
||||
isLoading ? (
|
||||
<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 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Brain className="w-12 h-12 mx-auto text-muted-foreground/50" />
|
||||
<h3 className="mt-4 text-lg font-medium text-foreground">
|
||||
{formatMessage({ id: 'memory.emptyState.title' })}
|
||||
</h3>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{formatMessage({ id: 'memory.emptyState.message' })}
|
||||
</p>
|
||||
<Button className="mt-4" onClick={() => { setEditingMemory(null); setIsNewMemoryOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'memory.emptyState.createFirst' })}
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{memories.map((memory) => (
|
||||
<MemoryCard
|
||||
key={memory.id}
|
||||
memory={memory}
|
||||
onView={setViewingMemory}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onCopy={copyToClipboard}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onArchive={handleArchive}
|
||||
onUnarchive={handleUnarchive}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* View Memory Dialog */}
|
||||
<ViewMemoryDialog
|
||||
memory={viewingMemory}
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
useCcwInstallations,
|
||||
useUpgradeCcwInstallation,
|
||||
} from '@/hooks/useSystemSettings';
|
||||
import { RemoteNotificationSection } from '@/components/settings/RemoteNotificationSection';
|
||||
|
||||
// ========== File Path Input with Native File Picker ==========
|
||||
|
||||
@@ -1299,6 +1300,9 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Remote Notifications */}
|
||||
<RemoteNotificationSection />
|
||||
|
||||
{/* Reset Settings */}
|
||||
<Card className="p-6 border-destructive/50">
|
||||
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
|
||||
|
||||
Reference in New Issue
Block a user