AI backed processing

This commit is contained in:
2026-01-22 21:53:04 +05:30
parent 9d7109b60f
commit f50dd4692d
12 changed files with 368 additions and 33 deletions

View File

@@ -15,4 +15,12 @@ export class OcrService {
formData.append('file', file);
return this.http.post(`${this.apiUrl}/extract`, formData);
}
extractWithAI(text: string, filePath: string | null, modelType: string): Observable<any> {
return this.http.post(`http://localhost:8000/api/extract/ai`, { text, file_path: filePath, model_type: modelType });
}
saveDocument(data: any): Observable<any> {
return this.http.post(`http://localhost:8000/api/documents/save`, data);
}
}

View File

@@ -9,6 +9,11 @@ import { FileUploadModule } from 'primeng/fileupload';
import { ProgressBarModule } from 'primeng/progressbar';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ToastModule } from 'primeng/toast';
import { ButtonModule } from 'primeng/button';
import { TableModule } from 'primeng/table';
import { CardModule } from 'primeng/card';
import { RadioButtonModule } from 'primeng/radiobutton';
import { InputTextModule } from 'primeng/inputtext';
@Component({
selector: 'app-ocr',
@@ -19,16 +24,18 @@ import { ToastModule } from 'primeng/toast';
FileUploadModule,
ProgressBarModule,
InputTextareaModule,
ToastModule
ToastModule,
ButtonModule,
TableModule,
CardModule,
RadioButtonModule,
InputTextModule
],
providers: [MessageService],
template: `
<div class="card">
<h2>OCR Extraction</h2>
<!--
Note: "customUpload" mode in PrimeNG FileUpload requires "uploadHandler".
"mode='advanced'" gives the sleek UI.
-->
<p-fileUpload mode="advanced"
chooseLabel="Select PDF or Image"
uploadLabel="Extract Text"
@@ -44,15 +51,120 @@ import { ToastModule } from 'primeng/toast';
<p-progressBar mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar>
</div>
<div class="mt-4" *ngIf="extractedText !== null">
<h3>Extracted Text Result:</h3>
<textarea pInputTextarea
[autoResize]="true"
[(ngModel)]="extractedText"
readonly
class="w-full"
style="min-height: 300px; width: 100%; border-color: #d1d5db; font-family: monospace;">
</textarea>
<div class="mt-4 grid" *ngIf="extractedText !== null">
<div class="col-12 md:col-6">
<h3>Extracted Text Result:</h3>
<textarea pInputTextarea
[autoResize]="true"
[(ngModel)]="extractedText"
readonly
class="w-full"
style="min-height: 300px; width: 100%; border-color: #d1d5db; font-family: monospace;">
</textarea>
<div class="mt-3">
<div class="flex flex-column gap-2 mb-3">
<label>AI Analysis Mode:</label>
<div class="flex align-items-center">
<p-radioButton name="model" value="text" [(ngModel)]="modelType" inputId="mod1"></p-radioButton>
<label for="mod1" class="ml-2">Text Analysis (Fast - Gemma)</label>
</div>
<div class="flex align-items-center">
<p-radioButton name="model" value="vision" [(ngModel)]="modelType" inputId="mod2"></p-radioButton>
<label for="mod2" class="ml-2">Vision Analysis (Accurate - Qwen)</label>
</div>
</div>
<p-button label="Process with AI"
icon="pi pi-bolt"
[loading]="aiLoading"
(onClick)="processWithAI()">
</p-button>
</div>
</div>
<div class="col-12 md:col-6" *ngIf="aiResult">
<div class="flex justify-content-between align-items-center">
<h3>AI Analysis Result:</h3>
<p-button label="Save & Verify" icon="pi pi-check" styleClass="p-button-success" [loading]="saveLoading" (onClick)="saveDocument()"></p-button>
</div>
<p-card class="mb-3">
<div class="grid">
<div class="col-6">
<label class="block text-sm font-bold mb-1">Vendor</label>
<input pInputText [(ngModel)]="aiResult.vendor_name" class="w-full" />
</div>
<div class="col-6">
<label class="block text-sm font-bold mb-1">Date</label>
<input pInputText [(ngModel)]="aiResult.date" class="w-full" />
</div>
<div class="col-6 mt-2">
<label class="block text-sm font-bold mb-1">Invoice #</label>
<input pInputText [(ngModel)]="aiResult.invoice_number" class="w-full" />
</div>
<div class="col-6 mt-2">
<label class="block text-sm font-bold mb-1">Total</label>
<input pInputText [(ngModel)]="aiResult.total_amount" class="w-full" />
</div>
</div>
</p-card>
<p-table [value]="aiResult.line_items" styleClass="p-datatable-sm" [scrollable]="true" scrollHeight="200px">
<ng-template pTemplate="header">
<tr>
<th>Description</th>
<th>Qty</th>
<th>Price</th>
<th>Total</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td pEditableColumn>
<p-cellEditor>
<ng-template pTemplate="input">
<input pInputText type="text" [(ngModel)]="item.description">
</ng-template>
<ng-template pTemplate="output">
{{item.description}}
</ng-template>
</p-cellEditor>
</td>
<td pEditableColumn>
<p-cellEditor>
<ng-template pTemplate="input">
<input pInputText type="text" [(ngModel)]="item.quantity">
</ng-template>
<ng-template pTemplate="output">
{{item.quantity}}
</ng-template>
</p-cellEditor>
</td>
<td pEditableColumn>
<p-cellEditor>
<ng-template pTemplate="input">
<input pInputText type="text" [(ngModel)]="item.unit_price">
</ng-template>
<ng-template pTemplate="output">
{{item.unit_price}}
</ng-template>
</p-cellEditor>
</td>
<td pEditableColumn>
<p-cellEditor>
<ng-template pTemplate="input">
<input pInputText type="text" [(ngModel)]="item.total">
</ng-template>
<ng-template pTemplate="output">
{{item.total}}
</ng-template>
</p-cellEditor>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
<p-toast></p-toast>
</div>
@@ -65,16 +177,27 @@ import { ToastModule } from 'primeng/toast';
export class OcrComponent {
extractedText: string | null = null;
loading: boolean = false;
aiLoading: boolean = false;
saveLoading: boolean = false;
aiResult: any = null;
// Hybrid AI Props
modelType: string = 'text';
filePath: string | null = null;
constructor(private ocrService: OcrService, private messageService: MessageService) {}
onUpload(event: any) {
this.loading = true;
this.aiResult = null; // Reset AI result on new upload
this.filePath = null;
const file = event.files[0];
this.ocrService.extractText(file).subscribe({
next: (res) => {
this.extractedText = res.text;
this.filePath = res.file_path;
this.loading = false;
this.messageService.add({severity:'success', summary:'Success', detail:'Text Extracted Successfully'});
},
@@ -88,5 +211,50 @@ export class OcrComponent {
onClear() {
this.extractedText = null;
this.aiResult = null;
this.filePath = null;
}
processWithAI() {
if (!this.extractedText) return;
this.aiLoading = true;
// Pass text, filePath, and modelType
this.ocrService.extractWithAI(this.extractedText, this.filePath, this.modelType).subscribe({
next: (res) => {
this.aiResult = res;
this.aiLoading = false;
this.messageService.add({severity:'success', summary:'AI Processing Complete', detail:'Data Extracted'});
},
error: (err) => {
console.error(err);
this.aiLoading = false;
this.messageService.add({severity:'error', summary:'AI Error', detail:'Could not process with AI'});
}
});
}
saveDocument() {
if (!this.aiResult || !this.filePath) return;
this.saveLoading = true;
const payload = {
vendor_name: this.aiResult.vendor_name || 'Unknown Vendor',
file_path: this.filePath,
model_type: this.modelType,
data: this.aiResult
};
this.ocrService.saveDocument(payload).subscribe({
next: (res) => {
this.saveLoading = false;
this.messageService.add({severity:'success', summary:'Saved & Verified', detail:'Document and rules saved'});
},
error: (err) => {
console.error(err);
this.saveLoading = false;
this.messageService.add({severity:'error', summary:'Save Error', detail:'Failed to save document'});
}
});
}
}