Files
claudetools/imported-conversations/auto-builder/bcc2f2d7-2a47-410b-b948-6a9d0f44c92c/tool-results/toolu_012n7GM7C4jqymea7xvjBTBR.txt
Mike Swanson 75ce1c2fd5 feat: Add Sequential Thinking to Code Review + Frontend Validation
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>
2026-01-17 16:23:52 -07:00

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>