support canceling AI image recognition

This commit is contained in:
MaysWind
2025-09-27 23:53:40 +08:00
parent e9e6644e7f
commit 4d0d3959a9
19 changed files with 162 additions and 13 deletions
+22 -3
View File
@@ -159,6 +159,7 @@ import {
import { getTimezoneOffsetMinutes } from './datetime.ts';
import { generateRandomUUID } from './misc.ts';
import { getBasePath } from './web.ts';
import logger from './logger.ts';
interface ApiRequestConfig extends AxiosRequestConfig {
readonly headers: AxiosRequestHeaders;
@@ -166,12 +167,14 @@ interface ApiRequestConfig extends AxiosRequestConfig {
readonly ignoreBlocked?: boolean;
readonly ignoreError?: boolean;
readonly timeout?: number;
readonly cancelableUuid?: string;
}
export type ApiResponsePromise<T> = Promise<AxiosResponse<ApiResponse<T>>>;
let needBlockRequest = false;
const blockedRequests: ((token: string | undefined) => void)[] = [];
const cancelableRequests: Record<string, boolean> = {};
axios.defaults.baseURL = getBasePath() + BASE_API_URL_PATH;
axios.defaults.timeout = DEFAULT_API_TIMEOUT;
@@ -202,8 +205,20 @@ axios.interceptors.request.use((config: ApiRequestConfig) => {
});
axios.interceptors.response.use(response => {
if ('cancelableUuid' in response.config && response.config.cancelableUuid && cancelableRequests[response.config.cancelableUuid as string]) {
logger.debug('Response canceled by user request, url: ' + response.config.url + ', cancelableUuid: ' + response.config.cancelableUuid);
delete cancelableRequests[response.config.cancelableUuid as string];
return Promise.reject({ canceled: true });
}
return response;
}, error => {
if ('cancelableUuid' in error.response.config && error.response.config.cancelableUuid && cancelableRequests[error.response.config.cancelableUuid]) {
logger.debug('Response canceled by user request, url: ' + error.response.config.url + ', cancelableUuid: ' + error.response.config.cancelableUuid);
delete cancelableRequests[error.response.config.cancelableUuid];
return Promise.reject({ canceled: true });
}
if (error.response && !error.response.config.ignoreError && error.response.data && error.response.data.errorCode) {
const errorCode = error.response.data.errorCode;
@@ -650,12 +665,13 @@ export default {
deleteTransactionTemplate: (req: TransactionTemplateDeleteRequest): ApiResponsePromise<boolean> => {
return axios.post<ApiResponse<boolean>>('v1/transaction/templates/delete.json', req);
},
recognizeReceiptImage: ({ imageFile }: { imageFile: File }): ApiResponsePromise<RecognizedReceiptImageResponse> => {
recognizeReceiptImage: ({ imageFile, cancelableUuid }: { imageFile: File, cancelableUuid?: string }): ApiResponsePromise<RecognizedReceiptImageResponse> => {
return axios.postForm<ApiResponse<RecognizedReceiptImageResponse>>('v1/llm/transactions/recognize_receipt_image.json', {
image: imageFile
}, {
timeout: DEFAULT_LLM_API_TIMEOUT
});
timeout: DEFAULT_LLM_API_TIMEOUT,
cancelableUuid: cancelableUuid
} as ApiRequestConfig);
},
getLatestExchangeRates: (param: { ignoreError?: boolean }): ApiResponsePromise<LatestExchangeRateResponse> => {
return axios.get<ApiResponse<LatestExchangeRateResponse>>('v1/exchange_rates/latest.json', {
@@ -672,6 +688,9 @@ export default {
getServerVersion: (): ApiResponsePromise<VersionInfo> => {
return axios.get<ApiResponse<VersionInfo>>('v1/systems/version.json');
},
cancelRequest: (cancelableUuid: string) => {
cancelableRequests[cancelableUuid] = true;
},
generateQrCodeUrl: (qrCodeName: string): string => {
return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`;
},
+28
View File
@@ -42,6 +42,12 @@ export function hideLoading(): void {
});
}
export function closeAllDialog(): void {
f7ready((f7) => {
return f7.dialog.close();
});
}
export function createInlinePicker(containerEl: string, inputEl: string, cols: Picker.ColumnParameters[], value: string[], events?: { change: (picker: Picker.Picker, value: unknown, displayValue: unknown) => void }): Picker.Picker {
return f7.picker.create({
containerEl: containerEl,
@@ -282,6 +288,27 @@ export function useI18nUIComponents() {
});
}
function showCancelableLoading(message: string, cancelButtonText: string, cancelCallback?: (dialog: Dialog.Dialog, e: Event) => void): void {
const cancelButton: Dialog.DialogButton = {
text: tt(cancelButtonText),
onClick: (dialog, event) => {
if (cancelCallback) {
cancelCallback(dialog, event);
}
}
};
f7ready((f7) => {
f7.dialog.create({
title: tt(message),
content: `<div class="preloader"><span class="preloader-inner">${[0, 1, 2, 3, 4, 5, 6, 7].map(() => '<span class="preloader-inner-line"></span>').join('')}</span></div>`,
cssClass: 'dialog-preloader',
animate: isEnableAnimate(),
buttons: [cancelButton]
}).open();
});
}
function showToast(message: string, timeout?: number): void {
f7ready((f7) => {
f7.toast.create({
@@ -296,6 +323,7 @@ export function useI18nUIComponents() {
showAlert: showAlert,
showConfirm: showConfirm,
showPrompt: showPrompt,
showCancelableLoading: showCancelableLoading,
showToast: showToast,
routeBackOnError
}