Files
ezbookkeeping/src/components/mobile/AIImageRecognitionSheet.vue
T
2025-09-21 12:29:04 +08:00

166 lines
4.6 KiB
Vue

<template>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left">
<f7-link :class="{ 'disabled': loading || recognizing }" :text="tt('Cancel')" @click="cancel"></f7-link>
</div>
<div class="right">
<f7-link :class="{ 'disabled': loading || recognizing || !imageFile }" :text="tt('Recognize')" @click="confirm"></f7-link>
</div>
</f7-toolbar>
<f7-page-content class="no-margin-vertical no-padding-vertical">
<div class="image-container display-flex justify-content-center"
style="height: 400px" @click="showOpenImage">
<img height="400px" :src="imageSrc" v-if="imageSrc" />
<div class="image-container-background display-flex justify-content-center align-items-center" v-if="!imageSrc">
<span>{{ tt('Click here to select a receipt or transaction image') }}</span>
</div>
</div>
</f7-page-content>
<input ref="imageInput" type="file" style="display: none" :accept="`${SUPPORTED_IMAGE_EXTENSIONS};capture=camera`" @change="openImage($event)" />
</f7-sheet>
</template>
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.ts';
import { useTransactionsStore } from '@/stores/transaction.ts';
import { KnownFileType } from '@/core/file.ts';
import { SUPPORTED_IMAGE_EXTENSIONS } from '@/consts/file.ts';
import type { RecognizedReceiptImageResponse } from '@/models/large_language_model.ts';
import { compressJpgImage } from '@/lib/ui/common.ts';
import logger from '@/lib/logger.ts';
defineProps<{
show: boolean;
}>();
const emit = defineEmits<{
(e: 'update:show', value: boolean): void;
(e: 'recognition:change', value: RecognizedReceiptImageResponse): void;
}>();
const { tt } = useI18n();
const { showToast } = useI18nUIComponents();
const transactionsStore = useTransactionsStore();
const imageInput = useTemplateRef<HTMLInputElement>('imageInput');
const loading = ref<boolean>(false);
const recognizing = ref<boolean>(false);
const imageFile = ref<File | null>(null);
const imageSrc = ref<string | undefined>(undefined);
function loadImage(file: File): void {
compressJpgImage(file, 1280, 1280, 0.8).then(blob => {
imageFile.value = KnownFileType.JPG.createFileFromBlob(blob, "image");
imageSrc.value = URL.createObjectURL(blob);
}).catch(error => {
imageFile.value = null;
imageSrc.value = undefined;
logger.error('failed to compress image', error);
showToast('Unable to load image');
});
}
function showOpenImage(): void {
if (loading.value || recognizing.value) {
return;
}
imageInput.value?.click();
}
function openImage(event: Event): void {
if (!event || !event.target) {
return;
}
const el = event.target as HTMLInputElement;
if (!el.files || !el.files.length || !el.files[0]) {
return;
}
const image = el.files[0] as File;
el.value = '';
loadImage(image);
}
function confirm(): void {
if (loading.value || recognizing.value || !imageFile.value) {
return;
}
recognizing.value = true;
showLoading(() => recognizing.value);
transactionsStore.recognizeReceiptImage({
imageFile: imageFile.value
}).then(response => {
recognizing.value = false;
hideLoading();
emit('update:show', false);
emit('recognition:change', response);
}).catch(error => {
recognizing.value = false;
hideLoading();
if (!error.processed) {
showToast(error.message || error);
}
});
}
function cancel(): void {
close();
}
function close(): void {
emit('update:show', false);
loading.value = false;
recognizing.value = false;
imageFile.value = null;
imageSrc.value = undefined;
}
function onSheetOpen(): void {
loading.value = false;
recognizing.value = false;
imageFile.value = null;
imageSrc.value = undefined;
}
function onSheetClosed(): void {
close();
}
</script>
<style>
.image-container {
border: 1px solid var(--f7-page-master-border-color);
}
.image-container-background {
width: 100%;
height: 100%;
> span {
font-size: var(--f7-input-font-size);
}
}
</style>