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>
This commit is contained in:
@@ -0,0 +1,712 @@
|
||||
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>
|
||||
@@ -0,0 +1,8 @@
|
||||
C:\Users\MikeSwanson\AutoBuilder\My-Auto-Builder\frontend\src\pages\AgentTerminals.tsx
|
||||
C:\Users\MikeSwanson\AutoBuilder\My-Auto-Builder\frontend\src\pages\NotFound.tsx
|
||||
C:\Users\MikeSwanson\AutoBuilder\My-Auto-Builder\frontend\src\pages\Dashboard.tsx
|
||||
C:\Users\MikeSwanson\AutoBuilder\My-Auto-Builder\frontend\src\pages\InfrastructureConfig.tsx
|
||||
C:\Users\MikeSwanson\AutoBuilder\My-Auto-Builder\frontend\src\pages\KanbanBoard.tsx
|
||||
C:\Users\MikeSwanson\AutoBuilder\My-Auto-Builder\frontend\src\pages\ProjectWizard.tsx
|
||||
C:\Users\MikeSwanson\AutoBuilder\My-Auto-Builder\frontend\src\pages\MemoryManager.tsx
|
||||
C:\Users\MikeSwanson\AutoBuilder\My-Auto-Builder\frontend\src\pages\Settings.tsx
|
||||
@@ -0,0 +1,452 @@
|
||||
1→import { useEffect, useState } from 'react'
|
||||
2→import { useProjectStore } from '../stores/projectStore'
|
||||
3→import { useAgentStore } from '../stores/agentStore'
|
||||
4→import { useToastStore } from '../components/Toast'
|
||||
5→import {
|
||||
6→ FolderOpen,
|
||||
7→ CheckCircle,
|
||||
8→ Clock,
|
||||
9→ AlertTriangle,
|
||||
10→ Activity,
|
||||
11→ Trash2,
|
||||
12→ Pencil,
|
||||
13→ Check,
|
||||
14→ Palette,
|
||||
15→ X,
|
||||
16→} from 'lucide-react'
|
||||
17→import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||
18→import * as Dialog from '@radix-ui/react-dialog'
|
||||
19→
|
||||
20→interface TaskStats {
|
||||
21→ completed: number
|
||||
22→ inProgress: number
|
||||
23→ blocked: number
|
||||
24→ backlog: number
|
||||
25→ qa: number
|
||||
26→}
|
||||
27→
|
||||
28→export default function Dashboard() {
|
||||
29→ const { currentProject, fetchProjects, deleteProject, updateProject } = useProjectStore()
|
||||
30→ const { activeSessions, fetchActiveSessions } = useAgentStore()
|
||||
31→ const { addToast } = useToastStore()
|
||||
32→ const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
33→ const [isDeleting, setIsDeleting] = useState(false)
|
||||
34→ const [showEditDialog, setShowEditDialog] = useState(false)
|
||||
35→ const [editName, setEditName] = useState('')
|
||||
36→ const [editDescription, setEditDescription] = useState('')
|
||||
37→ const [editDesignMode, setEditDesignMode] = useState<'mvp' | 'finished'>('mvp')
|
||||
38→ const [isSaving, setIsSaving] = useState(false)
|
||||
39→ const [saveSuccess, setSaveSuccess] = useState(false)
|
||||
40→ const [taskStats, setTaskStats] = useState<TaskStats>({
|
||||
41→ completed: 0,
|
||||
42→ inProgress: 0,
|
||||
43→ blocked: 0,
|
||||
44→ backlog: 0,
|
||||
45→ qa: 0,
|
||||
46→ })
|
||||
47→
|
||||
48→ useEffect(() => {
|
||||
49→ fetchProjects()
|
||||
50→ fetchActiveSessions()
|
||||
51→ }, [fetchProjects, fetchActiveSessions])
|
||||
52→
|
||||
53→ // Fetch task statistics when current project changes
|
||||
54→ useEffect(() => {
|
||||
55→ const fetchTaskStats = async () => {
|
||||
56→ if (!currentProject) return
|
||||
57→
|
||||
58→ try {
|
||||
59→ const response = await fetch(`http://localhost:8000/api/projects/${currentProject.id}/tasks`)
|
||||
60→ if (!response.ok) return
|
||||
61→
|
||||
62→ const tasks = await response.json()
|
||||
63→ const stats: TaskStats = {
|
||||
64→ completed: 0,
|
||||
65→ inProgress: 0,
|
||||
66→ blocked: 0,
|
||||
67→ backlog: 0,
|
||||
68→ qa: 0,
|
||||
69→ }
|
||||
70→
|
||||
71→ tasks.forEach((task: { status: string }) => {
|
||||
72→ switch (task.status) {
|
||||
73→ case 'done':
|
||||
74→ stats.completed++
|
||||
75→ break
|
||||
76→ case 'in_progress':
|
||||
77→ stats.inProgress++
|
||||
78→ break
|
||||
79→ case 'blocked':
|
||||
80→ stats.blocked++
|
||||
81→ break
|
||||
82→ case 'backlog':
|
||||
83→ stats.backlog++
|
||||
84→ break
|
||||
85→ case 'qa':
|
||||
86→ stats.qa++
|
||||
87→ break
|
||||
88→ }
|
||||
89→ })
|
||||
90→
|
||||
91→ setTaskStats(stats)
|
||||
92→ } catch (error) {
|
||||
93→ console.error('Failed to fetch task stats:', error)
|
||||
94→ }
|
||||
95→ }
|
||||
96→
|
||||
97→ fetchTaskStats()
|
||||
98→ }, [currentProject])
|
||||
99→
|
||||
100→ const handleDeleteProject = async () => {
|
||||
101→ if (!currentProject) return
|
||||
102→ setIsDeleting(true)
|
||||
103→ try {
|
||||
104→ await deleteProject(currentProject.id)
|
||||
105→ setShowDeleteDialog(false)
|
||||
106→ } catch (error) {
|
||||
107→ console.error('Failed to delete project:', error)
|
||||
108→ addToast('error', 'Delete Failed', `Failed to delete project "${currentProject.name}". Please try again.`)
|
||||
109→ } finally {
|
||||
110→ setIsDeleting(false)
|
||||
111→ }
|
||||
112→ }
|
||||
113→
|
||||
114→ const handleOpenEditDialog = () => {
|
||||
115→ if (currentProject) {
|
||||
116→ setEditName(currentProject.name)
|
||||
117→ setEditDescription(currentProject.description || '')
|
||||
118→ setEditDesignMode((currentProject.design_mode as 'mvp' | 'finished') || 'mvp')
|
||||
119→ setSaveSuccess(false)
|
||||
120→ setShowEditDialog(true)
|
||||
121→ }
|
||||
122→ }
|
||||
123→
|
||||
124→ const handleSaveProject = async () => {
|
||||
125→ if (!currentProject) return
|
||||
126→ setIsSaving(true)
|
||||
127→ setSaveSuccess(false)
|
||||
128→ try {
|
||||
129→ const response = await fetch(`http://localhost:8000/api/projects/${currentProject.id}`, {
|
||||
130→ method: 'PUT',
|
||||
131→ headers: { 'Content-Type': 'application/json' },
|
||||
132→ body: JSON.stringify({ name: editName, description: editDescription, design_mode: editDesignMode }),
|
||||
133→ })
|
||||
134→ if (!response.ok) {
|
||||
135→ const errorData = await response.json().catch(() => ({}))
|
||||
136→ throw new Error(errorData.detail || 'Failed to update project')
|
||||
137→ }
|
||||
138→ const updated = await response.json()
|
||||
139→ updateProject(currentProject.id, updated)
|
||||
140→ setSaveSuccess(true)
|
||||
141→ setTimeout(() => {
|
||||
142→ setShowEditDialog(false)
|
||||
143→ setSaveSuccess(false)
|
||||
144→ }, 1000)
|
||||
145→ } catch (error) {
|
||||
146→ console.error('Failed to save project:', error)
|
||||
147→ const message = error instanceof Error ? error.message : 'Failed to save project'
|
||||
148→ addToast('error', 'Save Failed', message)
|
||||
149→ } finally {
|
||||
150→ setIsSaving(false)
|
||||
151→ }
|
||||
152→ }
|
||||
153→
|
||||
154→ if (!currentProject) {
|
||||
155→ return (
|
||||
156→ <div className="flex flex-col items-center justify-center h-full text-center">
|
||||
157→ <FolderOpen size={64} className="text-slate-600 mb-4" />
|
||||
158→ <h2 className="text-2xl font-semibold text-slate-400">
|
||||
159→ No Project Selected
|
||||
160→ </h2>
|
||||
161→ <p className="text-slate-500 mt-2">
|
||||
162→ Select a project from the sidebar or create a new one to get started.
|
||||
163→ </p>
|
||||
164→ </div>
|
||||
165→ )
|
||||
166→ }
|
||||
167→
|
||||
168→ const stats = [
|
||||
169→ {
|
||||
170→ label: 'Tasks Completed',
|
||||
171→ value: taskStats.completed,
|
||||
172→ icon: CheckCircle,
|
||||
173→ color: 'text-green-400',
|
||||
174→ },
|
||||
175→ {
|
||||
176→ label: 'Tasks In Progress',
|
||||
177→ value: taskStats.inProgress,
|
||||
178→ icon: Clock,
|
||||
179→ color: 'text-accent',
|
||||
180→ },
|
||||
181→ {
|
||||
182→ label: 'Blocked Tasks',
|
||||
183→ value: taskStats.blocked,
|
||||
184→ icon: AlertTriangle,
|
||||
185→ color: 'text-warning',
|
||||
186→ },
|
||||
187→ {
|
||||
188→ label: 'Active Agents',
|
||||
189→ value: activeSessions.filter((s) => s.status === 'running').length,
|
||||
190→ icon: Activity,
|
||||
191→ color: 'text-primary-400',
|
||||
192→ },
|
||||
193→ ]
|
||||
194→
|
||||
195→ return (
|
||||
196→ <div className="space-y-6">
|
||||
197→ {/* Project Header */}
|
||||
198→ <div className="flex items-start justify-between">
|
||||
199→ <div>
|
||||
200→ <div className="flex items-center gap-3">
|
||||
201→ <h1 className="text-3xl font-bold">{currentProject.name}</h1>
|
||||
202→ {/* Design Mode Badge */}
|
||||
203→ <span
|
||||
204→ className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
|
||||
205→ currentProject.design_mode === 'finished'
|
||||
206→ ? 'bg-accent/20 text-accent border border-accent/30'
|
||||
207→ : 'bg-amber-500/20 text-amber-400 border border-amber-500/30'
|
||||
208→ }`}
|
||||
209→ >
|
||||
210→ <Palette size={12} />
|
||||
211→ {currentProject.design_mode === 'finished' ? 'Finished Mode' : 'MVP Mode'}
|
||||
212→ </span>
|
||||
213→ </div>
|
||||
214→ {currentProject.description && (
|
||||
215→ <p className="text-slate-400 mt-1">{currentProject.description}</p>
|
||||
216→ )}
|
||||
217→ </div>
|
||||
218→ <div className="flex items-center gap-2">
|
||||
219→ {/* Edit Button and Dialog */}
|
||||
220→ <Dialog.Root open={showEditDialog} onOpenChange={setShowEditDialog}>
|
||||
221→ <Dialog.Trigger asChild>
|
||||
222→ <button
|
||||
223→ onClick={handleOpenEditDialog}
|
||||
224→ className="flex items-center gap-2 px-3 py-2 text-sm text-slate-400 hover:text-accent hover:bg-accent/10 rounded-lg transition-colors"
|
||||
225→ title="Edit Project Settings"
|
||||
226→ >
|
||||
227→ <Pencil size={16} />
|
||||
228→ <span>Edit</span>
|
||||
229→ </button>
|
||||
230→ </Dialog.Trigger>
|
||||
231→ <Dialog.Portal>
|
||||
232→ <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
|
||||
233→ <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">
|
||||
234→ <div className="flex items-center justify-between">
|
||||
235→ <Dialog.Title className="text-xl font-semibold text-white">
|
||||
236→ Edit Project Settings
|
||||
237→ </Dialog.Title>
|
||||
238→ <Dialog.Close asChild>
|
||||
239→ <button className="p-1 text-slate-400 hover:text-white hover:bg-slate-700 rounded transition-colors" aria-label="Close">
|
||||
240→ <X size={20} />
|
||||
241→ </button>
|
||||
242→ </Dialog.Close>
|
||||
243→ </div>
|
||||
244→ <Dialog.Description className="mt-2 text-slate-400">
|
||||
245→ Update your project name, description, and design mode.
|
||||
246→ </Dialog.Description>
|
||||
247→ <div className="mt-4 space-y-4">
|
||||
248→ <div>
|
||||
249→ <label className="block text-sm font-medium text-slate-300 mb-1">Project Name</label>
|
||||
250→ <input
|
||||
251→ type="text"
|
||||
252→ value={editName}
|
||||
253→ onChange={(e) => setEditName(e.target.value)}
|
||||
254→ 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 focus:border-transparent"
|
||||
255→ placeholder="Enter project name"
|
||||
256→ />
|
||||
257→ </div>
|
||||
258→ <div>
|
||||
259→ <label className="block text-sm font-medium text-slate-300 mb-1">Description</label>
|
||||
260→ <textarea
|
||||
261→ value={editDescription}
|
||||
262→ onChange={(e) => setEditDescription(e.target.value)}
|
||||
263→ rows={3}
|
||||
264→ 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 focus:border-transparent resize-none"
|
||||
265→ placeholder="Enter project description"
|
||||
266→ />
|
||||
267→ </div>
|
||||
268→ <div>
|
||||
269→ <label className="block text-sm font-medium text-slate-300 mb-2">Design Mode</label>
|
||||
270→ <div className="grid grid-cols-2 gap-2">
|
||||
271→ <button
|
||||
272→ type="button"
|
||||
273→ onClick={() => setEditDesignMode('mvp')}
|
||||
274→ className={`p-3 rounded-lg border text-left transition-colors ${
|
||||
275→ editDesignMode === 'mvp'
|
||||
276→ ? 'border-amber-500 bg-amber-500/10'
|
||||
277→ : 'border-slate-700 hover:border-slate-600'
|
||||
278→ }`}
|
||||
279→ >
|
||||
280→ <p className="font-medium text-amber-400">MVP</p>
|
||||
281→ <p className="text-xs text-slate-400">Functional drafts OK</p>
|
||||
282→ </button>
|
||||
283→ <button
|
||||
284→ type="button"
|
||||
285→ onClick={() => setEditDesignMode('finished')}
|
||||
286→ className={`p-3 rounded-lg border text-left transition-colors ${
|
||||
287→ editDesignMode === 'finished'
|
||||
288→ ? 'border-accent bg-accent/10'
|
||||
289→ : 'border-slate-700 hover:border-slate-600'
|
||||
290→ }`}
|
||||
291→ >
|
||||
292→ <p className="font-medium text-accent">Finished</p>
|
||||
293→ <p className="text-xs text-slate-400">Polished required</p>
|
||||
294→ </button>
|
||||
295→ </div>
|
||||
296→ </div>
|
||||
297→ </div>
|
||||
298→ <div className="mt-6 flex justify-end gap-3">
|
||||
299→ <Dialog.Close asChild>
|
||||
300→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
|
||||
301→ Cancel
|
||||
302→ </button>
|
||||
303→ </Dialog.Close>
|
||||
304→ <button
|
||||
305→ onClick={handleSaveProject}
|
||||
306→ disabled={isSaving || !editName.trim()}
|
||||
307→ className="flex items-center gap-2 px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
308→ >
|
||||
309→ {saveSuccess ? (
|
||||
310→ <>
|
||||
311→ <Check size={16} />
|
||||
312→ Saved!
|
||||
313→ </>
|
||||
314→ ) : isSaving ? (
|
||||
315→ 'Saving...'
|
||||
316→ ) : (
|
||||
317→ 'Save Changes'
|
||||
318→ )}
|
||||
319→ </button>
|
||||
320→ </div>
|
||||
321→ </Dialog.Content>
|
||||
322→ </Dialog.Portal>
|
||||
323→ </Dialog.Root>
|
||||
324→
|
||||
325→ {/* Delete Button and Dialog */}
|
||||
326→ <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
327→ <AlertDialog.Trigger asChild>
|
||||
328→ <button
|
||||
329→ className="flex items-center gap-2 px-3 py-2 text-sm text-slate-400 hover:text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
||||
330→ title="Delete Project"
|
||||
331→ >
|
||||
332→ <Trash2 size={16} />
|
||||
333→ <span>Delete</span>
|
||||
334→ </button>
|
||||
335→ </AlertDialog.Trigger>
|
||||
336→ <AlertDialog.Portal>
|
||||
337→ <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
|
||||
338→ <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">
|
||||
339→ <AlertDialog.Title className="text-xl font-semibold text-white">
|
||||
340→ Delete Project
|
||||
341→ </AlertDialog.Title>
|
||||
342→ <AlertDialog.Description className="mt-3 text-slate-400">
|
||||
343→ Are you sure you want to delete <span className="font-semibold text-white">{currentProject.name}</span>?
|
||||
344→ This action cannot be undone. All project data, tasks, and history will be permanently deleted.
|
||||
345→ </AlertDialog.Description>
|
||||
346→ <div className="mt-6 flex justify-end gap-3">
|
||||
347→ <AlertDialog.Cancel asChild>
|
||||
348→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
|
||||
349→ Cancel
|
||||
350→ </button>
|
||||
351→ </AlertDialog.Cancel>
|
||||
352→ <AlertDialog.Action asChild>
|
||||
353→ <button
|
||||
354→ onClick={handleDeleteProject}
|
||||
355→ disabled={isDeleting}
|
||||
356→ className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
357→ >
|
||||
358→ {isDeleting ? 'Deleting...' : 'Delete Project'}
|
||||
359→ </button>
|
||||
360→ </AlertDialog.Action>
|
||||
361→ </div>
|
||||
362→ </AlertDialog.Content>
|
||||
363→ </AlertDialog.Portal>
|
||||
364→ </AlertDialog.Root>
|
||||
365→ </div>
|
||||
366→ </div>
|
||||
367→
|
||||
368→ {/* Stats Grid */}
|
||||
369→ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
370→ {stats.map((stat) => {
|
||||
371→ const Icon = stat.icon
|
||||
372→ return (
|
||||
373→ <div
|
||||
374→ key={stat.label}
|
||||
375→ className="bg-slate-800 rounded-lg p-4 border border-slate-700"
|
||||
376→ >
|
||||
377→ <div className="flex items-center gap-3">
|
||||
378→ <Icon className={stat.color} size={24} />
|
||||
379→ <div>
|
||||
380→ <p className="text-sm text-slate-400">{stat.label}</p>
|
||||
381→ <p className="text-2xl font-bold">{stat.value}</p>
|
||||
382→ </div>
|
||||
383→ </div>
|
||||
384→ </div>
|
||||
385→ )
|
||||
386→ })}
|
||||
387→ </div>
|
||||
388→
|
||||
389→ {/* Phase Progress */}
|
||||
390→ <div className="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
||||
391→ <h2 className="text-lg font-semibold mb-4">Phase Progress</h2>
|
||||
392→ <div className="flex items-center gap-4">
|
||||
393→ <div className="flex-1">
|
||||
394→ <div className="flex justify-between text-sm mb-2">
|
||||
395→ <span>Phase {currentProject.current_phase}</span>
|
||||
396→ <span>{currentProject.total_phases} total</span>
|
||||
397→ </div>
|
||||
398→ <div className="w-full h-3 bg-slate-700 rounded-full overflow-hidden">
|
||||
399→ <div
|
||||
400→ className="h-full bg-accent transition-all duration-500"
|
||||
401→ style={{
|
||||
402→ width: `${(currentProject.current_phase / currentProject.total_phases) * 100}%`,
|
||||
403→ }}
|
||||
404→ />
|
||||
405→ </div>
|
||||
406→ </div>
|
||||
407→ <span className="text-2xl font-bold">
|
||||
408→ {Math.round((currentProject.current_phase / currentProject.total_phases) * 100)}%
|
||||
409→ </span>
|
||||
410→ </div>
|
||||
411→ </div>
|
||||
412→
|
||||
413→ {/* Recent Activity */}
|
||||
414→ <div className="bg-slate-800 rounded-lg p-6 border border-slate-700">
|
||||
415→ <h2 className="text-lg font-semibold mb-4">Recent Activity</h2>
|
||||
416→ <div className="space-y-3">
|
||||
417→ {activeSessions.length === 0 ? (
|
||||
418→ <p className="text-slate-500 italic">No recent activity</p>
|
||||
419→ ) : (
|
||||
420→ activeSessions.slice(0, 5).map((session) => (
|
||||
421→ <div
|
||||
422→ key={session.id}
|
||||
423→ className="flex items-center justify-between py-2 border-b border-slate-700 last:border-0"
|
||||
424→ >
|
||||
425→ <div className="flex items-center gap-3">
|
||||
426→ <div
|
||||
427→ className={`w-2 h-2 rounded-full ${
|
||||
428→ session.status === 'running'
|
||||
429→ ? 'bg-accent animate-pulse'
|
||||
430→ : session.status === 'completed'
|
||||
431→ ? 'bg-green-400'
|
||||
432→ : session.status === 'failed'
|
||||
433→ ? 'bg-error'
|
||||
434→ : 'bg-slate-500'
|
||||
435→ }`}
|
||||
436→ />
|
||||
437→ <span className="capitalize">{session.agent_type.replace('_', ' ')} Agent</span>
|
||||
438→ </div>
|
||||
439→ <span className="text-sm text-slate-400">{session.status}</span>
|
||||
440→ </div>
|
||||
441→ ))
|
||||
442→ )}
|
||||
443→ </div>
|
||||
444→ </div>
|
||||
445→ </div>
|
||||
446→ )
|
||||
447→}
|
||||
448→
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,967 @@
|
||||
1→import { useEffect, useState, useMemo } from 'react'
|
||||
2→import { useSearchParams } from 'react-router-dom'
|
||||
3→import { Plus, X, GripVertical, Clock, User, RefreshCw, Flag, Pencil, Check, Trash2, Filter, ArrowUpDown, LayoutGrid, List, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
4→import * as Dialog from '@radix-ui/react-dialog'
|
||||
5→import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||
6→import { useProjectStore } from '../stores/projectStore'
|
||||
7→import { useTaskStore, Task, TaskCreate, TaskUpdate } from '../stores/taskStore'
|
||||
8→import { useToastStore } from '../components/Toast'
|
||||
9→
|
||||
10→const ITEMS_PER_PAGE = 10
|
||||
11→
|
||||
12→const COLUMNS = [
|
||||
13→ { id: 'backlog', title: 'Backlog', color: 'border-slate-500' },
|
||||
14→ { id: 'in_progress', title: 'In Progress', color: 'border-accent' },
|
||||
15→ { id: 'qa', title: 'QA', color: 'border-primary-400' },
|
||||
16→ { id: 'done', title: 'Done', color: 'border-green-400' },
|
||||
17→ { id: 'blocked', title: 'Blocked', color: 'border-destructive' },
|
||||
18→]
|
||||
19→
|
||||
20→const PRIORITY_LABELS: Record<number, string> = {
|
||||
21→ 0: 'Low',
|
||||
22→ 1: 'Medium',
|
||||
23→ 2: 'High',
|
||||
24→ 3: 'Critical',
|
||||
25→}
|
||||
26→
|
||||
27→const STATUS_LABELS: Record<string, string> = {
|
||||
28→ backlog: 'Backlog',
|
||||
29→ in_progress: 'In Progress',
|
||||
30→ qa: 'QA',
|
||||
31→ done: 'Done',
|
||||
32→ blocked: 'Blocked',
|
||||
33→}
|
||||
34→
|
||||
35→export default function KanbanBoard() {
|
||||
36→ const [searchParams, setSearchParams] = useSearchParams()
|
||||
37→ const { currentProject } = useProjectStore()
|
||||
38→ const { tasks, fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask, isTaskDeleting, isLoading } = useTaskStore()
|
||||
39→ const { addToast } = useToastStore()
|
||||
40→ const [showAddDialog, setShowAddDialog] = useState(false)
|
||||
41→ const [newTaskName, setNewTaskName] = useState('')
|
||||
42→ const [newTaskDescription, setNewTaskDescription] = useState('')
|
||||
43→ const [newTaskPriority, setNewTaskPriority] = useState(0)
|
||||
44→ const [isCreating, setIsCreating] = useState(false)
|
||||
45→ const [draggedTask, setDraggedTask] = useState<number | null>(null)
|
||||
46→ const [selectedTask, setSelectedTask] = useState<Task | null>(null)
|
||||
47→ const [isEditing, setIsEditing] = useState(false)
|
||||
48→ const [editName, setEditName] = useState('')
|
||||
49→ const [editDescription, setEditDescription] = useState('')
|
||||
50→ const [editPriority, setEditPriority] = useState(0)
|
||||
51→ const [isSaving, setIsSaving] = useState(false)
|
||||
52→ const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
53→ const [isDeleting, setIsDeleting] = useState(false)
|
||||
54→ const [showUnsavedDialog, setShowUnsavedDialog] = useState(false)
|
||||
55→ const [pendingCloseAction, setPendingCloseAction] = useState<'close' | 'cancel' | null>(null)
|
||||
56→ // Filter and Sort state - initialize from URL params
|
||||
57→ const getInitialPriorityFilter = (): number | 'all' => {
|
||||
58→ const param = searchParams.get('priority')
|
||||
59→ if (param === 'all' || param === null) return 'all'
|
||||
60→ const num = parseInt(param, 10)
|
||||
61→ return [0, 1, 2, 3].includes(num) ? num : 'all'
|
||||
62→ }
|
||||
63→ const getInitialSortOrder = (): 'newest' | 'oldest' | 'priority' => {
|
||||
64→ const param = searchParams.get('sort')
|
||||
65→ if (param === 'newest' || param === 'oldest' || param === 'priority') return param
|
||||
66→ return 'newest'
|
||||
67→ }
|
||||
68→ const getInitialViewMode = (): 'kanban' | 'list' => {
|
||||
69→ const param = searchParams.get('view')
|
||||
70→ if (param === 'list') return 'list'
|
||||
71→ return 'kanban'
|
||||
72→ }
|
||||
73→
|
||||
74→ const [priorityFilter, setPriorityFilter] = useState<number | 'all'>(getInitialPriorityFilter)
|
||||
75→ const [sortOrder, setSortOrder] = useState<'newest' | 'oldest' | 'priority'>(getInitialSortOrder)
|
||||
76→ // View mode and pagination state
|
||||
77→ const [viewMode, setViewMode] = useState<'kanban' | 'list'>(getInitialViewMode)
|
||||
78→ const [currentPage, setCurrentPage] = useState(1)
|
||||
79→
|
||||
80→ useEffect(() => {
|
||||
81→ if (currentProject?.id) {
|
||||
82→ fetchTasks(currentProject.id)
|
||||
83→ }
|
||||
84→ }, [currentProject?.id, fetchTasks])
|
||||
85→
|
||||
86→ // Handle deep linking - select task from URL param after tasks are loaded
|
||||
87→ useEffect(() => {
|
||||
88→ const taskIdParam = searchParams.get('task')
|
||||
89→ if (taskIdParam && tasks.length > 0) {
|
||||
90→ const taskId = parseInt(taskIdParam, 10)
|
||||
91→ const task = tasks.find(t => t.id === taskId)
|
||||
92→ if (task) {
|
||||
93→ setSelectedTask(task)
|
||||
94→ }
|
||||
95→ }
|
||||
96→ }, [searchParams, tasks])
|
||||
97→
|
||||
98→ // Update URL when task is selected
|
||||
99→ const handleSelectTask = (task: Task) => {
|
||||
100→ setSelectedTask(task)
|
||||
101→ const newParams = new URLSearchParams(searchParams)
|
||||
102→ newParams.set('task', task.id.toString())
|
||||
103→ setSearchParams(newParams)
|
||||
104→ }
|
||||
105→
|
||||
106→ // Clear task from URL when panel is closed
|
||||
107→ const clearTaskFromUrl = () => {
|
||||
108→ const newParams = new URLSearchParams(searchParams)
|
||||
109→ newParams.delete('task')
|
||||
110→ setSearchParams(newParams)
|
||||
111→ }
|
||||
112→
|
||||
113→ // Update URL when filter changes
|
||||
114→ const handlePriorityFilterChange = (value: number | 'all') => {
|
||||
115→ setPriorityFilter(value)
|
||||
116→ const newParams = new URLSearchParams(searchParams)
|
||||
117→ if (value === 'all') {
|
||||
118→ newParams.delete('priority')
|
||||
119→ } else {
|
||||
120→ newParams.set('priority', value.toString())
|
||||
121→ }
|
||||
122→ setSearchParams(newParams)
|
||||
123→ }
|
||||
124→
|
||||
125→ // Update URL when sort changes
|
||||
126→ const handleSortOrderChange = (value: 'newest' | 'oldest' | 'priority') => {
|
||||
127→ setSortOrder(value)
|
||||
128→ const newParams = new URLSearchParams(searchParams)
|
||||
129→ if (value === 'newest') {
|
||||
130→ newParams.delete('sort') // default value, no need to store
|
||||
131→ } else {
|
||||
132→ newParams.set('sort', value)
|
||||
133→ }
|
||||
134→ setSearchParams(newParams)
|
||||
135→ }
|
||||
136→
|
||||
137→ // Update URL when view mode changes
|
||||
138→ const handleViewModeChange = (value: 'kanban' | 'list') => {
|
||||
139→ setViewMode(value)
|
||||
140→ const newParams = new URLSearchParams(searchParams)
|
||||
141→ if (value === 'kanban') {
|
||||
142→ newParams.delete('view') // default value, no need to store
|
||||
143→ } else {
|
||||
144→ newParams.set('view', value)
|
||||
145→ }
|
||||
146→ setSearchParams(newParams)
|
||||
147→ }
|
||||
148→
|
||||
149→ // Reset all filters to defaults
|
||||
150→ const handleResetFilters = () => {
|
||||
151→ setPriorityFilter('all')
|
||||
152→ setSortOrder('newest')
|
||||
153→ setCurrentPage(1)
|
||||
154→ const newParams = new URLSearchParams(searchParams)
|
||||
155→ newParams.delete('priority')
|
||||
156→ newParams.delete('sort')
|
||||
157→ setSearchParams(newParams)
|
||||
158→ }
|
||||
159→
|
||||
160→ // Check if filters are active (non-default)
|
||||
161→ const hasActiveFilters = priorityFilter !== 'all' || sortOrder !== 'newest'
|
||||
162→
|
||||
163→ // Filtered and sorted tasks
|
||||
164→ const filteredAndSortedTasks = useMemo(() => {
|
||||
165→ let result = [...tasks]
|
||||
166→
|
||||
167→ // Apply priority filter
|
||||
168→ if (priorityFilter !== 'all') {
|
||||
169→ result = result.filter(task => task.priority === priorityFilter)
|
||||
170→ }
|
||||
171→
|
||||
172→ // Apply sort
|
||||
173→ result.sort((a, b) => {
|
||||
174→ if (sortOrder === 'newest') {
|
||||
175→ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
176→ } else if (sortOrder === 'oldest') {
|
||||
177→ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
178→ } else {
|
||||
179→ // priority - highest first
|
||||
180→ return b.priority - a.priority
|
||||
181→ }
|
||||
182→ })
|
||||
183→
|
||||
184→ return result
|
||||
185→ }, [tasks, priorityFilter, sortOrder])
|
||||
186→
|
||||
187→ // Pagination calculations
|
||||
188→ const totalPages = Math.ceil(filteredAndSortedTasks.length / ITEMS_PER_PAGE)
|
||||
189→ const paginatedTasks = useMemo(() => {
|
||||
190→ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
|
||||
191→ return filteredAndSortedTasks.slice(startIndex, startIndex + ITEMS_PER_PAGE)
|
||||
192→ }, [filteredAndSortedTasks, currentPage])
|
||||
193→
|
||||
194→ // Reset page when filter changes
|
||||
195→ useEffect(() => {
|
||||
196→ setCurrentPage(1)
|
||||
197→ }, [priorityFilter, sortOrder])
|
||||
198→
|
||||
199→ const getTasksByStatus = (status: string) => {
|
||||
200→ return filteredAndSortedTasks.filter((task) => task.status === status)
|
||||
201→ }
|
||||
202→
|
||||
203→ const handleCreateTask = async () => {
|
||||
204→ if (!currentProject || !newTaskName.trim()) return
|
||||
205→ setIsCreating(true)
|
||||
206→ try {
|
||||
207→ const taskData: TaskCreate = {
|
||||
208→ name: newTaskName.trim(),
|
||||
209→ description: newTaskDescription.trim() || undefined,
|
||||
210→ priority: newTaskPriority,
|
||||
211→ }
|
||||
212→ await createTask(currentProject.id, taskData)
|
||||
213→ setNewTaskName('')
|
||||
214→ setNewTaskDescription('')
|
||||
215→ setNewTaskPriority(0)
|
||||
216→ setShowAddDialog(false)
|
||||
217→ } catch (error) {
|
||||
218→ console.error('Failed to create task:', error)
|
||||
219→ addToast('error', 'Create Failed', 'Failed to create task. Please try again.')
|
||||
220→ } finally {
|
||||
221→ setIsCreating(false)
|
||||
222→ }
|
||||
223→ }
|
||||
224→
|
||||
225→ const handleDragStart = (taskId: number) => {
|
||||
226→ setDraggedTask(taskId)
|
||||
227→ }
|
||||
228→
|
||||
229→ const handleDragOver = (e: React.DragEvent) => {
|
||||
230→ e.preventDefault()
|
||||
231→ }
|
||||
232→
|
||||
233→ const handleDrop = async (targetStatus: string) => {
|
||||
234→ if (draggedTask === null) return
|
||||
235→ try {
|
||||
236→ await updateTaskStatus(draggedTask, targetStatus)
|
||||
237→ } catch (error) {
|
||||
238→ console.error('Failed to update task status:', error)
|
||||
239→ addToast('error', 'Update Failed', 'Failed to update task status. Please try again.')
|
||||
240→ }
|
||||
241→ setDraggedTask(null)
|
||||
242→ }
|
||||
243→
|
||||
244→ const handleStartEdit = () => {
|
||||
245→ if (!selectedTask) return
|
||||
246→ setEditName(selectedTask.name)
|
||||
247→ setEditDescription(selectedTask.description || '')
|
||||
248→ setEditPriority(selectedTask.priority)
|
||||
249→ setIsEditing(true)
|
||||
250→ }
|
||||
251→
|
||||
252→ // Check if edit form has unsaved changes
|
||||
253→ const hasUnsavedChanges = () => {
|
||||
254→ if (!selectedTask || !isEditing) return false
|
||||
255→ return (
|
||||
256→ editName.trim() !== selectedTask.name ||
|
||||
257→ (editDescription.trim() || '') !== (selectedTask.description || '') ||
|
||||
258→ editPriority !== selectedTask.priority
|
||||
259→ )
|
||||
260→ }
|
||||
261→
|
||||
262→ const handleCancelEdit = () => {
|
||||
263→ if (hasUnsavedChanges()) {
|
||||
264→ setPendingCloseAction('cancel')
|
||||
265→ setShowUnsavedDialog(true)
|
||||
266→ } else {
|
||||
267→ setIsEditing(false)
|
||||
268→ }
|
||||
269→ }
|
||||
270→
|
||||
271→ const handleClosePanel = () => {
|
||||
272→ if (hasUnsavedChanges()) {
|
||||
273→ setPendingCloseAction('close')
|
||||
274→ setShowUnsavedDialog(true)
|
||||
275→ } else {
|
||||
276→ setSelectedTask(null)
|
||||
277→ setIsEditing(false)
|
||||
278→ clearTaskFromUrl()
|
||||
279→ }
|
||||
280→ }
|
||||
281→
|
||||
282→ const handleDiscardChanges = () => {
|
||||
283→ setShowUnsavedDialog(false)
|
||||
284→ if (pendingCloseAction === 'close') {
|
||||
285→ setSelectedTask(null)
|
||||
286→ clearTaskFromUrl()
|
||||
287→ }
|
||||
288→ setIsEditing(false)
|
||||
289→ setPendingCloseAction(null)
|
||||
290→ }
|
||||
291→
|
||||
292→ const handleKeepEditing = () => {
|
||||
293→ setShowUnsavedDialog(false)
|
||||
294→ setPendingCloseAction(null)
|
||||
295→ }
|
||||
296→
|
||||
297→ const handleSaveEdit = async () => {
|
||||
298→ if (!selectedTask) return
|
||||
299→ setIsSaving(true)
|
||||
300→ try {
|
||||
301→ const updates: TaskUpdate = {
|
||||
302→ name: editName.trim(),
|
||||
303→ description: editDescription.trim() || undefined,
|
||||
304→ priority: editPriority,
|
||||
305→ }
|
||||
306→ const updatedTask = await updateTask(selectedTask.id, updates)
|
||||
307→ setSelectedTask(updatedTask)
|
||||
308→ setIsEditing(false)
|
||||
309→ } catch (error) {
|
||||
310→ console.error('Failed to update task:', error)
|
||||
311→ addToast('error', 'Save Failed', 'Failed to save task changes. Please try again.')
|
||||
312→ } finally {
|
||||
313→ setIsSaving(false)
|
||||
314→ }
|
||||
315→ }
|
||||
316→
|
||||
317→ const handleDeleteTask = async () => {
|
||||
318→ if (!selectedTask) return
|
||||
319→ // Check if already being deleted (prevents rapid clicks)
|
||||
320→ if (isTaskDeleting(selectedTask.id)) return
|
||||
321→
|
||||
322→ const taskIdToDelete = selectedTask.id
|
||||
323→ const taskName = selectedTask.name
|
||||
324→ setIsDeleting(true)
|
||||
325→ // Close dialog immediately to prevent additional clicks
|
||||
326→ setShowDeleteDialog(false)
|
||||
327→
|
||||
328→ try {
|
||||
329→ await deleteTask(taskIdToDelete)
|
||||
330→ setSelectedTask(null)
|
||||
331→ clearTaskFromUrl()
|
||||
332→ } catch (error) {
|
||||
333→ console.error('Failed to delete task:', error)
|
||||
334→ addToast('error', 'Delete Failed', `Failed to delete task "${taskName}". Please try again.`)
|
||||
335→ // Re-open dialog if delete failed so user can retry
|
||||
336→ setShowDeleteDialog(true)
|
||||
337→ } finally {
|
||||
338→ setIsDeleting(false)
|
||||
339→ }
|
||||
340→ }
|
||||
341→
|
||||
342→ if (!currentProject) {
|
||||
343→ return (
|
||||
344→ <div className="flex items-center justify-center h-full">
|
||||
345→ <p className="text-slate-500">Select a project to view its Kanban board</p>
|
||||
346→ </div>
|
||||
347→ )
|
||||
348→ }
|
||||
349→
|
||||
350→ return (
|
||||
351→ <div className="h-full flex flex-col">
|
||||
352→ {/* Header */}
|
||||
353→ <div className="flex items-center justify-between mb-6">
|
||||
354→ <h1 className="text-2xl font-bold">Kanban Board</h1>
|
||||
355→ <div className="flex items-center gap-4">
|
||||
356→ {/* Priority Filter */}
|
||||
357→ <div className="flex items-center gap-2">
|
||||
358→ <Filter size={16} className="text-slate-400" />
|
||||
359→ <select
|
||||
360→ value={priorityFilter === 'all' ? 'all' : priorityFilter.toString()}
|
||||
361→ onChange={(e) => handlePriorityFilterChange(e.target.value === 'all' ? 'all' : Number(e.target.value))}
|
||||
362→ className="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
363→ >
|
||||
364→ <option value="all">All Priorities</option>
|
||||
365→ <option value="3">Critical</option>
|
||||
366→ <option value="2">High</option>
|
||||
367→ <option value="1">Medium</option>
|
||||
368→ <option value="0">Low</option>
|
||||
369→ </select>
|
||||
370→ </div>
|
||||
371→
|
||||
372→ {/* Sort Order */}
|
||||
373→ <div className="flex items-center gap-2">
|
||||
374→ <ArrowUpDown size={16} className="text-slate-400" />
|
||||
375→ <select
|
||||
376→ value={sortOrder}
|
||||
377→ onChange={(e) => handleSortOrderChange(e.target.value as 'newest' | 'oldest' | 'priority')}
|
||||
378→ className="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
379→ >
|
||||
380→ <option value="newest">Newest First</option>
|
||||
381→ <option value="oldest">Oldest First</option>
|
||||
382→ <option value="priority">By Priority</option>
|
||||
383→ </select>
|
||||
384→ </div>
|
||||
385→
|
||||
386→ {/* Reset Filters Button */}
|
||||
387→ {hasActiveFilters && (
|
||||
388→ <button
|
||||
389→ onClick={handleResetFilters}
|
||||
390→ className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-400 hover:text-white hover:bg-slate-700 rounded-lg transition-colors"
|
||||
391→ title="Reset Filters"
|
||||
392→ >
|
||||
393→ <RefreshCw size={14} />
|
||||
394→ Reset
|
||||
395→ </button>
|
||||
396→ )}
|
||||
397→
|
||||
398→ {/* View Mode Toggle */}
|
||||
399→ <div className="flex items-center gap-1 bg-slate-800 border border-slate-700 rounded-lg p-1">
|
||||
400→ <button
|
||||
401→ onClick={() => handleViewModeChange('kanban')}
|
||||
402→ className={`p-1.5 rounded ${viewMode === 'kanban' ? 'bg-accent text-white' : 'text-slate-400 hover:text-white'}`}
|
||||
403→ title="Kanban View"
|
||||
404→ >
|
||||
405→ <LayoutGrid size={18} />
|
||||
406→ </button>
|
||||
407→ <button
|
||||
408→ onClick={() => handleViewModeChange('list')}
|
||||
409→ className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-accent text-white' : 'text-slate-400 hover:text-white'}`}
|
||||
410→ title="List View"
|
||||
411→ >
|
||||
412→ <List size={18} />
|
||||
413→ </button>
|
||||
414→ </div>
|
||||
415→
|
||||
416→ <Dialog.Root open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
417→ <Dialog.Trigger asChild>
|
||||
418→ <button className="btn-accent flex items-center gap-2">
|
||||
419→ <Plus size={18} />
|
||||
420→ Add Task
|
||||
421→ </button>
|
||||
422→ </Dialog.Trigger>
|
||||
423→ <Dialog.Portal>
|
||||
424→ <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
|
||||
425→ <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">
|
||||
426→ <div className="flex items-center justify-between mb-4">
|
||||
427→ <Dialog.Title className="text-xl font-semibold text-white">
|
||||
428→ Add New Task
|
||||
429→ </Dialog.Title>
|
||||
430→ <Dialog.Close asChild>
|
||||
431→ <button className="text-slate-400 hover:text-white" aria-label="Close dialog">
|
||||
432→ <X size={20} />
|
||||
433→ </button>
|
||||
434→ </Dialog.Close>
|
||||
435→ </div>
|
||||
436→ <Dialog.Description className="text-slate-400 mb-4">
|
||||
437→ Create a new task for this project.
|
||||
438→ </Dialog.Description>
|
||||
439→ <div className="space-y-4">
|
||||
440→ <div>
|
||||
441→ <label className="block text-sm font-medium text-slate-300 mb-1">
|
||||
442→ Task Name *
|
||||
443→ </label>
|
||||
444→ <input
|
||||
445→ type="text"
|
||||
446→ value={newTaskName}
|
||||
447→ onChange={(e) => setNewTaskName(e.target.value)}
|
||||
448→ 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 focus:border-transparent"
|
||||
449→ placeholder="Enter task name"
|
||||
450→ />
|
||||
451→ </div>
|
||||
452→ <div>
|
||||
453→ <label className="block text-sm font-medium text-slate-300 mb-1">
|
||||
454→ Description
|
||||
455→ </label>
|
||||
456→ <textarea
|
||||
457→ value={newTaskDescription}
|
||||
458→ onChange={(e) => setNewTaskDescription(e.target.value)}
|
||||
459→ rows={3}
|
||||
460→ 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 focus:border-transparent resize-none"
|
||||
461→ placeholder="Enter task description"
|
||||
462→ />
|
||||
463→ </div>
|
||||
464→ <div>
|
||||
465→ <label className="block text-sm font-medium text-slate-300 mb-1">
|
||||
466→ Priority
|
||||
467→ </label>
|
||||
468→ <select
|
||||
469→ value={newTaskPriority}
|
||||
470→ onChange={(e) => setNewTaskPriority(Number(e.target.value))}
|
||||
471→ 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 focus:border-transparent"
|
||||
472→ >
|
||||
473→ <option value={0}>Low</option>
|
||||
474→ <option value={1}>Medium</option>
|
||||
475→ <option value={2}>High</option>
|
||||
476→ <option value={3}>Critical</option>
|
||||
477→ </select>
|
||||
478→ </div>
|
||||
479→ </div>
|
||||
480→ <div className="mt-6 flex justify-end gap-3">
|
||||
481→ <Dialog.Close asChild>
|
||||
482→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
|
||||
483→ Cancel
|
||||
484→ </button>
|
||||
485→ </Dialog.Close>
|
||||
486→ <button
|
||||
487→ onClick={handleCreateTask}
|
||||
488→ disabled={isCreating || !newTaskName.trim()}
|
||||
489→ className="px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
490→ >
|
||||
491→ {isCreating ? 'Creating...' : 'Create Task'}
|
||||
492→ </button>
|
||||
493→ </div>
|
||||
494→ </Dialog.Content>
|
||||
495→ </Dialog.Portal>
|
||||
496→ </Dialog.Root>
|
||||
497→ </div>
|
||||
498→ </div>
|
||||
499→
|
||||
500→ {/* Main Content Area */}
|
||||
501→ <div className="flex-1 flex gap-4 overflow-hidden">
|
||||
502→ {viewMode === 'list' ? (
|
||||
503→ /* List View with Pagination */
|
||||
504→ <div className="flex-1 flex flex-col">
|
||||
505→ {/* Task List Table */}
|
||||
506→ <div className="flex-1 overflow-auto bg-slate-800 rounded-lg border border-slate-700">
|
||||
507→ <table className="w-full">
|
||||
508→ <thead className="bg-slate-900 sticky top-0">
|
||||
509→ <tr className="text-left text-sm text-slate-400">
|
||||
510→ <th className="px-4 py-3 font-medium">Task Name</th>
|
||||
511→ <th className="px-4 py-3 font-medium">Status</th>
|
||||
512→ <th className="px-4 py-3 font-medium">Priority</th>
|
||||
513→ <th className="px-4 py-3 font-medium">Created</th>
|
||||
514→ </tr>
|
||||
515→ </thead>
|
||||
516→ <tbody>
|
||||
517→ {isLoading && tasks.length === 0 ? (
|
||||
518→ <tr>
|
||||
519→ <td colSpan={4} className="px-4 py-8 text-center text-slate-500">
|
||||
520→ Loading...
|
||||
521→ </td>
|
||||
522→ </tr>
|
||||
523→ ) : paginatedTasks.length === 0 ? (
|
||||
524→ <tr>
|
||||
525→ <td colSpan={4} className="px-4 py-8 text-center text-slate-500">
|
||||
526→ No tasks found
|
||||
527→ </td>
|
||||
528→ </tr>
|
||||
529→ ) : (
|
||||
530→ paginatedTasks.map((task) => (
|
||||
531→ <tr
|
||||
532→ key={task.id}
|
||||
533→ onClick={() => handleSelectTask(task)}
|
||||
534→ className={`border-t border-slate-700 hover:bg-slate-700/50 cursor-pointer ${
|
||||
535→ selectedTask?.id === task.id ? 'bg-slate-700' : ''
|
||||
536→ }`}
|
||||
537→ >
|
||||
538→ <td className="px-4 py-3">
|
||||
539→ <div className="font-medium">{task.name}</div>
|
||||
540→ {task.description && (
|
||||
541→ <div className="text-sm text-slate-400 truncate max-w-md">
|
||||
542→ {task.description}
|
||||
543→ </div>
|
||||
544→ )}
|
||||
545→ </td>
|
||||
546→ <td className="px-4 py-3">
|
||||
547→ <span className={`badge ${
|
||||
548→ task.status === 'backlog' ? 'badge-backlog' :
|
||||
549→ task.status === 'in_progress' ? 'badge-in-progress' :
|
||||
550→ task.status === 'qa' ? 'badge-qa' :
|
||||
551→ task.status === 'done' ? 'badge-done' :
|
||||
552→ 'badge-blocked'
|
||||
553→ }`}>
|
||||
554→ {STATUS_LABELS[task.status] || task.status}
|
||||
555→ </span>
|
||||
556→ </td>
|
||||
557→ <td className="px-4 py-3">
|
||||
558→ <span className={`text-sm ${
|
||||
559→ task.priority === 3 ? 'text-destructive' :
|
||||
560→ task.priority === 2 ? 'text-warning' :
|
||||
561→ task.priority === 1 ? 'text-accent' :
|
||||
562→ 'text-slate-400'
|
||||
563→ }`}>
|
||||
564→ {PRIORITY_LABELS[task.priority] || 'Unknown'}
|
||||
565→ </span>
|
||||
566→ </td>
|
||||
567→ <td className="px-4 py-3 text-sm text-slate-400">
|
||||
568→ {new Date(task.created_at).toLocaleDateString()}
|
||||
569→ </td>
|
||||
570→ </tr>
|
||||
571→ ))
|
||||
572→ )}
|
||||
573→ </tbody>
|
||||
574→ </table>
|
||||
575→ </div>
|
||||
576→
|
||||
577→ {/* Pagination Controls */}
|
||||
578→ {totalPages > 1 && (
|
||||
579→ <div className="flex items-center justify-between mt-4 px-2">
|
||||
580→ <div className="text-sm text-slate-400">
|
||||
581→ Showing {((currentPage - 1) * ITEMS_PER_PAGE) + 1} to {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedTasks.length)} of {filteredAndSortedTasks.length} tasks
|
||||
582→ </div>
|
||||
583→ <div className="flex items-center gap-2">
|
||||
584→ <button
|
||||
585→ onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
586→ disabled={currentPage === 1}
|
||||
587→ className="p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
588→ >
|
||||
589→ <ChevronLeft size={18} />
|
||||
590→ </button>
|
||||
591→ <div className="flex items-center gap-1">
|
||||
592→ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
||||
593→ <button
|
||||
594→ key={page}
|
||||
595→ onClick={() => setCurrentPage(page)}
|
||||
596→ className={`w-8 h-8 rounded-lg text-sm ${
|
||||
597→ page === currentPage
|
||||
598→ ? 'bg-accent text-white'
|
||||
599→ : 'bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700'
|
||||
600→ }`}
|
||||
601→ >
|
||||
602→ {page}
|
||||
603→ </button>
|
||||
604→ ))}
|
||||
605→ </div>
|
||||
606→ <button
|
||||
607→ onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
608→ disabled={currentPage === totalPages}
|
||||
609→ className="p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
610→ >
|
||||
611→ <ChevronRight size={18} />
|
||||
612→ </button>
|
||||
613→ </div>
|
||||
614→ </div>
|
||||
615→ )}
|
||||
616→ </div>
|
||||
617→ ) : (
|
||||
618→ /* Kanban Columns */
|
||||
619→ <div className={`flex-1 flex gap-4 overflow-x-auto pb-4 ${selectedTask ? 'mr-0' : ''}`}>
|
||||
620→ {COLUMNS.map((column) => (
|
||||
621→ <div
|
||||
622→ key={column.id}
|
||||
623→ className={`flex-shrink-0 w-72 bg-slate-800 rounded-lg border-t-2 ${column.color}`}
|
||||
624→ onDragOver={handleDragOver}
|
||||
625→ onDrop={() => handleDrop(column.id)}
|
||||
626→ >
|
||||
627→ {/* Column Header */}
|
||||
628→ <div className="p-4 border-b border-slate-700">
|
||||
629→ <div className="flex items-center justify-between">
|
||||
630→ <h2 className="font-semibold">{column.title}</h2>
|
||||
631→ <span className="text-sm text-slate-400 bg-slate-700 px-2 py-0.5 rounded">
|
||||
632→ {getTasksByStatus(column.id).length}
|
||||
633→ </span>
|
||||
634→ </div>
|
||||
635→ </div>
|
||||
636→
|
||||
637→ {/* Column Content */}
|
||||
638→ <div className="p-3 space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
639→ {isLoading && tasks.length === 0 ? (
|
||||
640→ <div className="text-center py-8 text-slate-500 text-sm">
|
||||
641→ Loading...
|
||||
642→ </div>
|
||||
643→ ) : getTasksByStatus(column.id).length === 0 ? (
|
||||
644→ <div className="text-center py-8 text-slate-500 text-sm">
|
||||
645→ No tasks
|
||||
646→ </div>
|
||||
647→ ) : (
|
||||
648→ getTasksByStatus(column.id).map((task) => (
|
||||
649→ <div
|
||||
650→ key={task.id}
|
||||
651→ draggable
|
||||
652→ onDragStart={() => handleDragStart(task.id)}
|
||||
653→ onClick={() => handleSelectTask(task)}
|
||||
654→ className={`kanban-card bg-slate-900 rounded-lg p-4 border border-slate-700 cursor-pointer hover:border-slate-600 ${
|
||||
655→ draggedTask === task.id ? 'opacity-50' : ''
|
||||
656→ } ${selectedTask?.id === task.id ? 'ring-2 ring-accent' : ''}`}
|
||||
657→ >
|
||||
658→ <div className="flex items-start gap-2">
|
||||
659→ <GripVertical size={16} className="text-slate-600 mt-0.5 flex-shrink-0" />
|
||||
660→ <div className="flex-1 min-w-0">
|
||||
661→ <h3 className="font-medium mb-1">{task.name}</h3>
|
||||
662→ {task.description && (
|
||||
663→ <p className="text-sm text-slate-400 line-clamp-2 mb-2">
|
||||
664→ {task.description}
|
||||
665→ </p>
|
||||
666→ )}
|
||||
667→ <div className="flex items-center justify-between">
|
||||
668→ <span
|
||||
669→ className={`badge ${
|
||||
670→ column.id === 'backlog'
|
||||
671→ ? 'badge-backlog'
|
||||
672→ : column.id === 'in_progress'
|
||||
673→ ? 'badge-in-progress'
|
||||
674→ : column.id === 'qa'
|
||||
675→ ? 'badge-qa'
|
||||
676→ : column.id === 'done'
|
||||
677→ ? 'badge-done'
|
||||
678→ : 'badge-blocked'
|
||||
679→ }`}
|
||||
680→ >
|
||||
681→ {column.title}
|
||||
682→ </span>
|
||||
683→ <span className={`text-xs ${
|
||||
684→ task.priority === 3 ? 'text-destructive' :
|
||||
685→ task.priority === 2 ? 'text-warning' :
|
||||
686→ task.priority === 1 ? 'text-accent' :
|
||||
687→ 'text-slate-500'
|
||||
688→ }`}>
|
||||
689→ {task.priority === 3 ? 'Critical' :
|
||||
690→ task.priority === 2 ? 'High' :
|
||||
691→ task.priority === 1 ? 'Medium' : 'Low'}
|
||||
692→ </span>
|
||||
693→ </div>
|
||||
694→ </div>
|
||||
695→ </div>
|
||||
696→ </div>
|
||||
697→ ))
|
||||
698→ )}
|
||||
699→ </div>
|
||||
700→ </div>
|
||||
701→ ))}
|
||||
702→ </div>
|
||||
703→ )}
|
||||
704→
|
||||
705→ {/* Task Details Panel */}
|
||||
706→ {selectedTask && (
|
||||
707→ <div className="w-96 flex-shrink-0 bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
||||
708→ <div className="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
709→ <h2 className="font-semibold text-lg">Task Details</h2>
|
||||
710→ <div className="flex items-center gap-2">
|
||||
711→ {!isEditing && (
|
||||
712→ <>
|
||||
713→ <button
|
||||
714→ onClick={handleStartEdit}
|
||||
715→ className="text-slate-400 hover:text-accent transition-colors"
|
||||
716→ title="Edit Task"
|
||||
717→ >
|
||||
718→ <Pencil size={18} />
|
||||
719→ </button>
|
||||
720→ <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
721→ <AlertDialog.Trigger asChild>
|
||||
722→ <button
|
||||
723→ className="text-slate-400 hover:text-destructive transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
724→ title="Delete Task"
|
||||
725→ disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
|
||||
726→ >
|
||||
727→ <Trash2 size={18} />
|
||||
728→ </button>
|
||||
729→ </AlertDialog.Trigger>
|
||||
730→ <AlertDialog.Portal>
|
||||
731→ <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
|
||||
732→ <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">
|
||||
733→ <AlertDialog.Title className="text-xl font-semibold text-white">
|
||||
734→ Delete Task
|
||||
735→ </AlertDialog.Title>
|
||||
736→ <AlertDialog.Description className="mt-3 text-slate-400">
|
||||
737→ Are you sure you want to delete <span className="font-semibold text-white">{selectedTask.name}</span>?
|
||||
738→ This action cannot be undone.
|
||||
739→ </AlertDialog.Description>
|
||||
740→ <div className="mt-6 flex justify-end gap-3">
|
||||
741→ <AlertDialog.Cancel asChild>
|
||||
742→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
|
||||
743→ Cancel
|
||||
744→ </button>
|
||||
745→ </AlertDialog.Cancel>
|
||||
746→ <AlertDialog.Action asChild>
|
||||
747→ <button
|
||||
748→ onClick={handleDeleteTask}
|
||||
749→ disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
|
||||
750→ className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
751→ >
|
||||
752→ {isDeleting || (selectedTask && isTaskDeleting(selectedTask.id)) ? 'Deleting...' : 'Delete Task'}
|
||||
753→ </button>
|
||||
754→ </AlertDialog.Action>
|
||||
755→ </div>
|
||||
756→ </AlertDialog.Content>
|
||||
757→ </AlertDialog.Portal>
|
||||
758→ </AlertDialog.Root>
|
||||
759→ </>
|
||||
760→ )}
|
||||
761→ <button
|
||||
762→ onClick={handleClosePanel}
|
||||
763→ className="text-slate-400 hover:text-white transition-colors"
|
||||
764→ title="Close"
|
||||
765→ >
|
||||
766→ <X size={20} />
|
||||
767→ </button>
|
||||
768→ </div>
|
||||
769→ </div>
|
||||
770→ <div className="p-4 space-y-4 overflow-y-auto max-h-[calc(100vh-300px)]">
|
||||
771→ {isEditing ? (
|
||||
772→ /* Edit Form */
|
||||
773→ <div className="space-y-4">
|
||||
774→ <div>
|
||||
775→ <label className="block text-sm text-slate-400 mb-1">Name *</label>
|
||||
776→ <input
|
||||
777→ type="text"
|
||||
778→ value={editName}
|
||||
779→ onChange={(e) => setEditName(e.target.value)}
|
||||
780→ 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 focus:border-transparent"
|
||||
781→ placeholder="Task name"
|
||||
782→ />
|
||||
783→ </div>
|
||||
784→ <div>
|
||||
785→ <label className="block text-sm text-slate-400 mb-1">Description</label>
|
||||
786→ <textarea
|
||||
787→ value={editDescription}
|
||||
788→ onChange={(e) => setEditDescription(e.target.value)}
|
||||
789→ rows={3}
|
||||
790→ 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 focus:border-transparent resize-none"
|
||||
791→ placeholder="Task description"
|
||||
792→ />
|
||||
793→ </div>
|
||||
794→ <div>
|
||||
795→ <label className="block text-sm text-slate-400 mb-1">Priority</label>
|
||||
796→ <select
|
||||
797→ value={editPriority}
|
||||
798→ onChange={(e) => setEditPriority(Number(e.target.value))}
|
||||
799→ 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 focus:border-transparent"
|
||||
800→ >
|
||||
801→ <option value={0}>Low</option>
|
||||
802→ <option value={1}>Medium</option>
|
||||
803→ <option value={2}>High</option>
|
||||
804→ <option value={3}>Critical</option>
|
||||
805→ </select>
|
||||
806→ </div>
|
||||
807→ <div className="flex gap-2 pt-2">
|
||||
808→ <button
|
||||
809→ onClick={handleCancelEdit}
|
||||
810→ className="flex-1 px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
|
||||
811→ >
|
||||
812→ Cancel
|
||||
813→ </button>
|
||||
814→ <button
|
||||
815→ onClick={handleSaveEdit}
|
||||
816→ disabled={isSaving || !editName.trim()}
|
||||
817→ className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
818→ >
|
||||
819→ {isSaving ? 'Saving...' : <><Check size={16} /> Save</>}
|
||||
820→ </button>
|
||||
821→ </div>
|
||||
822→ </div>
|
||||
823→ ) : (
|
||||
824→ /* View Mode */
|
||||
825→ <>
|
||||
826→ {/* Task Name */}
|
||||
827→ <div>
|
||||
828→ <label className="block text-sm text-slate-400 mb-1">Name</label>
|
||||
829→ <p className="font-medium text-lg break-words">{selectedTask.name}</p>
|
||||
830→ </div>
|
||||
831→
|
||||
832→ {/* Description */}
|
||||
833→ <div>
|
||||
834→ <label className="block text-sm text-slate-400 mb-1">Description</label>
|
||||
835→ <p className="text-slate-300">
|
||||
836→ {selectedTask.description || <span className="italic text-slate-500">No description</span>}
|
||||
837→ </p>
|
||||
838→ </div>
|
||||
839→
|
||||
840→ {/* Status */}
|
||||
841→ <div>
|
||||
842→ <label className="block text-sm text-slate-400 mb-1">Status</label>
|
||||
843→ <span className={`badge ${
|
||||
844→ selectedTask.status === 'backlog' ? 'badge-backlog' :
|
||||
845→ selectedTask.status === 'in_progress' ? 'badge-in-progress' :
|
||||
846→ selectedTask.status === 'qa' ? 'badge-qa' :
|
||||
847→ selectedTask.status === 'done' ? 'badge-done' :
|
||||
848→ 'badge-blocked'
|
||||
849→ }`}>
|
||||
850→ {STATUS_LABELS[selectedTask.status] || selectedTask.status}
|
||||
851→ </span>
|
||||
852→ </div>
|
||||
853→
|
||||
854→ {/* Priority */}
|
||||
855→ <div>
|
||||
856→ <label className="block text-sm text-slate-400 mb-1">Priority</label>
|
||||
857→ <span className={`font-medium ${
|
||||
858→ selectedTask.priority === 3 ? 'text-destructive' :
|
||||
859→ selectedTask.priority === 2 ? 'text-warning' :
|
||||
860→ selectedTask.priority === 1 ? 'text-accent' :
|
||||
861→ 'text-slate-400'
|
||||
862→ }`}>
|
||||
863→ {PRIORITY_LABELS[selectedTask.priority] || 'Unknown'}
|
||||
864→ </span>
|
||||
865→ </div>
|
||||
866→
|
||||
867→ {/* Agent Assignment */}
|
||||
868→ <div>
|
||||
869→ <label className="block text-sm text-slate-400 mb-1 flex items-center gap-1">
|
||||
870→ <User size={14} />
|
||||
871→ Agent Assignment
|
||||
872→ </label>
|
||||
873→ <p className="text-slate-300">
|
||||
874→ {selectedTask.agent_type ? (
|
||||
875→ <span className="capitalize">{selectedTask.agent_type.replace('_', ' ')}</span>
|
||||
876→ ) : (
|
||||
877→ <span className="italic text-slate-500">Not assigned</span>
|
||||
878→ )}
|
||||
879→ </p>
|
||||
880→ </div>
|
||||
881→
|
||||
882→ {/* Iteration Count */}
|
||||
883→ <div>
|
||||
884→ <label className="block text-sm text-slate-400 mb-1 flex items-center gap-1">
|
||||
885→ <RefreshCw size={14} />
|
||||
886→ Iteration Count
|
||||
887→ </label>
|
||||
888→ <p className="text-slate-300">{selectedTask.iteration_count}</p>
|
||||
889→ </div>
|
||||
890→
|
||||
891→ {/* Flagged for Review */}
|
||||
892→ <div>
|
||||
893→ <label className="block text-sm text-slate-400 mb-1 flex items-center gap-1">
|
||||
894→ <Flag size={14} />
|
||||
895→ Flagged for Review
|
||||
896→ </label>
|
||||
897→ <p className={selectedTask.flagged_for_review ? 'text-warning' : 'text-slate-300'}>
|
||||
898→ {selectedTask.flagged_for_review ? 'Yes' : 'No'}
|
||||
899→ </p>
|
||||
900→ </div>
|
||||
901→
|
||||
902→ {/* Time Estimates */}
|
||||
903→ {(selectedTask.estimated_time || selectedTask.actual_time) && (
|
||||
904→ <div>
|
||||
905→ <label className="block text-sm text-slate-400 mb-1 flex items-center gap-1">
|
||||
906→ <Clock size={14} />
|
||||
907→ Time
|
||||
908→ </label>
|
||||
909→ <div className="text-slate-300 text-sm">
|
||||
910→ {selectedTask.estimated_time && <p>Estimated: {selectedTask.estimated_time} min</p>}
|
||||
911→ {selectedTask.actual_time && <p>Actual: {selectedTask.actual_time} min</p>}
|
||||
912→ </div>
|
||||
913→ </div>
|
||||
914→ )}
|
||||
915→
|
||||
916→ {/* Created/Updated timestamps */}
|
||||
917→ <div className="pt-4 border-t border-slate-700 text-xs text-slate-500">
|
||||
918→ <p>Created: {new Date(selectedTask.created_at).toLocaleString()}</p>
|
||||
919→ <p>Updated: {new Date(selectedTask.updated_at).toLocaleString()}</p>
|
||||
920→ </div>
|
||||
921→ </>
|
||||
922→ )}
|
||||
923→ </div>
|
||||
924→ </div>
|
||||
925→ )}
|
||||
926→
|
||||
927→ {/* Unsaved Changes Warning Dialog */}
|
||||
928→ <AlertDialog.Root open={showUnsavedDialog} onOpenChange={setShowUnsavedDialog}>
|
||||
929→ <AlertDialog.Portal>
|
||||
930→ <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
|
||||
931→ <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">
|
||||
932→ <AlertDialog.Title className="text-xl font-semibold text-white">
|
||||
933→ Unsaved Changes
|
||||
934→ </AlertDialog.Title>
|
||||
935→ <AlertDialog.Description className="mt-3 text-slate-400">
|
||||
936→ You have unsaved changes. Are you sure you want to discard them?
|
||||
937→ </AlertDialog.Description>
|
||||
938→ <div className="mt-6 flex justify-end gap-3">
|
||||
939→ <AlertDialog.Cancel asChild>
|
||||
940→ <button
|
||||
941→ onClick={handleKeepEditing}
|
||||
942→ className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
|
||||
943→ >
|
||||
944→ Keep Editing
|
||||
945→ </button>
|
||||
946→ </AlertDialog.Cancel>
|
||||
947→ <AlertDialog.Action asChild>
|
||||
948→ <button
|
||||
949→ onClick={handleDiscardChanges}
|
||||
950→ className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors"
|
||||
951→ >
|
||||
952→ Discard Changes
|
||||
953→ </button>
|
||||
954→ </AlertDialog.Action>
|
||||
955→ </div>
|
||||
956→ </AlertDialog.Content>
|
||||
957→ </AlertDialog.Portal>
|
||||
958→ </AlertDialog.Root>
|
||||
959→ </div>
|
||||
960→ </div>
|
||||
961→ )
|
||||
962→}
|
||||
963→
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user