Skip to content

Commit de7d4d8

Browse files
committed
Fix UI paths, add view icons in duplicates, and vastly improve deep content regex for intelligent naming and routing
1 parent 316e1eb commit de7d4d8

3 files changed

Lines changed: 142 additions & 111 deletions

File tree

src/organization/proposals.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,25 +117,29 @@ function proposeFileName(file) {
117117
const extension = file.extension ?? path.extname(file.absolutePath);
118118
const currentBaseName = path.basename(file.absolutePath, extension);
119119

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
128120
if (currentBaseName.toLowerCase().startsWith(label.toLowerCase())) {
129121
return `${currentBaseName}${extension}`;
130122
}
131123

132-
// If the purpose is explicitly identified (like Identity, Finance, Resume), prefix it for clarity
133124
if (purpose !== "other" && purpose !== "image" && purpose !== "document") {
134-
return `${label}_${currentBaseName}${extension}`;
125+
const isGarbageName = /^(document|scan|img|whatsapp|signal|screenshot|untitled|\d+)[_\s\-]*\d*$/i.test(currentBaseName) || currentBaseName.length > 25;
126+
127+
let extractedName = "";
128+
if (file.extractedText) {
129+
// Try to find a human name in the text
130+
const nameMatch = file.extractedText.match(/(?:Name|Employee|Customer|Account|Mr\.|Mrs\.|Ms\.)[\s:]*([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,2})/);
131+
if (nameMatch && nameMatch[1].length > 3) {
132+
extractedName = nameMatch[1].trim() + "_";
133+
}
134+
}
135+
136+
if (isGarbageName) {
137+
return `${extractedName}${label}${extension}`.replace(/\s+/g, '_');
138+
}
139+
140+
return `${extractedName}${label}_${currentBaseName}${extension}`.replace(/\s+/g, '_');
135141
}
136142

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.
139143
return `${currentBaseName}${extension}`;
140144
}
141145

src/organization/purpose-rules.js

Lines changed: 116 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,43 @@ export const DEFAULT_FOLDERS_BY_CATEGORY = {
99
video: ["Videos"]
1010
};
1111

