|
1 | 1 | /* eslint-disable react-hooks/set-state-in-effect */ |
2 | 2 | import { useState, useEffect, useRef } from 'react'; |
3 | | -import { Layout, Copy, Wand2, CheckCircle, ArrowRight, RefreshCw, FolderSearch, Cloud, Sparkles, KeyRound, Trash2, Info, FileMinus, FilePlus, FolderOpen } from 'lucide-react'; |
| 3 | +import { Layout, Copy, Wand2, CheckCircle, ArrowRight, RefreshCw, FolderSearch, Cloud, Sparkles, KeyRound, Trash2, Info, FileMinus, FilePlus, FolderOpen, Eye, Edit2, Check, X } from 'lucide-react'; |
4 | 4 |
|
5 | 5 | interface Stats { |
6 | 6 | totalFiles: number; |
@@ -38,6 +38,8 @@ function App() { |
38 | 38 | const [items, setItems] = useState<ReviewItem[]>([]); |
39 | 39 | const [applying, setApplying] = useState(false); |
40 | 40 | const [proposalFilter, setProposalFilter] = useState<'all' | 'move' | 'rename'>('all'); |
| 41 | + const [editingItemId, setEditingItemId] = useState<string | null>(null); |
| 42 | + const [editingName, setEditingName] = useState<string>(""); |
41 | 43 |
|
42 | 44 | const [aiExclusions, setAiExclusions] = useState<{ exclusions: string[], reasoning: string } | null>(null); |
43 | 45 | const [aiReasoning, setAiReasoning] = useState<Record<string, string>>({}); |
@@ -185,6 +187,30 @@ function App() { |
185 | 187 | fetchData(); |
186 | 188 | }; |
187 | 189 |
|
| 190 | + const handleSaveEdit = async (id: string) => { |
| 191 | + if (!editingName.trim()) return; |
| 192 | + |
| 193 | + // Optimistically update the UI |
| 194 | + setItems(prev => prev.map(i => { |
| 195 | + if (i.id === id) { |
| 196 | + return { |
| 197 | + ...i, |
| 198 | + proposedPath: i.proposedPath?.replace(/[^\\/]+$/, editingName), |
| 199 | + evidence: { ...i.evidence, proposedName: editingName } |
| 200 | + }; |
| 201 | + } |
| 202 | + return i; |
| 203 | + })); |
| 204 | + |
| 205 | + setEditingItemId(null); |
| 206 | + setEditingName(""); |
| 207 | + }; |
| 208 | + |
| 209 | + const handleCancelEdit = () => { |
| 210 | + setEditingItemId(null); |
| 211 | + setEditingName(""); |
| 212 | + }; |
| 213 | + |
188 | 214 | const getAiReasoning = async (item: ReviewItem) => { |
189 | 215 | if (aiReasoning[item.id]) return; // Use cache |
190 | 216 |
|
@@ -467,6 +493,9 @@ function App() { |
467 | 493 | <div className="flex items-center gap-3"> |
468 | 494 | <Copy className="w-4 h-4 text-slate-500" /> |
469 | 495 | <span className="text-sm font-mono text-slate-400">SHA256: {item.evidence.sha256.slice(0, 16)}...</span> |
| 496 | + <a href={`/api/file?path=${encodeURIComponent(item.subjectPath)}`} target="_blank" rel="noreferrer" className="text-sky-400 hover:text-sky-300 transition-colors ml-2" title="View File"> |
| 497 | + <Eye className="w-4 h-4" /> |
| 498 | + </a> |
470 | 499 | </div> |
471 | 500 | <div className="flex gap-2"> |
472 | 501 | <button |
@@ -558,10 +587,44 @@ function App() { |
558 | 587 | <div className="flex items-center gap-3 text-slate-500 line-through decoration-rose-500/50 overflow-hidden"> |
559 | 588 | <div className="p-2 bg-slate-950 rounded border border-slate-800 shrink-0"><FileMinus className="w-4 h-4 text-rose-400"/></div> |
560 | 589 | <span className="text-xs font-mono truncate w-full" title={item.subjectPath}>{item.subjectPath}</span> |
| 590 | + <a href={`/api/file?path=${encodeURIComponent(item.subjectPath)}`} target="_blank" rel="noreferrer" className="text-sky-400 hover:text-sky-300 transition-colors ml-auto shrink-0" title="View File"> |
| 591 | + <Eye className="w-4 h-4" /> |
| 592 | + </a> |
561 | 593 | </div> |
562 | | - <div className="flex items-center gap-3 bg-sky-500/10 p-3 rounded-xl border border-sky-500/20 overflow-hidden"> |
| 594 | + |
| 595 | + <div className="flex items-center gap-3 bg-sky-500/10 p-3 rounded-xl border border-sky-500/20 overflow-hidden group"> |
563 | 596 | <div className="p-2 bg-sky-500/20 rounded shrink-0"><FilePlus className="w-4 h-4 text-sky-400"/></div> |
564 | | - <span className="text-sm font-bold text-sky-400 truncate w-full" title={item.proposedPath || item.evidence?.proposedName}>{item.proposedPath || item.evidence?.proposedName}</span> |
| 597 | + |
| 598 | + {editingItemId === item.id ? ( |
| 599 | + <div className="flex items-center gap-2 w-full"> |
| 600 | + <input |
| 601 | + type="text" |
| 602 | + value={editingName} |
| 603 | + onChange={e => setEditingName(e.target.value)} |
| 604 | + className="flex-1 bg-slate-950 border border-sky-500 rounded px-2 py-1 text-sm font-bold text-sky-400 focus:outline-none" |
| 605 | + autoFocus |
| 606 | + onKeyDown={e => e.key === 'Enter' && handleSaveEdit(item.id)} |
| 607 | + /> |
| 608 | + <button onClick={() => handleSaveEdit(item.id)} className="p-1 hover:bg-green-500/20 rounded text-green-400"><Check className="w-4 h-4"/></button> |
| 609 | + <button onClick={handleCancelEdit} className="p-1 hover:bg-rose-500/20 rounded text-rose-400"><X className="w-4 h-4"/></button> |
| 610 | + </div> |
| 611 | + ) : ( |
| 612 | + <> |
| 613 | + <span className="text-sm font-bold text-sky-400 truncate w-full" title={item.proposedPath || item.evidence?.proposedName}> |
| 614 | + {item.proposedPath || item.evidence?.proposedName} |
| 615 | + </span> |
| 616 | + <button |
| 617 | + onClick={() => { |
| 618 | + setEditingItemId(item.id); |
| 619 | + setEditingName(item.evidence?.proposedName || item.proposedPath?.split('\\').pop()?.split('/').pop() || ""); |
| 620 | + }} |
| 621 | + className="text-slate-500 hover:text-sky-400 opacity-0 group-hover:opacity-100 transition-all shrink-0 ml-auto" |
| 622 | + title="Edit Name" |
| 623 | + > |
| 624 | + <Edit2 className="w-4 h-4" /> |
| 625 | + </button> |
| 626 | + </> |
| 627 | + )} |
565 | 628 | </div> |
566 | 629 | </div> |
567 | 630 |
|
|
0 commit comments