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); }