"use client"; import { LightButton, PrimaryButton, IconClick, CircleButton } from "@/design-system/base/button"; import { Input } from "@/design-system/base/input"; import { Textarea } from "@/design-system/base/textarea"; import { Select } from "@/design-system/base/select"; import { IMAGES } from "@/config/images"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useRef, useState } from "react"; import { actionTranslateText } from "@/modules/translator/translator-action"; import { actionCreateCard } from "@/modules/card/card-action"; import { actionGetDecksByUserId } from "@/modules/deck/deck-action"; import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto"; import type { CardType } from "@/modules/card/card-action-dto"; import { toast } from "sonner"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; import { TSharedTranslationResult } from "@/shared/translator-type"; import { Plus } from "lucide-react"; import { authClient } from "@/lib/auth-client"; const SOURCE_LANGUAGES = [ { value: "Auto", label: "auto" }, { value: "Chinese", label: "chinese" }, { value: "English", label: "english" }, { value: "Japanese", label: "japanese" }, { value: "Korean", label: "korean" }, { value: "French", label: "french" }, { value: "German", label: "german" }, { value: "Italian", label: "italian" }, { value: "Spanish", label: "spanish" }, { value: "Portuguese", label: "portuguese" }, { value: "Russian", label: "russian" }, ] as const; const TARGET_LANGUAGES = [ { value: "Chinese", label: "chinese" }, { value: "English", label: "english" }, { value: "Japanese", label: "japanese" }, { value: "Korean", label: "korean" }, { value: "French", label: "french" }, { value: "German", label: "german" }, { value: "Italian", label: "italian" }, { value: "Spanish", label: "spanish" }, { value: "Portuguese", label: "portuguese" }, { value: "Russian", label: "russian" }, ] as const; type LangLabel = typeof SOURCE_LANGUAGES[number]["label"]; function getLangLabel(t: (key: string) => string, label: LangLabel): string { switch (label) { case "auto": return t("auto"); case "chinese": return t("chinese"); case "english": return t("english"); case "japanese": return t("japanese"); case "korean": return t("korean"); case "french": return t("french"); case "german": return t("german"); case "italian": return t("italian"); case "spanish": return t("spanish"); case "portuguese": return t("portuguese"); case "russian": return t("russian"); } } // Estimated button width in pixels (including gap) const BUTTON_WIDTH = 80; const LABEL_WIDTH = 100; const INPUT_WIDTH = 140; const IPA_BUTTON_WIDTH = 100; export default function TranslatorPage() { const t = useTranslations("translator"); const taref = useRef(null); const sourceContainerRef = useRef(null); const targetContainerRef = useRef(null); const [sourceLanguage, setSourceLanguage] = useState("Auto"); const [targetLanguage, setTargetLanguage] = useState("Chinese"); const [customSourceLanguage, setCustomSourceLanguage] = useState(""); const [customTargetLanguage, setCustomTargetLanguage] = useState(""); const [translationResult, setTranslationResult] = useState(null); const [needIpa, setNeedIpa] = useState(true); const [processing, setProcessing] = useState(false); const [lastTranslation, setLastTranslation] = useState<{ sourceText: string; sourceLanguage: string; targetLanguage: string; } | null>(null); const [sourceButtonCount, setSourceButtonCount] = useState(2); const [targetButtonCount, setTargetButtonCount] = useState(2); const { load, play } = useAudioPlayer(); const { data: session } = authClient.useSession(); const [decks, setDecks] = useState([]); const [showSaveModal, setShowSaveModal] = useState(false); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (session?.user?.id) { actionGetDecksByUserId(session.user.id).then((result) => { if (result.success && result.data) { setDecks(result.data); } }); } }, [session?.user?.id]); // Calculate how many buttons to show based on container width const calculateButtonCount = useCallback((containerWidth: number, hasIpa: boolean) => { // Reserve space for label, input, and IPA button (for source) const reservedWidth = LABEL_WIDTH + INPUT_WIDTH + (hasIpa ? IPA_BUTTON_WIDTH : 0); const availableWidth = containerWidth - reservedWidth; return Math.max(0, Math.floor(availableWidth / BUTTON_WIDTH)); }, []); useEffect(() => { const updateButtonCounts = () => { if (sourceContainerRef.current) { const width = sourceContainerRef.current.offsetWidth; setSourceButtonCount(calculateButtonCount(width, true)); } if (targetContainerRef.current) { const width = targetContainerRef.current.offsetWidth; setTargetButtonCount(calculateButtonCount(width, false)); } }; updateButtonCounts(); window.addEventListener("resize", updateButtonCounts); return () => window.removeEventListener("resize", updateButtonCounts); }, [calculateButtonCount]); const tts = useCallback(async (text: string, locale: string) => { try { // Map language name to TTS format let theLanguage = locale.toLowerCase().replace(/[^a-z]/g, '').replace(/^./, match => match.toUpperCase()); // Check if language is in TTS supported list const supportedLanguages: TTS_SUPPORTED_LANGUAGES[] = [ "Auto", "Chinese", "English", "German", "Italian", "Portuguese", "Spanish", "Japanese", "Korean", "French", "Russian" ]; if (!supportedLanguages.includes(theLanguage as TTS_SUPPORTED_LANGUAGES)) { theLanguage = "Auto"; } const url = await getTTSUrl(text, theLanguage as TTS_SUPPORTED_LANGUAGES); await load(url); await play(); } catch (error) { toast.error("Failed to generate audio"); } }, [load, play]); const translate = async () => { if (!taref.current || processing) return; setProcessing(true); const sourceText = taref.current.value; const effectiveSourceLanguage = customSourceLanguage.trim() || sourceLanguage; const effectiveTargetLanguage = customTargetLanguage.trim() || targetLanguage; // 判断是否需要强制重新翻译 const forceRetranslate = lastTranslation?.sourceText === sourceText && lastTranslation?.sourceLanguage === effectiveSourceLanguage && lastTranslation?.targetLanguage === effectiveTargetLanguage; try { const result = await actionTranslateText({ sourceText, targetLanguage: effectiveTargetLanguage, forceRetranslate, needIpa, sourceLanguage: effectiveSourceLanguage === "Auto" ? undefined : effectiveSourceLanguage, }); if (result.success && result.data) { setTranslationResult(result.data); setLastTranslation({ sourceText, sourceLanguage: effectiveSourceLanguage, targetLanguage: effectiveTargetLanguage, }); } else { toast.error(result.message || "翻译失败,请重试"); } } catch (error) { toast.error("翻译失败,请重试"); console.error("翻译错误:", error); } finally { setProcessing(false); } }; const visibleSourceButtons = SOURCE_LANGUAGES.slice(0, sourceButtonCount); const visibleTargetButtons = TARGET_LANGUAGES.slice(0, targetButtonCount); const handleSaveCard = async () => { if (!session) { toast.error(t("pleaseLogin")); return; } if (decks.length === 0) { toast.error(t("pleaseCreateDeck")); return; } if (!lastTranslation?.sourceText || !translationResult?.translatedText) { toast.error(t("noTranslationToSave")); return; } const deckSelect = document.getElementById("deck-select-translator") as HTMLSelectElement; const deckId = deckSelect?.value ? Number(deckSelect.value) : decks[0]?.id; if (!deckId) { toast.error(t("noDeckSelected")); return; } setIsSaving(true); try { const sourceText = lastTranslation.sourceText; const hasSpaces = sourceText.includes(" "); let cardType: CardType = "WORD"; if (!translationResult.sourceIpa) { cardType = "SENTENCE"; } else if (hasSpaces) { cardType = "PHRASE"; } await actionCreateCard({ deckId, word: sourceText, ipa: translationResult.sourceIpa || null, queryLang: lastTranslation.sourceLanguage, cardType, meanings: [{ partOfSpeech: null, definition: translationResult.translatedText, example: null, }], }); const deckName = decks.find((d) => d.id === deckId)?.name || "Unknown"; toast.success(t("savedToDeck", { deckName })); setShowSaveModal(false); } catch (error) { toast.error(t("saveFailed")); } finally { setIsSaving(false); } }; return (
{/* TCard Component */}
{/* Card Component - Left Side */}
{/* ICard1 Component */}