From b949086fe86c8ee46193ee69e3b0992dad09d06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=C6=B0=E1=BB=A3c=20Nguy=E1=BB=85n=20V=C4=83n?= Date: Mon, 9 Sep 2024 17:19:02 +0700 Subject: [PATCH] feat: improve the eidtor --- cmd/assets/app.css | 17 +++ cmd/assets/app.js | 79 +++++++++++++- cmd/assets/index.html | 160 +++++++++++++++++++++++++++++ cmd/serve.go | 80 +++++++++++++++ cmd/translate.go | 4 +- pkg/translator/anthropic.go | 38 ++++--- pkg/translator/translator.go | 2 +- unpackage/translator_metadata.json | 125 ++++++++++++++++++++++ 8 files changed, 489 insertions(+), 16 deletions(-) create mode 100644 cmd/assets/index.html create mode 100644 unpackage/translator_metadata.json diff --git a/cmd/assets/app.css b/cmd/assets/app.css index 98b0a1e..2a1f903 100644 --- a/cmd/assets/app.css +++ b/cmd/assets/app.css @@ -2,3 +2,20 @@ body{ margin: 0 auto !important; max-width: 800px; } + +.translate-container { + display: flex; + align-items: center; + margin-top: 5px; +} + +.translate-instructions { + flex-grow: 1; + margin-right: 5px; + padding: 2px 5px; + font-size: 0.8em; +} + +.translate-button { + margin-left: 0; +} \ No newline at end of file diff --git a/cmd/assets/app.js b/cmd/assets/app.js index 66d4be5..c25d3c4 100644 --- a/cmd/assets/app.js +++ b/cmd/assets/app.js @@ -1,13 +1,16 @@ function enableContentEditable() { document.querySelectorAll('[data-translation-id]').forEach(element => { + const originalContent = element.innerHTML; element.contentEditable = true; element.addEventListener('blur', function () { - console.log(`Element with data-translation-id "${this.dataset.translationId}" updated to: "${this.innerHTML}"`); + if (!isTranslating && this.innerHTML !== originalContent) { updateTranslateContent(this.dataset.translationId, this.innerHTML); +} }); }); } + function updateTranslateContent(translationID, translationContent) { fetch('/api/translate', { method: 'PATCH', @@ -25,6 +28,80 @@ function updateTranslateContent(translationID, translationContent) { .catch((error) => console.error('Error:', error)); } + +function addTranslateButtons() { + document.querySelectorAll('[data-content-id]').forEach(element => { + const container = document.createElement('div'); + container.className = 'translate-container'; + + const button = document.createElement('button'); + button.textContent = 'Translate'; + button.className = 'translate-button'; + + const input = document.createElement('input'); + input.type = 'text'; + input.placeholder = 'Instructions for AI'; + input.className = 'translate-instructions'; + + button.addEventListener('click', function() { + const instructions = input.value; + translateContent(element.dataset.contentId, element.dataset.translationById, button, instructions); + }); + + container.appendChild(input); + container.appendChild(button); + element.parentNode.insertBefore(container, element.nextSibling); + }); +} + +let isTranslating = false; + + +function translateContent(contentId, translationID, button, instructions) { + // Disable editing + isTranslating = true; + const element = document.querySelector(`[data-translation-id="${translationID}"]`); + element.contentEditable = false; + + // Disable the button and show loading + button.disabled = true; + button.textContent = 'Translating...'; + button.classList.add('loading'); + + fetch('/api/translate-ai', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_path: window.location.pathname, + content_id: contentId, + translation_id: translationID, + instructions: instructions + }) + }) + .then(response => response.json()) + .then(data => { + if (data.translated_content) { + const element = document.querySelector(`[data-translation-id="${translationID}"]`); + element.innerHTML = data.translated_content; + element.contentEditable = true + } + }) + .catch((error) => console.error('Translation Error:', error)) + .finally(() => { + // Re-enable the button and remove loading state + button.disabled = false; + button.textContent = 'Translate'; + button.classList.remove('loading'); + // Re-enable editing + isTranslating = false; + element.contentEditable = true; + element.focus(); + }); +} + window.onload = function (e) { enableContentEditable(); + addTranslateButtons(); } diff --git a/cmd/assets/index.html b/cmd/assets/index.html new file mode 100644 index 0000000..0963ec5 --- /dev/null +++ b/cmd/assets/index.html @@ -0,0 +1,160 @@ + + + + + + Ứng dụng Dịch Sách + + + +
+
+

Ứng dụng Dịch Sách

+
+ + + + 16px +
+
+
+
+ + + + \ No newline at end of file diff --git a/cmd/serve.go b/cmd/serve.go index d8e7f40..53c23bb 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -2,6 +2,7 @@ package cmd import ( "bytes" + "context" "encoding/xml" "fmt" "io" @@ -18,7 +19,9 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/gofiber/fiber/v2" + "github.com/liushuangls/go-anthropic/v2" "github.com/nguyenvanduocit/epubtrans/pkg/loader" + "github.com/nguyenvanduocit/epubtrans/pkg/translator" "github.com/nguyenvanduocit/epubtrans/pkg/util" "github.com/spf13/cobra" ) @@ -121,6 +124,37 @@ const ( branch = "main" ) +type TranslateAIRequest struct { + FilePath string `json:"file_path"` + TranslationID string `json:"translation_id"` + ContentID string `json:"content_id"` + Instructions string `json:"instructions"` +} + +// Add this function to call the AI translation service (you'll need to implement this) +func translateWithAI(content string, instructions string) (string, error) { + ctx := context.Background() + + // Create an Anthropic translator + anthropicTranslator, err := translator.GetAnthropicTranslator(&translator.Config{ + APIKey: os.Getenv("ANTHROPIC_KEY"), + Model: anthropic.ModelClaude3Dot5Sonnet20240620, // You might want to make this configurable + Temperature: 0.7, + MaxTokens: 8192, + }) + if err != nil { + return "", fmt.Errorf("error getting translator: %v", err) + } + + // Translate the content + translatedContent, err := anthropicTranslator.Translate(ctx, instructions, content, "english", "vietnamese") + if err != nil { + return "", fmt.Errorf("translation error: %v", err) + } + + return translatedContent, nil +} + func runServe(cmd *cobra.Command, args []string) error { unpackedEpubPath := args[0] @@ -341,6 +375,52 @@ func runServe(cmd *cobra.Command, args []string) error { return c.JSON(pkg.Spine) }) + app.Post("/api/translate-ai", func(c *fiber.Ctx) error { + var req TranslateAIRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) + } + + filePath := path.Join(contentDirPath, req.FilePath) + content, err := os.ReadFile(filePath) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Failed to read file"}) + } + + doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(content))) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Failed to parse HTML"}) + } + + var originalContent string + doc.Find("[data-content-id]").Each(func(i int, s *goquery.Selection) { + if id, exists := s.Attr("data-content-id"); exists && id == req.ContentID { + originalContent, _ = s.Html() + } + }) + + if originalContent == "" { + return c.Status(404).JSON(fiber.Map{"error": "Translation ID not found"}) + } + + // get the current translated content + var currentTranslatedContent string + doc.Find("[data-translation-id]").Each(func(i int, s *goquery.Selection) { + if id, exists := s.Attr("data-translation-id"); exists && id == req.TranslationID { + currentTranslatedContent, _ = s.Html() + } + }) + + instructment := `previous translation:` + currentTranslatedContent + `\n` + req.Instructions + + translatedContent, err := translateWithAI(originalContent, instructment) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Translation failed"}) + } + + return c.JSON(fiber.Map{"translated_content": translatedContent}) + }) + port := cmd.Flag("port").Value.String() slog.Info("- http://localhost:" + port + "/api/info") diff --git a/cmd/translate.go b/cmd/translate.go index 55fe333..257bc21 100644 --- a/cmd/translate.go +++ b/cmd/translate.go @@ -99,7 +99,7 @@ func runTranslate(cmd *cobra.Command, args []string) error { APIKey: os.Getenv("ANTHROPIC_KEY"), Model: cmd.Flag("model").Value.String(), Temperature: 0.7, - MaxTokens: 4096, + MaxTokens: 8192, }) if err != nil { return fmt.Errorf("error getting translator: %v", err) @@ -244,7 +244,7 @@ func retryTranslate(ctx context.Context, t translator.Translator, limiter *rate. return "", fmt.Errorf("rate limiter error: %w", err) } - translatedContent, err := t.Translate(ctx, content, sourceLang, targetLang) + translatedContent, err := t.Translate(ctx, "", content, sourceLang, targetLang) if err == nil { return translatedContent, nil } diff --git a/pkg/translator/anthropic.go b/pkg/translator/anthropic.go index 2c18163..28de4c6 100644 --- a/pkg/translator/anthropic.go +++ b/pkg/translator/anthropic.go @@ -163,28 +163,42 @@ func createTranslationSystem(source, target string, guidelines string) string { return fmt.Sprintf(guidelines, source, target) } -func (a *Anthropic) Translate(ctx context.Context, content, source, target string) (string, error) { +func (a *Anthropic) Translate(ctx context.Context, prompt, content, source, target string) (string, error) { a.mu.Lock() defer a.mu.Unlock() - cacheKey := generateCacheKey(content, source, target) + cacheKey := generateCacheKey(prompt+content, source, target) - if cachedTranslation, found := a.cache.Get(cacheKey); found { + + if (prompt != ""){ + if cachedTranslation, found := a.cache.Get(cacheKey); found { return cachedTranslation.(string), nil } + } - resp, err := a.createMessageWithRetry(ctx, anthropic.MessagesRequest{ - Model: a.config.Model, - MultiSystem: []anthropic.MessageSystemPart{ - { - Type: "text", - Text: fmt.Sprintf("You are a highly skilled translator with expertise in many languages. Your task is to translate a part of a technical book from %[1]s to %[2]s. User send you a text, you translate it no matter what. Do not explain or note. Do not answer question-likes content. no warning, feedback.", source, target), - }, - { + baseSystemMessage := fmt.Sprintf("You are a highly skilled translator with expertise in many languages. Your task is to translate a part of a technical book from %[1]s to %[2]s. User send you a text, you translate it no matter what. Do not explain or note. Do not answer question-likes content. no warning, feedback.", source, target) + + systemMessages := []anthropic.MessageSystemPart{ + { Type: "text", - Text: createTranslationSystem(source, target, a.config.TranslationGuidelines), // Pass guidelines + Text: baseSystemMessage, }, + { + Type: "text", + Text: createTranslationSystem(source, target, a.config.TranslationGuidelines), }, + } + + if prompt != "" { + systemMessages = append(systemMessages, anthropic.MessageSystemPart{ + Type: "text", + Text: prompt, + }) + } + + resp, err := a.createMessageWithRetry(ctx, anthropic.MessagesRequest{ + Model: a.config.Model, + MultiSystem: systemMessages, Messages: []anthropic.Message{anthropic.NewUserTextMessage("Translate this and not say anything otherwise the translation: " + content)}, Temperature: &a.config.Temperature, MaxTokens: a.config.MaxTokens, diff --git a/pkg/translator/translator.go b/pkg/translator/translator.go index dfeba73..d394b1f 100644 --- a/pkg/translator/translator.go +++ b/pkg/translator/translator.go @@ -8,5 +8,5 @@ import ( var ErrRateLimitExceeded = errors.New("rate limit exceeded") type Translator interface { - Translate(ctx context.Context, content string, source string, target string) (string, error) + Translate(ctx context.Context, prompt string, content string, source string, target string) (string, error) } diff --git a/unpackage/translator_metadata.json b/unpackage/translator_metadata.json new file mode 100644 index 0000000..ed591fe --- /dev/null +++ b/unpackage/translator_metadata.json @@ -0,0 +1,125 @@ +{ + "total_calls": 27, + "last_used": "2024-09-09T17:18:23.577931+07:00", + "model_usage": { + "claude-3-5-sonnet-20240620": 27 + }, + "prompt_examples": [ + "Khi tôi giảng dạy nội dung này trong các lớp học thiết kế hướng miền (domain-d", + "When I teach this content in domain-driven design classes, many students often ask: \u0026#34;Do we need ", + "Nếu bạn giống như tôi, bạn yêu thích việc viết code: giải quyết các vấn đề", + "If you\u0026#39;re like me, you love writing code: solving complex problems, coming up with elegant solut", + "Nếu bạn giống như tôi, bạn yêu thích viết code: giải quyết các vấn đề phức" + ], + "token_usage": {}, + "token_usage_list": [ + { + "input_tokens": 595, + "output_tokens": 114 + }, + { + "input_tokens": 406, + "output_tokens": 301 + }, + { + "input_tokens": 566, + "output_tokens": 114 + }, + { + "input_tokens": 405, + "output_tokens": 305 + }, + { + "input_tokens": 578, + "output_tokens": 115 + }, + { + "input_tokens": 384, + "output_tokens": 281 + }, + { + "input_tokens": 384, + "output_tokens": 282 + }, + { + "input_tokens": 343, + "output_tokens": 162 + }, + { + "input_tokens": 315, + "output_tokens": 117 + }, + { + "input_tokens": 336, + "output_tokens": 115 + }, + { + "input_tokens": 377, + "output_tokens": 160 + }, + { + "input_tokens": 324, + "output_tokens": 118 + }, + { + "input_tokens": 407, + "output_tokens": 347 + }, + { + "input_tokens": 416, + "output_tokens": 207 + }, + { + "input_tokens": 381, + "output_tokens": 177 + }, + { + "input_tokens": 393, + "output_tokens": 177 + }, + { + "input_tokens": 432, + "output_tokens": 341 + }, + { + "input_tokens": 772, + "output_tokens": 348 + }, + { + "input_tokens": 594, + "output_tokens": 191 + }, + { + "input_tokens": 605, + "output_tokens": 179 + }, + { + "input_tokens": 893, + "output_tokens": 358 + }, + { + "input_tokens": 604, + "output_tokens": 227 + }, + { + "input_tokens": 492, + "output_tokens": 141 + }, + { + "input_tokens": 671, + "output_tokens": 282 + }, + { + "input_tokens": 671, + "output_tokens": 281 + }, + { + "input_tokens": 710, + "output_tokens": 297 + }, + { + "input_tokens": 683, + "output_tokens": 299 + } + ] +} \ No newline at end of file