From c17e8344aff139f55d676e40043af6930a34091c Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 4 Dec 2024 15:32:11 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F(backend)=20change=20t?= =?UTF-8?q?ext=20type=20of=20ai-translate=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We change the text type of the ai-translate endpoint from `text` to `json` to be able to send a json object with the text associated with an id. The ai will return the translated text with the id. Thanks to that we can preserve totally the Blocknote json and replace only the text. --- CHANGELOG.md | 12 +++- src/backend/core/api/serializers.py | 7 +- src/backend/core/api/viewsets.py | 4 +- src/backend/core/services/ai_services.py | 10 ++- .../test_api_documents_ai_transform.py | 9 ++- .../test_api_documents_ai_translate.py | 70 +++++++++++-------- 6 files changed, 70 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e174b20..9c2b52b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to ## [Unreleased] +## Added + +- ✨(backend) annotate number of accesses on documents in list view #411 +- ✨(backend) allow users to mark/unmark documents as favorite #411 + +## Changes + +- ⚡️(backend) optimize number of queries on document list view #411 + ## [1.8.2] - 2024-11-28 @@ -28,8 +37,6 @@ and this project adheres to ## Added -- ✨(backend) annotate number of accesses on documents in list view #411 -- ✨(backend) allow users to mark/unmark documents as favorite #411 - 🌐(backend) add German translation #259 - 🌐(frontend) add German translation #255 - ✨(frontend) add a broadcast store #387 @@ -41,7 +48,6 @@ and this project adheres to ## Changed -- ⚡️(backend) optimize number of queries on document list view #411 - 🚸(backend) improve users similarity search and sort results #391 - ♻️(frontend) simplify stores #402 - ✨(frontend) update $css Box props type to add styled components RuleSet #423 diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 9a81fc47e..1ae17e688 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -426,11 +426,12 @@ class AITranslateSerializer(serializers.Serializer): language = serializers.ChoiceField( choices=tuple(enums.ALL_LANGUAGES.items()), required=True ) - text = serializers.CharField(required=True) + text = serializers.JSONField(required=True) def validate_text(self, value): """Ensure the text field is not empty.""" - if len(value.strip()) == 0: - raise serializers.ValidationError("Text field cannot be empty.") + if not isinstance(value, dict): + raise serializers.ValidationError("Text field must be a json object.") + return value diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 75c0f5c4c..df7f1dbb8 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -680,9 +680,9 @@ def ai_translate(self, request, *args, **kwargs): """ POST /api/v1.0/documents//ai-translate with expected data: - - text: str + - text: json - language: str [settings.LANGUAGES] - Return JSON response with the translated text. + Return the same json but with the value updated, keep the keys accordingly. """ # Check permissions first self.get_object() diff --git a/src/backend/core/services/ai_services.py b/src/backend/core/services/ai_services.py index b7c70405c..e94ef55f4 100644 --- a/src/backend/core/services/ai_services.py +++ b/src/backend/core/services/ai_services.py @@ -15,29 +15,33 @@ "Answer the prompt in markdown format. Return JSON: " '{"answer": "Your markdown answer"}. ' "Do not provide any other information." + "Preserve the language." ), "correct": ( "Correct grammar and spelling of the markdown text, " "preserving language and markdown formatting. " 'Return JSON: {"answer": "your corrected markdown text"}. ' "Do not provide any other information." + "Preserve the language." ), "rephrase": ( "Rephrase the given markdown text, " "preserving language and markdown formatting. " 'Return JSON: {"answer": "your rephrased markdown text"}. ' "Do not provide any other information." + "Preserve the language." ), "summarize": ( "Summarize the markdown text, preserving language and markdown formatting. " 'Return JSON: {"answer": "your markdown summary"}. ' "Do not provide any other information." + "Preserve the language." ), } AI_TRANSLATE = ( - "Translate the markdown text to {language:s}, preserving markdown formatting. " - 'Return JSON: {{"answer": "your translated markdown text in {language:s}"}}. ' + "Translate to {language:s} for every value of the json provided." + "Keep the same json but with the value updated, keep the keys accordingly." "Do not provide any other information." ) @@ -62,7 +66,7 @@ def call_ai_api(self, system_content, text): response_format={"type": "json_object"}, messages=[ {"role": "system", "content": system_content}, - {"role": "user", "content": json.dumps({"markdown_input": text})}, + {"role": "user", "content": json.dumps({"answer": text})}, ], ) diff --git a/src/backend/core/tests/documents/test_api_documents_ai_transform.py b/src/backend/core/tests/documents/test_api_documents_ai_transform.py index 91e16e4a5..51322ada2 100644 --- a/src/backend/core/tests/documents/test_api_documents_ai_transform.py +++ b/src/backend/core/tests/documents/test_api_documents_ai_transform.py @@ -86,9 +86,10 @@ def test_api_documents_ai_transform_anonymous_success(mock_create): "Summarize the markdown text, preserving language and markdown formatting. " 'Return JSON: {"answer": "your markdown summary"}. Do not provide any other ' "information." + "Preserve the language." ), }, - {"role": "user", "content": '{"markdown_input": "Hello"}'}, + {"role": "user", "content": '{"answer": "Hello"}'}, ], ) @@ -163,9 +164,10 @@ def test_api_documents_ai_transform_authenticated_success(mock_create, reach, ro "content": ( 'Answer the prompt in markdown format. Return JSON: {"answer": ' '"Your markdown answer"}. Do not provide any other information.' + "Preserve the language." ), }, - {"role": "user", "content": '{"markdown_input": "Hello"}'}, + {"role": "user", "content": '{"answer": "Hello"}'}, ], ) @@ -239,9 +241,10 @@ def test_api_documents_ai_transform_success(mock_create, via, role, mock_user_te "content": ( 'Answer the prompt in markdown format. Return JSON: {"answer": ' '"Your markdown answer"}. Do not provide any other information.' + "Preserve the language." ), }, - {"role": "user", "content": '{"markdown_input": "Hello"}'}, + {"role": "user", "content": '{"answer": "Hello"}'}, ], ) diff --git a/src/backend/core/tests/documents/test_api_documents_ai_translate.py b/src/backend/core/tests/documents/test_api_documents_ai_translate.py index 21547e7aa..2e026419d 100644 --- a/src/backend/core/tests/documents/test_api_documents_ai_translate.py +++ b/src/backend/core/tests/documents/test_api_documents_ai_translate.py @@ -86,16 +86,18 @@ def test_api_documents_ai_translate_anonymous_success(mock_create): """ document = factories.DocumentFactory(link_reach="public", link_role="editor") - answer = '{"answer": "Salut"}' + answer = '{"answer": {"tid-1": "Ola"}}' mock_create.return_value = MagicMock( choices=[MagicMock(message=MagicMock(content=answer))] ) url = f"/api/v1.0/documents/{document.id!s}/ai-translate/" - response = APIClient().post(url, {"text": "Hello", "language": "es"}) + response = APIClient().post( + url, {"text": {"tid-1": "Hello"}, "language": "es"}, format="json" + ) assert response.status_code == 200 - assert response.json() == {"answer": "Salut"} + assert response.json() == {"answer": {"tid-1": "Ola"}} mock_create.assert_called_once_with( model="llama", response_format={"type": "json_object"}, @@ -103,12 +105,12 @@ def test_api_documents_ai_translate_anonymous_success(mock_create): { "role": "system", "content": ( - "Translate the markdown text to Spanish, preserving markdown formatting. " - 'Return JSON: {"answer": "your translated markdown text in Spanish"}. ' + "Translate to Spanish for every value of the json provided." + "Keep the same json but with the value updated, keep the keys accordingly." "Do not provide any other information." ), }, - {"role": "user", "content": '{"markdown_input": "Hello"}'}, + {"role": "user", "content": '{"answer": {"tid-1": "Hello"}}'}, ], ) @@ -164,16 +166,18 @@ def test_api_documents_ai_translate_authenticated_success(mock_create, reach, ro document = factories.DocumentFactory(link_reach=reach, link_role=role) - answer = '{"answer": "Salut"}' + answer = '{"answer": {"tid-1": "Ola"}}' mock_create.return_value = MagicMock( choices=[MagicMock(message=MagicMock(content=answer))] ) url = f"/api/v1.0/documents/{document.id!s}/ai-translate/" - response = client.post(url, {"text": "Hello", "language": "es-co"}) + response = client.post( + url, {"text": {"tid-1": "Hello"}, "language": "es-co"}, format="json" + ) assert response.status_code == 200 - assert response.json() == {"answer": "Salut"} + assert response.json() == {"answer": {"tid-1": "Ola"}} mock_create.assert_called_once_with( model="llama", response_format={"type": "json_object"}, @@ -181,13 +185,12 @@ def test_api_documents_ai_translate_authenticated_success(mock_create, reach, ro { "role": "system", "content": ( - "Translate the markdown text to Colombian Spanish, " - "preserving markdown formatting. Return JSON: " - '{"answer": "your translated markdown text in Colombian Spanish"}. ' + "Translate to Colombian Spanish for every value of the json provided." + "Keep the same json but with the value updated, keep the keys accordingly." "Do not provide any other information." ), }, - {"role": "user", "content": '{"markdown_input": "Hello"}'}, + {"role": "user", "content": '{"answer": {"tid-1": "Hello"}}'}, ], ) @@ -242,16 +245,18 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te document=document, team="lasuite", role=role ) - answer = '{"answer": "Salut"}' + answer = '{"answer": {"tid-1": "Ola"}}' mock_create.return_value = MagicMock( choices=[MagicMock(message=MagicMock(content=answer))] ) url = f"/api/v1.0/documents/{document.id!s}/ai-translate/" - response = client.post(url, {"text": "Hello", "language": "es-co"}) + response = client.post( + url, {"text": {"tid-1": "Hello"}, "language": "es-co"}, format="json" + ) assert response.status_code == 200 - assert response.json() == {"answer": "Salut"} + assert response.json() == {"answer": {"tid-1": "Ola"}} mock_create.assert_called_once_with( model="llama", response_format={"type": "json_object"}, @@ -259,19 +264,18 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te { "role": "system", "content": ( - "Translate the markdown text to Colombian Spanish, " - "preserving markdown formatting. Return JSON: " - '{"answer": "your translated markdown text in Colombian Spanish"}. ' + "Translate to Colombian Spanish for every value of the json provided." + "Keep the same json but with the value updated, keep the keys accordingly." "Do not provide any other information." ), }, - {"role": "user", "content": '{"markdown_input": "Hello"}'}, + {"role": "user", "content": '{"answer": {"tid-1": "Hello"}}'}, ], ) -def test_api_documents_ai_translate_empty_text(): - """The text should not be empty when requesting AI translate.""" +def test_api_documents_ai_translate_should_be_json(): + """The text should be a json when requesting AI translate.""" user = factories.UserFactory() client = APIClient() @@ -283,7 +287,7 @@ def test_api_documents_ai_translate_empty_text(): response = client.post(url, {"text": " ", "language": "es"}) assert response.status_code == 400 - assert response.json() == {"text": ["This field may not be blank."]} + assert response.json() == {"text": ["Text field must be a json object."]} def test_api_documents_ai_translate_invalid_action(): @@ -296,7 +300,9 @@ def test_api_documents_ai_translate_invalid_action(): document = factories.DocumentFactory(link_reach="public", link_role="editor") url = f"/api/v1.0/documents/{document.id!s}/ai-translate/" - response = client.post(url, {"text": "Hello", "language": "invalid"}) + response = client.post( + url, {"text": {"tid-1": "Hello"}, "language": "invalid"}, format="json" + ) assert response.status_code == 400 assert response.json() == {"language": ['"invalid" is not a valid choice.']} @@ -322,13 +328,17 @@ def test_api_documents_ai_translate_throttling_document(mock_create): for _ in range(3): user = factories.UserFactory() client.force_login(user) - response = client.post(url, {"text": "Hello", "language": "es"}) + response = client.post( + url, {"text": {"tid-1": "Hello"}, "language": "es"}, format="json" + ) assert response.status_code == 200 assert response.json() == {"answer": "Salut"} user = factories.UserFactory() client.force_login(user) - response = client.post(url, {"text": "Hello", "language": "es"}) + response = client.post( + url, {"text": {"tid-1": "Hello"}, "language": "es"}, format="json" + ) assert response.status_code == 429 assert response.json() == { @@ -356,13 +366,17 @@ def test_api_documents_ai_translate_throttling_user(mock_create): for _ in range(3): document = factories.DocumentFactory(link_reach="public", link_role="editor") url = f"/api/v1.0/documents/{document.id!s}/ai-translate/" - response = client.post(url, {"text": "Hello", "language": "es"}) + response = client.post( + url, {"text": {"tid-1": "Hello"}, "language": "es"}, format="json" + ) assert response.status_code == 200 assert response.json() == {"answer": "Salut"} document = factories.DocumentFactory(link_reach="public", link_role="editor") url = f"/api/v1.0/documents/{document.id!s}/ai-translate/" - response = client.post(url, {"text": "Hello", "language": "es"}) + response = client.post( + url, {"text": {"tid-1": "Hello"}, "language": "es"}, format="json" + ) assert response.status_code == 429 assert response.json() == { From f45e724b1797693841b9304c4d7dcc6939309bad Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 5 Dec 2024 23:38:50 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20fix=20AI=20r?= =?UTF-8?q?equests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We improved the blocks translations. Only for translations: We are now preserving totally the Blocknote json structure like to avoid losing any information. We are not transforming the blocks in markdown anymore, we are adding "id" to the text type blocks, to identify them, we extract the text and send them to the ai service to translate them. The ai preserve the id, then we replace the text blocks with the translated ones. --- CHANGELOG.md | 2 + .../__tests__/app-impress/doc-editor.spec.ts | 2 +- .../docs/doc-editor/api/useDocAITranslate.tsx | 4 +- .../docs/doc-editor/components/AIButton.tsx | 63 +++++--- .../src/features/docs/doc-editor/utilsAI.ts | 145 ++++++++++++++++++ 5 files changed, 190 insertions(+), 26 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/utilsAI.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2b52b1e..7938a3d3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to ## Changes +- ♻️(frontend) Improve Ai translations #478 +- 🐛(frontend) Fix hidden menu on Firefox #468 - ⚡️(backend) optimize number of queries on document list view #411 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index d41660a5d..91570125e 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -262,7 +262,7 @@ test.describe('Doc Editor', () => { if (request.method().includes('POST')) { await route.fulfill({ json: { - answer: 'Bonjour le monde', + answer: { 'tid-0': 'Bonjour le monde' }, }, }); } else { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/api/useDocAITranslate.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/api/useDocAITranslate.tsx index 504d79b3e..c21b66f7c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/api/useDocAITranslate.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/api/useDocAITranslate.tsx @@ -4,12 +4,12 @@ import { APIError, errorCauses, fetchAPI } from '@/api'; export type DocAITranslate = { docId: string; - text: string; + text: Record; language: string; }; export type DocAITranslateResponse = { - answer: string; + answer: Record; }; export const docAITranslate = async ({ diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/AIButton.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/AIButton.tsx index 31c98291d..4ac58dd27 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/AIButton.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/AIButton.tsx @@ -1,3 +1,4 @@ +import { Block } from '@blocknote/core'; import { ComponentProps, useBlockNoteEditor, @@ -21,6 +22,12 @@ import { useDocAITransform, useDocAITranslate, } from '../api/'; +import { + Node, + addIdToTextNodes, + extractTextWithId, + updateTextsWithId, +} from '../utilsAI'; type LanguageTranslate = { value: string; @@ -212,14 +219,23 @@ const AIMenuItemTransform = ({ icon, }: PropsWithChildren) => { const { mutateAsync: requestAI, isPending } = useDocAITransform(); + const editor = useBlockNoteEditor(); + + const requestAIAction = async (selectedBlocks: Block[]) => { + const text = await editor.blocksToMarkdownLossy(selectedBlocks); - const requestAIAction = async (markdown: string) => { const responseAI = await requestAI({ - text: markdown, + text, action, docId, }); - return responseAI.answer; + + if (!responseAI || !responseAI.answer) { + throw new Error('No response from AI'); + } + + const markdown = await editor.tryParseMarkdownToBlocks(responseAI.answer); + editor.replaceBlocks(selectedBlocks, markdown); }; return ( @@ -242,14 +258,28 @@ const AIMenuItemTranslate = ({ language, }: PropsWithChildren) => { const { mutateAsync: requestAI, isPending } = useDocAITranslate(); + const editor = useBlockNoteEditor(); + + const requestAITranslate = async (selectedBlocks: Block[]) => { + addIdToTextNodes(selectedBlocks as Node); + const content = extractTextWithId(selectedBlocks as Node); - const requestAITranslate = async (markdown: string) => { const responseAI = await requestAI({ - text: markdown, + text: content, language, docId, }); - return responseAI.answer; + + if (!responseAI || !responseAI.answer) { + throw new Error('No response from AI'); + } + + updateTextsWithId(selectedBlocks as Node, responseAI.answer); + selectedBlocks.forEach((block) => { + if (editor.getBlock(block)) { + editor.replaceBlocks([block], [block]); + } + }); }; return ( @@ -264,7 +294,7 @@ const AIMenuItemTranslate = ({ }; interface AIMenuItemProps { - requestAI: (markdown: string) => Promise; + requestAI: (blocks: Block[]) => Promise; isPending: boolean; icon?: ReactNode; } @@ -280,31 +310,17 @@ const AIMenuItem = ({ const editor = useBlockNoteEditor(); const handleAIError = useHandleAIError(); - const handleAIAction = async () => { + const handleAIAction = () => { let selectedBlocks = editor.getSelection()?.blocks; if (!selectedBlocks || selectedBlocks.length === 0) { selectedBlocks = [editor.getTextCursorPosition().block]; - if (!selectedBlocks || selectedBlocks.length === 0) { return; } } - const markdown = await editor.blocksToMarkdownLossy(selectedBlocks); - - try { - const responseAI = await requestAI(markdown); - - if (!responseAI) { - return; - } - - const blockMarkdown = await editor.tryParseMarkdownToBlocks(responseAI); - editor.replaceBlocks(selectedBlocks, blockMarkdown); - } catch (error) { - handleAIError(error); - } + requestAI(selectedBlocks).catch(handleAIError); }; if (!Components) { @@ -338,6 +354,7 @@ const useHandleAIError = () => { return; } + console.error('AI', error); toast(t('AI seems busy! Please try again.'), VariantType.ERROR); }; }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/utilsAI.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/utilsAI.ts new file mode 100644 index 000000000..7dadeca00 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/utilsAI.ts @@ -0,0 +1,145 @@ +import { Block as CoreBlock } from '@blocknote/core'; + +type Block = Omit & { + tid?: string; + type: string; + text?: string; + content: Block[] | Block; + children?: Block[] | Block; +}; +export type Node = Block[] | Block; + +let idCounter = 0; + +// Function to generate a unique id +function generateId() { + return `tid-${idCounter++}`; +} + +// Function to add a unique id to each text node +export function addIdToTextNodes(node: Node) { + if (Array.isArray(node)) { + node.forEach((child) => addIdToTextNodes(child)); + } else if (typeof node === 'object' && node !== null) { + if (node.type === 'text') { + node.tid = generateId(); + } + + // Recursively process content and children + if (node.content) { + addIdToTextNodes(node.content); + } + + if (node.children) { + addIdToTextNodes(node.children); + } + + // Handle table content + if ( + !Array.isArray(node.content) && + node.type === 'table' && + node.content && + node.content.type === 'tableContent' + ) { + const tableContent = node.content; + if (tableContent.rows) { + tableContent.rows.forEach((row) => { + if (row.cells) { + row.cells.forEach((cell) => { + addIdToTextNodes(cell as unknown as Node); + }); + } + }); + } + } + } +} + +// Function to extract texts with their tids into a flat JSON object +export function extractTextWithId( + node: Node, + texts: Record = {}, +) { + if (Array.isArray(node)) { + node.forEach((child) => extractTextWithId(child, texts)); + } else if (typeof node === 'object' && node !== null) { + if (node.type === 'text' && node.tid) { + texts[node.tid] = node.text || ''; + } + + // Recursively process content and children + if (node.content) { + extractTextWithId(node.content, texts); + } + + if (node.children) { + extractTextWithId(node.children, texts); + } + + // Handle table content + if ( + !Array.isArray(node.content) && + node.type === 'table' && + node.content && + node.content.type === 'tableContent' + ) { + const tableContent = node.content; + if (tableContent.rows) { + tableContent.rows.forEach((row) => { + if (row.cells) { + row.cells.forEach((cell) => { + extractTextWithId(cell as unknown as Node, texts); + }); + } + }); + } + } + } + return texts; +} + +// Function to update the original JSON using a second JSON containing updated texts +export function updateTextsWithId( + node: Node, + updatedTexts: Record, +) { + if (Array.isArray(node)) { + node.forEach((child) => updateTextsWithId(child, updatedTexts)); + } else if (typeof node === 'object' && node !== null) { + if ( + node.type === 'text' && + node.tid && + updatedTexts[node.tid] !== undefined + ) { + node.text = updatedTexts[node.tid]; + } + + // Recursively process content and children + if (node.content) { + updateTextsWithId(node.content, updatedTexts); + } + + if (node.children) { + updateTextsWithId(node.children, updatedTexts); + } + + // Handle table content + if ( + !Array.isArray(node.content) && + node.type === 'table' && + node.content && + node.content.type === 'tableContent' + ) { + const tableContent = node.content; + if (tableContent.rows) { + tableContent.rows.forEach((row) => { + if (row.cells) { + row.cells.forEach((cell) => { + updateTextsWithId(cell as unknown as Node, updatedTexts); + }); + } + }); + } + } + } +} From 85626c659ccda10da41cbb25c4551b6a720a0174 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 6 Dec 2024 09:55:24 +0100 Subject: [PATCH 3/3] =?UTF-8?q?fixup!=20=E2=99=BB=EF=B8=8F(frontend)=20fix?= =?UTF-8?q?=20AI=20requests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/docs/doc-editor/utilsAI.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/utilsAI.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/utilsAI.ts index 7dadeca00..c71ded886 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/utilsAI.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/utilsAI.ts @@ -2,7 +2,7 @@ import { Block as CoreBlock } from '@blocknote/core'; type Block = Omit & { tid?: string; - type: string; + type?: string; text?: string; content: Block[] | Block; children?: Block[] | Block; @@ -21,7 +21,7 @@ export function addIdToTextNodes(node: Node) { if (Array.isArray(node)) { node.forEach((child) => addIdToTextNodes(child)); } else if (typeof node === 'object' && node !== null) { - if (node.type === 'text') { + if (node?.type === 'text') { node.tid = generateId(); } @@ -37,7 +37,7 @@ export function addIdToTextNodes(node: Node) { // Handle table content if ( !Array.isArray(node.content) && - node.type === 'table' && + node?.type === 'table' && node.content && node.content.type === 'tableContent' ) { @@ -63,7 +63,7 @@ export function extractTextWithId( if (Array.isArray(node)) { node.forEach((child) => extractTextWithId(child, texts)); } else if (typeof node === 'object' && node !== null) { - if (node.type === 'text' && node.tid) { + if (node?.type === 'text' && node.tid) { texts[node.tid] = node.text || ''; } @@ -79,7 +79,7 @@ export function extractTextWithId( // Handle table content if ( !Array.isArray(node.content) && - node.type === 'table' && + node?.type === 'table' && node.content && node.content.type === 'tableContent' ) { @@ -107,7 +107,7 @@ export function updateTextsWithId( node.forEach((child) => updateTextsWithId(child, updatedTexts)); } else if (typeof node === 'object' && node !== null) { if ( - node.type === 'text' && + node?.type === 'text' && node.tid && updatedTexts[node.tid] !== undefined ) { @@ -126,7 +126,7 @@ export function updateTextsWithId( // Handle table content if ( !Array.isArray(node.content) && - node.type === 'table' && + node?.type === 'table' && node.content && node.content.type === 'tableContent' ) {