diff --git a/ai/analyze.ts b/ai/analyze.ts index 6919912..bb2399a 100644 --- a/ai/analyze.ts +++ b/ai/analyze.ts @@ -1,9 +1,9 @@ "use server" import { ActionState } from "@/lib/actions" -import { AnalyzeAttachment } from "./attachments" import { updateFile } from "@/models/files" -import { getSettings, getLLMSettings } from "@/models/settings" +import { getLLMSettings, getSettings } from "@/models/settings" +import { AnalyzeAttachment } from "./attachments" import { requestLLM } from "./providers/llmProvider" export type AnalysisResult = { @@ -18,7 +18,6 @@ export async function analyzeTransaction( fileId: string, userId: string ): Promise> { - const settings = await getSettings(userId) const llmSettings = getLLMSettings(settings) @@ -45,7 +44,7 @@ export async function analyzeTransaction( success: true, data: { output: result, - tokensUsed: tokensUsed + tokensUsed: tokensUsed, }, } } catch (error) { diff --git a/ai/providers/llmProvider.ts b/ai/providers/llmProvider.ts index c996ed2..ec56100 100644 --- a/ai/providers/llmProvider.ts +++ b/ai/providers/llmProvider.ts @@ -30,79 +30,76 @@ export interface LLMResponse { async function requestLLMUnified(config: LLMConfig, req: LLMRequest): Promise { try { - const temperature = 0; - let model: any; + const temperature = 0 + let model: any if (config.provider === "openai") { model = new ChatOpenAI({ apiKey: config.apiKey, model: config.model, temperature: temperature, - }); + }) } else if (config.provider === "google") { model = new ChatGoogleGenerativeAI({ apiKey: config.apiKey, model: config.model, temperature: temperature, - }); + }) } else if (config.provider === "mistral") { model = new ChatMistralAI({ apiKey: config.apiKey, model: config.model, temperature: temperature, - }); + }) } else { return { output: {}, provider: config.provider, error: "Unknown provider", - }; + } } - const structuredModel = model.withStructuredOutput(req.schema, { 'name': 'transaction'}); + const structuredModel = model.withStructuredOutput(req.schema, { name: "transaction" }) - let message_content: any = [{ type: "text", text: req.prompt }]; + let message_content: any = [{ type: "text", text: req.prompt }] if (req.attachments && req.attachments.length > 0) { - const images = req.attachments.map(att => ({ + const images = req.attachments.map((att) => ({ type: "image_url", image_url: { - url: `data:${att.contentType};base64,${att.base64}` + url: `data:${att.contentType};base64,${att.base64}`, }, - })); - message_content.push(...images); + })) + message_content.push(...images) } - const messages: BaseMessage[] = [ - new HumanMessage({ content: message_content }) - ]; + const messages: BaseMessage[] = [new HumanMessage({ content: message_content })] - const response = await structuredModel.invoke(messages); + const response = await structuredModel.invoke(messages) return { output: response, provider: config.provider, - }; + } } catch (error: any) { return { output: {}, provider: config.provider, error: error instanceof Error ? error.message : `${config.provider} request failed`, - }; + } } } export async function requestLLM(settings: LLMSettings, req: LLMRequest): Promise { for (const config of settings.providers) { if (!config.apiKey || !config.model) { - console.info('Skipping provider:', config.provider); - continue; + console.info("Skipping provider:", config.provider) + continue } - console.info('Use provider:', config.provider); + console.info("Use provider:", config.provider) - const response = await requestLLMUnified(config, req); + const response = await requestLLMUnified(config, req) if (!response.error) { - return response; - } - else { + return response + } else { console.error(response.error) } } @@ -111,5 +108,5 @@ export async function requestLLM(settings: LLMSettings, req: LLMRequest): Promis output: {}, provider: settings.providers[0]?.provider || "openai", error: "All LLM providers failed or are not configured", - }; + } } diff --git a/ai/schema.ts b/ai/schema.ts index fa5706f..e7a2240 100644 --- a/ai/schema.ts +++ b/ai/schema.ts @@ -16,7 +16,8 @@ export const fieldsToJsonSchema = (fields: Field[]) => { ...schemaProperties, items: { type: "array", - description: "Separate items, products or transactions in the file which have own name and price or sum. Find all items!", + description: + "Separate items, products or transactions in the file which have own name and price or sum. Find all items!", items: { type: "object", properties: schemaProperties, diff --git a/app/(app)/unsorted/actions.ts b/app/(app)/unsorted/actions.ts index 7d5c4e3..5296e8d 100644 --- a/app/(app)/unsorted/actions.ts +++ b/app/(app)/unsorted/actions.ts @@ -7,19 +7,22 @@ import { fieldsToJsonSchema } from "@/ai/schema" import { transactionFormSchema } from "@/forms/transactions" import { ActionState } from "@/lib/actions" import { getCurrentUser, isAiBalanceExhausted, isSubscriptionExpired } from "@/lib/auth" -import config from "@/lib/config" -import { getTransactionFileUploadPath, getUserUploadsDirectory, safePathJoin, unsortedFilePath } from "@/lib/files" +import { + getDirectorySize, + getTransactionFileUploadPath, + getUserUploadsDirectory, + safePathJoin, + unsortedFilePath, +} from "@/lib/files" import { DEFAULT_PROMPT_ANALYSE_NEW_FILE } from "@/models/defaults" import { createFile, deleteFile, getFileById, updateFile } from "@/models/files" -import { createTransaction, updateTransactionFiles, TransactionData } from "@/models/transactions" +import { createTransaction, TransactionData, updateTransactionFiles } from "@/models/transactions" import { updateUser } from "@/models/users" import { Category, Field, File, Project, Transaction } from "@/prisma/client" -import { mkdir, rename } from "fs/promises" +import { randomUUID } from "crypto" +import { mkdir, readFile, rename, writeFile } from "fs/promises" import { revalidatePath } from "next/cache" import path from "path" -import { randomUUID } from "crypto" -import { readFile, writeFile } from "fs/promises" -import { getDirectorySize } from "@/lib/files" export async function analyzeFileAction( file: File, @@ -31,15 +34,15 @@ export async function analyzeFileAction( const user = await getCurrentUser() if (!file || file.userId !== user.id) { - return { success: false, error: "File not found or does not belong to the user" } - } + return { success: false, error: "File not found or does not belong to the user" } + } - if (isAiBalanceExhausted(user)) { - return { - success: false, - error: "You used all of your pre-paid AI scans, please upgrade your account or buy new subscription plan", - } + if (isAiBalanceExhausted(user)) { + return { + success: false, + error: "You used all of your pre-paid AI scans, please upgrade your account or buy new subscription plan", } + } if (isSubscriptionExpired(user)) { return { diff --git a/app/(auth)/self-hosted/page.tsx b/app/(auth)/self-hosted/page.tsx index 6159af0..a746efd 100644 --- a/app/(auth)/self-hosted/page.tsx +++ b/app/(auth)/self-hosted/page.tsx @@ -1,11 +1,11 @@ import { Card, CardDescription, CardTitle } from "@/components/ui/card" import { ColoredText } from "@/components/ui/colored-text" import config from "@/lib/config" +import { PROVIDERS } from "@/lib/llm-providers" import { getSelfHostedUser } from "@/models/users" import { ShieldAlert } from "lucide-react" import Image from "next/image" import { redirect } from "next/navigation" -import { PROVIDERS } from "@/lib/llm-providers" import SelfHostedSetupFormClient from "./setup-form-client" export default async function SelfHostedWelcomePage() { @@ -32,7 +32,6 @@ export default async function SelfHostedWelcomePage() { redirect(config.selfHosted.redirectUrl) } - // Собираем дефолтные ключи для всех провайдеров const defaultProvider = PROVIDERS[0].key const defaultApiKeys: Record = { openai: config.ai.openaiApiKey ?? "", diff --git a/package-lock.json b/package-lock.json index f9689a1..223976a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -941,9 +941,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -956,9 +956,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", - "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -966,9 +966,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -979,9 +979,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1003,13 +1003,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", - "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -1023,13 +1026,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -5558,9 +5561,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -7101,20 +7104,20 @@ } }, "node_modules/eslint": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", - "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.1.0", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.22.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -7125,9 +7128,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7433,9 +7436,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7450,9 +7453,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7463,15 +7466,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7744,9 +7747,9 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "optional": true, "peer": true, @@ -7754,6 +7757,7 @@ "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": {