Skip to content

Commit

Permalink
feat: improve the eidtor
Browse files Browse the repository at this point in the history
  • Loading branch information
duocnv-firegroup committed Sep 9, 2024
1 parent 768735e commit b949086
Show file tree
Hide file tree
Showing 8 changed files with 489 additions and 16 deletions.
17 changes: 17 additions & 0 deletions cmd/assets/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
79 changes: 78 additions & 1 deletion cmd/assets/app.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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();
}
160 changes: 160 additions & 0 deletions cmd/assets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ứng dụng Dịch Sách</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
transition: background-color 0.3s, color 0.3s;
}
.dark-mode {
background-color: #1a1a1a;
color: #ffffff;
}
.container {
max-width: 800px;
margin: 0 auto;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
button {
padding: 5px 10px;
margin-left: 10px;
cursor: pointer;
}
.paragraph {
margin-bottom: 20px;
}
.translation {
color: #666;
position: relative;
}
.dark-mode .translation {
color: #aaa;
}
.editing {
border: 1px solid #ccc;
padding: 5px;
}
.edit-buttons {
position: absolute;
right: 0;
top: 0;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Ứng dụng Dịch Sách</h1>
<div>
<button id="darkModeToggle">🌙</button>
<button id="translationToggle">📖</button>
<input type="range" id="fontSizeSlider" min="12" max="24" value="16">
<span id="fontSizeDisplay">16px</span>
</div>
</header>
<main id="content"></main>
</div>

<script>
const paragraphs = [
{
original: "It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness...",
translated: "Đó là thời kỳ tốt đẹp nhất, cũng là thời kỳ tồi tệ nhất, đó là thời đại của sự khôn ngoan, cũng là thời đại của sự ngu xuẩn...",
isEditing: false
},
{
original: "It was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair.",
translated: "Đó là thời đại của niềm tin, là thời đại của sự hoài nghi, là mùa của Ánh sáng, là mùa của Bóng tối, là mùa xuân của hy vọng, là mùa đông của tuyệt vọng.",
isEditing: false
},
{
original: "We had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way—in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only.",
translated: "Chúng ta có tất cả trước mắt, chúng ta chẳng có gì trước mắt, tất cả chúng ta đều đang đi thẳng tới Thiên đường, tất cả chúng ta đều đang đi thẳng theo hướng ngược lại—tóm lại, thời kỳ đó giống thời kỳ hiện tại đến nỗi một số nhà chức trách ồn ào nhất của nó khăng khăng rằng nó chỉ có thể được chấp nhận, dù tốt hay xấu, ở mức độ so sánh tối cao.",
isEditing: false
}
];

let showTranslation = true;

function renderParagraphs() {
const content = document.getElementById('content');
content.innerHTML = '';
paragraphs.forEach((paragraph, index) => {
const div = document.createElement('div');
div.className = 'paragraph';
div.innerHTML = `
<p>${paragraph.original}</p>
<div class="translation ${showTranslation ? '' : 'hidden'}">
<p ondblclick="startEditing(${index})">${paragraph.translated}</p>
<div class="edit-buttons ${paragraph.isEditing ? '' : 'hidden'}">
<button onclick="aiTranslate(${index})">AI</button>
<button onclick="saveTranslation(${index})">Lưu</button>
</div>
</div>
`;
content.appendChild(div);
});
}

function startEditing(index) {
paragraphs[index].isEditing = true;
renderParagraphs();
const translationElement = document.querySelectorAll('.translation')[index];
const paragraphElement = translationElement.querySelector('p');
paragraphElement.contentEditable = true;
paragraphElement.focus();
paragraphElement.className = 'editing';
}

function saveTranslation(index) {
const translationElement = document.querySelectorAll('.translation')[index];
const paragraphElement = translationElement.querySelector('p');
paragraphs[index].translated = paragraphElement.innerText;
paragraphs[index].isEditing = false;
renderParagraphs();
}

function aiTranslate(index) {
console.log(`Đang dịch đoạn ${index + 1} bằng AI...`);
setTimeout(() => {
paragraphs[index].translated = `Đây là bản dịch AI mới cho đoạn ${index + 1}.`;
console.log(`Dịch xong đoạn ${index + 1}!`);
renderParagraphs();
}, 2000);
}

document.getElementById('darkModeToggle').addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
});

document.getElementById('translationToggle').addEventListener('click', () => {
showTranslation = !showTranslation;
renderParagraphs();
});

const fontSizeSlider = document.getElementById('fontSizeSlider');
const fontSizeDisplay = document.getElementById('fontSizeDisplay');
fontSizeSlider.addEventListener('input', (e) => {
const fontSize = e.target.value;
document.body.style.fontSize = `${fontSize}px`;
fontSizeDisplay.textContent = `${fontSize}px`;
});

renderParagraphs();
</script>
</body>
</html>
80 changes: 80 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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")
Expand Down
Loading

0 comments on commit b949086

Please sign in to comment.