import { BadRequestException, Injectable, NotFoundException, ServiceUnavailableException, } from '@nestjs/common'; import type { ApplyNoteAiSuggestionRequest, NoteAiPayloadDto, NoteAiSuggestionDto, } from '@mento/shared'; import { PrismaService } from '../prisma/prisma.service'; import { AiService } from '../ai/ai.service'; import { MemoryService } from '../memory/memory.service'; import { type Actor, noteAccessWhere } from '../common/record-scope'; @Injectable() export class NoteAiService { constructor( private readonly prisma: PrismaService, private readonly ai: AiService, private readonly memory: MemoryService, ) {} async analyze( organizationId: string, noteId: string, actor: Actor, ): Promise { const note = await this.loadNote(organizationId, noteId, actor); if (!this.ai.isEnabled()) { throw new ServiceUnavailableException('هوش مصنوعی فعال نیست'); } try { const extract = await this.ai.extractFromNote({ body: note.body, title: note.title, noteType: note.noteType, personName: note.person ? `${note.person.firstName} ${note.person.lastName}`.trim() : null, }); const row = await this.prisma.noteAiSuggestion.upsert({ where: { noteId }, create: { organizationId, noteId, status: 'pending', payload: extract, analyzedAt: new Date(), }, update: { status: 'pending', payload: extract, errorMessage: null, analyzedAt: new Date(), }, }); return this.toDto(row); } catch (err) { const message = err instanceof Error ? err.message : 'تحلیل یادداشت ناموفق بود'; await this.prisma.noteAiSuggestion.upsert({ where: { noteId }, create: { organizationId, noteId, status: 'failed', errorMessage: message, analyzedAt: new Date(), }, update: { status: 'failed', errorMessage: message, analyzedAt: new Date(), }, }); throw err instanceof ServiceUnavailableException || err instanceof BadRequestException ? err : new ServiceUnavailableException(message); } } async getSuggestion( organizationId: string, noteId: string, actor: Actor, ): Promise { await this.ensureNoteAccess(organizationId, noteId, actor); const row = await this.prisma.noteAiSuggestion.findUnique({ where: { noteId }, }); return row ? this.toDto(row) : null; } async apply( organizationId: string, userId: string, noteId: string, actor: Actor, dto: ApplyNoteAiSuggestionRequest, ): Promise<{ suggestion: NoteAiSuggestionDto; activityId?: string; memoryItemIds: string[]; }> { const note = await this.loadNote(organizationId, noteId, actor); const suggestion = await this.prisma.noteAiSuggestion.findUnique({ where: { noteId }, }); if (!suggestion || suggestion.status !== 'pending') { throw new BadRequestException('پیشنهاد فعالی برای این یادداشت وجود ندارد'); } const payload = suggestion.payload as NoteAiPayloadDto | null; if (!payload) { throw new BadRequestException('داده پیشنهاد نامعتبر است'); } let activityId: string | undefined; const memoryItemIds: string[] = []; if (dto.applyNextAction) { const next = dto.nextAction ?? payload.nextAction; if (!next?.title?.trim()) { throw new BadRequestException('عنوان کار بعدی الزامی است'); } const activity = await this.prisma.activity.create({ data: { organizationId, type: 'task', title: next.title.trim().slice(0, 200), description: next.description?.trim() || note.body, dueAt: next.dueAt ? new Date(next.dueAt) : note.reminderAt, personId: note.personId, companyId: note.companyId, dealId: note.dealId, assigneeId: userId, }, }); activityId = activity.id; } if (dto.memoryIndices?.length) { if (!note.personId) { throw new BadRequestException( 'برای ذخیره حافظه، یادداشت باید به یک شخص متصل باشد', ); } const indices = [...new Set(dto.memoryIndices)].filter( (i) => i >= 0 && i < payload.memoryItems.length, ); for (const index of indices) { const edited = dto.memoryItems?.find((m) => m.index === index); const content = edited?.content?.trim() || payload.memoryItems[index]?.content?.trim(); if (!content) continue; const item = await this.memory.createFromNote( organizationId, userId, note.personId, noteId, content, ); memoryItemIds.push(item.id); } } const updated = await this.prisma.noteAiSuggestion.update({ where: { noteId }, data: { status: 'applied' }, }); return { suggestion: this.toDto(updated), activityId, memoryItemIds, }; } async dismiss( organizationId: string, noteId: string, actor: Actor, ): Promise { await this.ensureNoteAccess(organizationId, noteId, actor); const existing = await this.prisma.noteAiSuggestion.findUnique({ where: { noteId }, }); if (!existing) { throw new NotFoundException('پیشنهادی برای این یادداشت وجود ندارد'); } const row = await this.prisma.noteAiSuggestion.update({ where: { noteId }, data: { status: 'dismissed' }, }); return this.toDto(row); } private async loadNote( organizationId: string, noteId: string, actor: Actor, ) { const note = await this.prisma.note.findFirst({ where: { id: noteId, organizationId, ...noteAccessWhere(actor) }, include: { person: { select: { id: true, firstName: true, lastName: true } }, }, }); if (!note) throw new NotFoundException('یادداشت پیدا نشد'); return note; } private async ensureNoteAccess( organizationId: string, noteId: string, actor: Actor, ) { await this.loadNote(organizationId, noteId, actor); } private toDto(row: { id: string; noteId: string; status: string; payload: unknown; errorMessage: string | null; analyzedAt: Date | null; createdAt: Date; updatedAt: Date; }): NoteAiSuggestionDto { return { id: row.id, noteId: row.noteId, status: row.status as NoteAiSuggestionDto['status'], payload: (row.payload as NoteAiPayloadDto | null) ?? null, errorMessage: row.errorMessage, analyzedAt: row.analyzedAt?.toISOString() ?? null, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } }