feat: enhance document processing with AI suggestions and preview image support
- Added AI_SUGGESTION event to DocumentEvent enum for improved document interaction. - Implemented updateDocumentPreviewImage mutation in DocumentSchema to allow updating of document preview images. - Integrated grammar checking functionality in DocumentService, utilizing OpenAI for real-time suggestions and caching results in Redis. - Enhanced MinioService with methods for file upsert and document content retrieval. - Updated PrismaTypes to include new relations and fields for better data structure alignment. - Commented out unused RESTful service methods for future cleanup. These changes improve the document editing experience by leveraging AI capabilities and enhancing the schema for better data management.
This commit is contained in:
@@ -2,13 +2,152 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common'
|
||||
|
||||
import { Logger } from '@nestjs/common'
|
||||
import Delta, { Op } from 'quill-delta'
|
||||
import { OpenaiService, PromptType } from 'src/OpenAI/openai.service'
|
||||
import { PrismaService } from 'src/Prisma/prisma.service'
|
||||
import { PubSubService } from 'src/PubSub/pubsub.service'
|
||||
import { RedisService } from 'src/Redis/redis.service'
|
||||
import { MinioService } from '../Minio/minio.service'
|
||||
import { DocumentEvent } from './document.event'
|
||||
import { DocumentDelta } from './document.type'
|
||||
@Injectable()
|
||||
export class DocumentService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly minio: MinioService,
|
||||
private readonly openai: OpenaiService,
|
||||
private readonly redis: RedisService,
|
||||
private readonly pubSub: PubSubService,
|
||||
) {}
|
||||
|
||||
// page to list paragraph as delta
|
||||
async pageToParagraph(content: Delta): Promise<Delta> {
|
||||
// Create a new Delta to store paragraphs
|
||||
const paragraphDelta = new Delta()
|
||||
|
||||
// Iterate through the operations in the content Delta
|
||||
for (const op of content.ops) {
|
||||
// If the operation is a string insert
|
||||
if (typeof op.insert === 'string') {
|
||||
// Split the text by newline characters
|
||||
const paragraphs = op.insert.split('\n').filter((p) => p.trim() !== '')
|
||||
|
||||
// Add each non-empty paragraph as a separate insert with a newline
|
||||
paragraphs.forEach((paragraph, index) => {
|
||||
paragraphDelta.insert(paragraph)
|
||||
|
||||
// Add a newline after each paragraph except the last one
|
||||
if (index < paragraphs.length - 1) {
|
||||
paragraphDelta.insert('\n')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// If the operation is not a string (e.g., an embedded object),
|
||||
// add it to the paragraphDelta as-is
|
||||
paragraphDelta.push(op)
|
||||
}
|
||||
}
|
||||
|
||||
return paragraphDelta
|
||||
}
|
||||
|
||||
async paragraphToSentence(content: Delta): Promise<Delta> {
|
||||
// Create a new Delta to store sentences
|
||||
const sentenceDelta = new Delta()
|
||||
|
||||
// Iterate through the operations in the content Delta
|
||||
for (const op of content.ops) {
|
||||
// If the operation is a string insert
|
||||
if (typeof op.insert === 'string') {
|
||||
// Split the text into paragraphs first
|
||||
const paragraphs = op.insert.split('\n').filter((p) => p.trim() !== '')
|
||||
|
||||
paragraphs.forEach((paragraph, paragraphIndex) => {
|
||||
// Split paragraph into sentences using regex
|
||||
// This handles common sentence-ending punctuation: . ! ?
|
||||
const sentences = paragraph.split(/(?<=[.!?])\s+/).filter((s) => s.trim() !== '')
|
||||
|
||||
sentences.forEach((sentence, sentenceIndex) => {
|
||||
sentenceDelta.insert(sentence.trim())
|
||||
|
||||
// Add a newline after each sentence except the last one in the paragraph
|
||||
if (sentenceIndex < sentences.length - 1) {
|
||||
sentenceDelta.insert('\n')
|
||||
}
|
||||
})
|
||||
|
||||
// Add a newline between paragraphs, except after the last paragraph
|
||||
if (paragraphIndex < paragraphs.length - 1) {
|
||||
sentenceDelta.insert('\n')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// If the operation is not a string (e.g., an embedded object),
|
||||
// add it to the sentenceDelta as-is
|
||||
sentenceDelta.push(op)
|
||||
}
|
||||
}
|
||||
|
||||
return sentenceDelta
|
||||
}
|
||||
|
||||
async pageToSentence(documentId: string, pageId: number): Promise<Delta> {
|
||||
const content = await this.minio.getDocumentContent(documentId, pageId)
|
||||
const paragraphDelta = await this.pageToParagraph(content)
|
||||
const sentenceDelta = await this.paragraphToSentence(paragraphDelta)
|
||||
return sentenceDelta
|
||||
}
|
||||
|
||||
// check grammar for a page by parallely send each sentence to OpenAI service as text and get the result, after that return the result as delta and publish the result to the document
|
||||
async checkGrammarForPage(documentId: string, pageId: number): Promise<void> {
|
||||
const sentenceDelta = await this.pageToSentence(documentId, pageId)
|
||||
|
||||
// Extract the entire page content as a single text
|
||||
const pageText = sentenceDelta.ops
|
||||
.filter((op) => typeof op.insert === 'string')
|
||||
.map((op) => op.insert as string)
|
||||
.join(' ')
|
||||
|
||||
// Create a unique cache key for the entire page
|
||||
const cacheKey = `grammar_check:${documentId}:${pageId}`
|
||||
|
||||
// Try to get cached result first
|
||||
const cachedResult = await this.redis.get(cacheKey)
|
||||
|
||||
if (cachedResult) {
|
||||
Logger.log('Cached grammar check result exists', 'Grammar Check')
|
||||
return
|
||||
}
|
||||
|
||||
// Process the entire page text
|
||||
const grammarCheckResult = await this.openai.processText(pageText, 0, PromptType.CHECK_GRAMMAR)
|
||||
|
||||
if (!grammarCheckResult.result) {
|
||||
return
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
await this.redis.setPermanent(cacheKey, JSON.stringify(grammarCheckResult))
|
||||
|
||||
// Calculate diff between original page and corrected page
|
||||
const diff = await this.diff(pageText, grammarCheckResult.result)
|
||||
|
||||
// Build payload
|
||||
const payload: DocumentDelta = {
|
||||
documentId,
|
||||
pageIndex: pageId,
|
||||
ops: diff.ops,
|
||||
}
|
||||
|
||||
// Publish the result to the subscriber
|
||||
this.pubSub.publish(`${DocumentEvent.AI_SUGGESTION}.${documentId}.${pageId}`, payload)
|
||||
}
|
||||
|
||||
async diff(original: string, corrected: string): Promise<Delta> {
|
||||
const originalDelta = new Delta().insert(original)
|
||||
const correctedDelta = new Delta().insert(corrected)
|
||||
const diff = originalDelta.diff(correctedDelta)
|
||||
Logger.log(diff, 'diff')
|
||||
return diff
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user