From cbf9091d1c3af8454b0a06fe24dd03ef4f8c40eb Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 12 Mar 2025 10:14:47 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(AI)=20improve=20formating=20?= =?UTF-8?q?of=20ai=20translation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ai translation were quite lossy about formatting. Colors, background, breaklines, table sizes were lost in the translation. We improve the AI translation request to keep the formatting as close as possible by using html instead of markdown. --- CHANGELOG.md | 1 + src/backend/core/services/ai_services.py | 47 +++++------- .../test_api_documents_ai_transform.py | 35 ++++----- .../test_api_documents_ai_translate.py | 45 ++++++------ .../core/tests/test_services_ai_services.py | 45 +----------- .../components/BlockNoteToolBar/AIButton.tsx | 71 ++++++++++++------- 6 files changed, 99 insertions(+), 145 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d2fb6a..2186237a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ and this project adheres to - 📝(doc) minor README.md formatting and wording enhancements - ♻️Stop setting a default title on doc creation #634 - ♻️(frontend) misc ui improvements #644 +- ♻️(frontend) Improve AI translations #478 ## Fixed diff --git a/src/backend/core/services/ai_services.py b/src/backend/core/services/ai_services.py index 102e8689..819cec0d 100644 --- a/src/backend/core/services/ai_services.py +++ b/src/backend/core/services/ai_services.py @@ -1,8 +1,5 @@ """AI services.""" -import json -import re - from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -12,32 +9,34 @@ from core import enums AI_ACTIONS = { "prompt": ( - "Answer the prompt in markdown format. Return JSON: " - '{"answer": "Your markdown answer"}. ' - "Do not provide any other information." + "Answer the prompt in markdown format. " + "Preserve the language and markdown formatting. " + "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." + "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." + "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." + "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}"}}. ' + "Keep the same html stucture and formatting. " + "Translate the content in the html to the specified language {language:s}. " + "Check the translation for accuracy and make any necessary corrections. " "Do not provide any other information." ) @@ -59,32 +58,18 @@ class AIService: """Helper method to call the OpenAI API and process the response.""" response = self.client.chat.completions.create( model=settings.AI_MODEL, - response_format={"type": "json_object"}, messages=[ {"role": "system", "content": system_content}, - {"role": "user", "content": json.dumps({"markdown_input": text})}, + {"role": "user", "content": text}, ], ) content = response.choices[0].message.content - try: - sanitized_content = re.sub(r'\s*"answer"\s*:\s*', '"answer": ', content) - sanitized_content = re.sub(r"\s*\}", "}", sanitized_content) - sanitized_content = re.sub(r"(?) => { 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?.answer) { + throw new Error('No response from AI'); + } + + const markdown = await editor.tryParseMarkdownToBlocks(responseAI.answer); + editor.replaceBlocks(selectedBlocks, markdown); }; return ( @@ -255,14 +265,35 @@ const AIMenuItemTranslate = ({ language, }: PropsWithChildren) => { const { mutateAsync: requestAI, isPending } = useDocAITranslate(); + const editor = useBlockNoteEditor(); + + const requestAITranslate = async (selectedBlocks: Block[]) => { + let fullHtml = ''; + for (const block of selectedBlocks) { + if (Array.isArray(block.content) && block.content.length === 0) { + fullHtml += '


'; + continue; + } + + fullHtml += await editor.blocksToHTMLLossy([block]); + } - const requestAITranslate = async (markdown: string) => { const responseAI = await requestAI({ - text: markdown, + text: fullHtml, language, docId, }); - return responseAI.answer; + + if (!responseAI || !responseAI.answer) { + throw new Error('No response from AI'); + } + + try { + const blocks = await editor.tryParseHTMLToBlocks(responseAI.answer); + editor.replaceBlocks(selectedBlocks, blocks); + } catch { + editor.replaceBlocks(selectedBlocks, selectedBlocks); + } }; return ( @@ -277,7 +308,7 @@ const AIMenuItemTranslate = ({ }; interface AIMenuItemProps { - requestAI: (markdown: string) => Promise; + requestAI: (blocks: Block[]) => Promise; isPending: boolean; icon?: ReactNode; } @@ -289,32 +320,24 @@ const AIMenuItem = ({ icon, }: PropsWithChildren) => { const Components = useComponentsContext(); + const { toast } = useToastProvider(); + const { t } = useTranslation(); const editor = useBlockNoteEditor(); const handleAIError = useHandleAIError(); const handleAIAction = async () => { - let selectedBlocks = editor.getSelection()?.blocks; + const selectedBlocks = editor.getSelection()?.blocks ?? [ + editor.getTextCursorPosition().block, + ]; - if (!selectedBlocks || selectedBlocks.length === 0) { - selectedBlocks = [editor.getTextCursorPosition().block]; - - if (!selectedBlocks || selectedBlocks.length === 0) { - return; - } + if (!selectedBlocks?.length) { + toast(t('No text selected'), VariantType.WARNING); + 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); + await requestAI(selectedBlocks); } catch (error) { handleAIError(error); }