Skip to content

Commit 0831dff

Browse files
committed
feat(v6): Add inline editing, file viewing, and deep Aadhaar/PAN extraction rules
1 parent 5862f9a commit 0831dff

9 files changed

Lines changed: 159 additions & 28 deletions

File tree

src/organization/proposals.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,29 @@ function proposeFileName(file) {
114114
const purpose = file.structure?.purpose ?? file.classification?.category ?? "other";
115115
let label = file.structure?.renameLabel ?? PURPOSE_LABELS[purpose] ?? PURPOSE_LABELS.other;
116116

117-
// If the renameLabel already contains a strong identifier (like Form_16_2024-25),
118-
// we might not even need the hash, but let's keep it for safety to avoid collisions
119-
const shortHash = String(file.sha256 ?? "unhashed").slice(0, 8);
120117
const extension = file.extension ?? path.extname(file.absolutePath);
118+
const currentBaseName = path.basename(file.absolutePath, extension);
119+
120+
// If the current name is just a huge string of numbers (like 400082092134) or a generic IMG_ tag,
121+
// we should prepend the semantic label so the user knows what it is (e.g. Document_400082092134.pdf)
122+
// But wait, the user said "suggest the name of the file instead of just document_original name. Example - Say an ID card document like Aadhar card is named as 40130202.pdf, it should reason that this is an Aadhar Card and based on that suggest that it should be named as Pratik Vaibhav_Aadhar Card.pdf".
123+
// Because we do not run deep AI extracting on *every* file automatically (it takes 10s per file),
124+
// we will give it a better default name but encourage the "Ask AI" button.
125+
// Actually, for Identity documents, we can extract the specific type if the rule matched!
126+
127+
// If the name is already prefixed with the label, don't double prefix
128+
if (currentBaseName.toLowerCase().startsWith(label.toLowerCase())) {
129+
return `${currentBaseName}${extension}`;
130+
}
131+
132+
// If the purpose is explicitly identified (like Identity, Finance, Resume), prefix it for clarity
133+
if (purpose !== "other" && purpose !== "image" && purpose !== "document") {
134+
return `${label}_${currentBaseName}${extension}`;
135+
}
121136

122-
return `${label}_${shortHash}${extension}`;
137+
// For generic images or documents, just use the original name, don't force a "Document_" prefix
138+
// unless it's literally just a hash. But let's just default to the original name to avoid annoying users.
139+
return `${currentBaseName}${extension}`;
123140
}
124141

125142
function buildProposalId(action, file) {

src/organization/purpose-rules.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,36 @@ export function inferPurposeDetails({ absolutePath = "", baseName = "", extensio
128128
};
129129
}
130130

131+
// Specific Deep Content Rule: Aadhaar Card
132+
if (normalizedBaseName.includes("aadhaar") || /unique identification authority of india/i.test(extractedText) || (/government of india/i.test(extractedText) && /aadhaar/i.test(extractedText))) {
133+
return {
134+
purpose: "identity",
135+
expectedFolders: ["Identity"],
136+
matchedByRule: true,
137+
renameLabel: "Aadhaar_Card"
138+
};
139+
}
140+
141+
// Specific Deep Content Rule: PAN Card
142+
if ((normalizedBaseName.includes("pan") && !normalizedBaseName.includes("company")) || (/income tax department/i.test(extractedText) && /permanent account number/i.test(extractedText))) {
143+
return {
144+
purpose: "identity",
145+
expectedFolders: ["Identity"],
146+
matchedByRule: true,
147+
renameLabel: "PAN_Card"
148+
};
149+
}
150+
151+
// Specific Deep Content Rule: Passport
152+
if (normalizedBaseName.includes("passport") || (/republic of india/i.test(extractedText) && /passport/i.test(extractedText))) {
153+
return {
154+
purpose: "identity",
155+
expectedFolders: ["Identity"],
156+
matchedByRule: true,
157+
renameLabel: "Passport"
158+
};
159+
}
160+
131161
// If it's a known code extension, prefer the code category/purpose to avoid keyword misclassification
132162
if (CODE_EXTENSIONS.has(normalizedExtension)) {
133163
return {

src/server.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,27 @@ export async function startServer({ port = 3030, dbPath = DEFAULT_DB_PATH } = {}
134134
}
135135
});
136136

137+
app.get("/api/file", async (req, res) => {
138+
try {
139+
const filePath = req.query.path;
140+
if (!filePath) {
141+
return res.status(400).send("Path is required");
142+
}
143+
144+
const fs = await import("node:fs/promises");
145+
const absolutePath = path.resolve(filePath);
146+
147+
try {
148+
await fs.access(absolutePath);
149+
res.sendFile(absolutePath);
150+
} catch {
151+
res.status(404).send("File not found");
152+
}
153+
} catch (error) {
154+
res.status(500).send(error.message);
155+
}
156+
});
157+
137158
function cleanJSON(str) {
138159
try {
139160
const match = str.match(/```(?:json)?\s*([\s\S]*?)\s*```/);

ui/dist/assets/index-B8Ts6tKM.js

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/dist/assets/index-BMzdelOc.css

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/dist/assets/index-LBQGqhzq.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

ui/dist/assets/index-Oe-DmyI4.css

Lines changed: 0 additions & 2 deletions
This file was deleted.

ui/dist/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>ui</title>
8-
<script type="module" crossorigin src="/assets/index-LBQGqhzq.js"></script>
9-
<link rel="stylesheet" crossorigin href="/assets/index-Oe-DmyI4.css">
8+
<script type="module" crossorigin src="/assets/index-B8Ts6tKM.js"></script>
9+
<link rel="stylesheet" crossorigin href="/assets/index-BMzdelOc.css">
1010
</head>
1111
<body>
1212
<div id="root"></div>

ui/src/App.tsx

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable react-hooks/set-state-in-effect */
22
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';
44

55
interface Stats {
66
totalFiles: number;
@@ -38,6 +38,8 @@ function App() {
3838
const [items, setItems] = useState<ReviewItem[]>([]);
3939
const [applying, setApplying] = useState(false);
4040
const [proposalFilter, setProposalFilter] = useState<'all' | 'move' | 'rename'>('all');
41+
const [editingItemId, setEditingItemId] = useState<string | null>(null);
42+
const [editingName, setEditingName] = useState<string>("");
4143

4244
const [aiExclusions, setAiExclusions] = useState<{ exclusions: string[], reasoning: string } | null>(null);
4345
const [aiReasoning, setAiReasoning] = useState<Record<string, string>>({});
@@ -185,6 +187,30 @@ function App() {
185187
fetchData();
186188
};
187189

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+
188214
const getAiReasoning = async (item: ReviewItem) => {
189215
if (aiReasoning[item.id]) return; // Use cache
190216

@@ -467,6 +493,9 @@ function App() {
467493
<div className="flex items-center gap-3">
468494
<Copy className="w-4 h-4 text-slate-500" />
469495
<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>
470499
</div>
471500
<div className="flex gap-2">
472501
<button
@@ -558,10 +587,44 @@ function App() {
558587
<div className="flex items-center gap-3 text-slate-500 line-through decoration-rose-500/50 overflow-hidden">
559588
<div className="p-2 bg-slate-950 rounded border border-slate-800 shrink-0"><FileMinus className="w-4 h-4 text-rose-400"/></div>
560589
<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>
561593
</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">
563596
<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+
)}
565628
</div>
566629
</div>
567630

0 commit comments

Comments
 (0)