12+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
13+
14+
function extractDate(text, baseName) {
15+
// Try to find YYYY-MM or MM-YYYY or DD-MM-YYYY or Month YYYY
16+
const combined = (text + " " + baseName).replace(/\n/g, ' ');
17+
18+
// Look for "Month YYYY" e.g., "January 2024" or "Jan 2024"
19+
const monthNames = "(january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|october|oct|november|nov|december|dec)";
20+
const monthYearRegex = new RegExp(monthNames + "\\s*,?\\s*-?\\s*(\\d{4})", "i");
21+
let match = combined.match(monthYearRegex);
22+
if (match) {
23+
const mStr = match[1].toLowerCase().substring(0, 3);
24+
const mIndex = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"].indexOf(mStr);
25+
return { year: match[2], month: String(mIndex + 1).padStart(2, '0') + "_" + MONTHS[mIndex] };
26+
}
27+
28+
// Look for YYYY-MM or YYYY_MM
29+
match = combined.match(/20[0-2]\d[-_](0[1-9]|1[0-2])/);
30+
if (match) {
31+
const mIndex = parseInt(match[1], 10) - 1;
32+
return { year: match[0].substring(0,4), month: match[1] + "_" + MONTHS[mIndex] };
33+
}
34+
35+
// Look for DD/MM/YYYY or DD-MM-YYYY
36+
match = combined.match(/(?:^|\D)([0-3]\d)[/.-](0[1-9]|1[0-2])[/.-](20[0-2]\d)(?:\D|$)/);
37+
if (match) {
38+
const mIndex = parseInt(match[2], 10) - 1;
39+
return { year: match[3], month: match[2] + "_" + MONTHS[mIndex] };
40+
}
41+
42+
// Look for standalone year 2010-2029
43+
match = combined.match(/(?:^|\D)(20[1-2]\d)(?:\D|$)/);
44+
if (match) return { year: match[1], month: null };
45+
46+
return { year: null, month: null };
47+
}
48+
1249
export const PURPOSE_RULES = [
1350
{
1451
purpose: "resume",
@@ -22,12 +59,6 @@ export const PURPOSE_RULES = [
2259
renameLabel: "Travel_Ticket",
2360
pattern: /(^|[\s._-])(ticket|flight|train|irctc|boarding|itinerary|booking|cleartrip|makemytrip)([\s._-]|$)/i
2461
},
25-
{
26-
purpose: "finance",
27-
expectedFolders: ["Finance"],
28-
renameLabel: "Finance_Record",
29-
pattern: /(^|[\s._-])(invoice|receipt|tax-?form|pay[\s._-]?slip|salary[\s._-]?slip|epfo-?statement|bank[\s._-]?statement|account[\s._-]?statement|transactions?|nomination-?form|ctc-?letter|increment-?letter)([\s._-]|$)/i
30-
},
3162
{
3263
purpose: "insurance",
3364
expectedFolders: ["Finance/Insurance"],
@@ -72,136 +103,126 @@ export const PURPOSE_RULES = [
72103
}
73104
];
74105

75-
// Supports: _v1, -v1, .v1, (1), - Copy, - Copy (1), 2024.4, 1.2.3
76-
export const VERSION_PATTERN = /([._-]v?(\d+([._]\d+)*))|(\((\d+)\))|([- ]Copy( \((\d+)\))?)$/i;
106+
export const VERSION_PATTERN = /([._-]v?(\d+([._]\d+)*))|(\\((\d+)\\))|([- ]Copy( \\((\d+)\\))?)$/i;
77107

78108
export const CODE_EXTENSIONS = new Set([
79109
".js", ".ts", ".jsx", ".tsx", ".py", ".java", ".c", ".cpp", ".h", ".hpp", ".cs", ".go", ".rs", ".rb", ".php", ".html", ".css", ".sql", ".sh", ".bat", ".ps1", ".yml", ".yaml", ".json", ".xml", ".md", ".sol"
80110
]);
81111

82112
export function inferPurposeDetails({ absolutePath = "", baseName = "", extension = "", category = "other", extractedText = "", isEntity = false, entityType = "" }) {
83-
// If it's a cohesive entity, categorize it immediately to prevent fragmentation
84113
if (isEntity) {
85-
if (entityType === "software_project") {
86-
return {
87-
purpose: "code",
88-
expectedFolders: ["Projects"],
89-
matchedByRule: true,
90-
renameLabel: "Project"
91-
};
92-
}
93-
if (entityType === "application") {
94-
return {
95-
purpose: "installer",
96-
expectedFolders: ["Applications"],
97-
matchedByRule: true,
98-
renameLabel: "App"
99-
};
100-
}
114+
if (entityType === "software_project") return { purpose: "code", expectedFolders: ["Projects"], matchedByRule: true, renameLabel: "Project" };
115+
if (entityType === "application") return { purpose: "installer", expectedFolders: ["Applications"], matchedByRule: true, renameLabel: "App" };
101116
}
102117

103118
const normalizedBaseName = String(baseName || path.basename(absolutePath)).toLowerCase();
104119
const normalizedExtension = String(extension || path.extname(absolutePath)).toLowerCase();
105120

106-
// Specific Deep Content Rule: Form 16 Segregation
107-
if (normalizedBaseName.includes("f16") || normalizedBaseName.includes("form 16") || /form\s*no\.?\s*16/i.test(extractedText)) {
108-
// Try to extract Assessment Year from text (e.g. "Assessment Year: 2024-25" or "Assessment Year 2024-25")
109-
let yearMatch = extractedText.match(/Assessment Year[:\s]*(\d{4}-\d{2})/i) || extractedText.match(/(\d{4}-\d{2})/);
110-
// Fallback to year in filename
111-
if (!yearMatch) {
112-
yearMatch = normalizedBaseName.match(/(\d{4}-\d{2})/);
121+
const dateInfo = extractDate(extractedText, normalizedBaseName);
122+
123+
// Specific Deep Content Rule: Pay Slips
124+
if (/pay[\s._-]?slip/i.test(normalizedBaseName) || /salary[\s._-]?slip/i.test(normalizedBaseName) || /pay\s*slip/i.test(extractedText) || /salary\s*slip/i.test(extractedText)) {
125+
let folder = "Finance/Pay_Slips";
126+
let label = "Pay_Slip";
127+
if (dateInfo.year) {
128+
folder += `/${dateInfo.year}`;
129+
label += `_${dateInfo.year}`;
130+
if (dateInfo.month) {
131+
folder += `/${dateInfo.month}`;
132+
label += `_${dateInfo.month.split('_')[1]}`;
133+
}
113134
}
114-
115-
let folder = "Finance/Form_16";
135+
return { purpose: "finance", expectedFolders: [folder], matchedByRule: true, renameLabel: label };
136+
}
137+
138+
// Specific Deep Content Rule: Form 16 / IT Returns
139+
if (normalizedBaseName.includes("f16") || normalizedBaseName.includes("form 16") || /form\s*no\\.?\s*16/i.test(extractedText) || /income\s*tax\s*return/i.test(extractedText) || /it\s*computation/i.test(extractedText)) {
140+
let yearMatch = extractedText.match(/Assessment Year[:\s]*(\d{4}-\d{2})/i) || extractedText.match(/(\d{4}-\d{2})/);
141+
if (!yearMatch) yearMatch = normalizedBaseName.match(/(\d{4}-\d{2})/);
142+
let folder = "Finance/IT_Returns";
143+
let label = normalizedBaseName.includes("form") || /form/i.test(extractedText) ? "Form_16" : "IT_Computation";
116144
if (yearMatch) {
117-
// Convert '2024-25' to '2024' or keep '2024-25'
118-
const yearStr = yearMatch[1];
119-
const startYear = yearStr.split('-')[0];
120-
folder = `Finance/Form_16/${startYear}`;
145+
const startYear = yearMatch[1].split('-')[0];
146+
folder += `/${startYear}`;
147+
label += `_${startYear}`;
148+
} else if (dateInfo.year) {
149+
folder += `/${dateInfo.year}`;
150+
label += `_${dateInfo.year}`;
121151
}
152+
return { purpose: "finance", expectedFolders: [folder], matchedByRule: true, renameLabel: label };
153+
}
122154

123-
return {
124-
purpose: "finance",
125-
expectedFolders: [folder],
126-
matchedByRule: true,
127-
renameLabel: "Form_16"
128-
};
155+
// Specific Deep Content Rule: EPF Statement
156+
if (/epf/i.test(normalizedBaseName) || /epfo/i.test(normalizedBaseName) || /provident\s*fund/i.test(extractedText) || /epfo\s*statement/i.test(extractedText)) {
157+
let folder = "Finance/EPF";
158+
let label = "EPF_Statement";
159+
if (dateInfo.year) {
160+
folder += `/${dateInfo.year}`;
161+
label += `_${dateInfo.year}`;
162+
}
163+
return { purpose: "finance", expectedFolders: [folder], matchedByRule: true, renameLabel: label };
129164
}
130165

131166
// Specific Deep Content Rule: Bank Statement
132167
if (normalizedBaseName.includes("statement") || /account\s*statement/i.test(extractedText) || /bank\s*statement/i.test(extractedText) || (/account\s*summary/i.test(extractedText) && /balance/i.test(extractedText))) {
133-
return {
134-
purpose: "finance",
135-
expectedFolders: ["Finance/Bank_Statements"],
136-
matchedByRule: true,
137-
renameLabel: "Bank_Statement"
138-
};
168+
let folder = "Finance/Bank_Statements";
169+
let label = "Bank_Statement";
170+
171+
// Try to extract bank name
172+
let bankName = "";
173+
if (/hdfc/i.test(extractedText) || /hdfc/i.test(normalizedBaseName)) bankName = "HDFC";
174+
else if (/sbi/i.test(extractedText) || /state bank/i.test(extractedText) || /sbi/i.test(normalizedBaseName)) bankName = "SBI";
175+
else if (/icici/i.test(extractedText) || /icici/i.test(normalizedBaseName)) bankName = "ICICI";
176+
else if (/axis/i.test(extractedText) || /axis/i.test(normalizedBaseName)) bankName = "Axis";
177+
178+
if (bankName) label = `${bankName}_Statement`;
179+
180+
if (dateInfo.year) {
181+
folder += `/${dateInfo.year}`;
182+
label += `_${dateInfo.year}`;
183+
if (dateInfo.month) {
184+
folder += `/${dateInfo.month}`;
185+
label += `_${dateInfo.month.split('_')[1]}`;
186+
}
187+
}
188+
return { purpose: "finance", expectedFolders: [folder], matchedByRule: true, renameLabel: label };
139189
}
140190

141191
// Specific Deep Content Rule: Aadhaar Card
142192
if (normalizedBaseName.includes("aadhaar") || /unique identification authority of india/i.test(extractedText) || (/government of india/i.test(extractedText) && /aadhaar/i.test(extractedText))) {
143-
return {
144-
purpose: "identity",
145-
expectedFolders: ["Identity"],
146-
matchedByRule: true,
147-
renameLabel: "Aadhaar_Card"
148-
};
193+
return { purpose: "identity", expectedFolders: ["Identity/Aadhaar"], matchedByRule: true, renameLabel: "Aadhaar_Card" };
149194
}
150195

151196
// Specific Deep Content Rule: PAN Card
152197
if ((normalizedBaseName.includes("pan") && !normalizedBaseName.includes("company")) || (/income tax department/i.test(extractedText) && /permanent account number/i.test(extractedText))) {
153-
return {
154-
purpose: "identity",
155-
expectedFolders: ["Identity"],
156-
matchedByRule: true,
157-
renameLabel: "PAN_Card"
158-
};
198+
return { purpose: "identity", expectedFolders: ["Identity/PAN"], matchedByRule: true, renameLabel: "PAN_Card" };
159199
}
160200

161201
// Specific Deep Content Rule: Passport
162202
if (normalizedBaseName.includes("passport") || (/republic of india/i.test(extractedText) && /passport/i.test(extractedText))) {
163-
return {
164-
purpose: "identity",
165-
expectedFolders: ["Identity"],
166-
matchedByRule: true,
167-
renameLabel: "Passport"
168-
};
203+
return { purpose: "identity", expectedFolders: ["Identity/Passport"], matchedByRule: true, renameLabel: "Passport" };
204+
}
205+
206+
// Specific Deep Content Rule: Offer Letter
207+
if (/offer[\s._-]?letter/i.test(normalizedBaseName) || /appointment[\s._-]?letter/i.test(normalizedBaseName) || (/offer of employment/i.test(extractedText))) {
208+
let label = "Offer_Letter";
209+
if (dateInfo.year) label += `_${dateInfo.year}`;
210+
return { purpose: "legal", expectedFolders: ["Legal/Offer_Letters"], matchedByRule: true, renameLabel: label };
169211
}
170212

171-
// If it's a known code extension, prefer the code category/purpose to avoid keyword misclassification
172213
if (CODE_EXTENSIONS.has(normalizedExtension)) {
173-
return {
174-
purpose: "code",
175-
expectedFolders: DEFAULT_FOLDERS_BY_CATEGORY.code,
176-
matchedByRule: false,
177-
renameLabel: null
178-
};
214+
return { purpose: "code", expectedFolders: DEFAULT_FOLDERS_BY_CATEGORY.code, matchedByRule: false, renameLabel: null };
179215
}
180216

181217
for (const rule of PURPOSE_RULES) {
182218
if (rule.pattern.test(normalizedBaseName)) {
183-
return {
184-
purpose: rule.purpose,
185-
expectedFolders: rule.expectedFolders,
186-
matchedByRule: true,
187-
renameLabel: rule.renameLabel
188-
};
219+
return { purpose: rule.purpose, expectedFolders: rule.expectedFolders, matchedByRule: true, renameLabel: rule.renameLabel };
189220
}
190221
}
191222

192223
if ([".exe", ".msi", ".dmg", ".pkg", ".appx"].includes(normalizedExtension)) {
193-
return {
194-
purpose: "installer",
195-
expectedFolders: ["Installers"],
196-
matchedByRule: true,
197-
renameLabel: "Installer"
198-
};
199-
}
200-
201-
return {
202-
purpose: category,
203-
expectedFolders: DEFAULT_FOLDERS_BY_CATEGORY[category] ?? DEFAULT_FOLDERS_BY_CATEGORY.other,
204-
matchedByRule: false,
205-
renameLabel: null
206-
};
224+
return { purpose: "installer", expectedFolders: ["Installers"], matchedByRule: true, renameLabel: "Installer" };
225+
}
226+
227+
return { purpose: category, expectedFolders: DEFAULT_FOLDERS_BY_CATEGORY[category] ?? DEFAULT_FOLDERS_BY_CATEGORY.other, matchedByRule: false, renameLabel: null };
207228
}

ui/src/App.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -518,14 +518,20 @@ function App() {
518518
</div>
519519
<div>
520520
<div className="text-xs font-bold text-green-500 uppercase tracking-widest mb-3 flex items-center gap-2"><CheckCircle className="w-4 h-4"/> Keep Original</div>
521-
<div className="p-5 bg-green-500/5 border border-green-500/20 rounded-xl text-sm font-mono break-all shadow-inner" title={item.evidence.proposedKeepPath || item.evidence.keptPath}>
521+
<div className="p-5 bg-green-500/5 border border-green-500/20 rounded-xl text-sm font-mono whitespace-normal break-all shadow-inner relative pr-10" title={item.evidence.proposedKeepPath || item.evidence.keptPath}>
522522
{(item.evidence.proposedKeepPath || item.evidence.keptPath)?.split('\\').pop()}
523+
<a href={`/api/file?path=${encodeURIComponent(item.evidence.proposedKeepPath || item.evidence.keptPath)}`} target="_blank" rel="noreferrer" className="absolute right-3 top-1/2 -translate-y-1/2 text-sky-400 hover:text-sky-300 transition-colors" title="View Keep File">
524+
<Eye className="w-5 h-5" />
525+
</a>
523526
</div>
524527
</div>
525528
<div>
526529
<div className="text-xs font-bold text-rose-500 uppercase tracking-widest mb-3 flex items-center gap-2"><Trash2 className="w-4 h-4"/> Delete Duplicate</div>
527-
<div className="p-5 bg-rose-500/5 border border-rose-500/20 rounded-xl text-sm font-mono break-all opacity-60 line-through decoration-rose-500/50 shadow-inner" title={item.evidence.proposedDeletePaths?.[0] || item.evidence.deletedPaths?.[0]}>
530+
<div className="p-5 bg-rose-500/5 border border-rose-500/20 rounded-xl text-sm font-mono whitespace-normal break-all opacity-60 line-through decoration-rose-500/50 shadow-inner relative pr-10" title={item.evidence.proposedDeletePaths?.[0] || item.evidence.deletedPaths?.[0]}>
528531
{(item.evidence.proposedDeletePaths?.[0] || item.evidence.deletedPaths?.[0])?.split('\\').pop()}
532+
<a href={`/api/file?path=${encodeURIComponent(item.evidence.proposedDeletePaths?.[0] || item.evidence.deletedPaths?.[0] || '')}`} target="_blank" rel="noreferrer" className="absolute right-3 top-1/2 -translate-y-1/2 text-sky-400 hover:text-sky-300 transition-colors opacity-100" title="View Duplicate File">
533+
<Eye className="w-5 h-5" />
534+
</a>
529535
</div>
530536
</div>
531537
</div>
@@ -586,7 +592,7 @@ function App() {
586592
<div className="flex flex-col gap-3 mb-4">
587593
<div className="flex items-center gap-3 text-slate-500 line-through decoration-rose-500/50 overflow-hidden">
588594
<div className="p-2 bg-slate-950 rounded border border-slate-800 shrink-0"><FileMinus className="w-4 h-4 text-rose-400"/></div>
589-
<span className="text-xs font-mono break-all w-full" title={item.subjectPath}>{item.subjectPath}</span>
595+
<span className="text-xs font-mono whitespace-normal break-all w-full" title={item.subjectPath}>{item.subjectPath}</span>
590596
<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">
591597
<Eye className="w-4 h-4" />
592598
</a>
@@ -610,7 +616,7 @@ function App() {
610616
</div>
611617
) : (
612618
<>
613-
<span className="text-sm font-bold text-sky-400 break-all w-full" title={item.proposedPath || item.evidence?.proposedName}>
619+
<span className="text-sm font-bold text-sky-400 whitespace-normal break-all w-full" title={item.proposedPath || item.evidence?.proposedName}>
614620
{item.proposedPath || item.evidence?.proposedName}
615621
</span>
616622
<button

0 commit comments

Comments
 (0)