Enhanced code review and frontend validation with intelligent triggers: Code Review Agent Enhancement: - Added Sequential Thinking MCP integration for complex issues - Triggers on 2+ rejections or 3+ critical issues - New escalation format with root cause analysis - Comprehensive solution strategies with trade-off evaluation - Educational feedback to break rejection cycles - Files: .claude/agents/code-review.md (+308 lines) - Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md Frontend Design Skill Enhancement: - Automatic invocation for ANY UI change - Comprehensive validation checklist (200+ checkpoints) - 8 validation categories (visual, interactive, responsive, a11y, etc.) - 3 validation levels (quick, standard, comprehensive) - Integration with code review workflow - Files: .claude/skills/frontend-design/SKILL.md (+120 lines) - Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines) Settings Optimization: - Repaired .claude/settings.local.json (fixed m365 pattern) - Reduced permissions from 49 to 33 (33% reduction) - Removed duplicates, sorted alphabetically - Created SETTINGS_PERMISSIONS.md documentation Checkpoint Command Enhancement: - Dual checkpoint system (git + database) - Saves session context to API for cross-machine recall - Includes git metadata in database context - Files: .claude/commands/checkpoint.md (+139 lines) Decision Rationale: - Sequential Thinking MCP breaks rejection cycles by identifying root causes - Automatic frontend validation catches UI issues before code review - Dual checkpoints enable complete project memory across machines - Settings optimization improves maintainability Total: 1,200+ lines of documentation and enhancements Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
713 lines
32 KiB
Plaintext
713 lines
32 KiB
Plaintext
1→import { useState, useEffect } from 'react'
|
|
2→import { Brain, Globe, Server, Plus, Search, Trash2, X, Pencil } from 'lucide-react'
|
|
3→import * as Dialog from '@radix-ui/react-dialog'
|
|
4→import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
|
5→import { useProjectStore } from '../stores/projectStore'
|
|
6→
|
|
7→type MemoryTab = 'project' | 'global' | 'infrastructure'
|
|
8→
|
|
9→interface ProjectMemoryItem {
|
|
10→ id: number
|
|
11→ project_id: number
|
|
12→ type: string
|
|
13→ content: string
|
|
14→ metadata: Record<string, unknown> | null
|
|
15→ created_at: string
|
|
16→}
|
|
17→
|
|
18→interface GlobalMemoryItem {
|
|
19→ id: number
|
|
20→ type: string
|
|
21→ content: string
|
|
22→ metadata: Record<string, unknown> | null
|
|
23→ usage_count: number
|
|
24→ created_at: string
|
|
25→}
|
|
26→
|
|
27→interface InfrastructureItem {
|
|
28→ id: number
|
|
29→ name: string
|
|
30→ type: string
|
|
31→ config: Record<string, unknown>
|
|
32→ projects: number[] | null
|
|
33→ created_at: string
|
|
34→ updated_at: string
|
|
35→}
|
|
36→
|
|
37→const API_BASE = 'http://localhost:8000/api/memory'
|
|
38→
|
|
39→export default function MemoryManager() {
|
|
40→ const { currentProject } = useProjectStore()
|
|
41→ const [activeTab, setActiveTab] = useState<MemoryTab>('project')
|
|
42→ const [searchQuery, setSearchQuery] = useState('')
|
|
43→
|
|
44→ // Data states
|
|
45→ const [projectMemory, setProjectMemory] = useState<ProjectMemoryItem[]>([])
|
|
46→ const [globalMemory, setGlobalMemory] = useState<GlobalMemoryItem[]>([])
|
|
47→ const [infrastructure, setInfrastructure] = useState<InfrastructureItem[]>([])
|
|
48→ const [isLoading, setIsLoading] = useState(false)
|
|
49→
|
|
50→ // Dialog states
|
|
51→ const [showAddDialog, setShowAddDialog] = useState(false)
|
|
52→ const [newItemType, setNewItemType] = useState('')
|
|
53→ const [newItemContent, setNewItemContent] = useState('')
|
|
54→ const [newItemName, setNewItemName] = useState('')
|
|
55→ const [isSaving, setIsSaving] = useState(false)
|
|
56→
|
|
57→ // Delete dialog states
|
|
58→ const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
|
59→ const [itemToDelete, setItemToDelete] = useState<{ id: number; type: MemoryTab } | null>(null)
|
|
60→ const [isDeleting, setIsDeleting] = useState(false)
|
|
61→
|
|
62→ // Edit dialog states
|
|
63→ const [showEditDialog, setShowEditDialog] = useState(false)
|
|
64→ const [editItem, setEditItem] = useState<{ id: number; type: string; content: string; memoryType: MemoryTab } | null>(null)
|
|
65→ const [editItemType, setEditItemType] = useState('')
|
|
66→ const [editItemContent, setEditItemContent] = useState('')
|
|
67→ const [isEditing, setIsEditing] = useState(false)
|
|
68→
|
|
69→ const tabs = [
|
|
70→ { id: 'project' as const, label: 'Project Memory', icon: Brain },
|
|
71→ { id: 'global' as const, label: 'Global Memory', icon: Globe },
|
|
72→ { id: 'infrastructure' as const, label: 'Infrastructure', icon: Server },
|
|
73→ ]
|
|
74→
|
|
75→ // Fetch data based on active tab
|
|
76→ useEffect(() => {
|
|
77→ const fetchData = async () => {
|
|
78→ setIsLoading(true)
|
|
79→ try {
|
|
80→ if (activeTab === 'project' && currentProject) {
|
|
81→ const response = await fetch(`${API_BASE}/project/${currentProject.id}`)
|
|
82→ if (response.ok) {
|
|
83→ const data = await response.json()
|
|
84→ setProjectMemory(data)
|
|
85→ }
|
|
86→ } else if (activeTab === 'global') {
|
|
87→ const response = await fetch(`${API_BASE}/global`)
|
|
88→ if (response.ok) {
|
|
89→ const data = await response.json()
|
|
90→ setGlobalMemory(data)
|
|
91→ }
|
|
92→ } else if (activeTab === 'infrastructure') {
|
|
93→ const response = await fetch(`${API_BASE}/infrastructure`)
|
|
94→ if (response.ok) {
|
|
95→ const data = await response.json()
|
|
96→ setInfrastructure(data)
|
|
97→ }
|
|
98→ }
|
|
99→ } catch (error) {
|
|
100→ console.error('Failed to fetch memory items:', error)
|
|
101→ } finally {
|
|
102→ setIsLoading(false)
|
|
103→ }
|
|
104→ }
|
|
105→ fetchData()
|
|
106→ }, [activeTab, currentProject])
|
|
107→
|
|
108→ const getTypeOptions = () => {
|
|
109→ switch (activeTab) {
|
|
110→ case 'project':
|
|
111→ return ['insight', 'pattern', 'gotcha', 'codebase_map']
|
|
112→ case 'global':
|
|
113→ return ['preference', 'pattern', 'solution']
|
|
114→ case 'infrastructure':
|
|
115→ return ['server', 'docker', 'database', 'deployment']
|
|
116→ default:
|
|
117→ return []
|
|
118→ }
|
|
119→ }
|
|
120→
|
|
121→ const handleOpenAddDialog = () => {
|
|
122→ setNewItemType(getTypeOptions()[0] || '')
|
|
123→ setNewItemContent('')
|
|
124→ setNewItemName('')
|
|
125→ setShowAddDialog(true)
|
|
126→ }
|
|
127→
|
|
128→ const handleAddItem = async () => {
|
|
129→ if (!newItemType || (!newItemContent.trim() && activeTab !== 'infrastructure')) return
|
|
130→ if (activeTab === 'infrastructure' && !newItemName.trim()) return
|
|
131→
|
|
132→ setIsSaving(true)
|
|
133→ try {
|
|
134→ let response: Response
|
|
135→
|
|
136→ if (activeTab === 'project' && currentProject) {
|
|
137→ response = await fetch(`${API_BASE}/project/${currentProject.id}`, {
|
|
138→ method: 'POST',
|
|
139→ headers: { 'Content-Type': 'application/json' },
|
|
140→ body: JSON.stringify({ type: newItemType, content: newItemContent.trim() }),
|
|
141→ })
|
|
142→ if (response.ok) {
|
|
143→ const newItem = await response.json()
|
|
144→ setProjectMemory([...projectMemory, newItem])
|
|
145→ }
|
|
146→ } else if (activeTab === 'global') {
|
|
147→ response = await fetch(`${API_BASE}/global`, {
|
|
148→ method: 'POST',
|
|
149→ headers: { 'Content-Type': 'application/json' },
|
|
150→ body: JSON.stringify({ type: newItemType, content: newItemContent.trim() }),
|
|
151→ })
|
|
152→ if (response.ok) {
|
|
153→ const newItem = await response.json()
|
|
154→ setGlobalMemory([...globalMemory, newItem])
|
|
155→ }
|
|
156→ } else if (activeTab === 'infrastructure') {
|
|
157→ response = await fetch(`${API_BASE}/infrastructure`, {
|
|
158→ method: 'POST',
|
|
159→ headers: { 'Content-Type': 'application/json' },
|
|
160→ body: JSON.stringify({
|
|
161→ name: newItemName.trim(),
|
|
162→ type: newItemType,
|
|
163→ config: { content: newItemContent.trim() }
|
|
164→ }),
|
|
165→ })
|
|
166→ if (response.ok) {
|
|
167→ const newItem = await response.json()
|
|
168→ setInfrastructure([...infrastructure, newItem])
|
|
169→ }
|
|
170→ }
|
|
171→
|
|
172→ setShowAddDialog(false)
|
|
173→ } catch (error) {
|
|
174→ console.error('Failed to add memory item:', error)
|
|
175→ } finally {
|
|
176→ setIsSaving(false)
|
|
177→ }
|
|
178→ }
|
|
179→
|
|
180→ const handleDeleteClick = (id: number, type: MemoryTab) => {
|
|
181→ setItemToDelete({ id, type })
|
|
182→ setShowDeleteDialog(true)
|
|
183→ }
|
|
184→
|
|
185→ const handleDeleteConfirm = async () => {
|
|
186→ if (!itemToDelete) return
|
|
187→
|
|
188→ setIsDeleting(true)
|
|
189→ try {
|
|
190→ let endpoint: string
|
|
191→
|
|
192→ if (itemToDelete.type === 'project' && currentProject) {
|
|
193→ endpoint = `${API_BASE}/project/${currentProject.id}/${itemToDelete.id}`
|
|
194→ } else if (itemToDelete.type === 'global') {
|
|
195→ endpoint = `${API_BASE}/global/${itemToDelete.id}`
|
|
196→ } else {
|
|
197→ endpoint = `${API_BASE}/infrastructure/${itemToDelete.id}`
|
|
198→ }
|
|
199→
|
|
200→ const response = await fetch(endpoint, { method: 'DELETE' })
|
|
201→
|
|
202→ if (response.ok || response.status === 204) {
|
|
203→ if (itemToDelete.type === 'project') {
|
|
204→ setProjectMemory(projectMemory.filter(item => item.id !== itemToDelete.id))
|
|
205→ } else if (itemToDelete.type === 'global') {
|
|
206→ setGlobalMemory(globalMemory.filter(item => item.id !== itemToDelete.id))
|
|
207→ } else {
|
|
208→ setInfrastructure(infrastructure.filter(item => item.id !== itemToDelete.id))
|
|
209→ }
|
|
210→ }
|
|
211→
|
|
212→ setShowDeleteDialog(false)
|
|
213→ setItemToDelete(null)
|
|
214→ } catch (error) {
|
|
215→ console.error('Failed to delete memory item:', error)
|
|
216→ } finally {
|
|
217→ setIsDeleting(false)
|
|
218→ }
|
|
219→ }
|
|
220→
|
|
221→ const handleEditClick = (id: number, type: string, content: string, memoryType: MemoryTab) => {
|
|
222→ setEditItem({ id, type, content, memoryType })
|
|
223→ setEditItemType(type)
|
|
224→ setEditItemContent(content)
|
|
225→ setShowEditDialog(true)
|
|
226→ }
|
|
227→
|
|
228→ const handleEditSave = async () => {
|
|
229→ if (!editItem || !editItemContent.trim()) return
|
|
230→
|
|
231→ setIsEditing(true)
|
|
232→ try {
|
|
233→ let endpoint: string
|
|
234→ let body: Record<string, unknown>
|
|
235→
|
|
236→ if (editItem.memoryType === 'project' && currentProject) {
|
|
237→ endpoint = `${API_BASE}/project/${currentProject.id}/${editItem.id}`
|
|
238→ body = { type: editItemType, content: editItemContent.trim() }
|
|
239→ } else if (editItem.memoryType === 'global') {
|
|
240→ endpoint = `${API_BASE}/global/${editItem.id}`
|
|
241→ body = { type: editItemType, content: editItemContent.trim() }
|
|
242→ } else {
|
|
243→ // Infrastructure uses different update endpoint
|
|
244→ setShowEditDialog(false)
|
|
245→ setEditItem(null)
|
|
246→ return
|
|
247→ }
|
|
248→
|
|
249→ const response = await fetch(endpoint, {
|
|
250→ method: 'PUT',
|
|
251→ headers: { 'Content-Type': 'application/json' },
|
|
252→ body: JSON.stringify(body),
|
|
253→ })
|
|
254→
|
|
255→ if (response.ok) {
|
|
256→ const updatedItem = await response.json()
|
|
257→ if (editItem.memoryType === 'project') {
|
|
258→ setProjectMemory(projectMemory.map(item =>
|
|
259→ item.id === editItem.id ? updatedItem : item
|
|
260→ ))
|
|
261→ } else if (editItem.memoryType === 'global') {
|
|
262→ setGlobalMemory(globalMemory.map(item =>
|
|
263→ item.id === editItem.id ? updatedItem : item
|
|
264→ ))
|
|
265→ }
|
|
266→ }
|
|
267→
|
|
268→ setShowEditDialog(false)
|
|
269→ setEditItem(null)
|
|
270→ } catch (error) {
|
|
271→ console.error('Failed to update memory item:', error)
|
|
272→ } finally {
|
|
273→ setIsEditing(false)
|
|
274→ }
|
|
275→ }
|
|
276→
|
|
277→ const filterItems = <T extends { content?: string; name?: string; type: string }>(items: T[]) => {
|
|
278→ if (!searchQuery) return items
|
|
279→ const query = searchQuery.toLowerCase()
|
|
280→ return items.filter(item =>
|
|
281→ item.content?.toLowerCase().includes(query) ||
|
|
282→ item.name?.toLowerCase().includes(query) ||
|
|
283→ item.type.toLowerCase().includes(query)
|
|
284→ )
|
|
285→ }
|
|
286→
|
|
287→ const filteredProjectMemory = filterItems(projectMemory)
|
|
288→ const filteredGlobalMemory = filterItems(globalMemory)
|
|
289→ const filteredInfrastructure = filterItems(infrastructure)
|
|
290→
|
|
291→ return (
|
|
292→ <div className="h-full flex flex-col">
|
|
293→ {/* Header */}
|
|
294→ <div className="flex items-center justify-between mb-6">
|
|
295→ <h1 className="text-2xl font-bold">Memory Manager</h1>
|
|
296→ <button
|
|
297→ onClick={handleOpenAddDialog}
|
|
298→ className="btn-accent flex items-center gap-2"
|
|
299→ >
|
|
300→ <Plus size={18} />
|
|
301→ Add Memory Item
|
|
302→ </button>
|
|
303→ </div>
|
|
304→
|
|
305→ {/* Tabs */}
|
|
306→ <div className="flex gap-1 mb-6 bg-slate-800 p-1 rounded-lg w-fit">
|
|
307→ {tabs.map((tab) => {
|
|
308→ const Icon = tab.icon
|
|
309→ return (
|
|
310→ <button
|
|
311→ key={tab.id}
|
|
312→ onClick={() => setActiveTab(tab.id)}
|
|
313→ className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors ${
|
|
314→ activeTab === tab.id
|
|
315→ ? 'bg-primary-700 text-white'
|
|
316→ : 'text-slate-400 hover:text-white'
|
|
317→ }`}
|
|
318→ >
|
|
319→ <Icon size={16} />
|
|
320→ {tab.label}
|
|
321→ </button>
|
|
322→ )
|
|
323→ })}
|
|
324→ </div>
|
|
325→
|
|
326→ {/* Search */}
|
|
327→ <div className="relative mb-6">
|
|
328→ <Search
|
|
329→ size={18}
|
|
330→ className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
|
|
331→ />
|
|
332→ <input
|
|
333→ type="text"
|
|
334→ value={searchQuery}
|
|
335→ onChange={(e) => setSearchQuery(e.target.value)}
|
|
336→ placeholder="Search memory items..."
|
|
337→ className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-accent"
|
|
338→ />
|
|
339→ </div>
|
|
340→
|
|
341→ {/* Content */}
|
|
342→ <div className="flex-1 overflow-auto">
|
|
343→ {isLoading ? (
|
|
344→ <div className="text-center py-12 text-slate-400">Loading...</div>
|
|
345→ ) : (
|
|
346→ <>
|
|
347→ {activeTab === 'project' && (
|
|
348→ <div className="grid gap-4">
|
|
349→ {filteredProjectMemory.length === 0 ? (
|
|
350→ <EmptyState
|
|
351→ title="No Project Memory"
|
|
352→ description="Memory items from your current project will appear here."
|
|
353→ />
|
|
354→ ) : (
|
|
355→ filteredProjectMemory.map(item => (
|
|
356→ <MemoryCard
|
|
357→ key={item.id}
|
|
358→ id={item.id}
|
|
359→ type={item.type}
|
|
360→ content={item.content}
|
|
361→ createdAt={item.created_at}
|
|
362→ onDelete={() => handleDeleteClick(item.id, 'project')}
|
|
363→ onEdit={() => handleEditClick(item.id, item.type, item.content, 'project')}
|
|
364→ />
|
|
365→ ))
|
|
366→ )}
|
|
367→ </div>
|
|
368→ )}
|
|
369→
|
|
370→ {activeTab === 'global' && (
|
|
371→ <div className="grid gap-4">
|
|
372→ {filteredGlobalMemory.length === 0 ? (
|
|
373→ <EmptyState
|
|
374→ title="No Global Memory"
|
|
375→ description="Cross-project patterns and preferences will appear here."
|
|
376→ />
|
|
377→ ) : (
|
|
378→ filteredGlobalMemory.map(item => (
|
|
379→ <MemoryCard
|
|
380→ key={item.id}
|
|
381→ id={item.id}
|
|
382→ type={item.type}
|
|
383→ content={item.content}
|
|
384→ createdAt={item.created_at}
|
|
385→ usageCount={item.usage_count}
|
|
386→ onDelete={() => handleDeleteClick(item.id, 'global')}
|
|
387→ onEdit={() => handleEditClick(item.id, item.type, item.content, 'global')}
|
|
388→ />
|
|
389→ ))
|
|
390→ )}
|
|
391→ </div>
|
|
392→ )}
|
|
393→
|
|
394→ {activeTab === 'infrastructure' && (
|
|
395→ <div className="grid gap-4">
|
|
396→ {filteredInfrastructure.length === 0 ? (
|
|
397→ <EmptyState
|
|
398→ title="No Infrastructure Config"
|
|
399→ description="Server and deployment configurations will appear here."
|
|
400→ />
|
|
401→ ) : (
|
|
402→ filteredInfrastructure.map(item => (
|
|
403→ <InfrastructureCard
|
|
404→ key={item.id}
|
|
405→ id={item.id}
|
|
406→ name={item.name}
|
|
407→ type={item.type}
|
|
408→ config={item.config}
|
|
409→ createdAt={item.created_at}
|
|
410→ onDelete={() => handleDeleteClick(item.id, 'infrastructure')}
|
|
411→ />
|
|
412→ ))
|
|
413→ )}
|
|
414→ </div>
|
|
415→ )}
|
|
416→ </>
|
|
417→ )}
|
|
418→ </div>
|
|
419→
|
|
420→ {/* Add Memory Dialog */}
|
|
421→ <Dialog.Root open={showAddDialog} onOpenChange={setShowAddDialog}>
|
|
422→ <Dialog.Portal>
|
|
423→ <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
|
|
424→ <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
|
|
425→ <div className="flex items-center justify-between">
|
|
426→ <Dialog.Title className="text-xl font-semibold text-white">
|
|
427→ Add {activeTab === 'project' ? 'Project' : activeTab === 'global' ? 'Global' : 'Infrastructure'} Memory
|
|
428→ </Dialog.Title>
|
|
429→ <Dialog.Close asChild>
|
|
430→ <button className="p-1 text-slate-400 hover:text-white hover:bg-slate-700 rounded transition-colors" aria-label="Close">
|
|
431→ <X size={20} />
|
|
432→ </button>
|
|
433→ </Dialog.Close>
|
|
434→ </div>
|
|
435→
|
|
436→ <div className="mt-4 space-y-4">
|
|
437→ {activeTab === 'infrastructure' && (
|
|
438→ <div>
|
|
439→ <label className="block text-sm font-medium text-slate-300 mb-1">Name *</label>
|
|
440→ <input
|
|
441→ type="text"
|
|
442→ value={newItemName}
|
|
443→ onChange={(e) => setNewItemName(e.target.value)}
|
|
444→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent"
|
|
445→ placeholder="Enter config name"
|
|
446→ />
|
|
447→ </div>
|
|
448→ )}
|
|
449→
|
|
450→ <div>
|
|
451→ <label className="block text-sm font-medium text-slate-300 mb-1">Type *</label>
|
|
452→ <select
|
|
453→ value={newItemType}
|
|
454→ onChange={(e) => setNewItemType(e.target.value)}
|
|
455→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-accent"
|
|
456→ >
|
|
457→ {getTypeOptions().map(type => (
|
|
458→ <option key={type} value={type}>{type}</option>
|
|
459→ ))}
|
|
460→ </select>
|
|
461→ </div>
|
|
462→
|
|
463→ <div>
|
|
464→ <label className="block text-sm font-medium text-slate-300 mb-1">Content *</label>
|
|
465→ <textarea
|
|
466→ value={newItemContent}
|
|
467→ onChange={(e) => setNewItemContent(e.target.value)}
|
|
468→ rows={4}
|
|
469→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent resize-none"
|
|
470→ placeholder="Enter memory content..."
|
|
471→ />
|
|
472→ </div>
|
|
473→ </div>
|
|
474→
|
|
475→ <div className="mt-6 flex justify-end gap-3">
|
|
476→ <Dialog.Close asChild>
|
|
477→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
|
|
478→ Cancel
|
|
479→ </button>
|
|
480→ </Dialog.Close>
|
|
481→ <button
|
|
482→ onClick={handleAddItem}
|
|
483→ disabled={isSaving || !newItemContent.trim() || (activeTab === 'infrastructure' && !newItemName.trim())}
|
|
484→ className="px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
|
|
485→ >
|
|
486→ {isSaving ? 'Saving...' : 'Add Item'}
|
|
487→ </button>
|
|
488→ </div>
|
|
489→ </Dialog.Content>
|
|
490→ </Dialog.Portal>
|
|
491→ </Dialog.Root>
|
|
492→
|
|
493→ {/* Delete Confirmation Dialog */}
|
|
494→ <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
|
495→ <AlertDialog.Portal>
|
|
496→ <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
|
|
497→ <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
|
|
498→ <AlertDialog.Title className="text-xl font-semibold text-white">
|
|
499→ Delete Memory Item
|
|
500→ </AlertDialog.Title>
|
|
501→ <AlertDialog.Description className="mt-3 text-slate-400">
|
|
502→ Are you sure you want to delete this memory item? This action cannot be undone.
|
|
503→ </AlertDialog.Description>
|
|
504→ <div className="mt-6 flex justify-end gap-3">
|
|
505→ <AlertDialog.Cancel asChild>
|
|
506→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
|
|
507→ Cancel
|
|
508→ </button>
|
|
509→ </AlertDialog.Cancel>
|
|
510→ <AlertDialog.Action asChild>
|
|
511→ <button
|
|
512→ onClick={handleDeleteConfirm}
|
|
513→ disabled={isDeleting}
|
|
514→ className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
|
|
515→ >
|
|
516→ {isDeleting ? 'Deleting...' : 'Delete'}
|
|
517→ </button>
|
|
518→ </AlertDialog.Action>
|
|
519→ </div>
|
|
520→ </AlertDialog.Content>
|
|
521→ </AlertDialog.Portal>
|
|
522→ </AlertDialog.Root>
|
|
523→
|
|
524→ {/* Edit Memory Dialog */}
|
|
525→ <Dialog.Root open={showEditDialog} onOpenChange={setShowEditDialog}>
|
|
526→ <Dialog.Portal>
|
|
527→ <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
|
|
528→ <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
|
|
529→ <div className="flex items-center justify-between">
|
|
530→ <Dialog.Title className="text-xl font-semibold text-white">
|
|
531→ Edit Memory Item
|
|
532→ </Dialog.Title>
|
|
533→ <Dialog.Close asChild>
|
|
534→ <button className="p-1 text-slate-400 hover:text-white hover:bg-slate-700 rounded transition-colors" aria-label="Close">
|
|
535→ <X size={20} />
|
|
536→ </button>
|
|
537→ </Dialog.Close>
|
|
538→ </div>
|
|
539→
|
|
540→ <div className="mt-4 space-y-4">
|
|
541→ <div>
|
|
542→ <label className="block text-sm font-medium text-slate-300 mb-1">Type *</label>
|
|
543→ <select
|
|
544→ value={editItemType}
|
|
545→ onChange={(e) => setEditItemType(e.target.value)}
|
|
546→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-accent"
|
|
547→ >
|
|
548→ {(editItem?.memoryType === 'project'
|
|
549→ ? ['insight', 'pattern', 'gotcha', 'codebase_map']
|
|
550→ : ['preference', 'pattern', 'solution']
|
|
551→ ).map(type => (
|
|
552→ <option key={type} value={type}>{type}</option>
|
|
553→ ))}
|
|
554→ </select>
|
|
555→ </div>
|
|
556→
|
|
557→ <div>
|
|
558→ <label className="block text-sm font-medium text-slate-300 mb-1">Content *</label>
|
|
559→ <textarea
|
|
560→ value={editItemContent}
|
|
561→ onChange={(e) => setEditItemContent(e.target.value)}
|
|
562→ rows={4}
|
|
563→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent resize-none"
|
|
564→ placeholder="Enter memory content..."
|
|
565→ />
|
|
566→ </div>
|
|
567→ </div>
|
|
568→
|
|
569→ <div className="mt-6 flex justify-end gap-3">
|
|
570→ <Dialog.Close asChild>
|
|
571→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
|
|
572→ Cancel
|
|
573→ </button>
|
|
574→ </Dialog.Close>
|
|
575→ <button
|
|
576→ onClick={handleEditSave}
|
|
577→ disabled={isEditing || !editItemContent.trim()}
|
|
578→ className="px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
|
|
579→ >
|
|
580→ {isEditing ? 'Saving...' : 'Save Changes'}
|
|
581→ </button>
|
|
582→ </div>
|
|
583→ </Dialog.Content>
|
|
584→ </Dialog.Portal>
|
|
585→ </Dialog.Root>
|
|
586→ </div>
|
|
587→ )
|
|
588→}
|
|
589→
|
|
590→function MemoryCard({
|
|
591→ id: _id,
|
|
592→ type,
|
|
593→ content,
|
|
594→ createdAt,
|
|
595→ usageCount,
|
|
596→ onDelete,
|
|
597→ onEdit,
|
|
598→}: {
|
|
599→ id: number
|
|
600→ type: string
|
|
601→ content: string
|
|
602→ createdAt: string
|
|
603→ usageCount?: number
|
|
604→ onDelete: () => void
|
|
605→ onEdit?: () => void
|
|
606→}) {
|
|
607→ return (
|
|
608→ <div className="bg-slate-800 rounded-lg border border-slate-700 p-4">
|
|
609→ <div className="flex items-start justify-between">
|
|
610→ <div className="flex-1">
|
|
611→ <div className="flex items-center gap-2 mb-2">
|
|
612→ <span className="px-2 py-0.5 bg-primary-700/50 text-primary-300 text-xs rounded-full">
|
|
613→ {type}
|
|
614→ </span>
|
|
615→ {usageCount !== undefined && (
|
|
616→ <span className="text-xs text-slate-500">Used {usageCount} times</span>
|
|
617→ )}
|
|
618→ </div>
|
|
619→ <p className="text-slate-300 whitespace-pre-wrap">{content}</p>
|
|
620→ <p className="text-xs text-slate-500 mt-2">
|
|
621→ Created: {new Date(createdAt).toLocaleString()}
|
|
622→ </p>
|
|
623→ </div>
|
|
624→ <div className="flex gap-1">
|
|
625→ {onEdit && (
|
|
626→ <button
|
|
627→ onClick={onEdit}
|
|
628→ className="p-2 text-slate-400 hover:text-accent hover:bg-accent/10 rounded-lg transition-colors"
|
|
629→ title="Edit"
|
|
630→ aria-label="Edit memory item"
|
|
631→ >
|
|
632→ <Pencil size={16} />
|
|
633→ </button>
|
|
634→ )}
|
|
635→ <button
|
|
636→ onClick={onDelete}
|
|
637→ className="p-2 text-slate-400 hover:text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
|
638→ title="Delete"
|
|
639→ aria-label="Delete memory item"
|
|
640→ >
|
|
641→ <Trash2 size={16} />
|
|
642→ </button>
|
|
643→ </div>
|
|
644→ </div>
|
|
645→ </div>
|
|
646→ )
|
|
647→}
|
|
648→
|
|
649→function InfrastructureCard({
|
|
650→ id: _id,
|
|
651→ name,
|
|
652→ type,
|
|
653→ config,
|
|
654→ createdAt,
|
|
655→ onDelete,
|
|
656→}: {
|
|
657→ id: number
|
|
658→ name: string
|
|
659→ type: string
|
|
660→ config: Record<string, unknown>
|
|
661→ createdAt: string
|
|
662→ onDelete: () => void
|
|
663→}) {
|
|
664→ return (
|
|
665→ <div className="bg-slate-800 rounded-lg border border-slate-700 p-4">
|
|
666→ <div className="flex items-start justify-between">
|
|
667→ <div className="flex-1">
|
|
668→ <div className="flex items-center gap-2 mb-2">
|
|
669→ <h3 className="font-semibold text-white">{name}</h3>
|
|
670→ <span className="px-2 py-0.5 bg-accent/20 text-accent text-xs rounded-full">
|
|
671→ {type}
|
|
672→ </span>
|
|
673→ </div>
|
|
674→ <pre className="text-slate-400 text-sm bg-slate-900 p-2 rounded overflow-auto max-h-32">
|
|
675→ {JSON.stringify(config, null, 2)}
|
|
676→ </pre>
|
|
677→ <p className="text-xs text-slate-500 mt-2">
|
|
678→ Created: {new Date(createdAt).toLocaleString()}
|
|
679→ </p>
|
|
680→ </div>
|
|
681→ <button
|
|
682→ onClick={onDelete}
|
|
683→ className="p-2 text-slate-400 hover:text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
|
684→ title="Delete"
|
|
685→ >
|
|
686→ <Trash2 size={16} />
|
|
687→ </button>
|
|
688→ </div>
|
|
689→ </div>
|
|
690→ )
|
|
691→}
|
|
692→
|
|
693→function EmptyState({
|
|
694→ title,
|
|
695→ description,
|
|
696→}: {
|
|
697→ title: string
|
|
698→ description: string
|
|
699→}) {
|
|
700→ return (
|
|
701→ <div className="text-center py-12 bg-slate-800 rounded-lg border border-slate-700">
|
|
702→ <Brain size={48} className="text-slate-600 mx-auto mb-4" />
|
|
703→ <h3 className="text-lg font-semibold text-slate-400">{title}</h3>
|
|
704→ <p className="text-slate-500 mt-2">{description}</p>
|
|
705→ </div>
|
|
706→ )
|
|
707→}
|
|
708→
|
|
|
|
<system-reminder>
|
|
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
|
|
</system-reminder>
|