|
| 1 | +import { Injectable, Logger } from "@nestjs/common"; |
| 2 | +import { Operation, ElementType, RemoteBlockInsertOperation, RemotePageCreateOperation, RemoteCharInsertOperation, CRDTOperation } from "@noctaCrdt/types/Interfaces"; |
| 3 | +import { Block } from "@noctaCrdt/Node"; |
| 4 | +import { EditorCRDT } from "@noctaCrdt/Crdt"; |
| 5 | +import { Page } from "@noctaCrdt/Page"; |
| 6 | +import { nanoid } from "nanoid"; |
| 7 | +import { WorkSpaceService } from "../workspace/workspace.service"; |
| 8 | +import { BlockId, CharId } from "@noctaCrdt/NodeId"; |
| 9 | +import { Char } from "@noctaCrdt/Node"; |
| 10 | + |
| 11 | +@Injectable() |
| 12 | +export class AiService { |
| 13 | + constructor(private readonly workspaceService: WorkSpaceService) {} |
| 14 | + |
| 15 | + // CLOVA Studio API에 요청을 보내는 로직 |
| 16 | + requestAI() { |
| 17 | + // 문자열 받고 |
| 18 | + |
| 19 | + // 문자열을 CRDT로 변환 |
| 20 | + |
| 21 | + // CRDT 연산들 적용해서 브로드캐스트 |
| 22 | + |
| 23 | + } |
| 24 | + |
| 25 | + // 요청받은 답변을 CRDT 연산으로 변환하는 로직 |
| 26 | + async generateDocumentToCRDT(workspaceId: string, clientId: number, document: String): Promise<Operation[]> { |
| 27 | + const operations = []; |
| 28 | + // 페이지 생성 |
| 29 | + const workspace = await this.workspaceService.getWorkspace(workspaceId); |
| 30 | + const newEditorCRDT = new EditorCRDT(clientId); |
| 31 | + const newPage = new Page(nanoid(), "새로운 페이지", "Docs", newEditorCRDT); |
| 32 | + workspace.pageList.push(newPage); |
| 33 | + this.workspaceService.updateWorkspace(workspace); |
| 34 | + // 페이지 생성 연산 추가 |
| 35 | + operations.push({ |
| 36 | + type: "pageCreate", |
| 37 | + workspaceId, |
| 38 | + clientId, |
| 39 | + page: newPage.serialize() |
| 40 | + } as RemotePageCreateOperation); |
| 41 | + |
| 42 | + let blockClock = 0; |
| 43 | + let charClock = 0; |
| 44 | + |
| 45 | + const documentLines = document.split('\n'); |
| 46 | + |
| 47 | + let lastBlock = null; |
| 48 | + documentLines.forEach(line => { |
| 49 | + const {type, length, indent} = this.parseBlockType(line); |
| 50 | + const newBlock = new Block("", new BlockId(blockClock++, clientId)); |
| 51 | + newBlock.next = null; |
| 52 | + newBlock.prev = lastBlock ? lastBlock.id : null; |
| 53 | + newBlock.type = type; |
| 54 | + newBlock.indent = indent; |
| 55 | + if (lastBlock) { |
| 56 | + lastBlock.next = newBlock.id; |
| 57 | + } |
| 58 | + lastBlock = newBlock; |
| 59 | + // 블록 추가 연산 |
| 60 | + operations.push({ |
| 61 | + type: "blockInsert", |
| 62 | + node: newBlock, |
| 63 | + pageId: newPage.id |
| 64 | + } as RemoteBlockInsertOperation); |
| 65 | + |
| 66 | + const slicedLine = [...line.slice(length + indent * 2)]; |
| 67 | + let lastNode = null; |
| 68 | + slicedLine.forEach(char => { |
| 69 | + const charNode = new Char(char, new CharId(charClock++, clientId)); |
| 70 | + charNode.next = null; |
| 71 | + charNode.prev = lastNode ? lastNode.id : null; |
| 72 | + |
| 73 | + // 이전 노드가 있는 경우, 해당 노드의 next를 현재 노드로 설정 |
| 74 | + if (lastNode) { |
| 75 | + lastNode.next = charNode.id; |
| 76 | + } |
| 77 | + |
| 78 | + lastNode = charNode; |
| 79 | + |
| 80 | + operations.push({ |
| 81 | + type: "charInsert", |
| 82 | + node: charNode, |
| 83 | + blockId: newBlock.id, |
| 84 | + pageId: newPage.id, |
| 85 | + style: charNode.style || [], |
| 86 | + color: charNode.color ? charNode.color : "black", |
| 87 | + backgroundColor: charNode.backgroundColor ? charNode.backgroundColor : "transparent", |
| 88 | + } as RemoteCharInsertOperation); |
| 89 | + }); |
| 90 | + |
| 91 | + // TODO: 스타일 적용 |
| 92 | + // let start = 0, end = 0; |
| 93 | + // while 순회 end <- |
| 94 | + // 특수기호 *, **, ~~, __ |
| 95 | + // 여는 애, 닫힌 애 **ㅁㄴㅇㄻㄴㅇㄹ** |
| 96 | + // stack [], cur ** |
| 97 | + // stack [**], cur ㅁ |
| 98 | + // ... |
| 99 | + // stack [**], cur ** |
| 100 | + // stack [], cur EOF |
| 101 | + |
| 102 | + }); |
| 103 | + |
| 104 | + // 클라이언트에서 workspaceId, clientId, message 받아옴 |
| 105 | + // 워크스페이스에서 페이지 생성 -> pageId: string(uuid) |
| 106 | + // 매 줄 처음마다 블록 생성 -> blockId: {client, clock} |
| 107 | + // 문자를 돌면서 CRDTOperation 연산 생성 |
| 108 | + // 서식 문자(**, *, ~, __)를 만나면 해당 문자는 연산을 만들지 않고, 내부 글자에 대해 스타일 속성 추가후 연산 생성 |
| 109 | + // 연산 배열 리턴 |
| 110 | + |
| 111 | + return operations; |
| 112 | + } |
| 113 | + |
| 114 | + // CRDT 연산들을 페이지에 적용하고 다른 클라이언트에 뿌리는 로직 (workspace.service) |
| 115 | + emitOperations() { |
| 116 | + |
| 117 | + } |
| 118 | + |
| 119 | + // 블록 타입을 판정하는 로직 |
| 120 | + parseBlockType(line: String): {type: ElementType, length: number, indent : number} { |
| 121 | + |
| 122 | + const indent = line.match(/^[\s]*/)[0].length / 2 || 0; |
| 123 | + const trimmed = line.trim(); |
| 124 | + if (trimmed.startsWith('# ')) return { type: "h1", length: 2, indent}; |
| 125 | + if (trimmed.startsWith('## ')) return { type: "h2", length: 3, indent }; |
| 126 | + if (trimmed.startsWith('### ')) return { type: "h3", length: 4, indent }; |
| 127 | + if (trimmed.startsWith('- ')) return { type: "ul", length: 2, indent }; |
| 128 | + if (/^\d+\. /.test(trimmed)) return { type: "ol", length: 3, indent }; |
| 129 | + if (trimmed.startsWith('> ')) return { type: "blockquote", length: 2, indent }; |
| 130 | + if (trimmed.startsWith('[] ')) return {type: "checkbox", length: 3, indent }; |
| 131 | + if (trimmed.startsWith('[x] ')) return {type: "checkbox", length: 4, indent}; |
| 132 | + if (trimmed === '---') return {type: "hr", length: 0, indent}; |
| 133 | + return {type: "p", length: 0, indent}; |
| 134 | + }; |
| 135 | + |
| 136 | + parseCharType() {}; |
| 137 | +} |
0 commit comments